
最近在重构我们的智能体底层代码时我彻底把原先那一大坨提示词工程拆了换成了现在这套以“agent-skills”为核心的技能化组织方式。如果你也做过生产级的大模型智能体一定懂那种痛提示词越写越长、越写越复杂稍微改一句就影响一大片场景不同任务之间完全没有复用可言。而当我把“技能”这个概念真正落地成一套可注册、可加载、可编排的代码结构之后整个系统的可维护性和稳定性都上了一个台阶。这篇博文不打算写教科书式的定义我会以自己实际重构一个智能体技能库的经验为线索把agent-skills从设计思路到代码实现、再到踩坑排查完完整整给你捋一遍。适合正在做智能体应用、被提示词工程折磨得想骂人、又不想放弃沉淀复用能力的开发者和AI应用工程师。1. 项目定位agent-skills 到底在解决什么问题1.1 从“堆提示词”到“攒技能”的转变先讲个直观的例子。假设你让一个大模型智能体去完成“调研某行业的最新动态并输出周报”这个任务。传统做法是什么把行业背景、输出格式、检索偏好、行文风格全部塞进一个一千多字的 system prompt然后靠模型自己发挥。问题是这个提示词高度耦合换一个行业或者调整一次输出粒度就要动整个字符串而且暴露给模型的内容太多它反而不知道该重点抓什么输出质量飘忽不定。agent-skills 的思路是把这件事拆成独立的子能力检索信息是一项技能、结构化落盘是一项技能、按固定模板写周报又是一项技能。每项技能通常由“描述 参数定义 执行逻辑 局部提示词”组成注册到一个统一的技能库里。智能体在运行时会根据当前目标的语义动态选择调用哪些技能像搭积木一样拼出完整工作流。我重构的时候体会最深的一点是技能不是简单地把提示词分个段而是给模型划分了清晰的能力边界。原来 system prompt 里所有内容对每个请求都生效模型每轮都要处理大量冗余约束而技能是被按需加载的模型当前只需要关注最相关的那部分指令。这既省 token又大幅降低上下文被无关信息干扰的概率。1.2 技能与函数、工具调用之间的关系很多读者听到 agent-skills 的第一反应是这不就是 function calling 吗我的回答是部分重叠但层次不同。函数工具描述的是“我提供一个接口你能传参、拿结果”而技能描述的是“我有一个可以完整完成的任务能力里面可能包含多次工具调用、内部决策和独特的上下文”。打个生活化的比方。函数调用相当于给你一本字典你想查哪个字就翻到对应页技能则是一个训练有素的助手你吩咐它“帮我整理书架”它会自己判断哪些书该归类、是按开本大小还是按类别排列、处理过程中遇到破损书脊该怎么登记。这个助手内部可能用了字典、可能自己拟了张表格、也可能边整理边调整策略但对外暴露的就是一个高层的“整理书架”能力。所以我在设计技能库时非常强调边界底层才放原子工具上层全部以技能形态存在。一个技能可以调用多个原子工具也可以调度其他技能。这样智能体的调度逻辑就变得极简——它不需要关心每一步怎么调用函数只需要按任务需求挑选技能组合真正把“思考”和“执行”分离开来。1.3 使用这套方案的收益从我们自己的项目数据来看改造后的系统在几个维度上提升非常明显技能复用率超过七成、原先三小时才能调通的新增流程压缩到二十分钟以内、线上请求因“模型不按提示词输出”导致的失败率大幅降低。最让我满意的是新成员接手项目时不需要再读那个几百行的 prompt 文件了直接在技能目录里看描述就能理解系统具备哪些能力。对团队协作、对模型稳定性、对快速扩展都是实打实的改善。2. 技能库设计先定好数据结构和存储规范2.1 为什么技能不能只是一段字符串最早我把技能设计成“名称 prompt”的简单键值对很快发现完全撑不住。真正跑起来你会发现缺少结构化的参数定义你没法在技能间做依赖校验缺少输入输出约束技能之间衔接的时候特别容易出现类型对不上的情况。举个实际例子摘要技能输出的是一段 Markdown 文本而归档技能期待的数据结构是带标题和 tag 的对象两边没对齐就直接运行时爆炸。所以我最后落地的技能 schema 包含这样几个核心字段技能名称、任务描述、版本号、依赖项、输入参数的 JSON Schema、输出格式定义、以及执行时的局部提示词模板。其中任务描述不是给人看的文档而是给模型看的调度依据。它需要写清楚“这项技能在什么情况下被选中、完成什么目标、不该做什么”影响的是智能体顶层规划时的路由正确性。输入输出定义我直接用了 JSON Schema 格式好处有两个一是天然适合做运行时校验二是可以被大模型原生理解。像字段的枚举值、格式要求、默认值能让模型在调用技能前自行约束好生成参数减少不必要的报错重试。输出定义我通常也鼓励枚举类型配合上代码里的解析器返回结果就变得非常可控。2.2 一个可落地的目录结构技能目录我习惯按“域domain”来组织每一类业务场景占一个顶层文件夹里面再放一个个独立技能子目录。这样做和代码模块化管理一样避免所有技能堆在一起变成大杂烩。agent-skills/ research/ web_search/ SKILL.yaml prompt.md execute.py extract_summary/ SKILL.yaml prompt.md execute.py report/ weekly_report/ SKILL.yaml prompt.md execute.py common/ format_with_toc/ SKILL.yaml prompt.md execute.pySKILL.yaml存元数据和参数定义prompt.md放技能特有的提示词模板execute.py放核心的执行逻辑。目录拆分得足够清晰之后技能本身也能够被单独测试、单独发布、单独回滚。我们用了一段时间之后又把技能目录直接做成了一个代码仓库分支策略和发布流程就跟普通代码库一样走非常顺。2.3 用 YAML 定义技能元信息直接给你看一个我常用的 SKILL.yaml 模板。这是研究域下的 web_search 技能负责在接到检索需求时执行搜索并整理结果。name: web_search version: 1.2.0 domain: research description: | 在需要获取外部实时信息、检索网页内容或佐证事实时使用。 执行搜索并返回结构化结果列表。如果用户的需求与实时数据无关不要使用本技能。 dependencies: - serper_api - html_cleaner inputs: type: object properties: query: type: string description: 搜索关键词或自然语言查询。 num_results: type: integer default: 5 minimum: 1 maximum: 20 region: type: string enum: [cn, us, jp] default: cn required: [query] output: type: array items: type: object properties: title: {type: string} link: {type: string} snippet: {type: string}description字段要当作给模型看的路由说明书来写。项目初期我写得很马虎都是“搜索并返回结果”结果智能体在明明该用本地知识库回答的场景也去调搜索既浪费时间又带偏答案。后来我把触发条件、排除条件都写清楚路由准确率才提上来。这里有个建议描述里尽量写“在什么情况下使用”和“什么情况下不要使用”比你写“这是一个搜索工具”有效得多。输入输出部分同样别糊弄。字段默认值、取值范围、枚举值写全了模型在决策的时候可以少犯很多低级错误。我甚至遇到过模型把整数参数传成字符串的情况最后是靠着 JSON Schema 的类型校验兜底才没让错误往下游蔓延。2.4 版本管理与依赖关系技能一旦多了版本和依赖就是躲不开的坎。我们的方案比较朴素每个技能目录是一个独立的 semver 版本号技能之间只允许依赖固定版本范围不允许依赖未发布的变动版本。每次修改技能时都要在变更记录里写明改动点和影响范围代码评审时这步是强制卡点。依赖管理我只做了一层不允许技能之间出现循环依赖。之前设计过一个有点复杂的技能 A它内部会调用技能 B 去提取主题标签而技能 B 又想在结尾统一格式化摘要跑去调了技能 A结果运行时上下文来回套把模型的上下文窗口都快撑爆了。后来加了个构建期的循环依赖检查脚本再没出现过类似问题。3. 核心实现技能加载与执行机制3.1 技能注册表把元信息加载进内存技能库的入口不能是每次调用都扫一遍硬盘我采用进程内加载 缓存失效策略。服务启动时把全部 SKILL.yaml 解析成技能对象注册进一个线程安全的注册表供智能体路由和执行共用。from pathlib import Path import yaml class SkillRegistry: def __init__(self, base_dir: str): self.base_dir Path(base_dir) self._skills {} def load_all(self): for yaml_file in self.base_dir.rglob(SKILL.yaml): skill_meta yaml.safe_load(yaml_file.read_text(encodingutf-8)) skill_dir yaml_file.parent skill Skill( metaskill_meta, prompt_template(skill_dir / prompt.md).read_text(encodingutf-8), executorself._load_executor(skill_dir), ) self._skills[skill_meta[name]] skill self._assert_no_cycle() return self def get(self, name: str) - Skill: if name not in self._skills: raise SkillNotFoundError(name) return self._skills[name] def list_skill_descriptions(self) - list[dict]: return [ {name: s.meta[name], description: s.meta[description]} for s in self._skills.values() ]executor我按 exec 函数或者接口类两种方式统一封装。如果技能简单到不需要额外代码直接用内置的 PromptExecutor 调大模型返回 JSON如果技能里包含搜索、代码解释、数据库查询等操作在 execute.py 里写一个继承基础类的执行器。重要的是所有执行器对上层暴露一样的run(context, **params)调用范式上层规划的收益才最大。3.2 技能上下文的传递逻辑技能执行时不是孤立运行的它需要拿到智能体当前的任务上下文。我定义了一个 SkillContext 数据类里面包含原始用户意图、已收集的信息片段、全局配置项、以及一组可用的基础工具句柄。dataclass class SkillContext: task_id: str original_query: str collected_memo: dict available_tools: dict max_tokens_limit: int 4096这个 context 对象在每轮技能编排时被复制成浅拷贝传给具体技能。为什么要浅拷贝而不是共享同一份因为不同技能之间经常需要隔离上下游写入避免 A 技能误改了 B 技能写进 memo 的数据。执行完结后再把技能返回的输出合并进主上下文让下一个技能能看到前一步的结果。上下文传递中特别容易踩的坑是上下文越传越大。我曾见过一个检索技能把全文网页内容都塞进 memo下一个技能又要重新读一遍大文本token 消耗翻了好几倍。现在统一做法是memo 只允许放结构化摘要和结果引用原始长文本一律持久化到对象存储memo 里只放对象路径。这条规则让我线上成本降了不少。3.3 路由与编排模型如何决定用哪个技能技能的自动编排我走的是两段式方案。第一段是“意图路由”把用户的原始请求连同注册表里的技能描述一起交给模型让模型给出候选技能列表和顺序。第二段才是“执行推进”每执行完一个技能就把输出喂回给模型由模型判断是否继续调用下一个技能或者把结果输出给用户。def route(registry: SkillRegistry, query: str) - list[str]: descriptions registry.list_skill_descriptions() prompt f 你是一个技能调度器。根据用户目标和以下技能列表选择需要依次执行的技能名称。 只返回 JSON 数组不要额外解释。 技能列表{descriptions} 用户目标{query} response llm.chat(prompt, response_format{type: json}) return json.loads(response)路由的核心在于技能描述写得好不好。描述不清楚再聪明的模型也排不出靠谱的流程。很多项目到这一步喜欢上各种复杂的状态机我反而劝你先让模型自由编排跑一段时间积累足够样本再把稳定路径固化成显式流程。过早固定流程会牺牲灵活性运行时会碰到各种没预料到的边角情况。3.4 错误处理与回退策略技能执行的真功夫全在异常处理上。我的原则是能自动回退的绝对不直接失败。比如搜索技能调用第三方 API 超时回退逻辑可以是先缩小查询范围重试一次再不行就用上一轮缓存结果最后才抛错。每层回退都记录日志方便事后分析哪一层服务不稳定。回退逻辑直接写在执行器内部比放在外层编排里更合适。因为只有技能自己最清楚哪类错误是可重试的、哪类错误是参数问题无需再试。我还给每个技能加了“失败原因摘要”的输出通道让智能体在技能失败时能拿到简短说明从而调整后续策略而不是盲目报错。4. 实战案例给智能体搭建一个“研究摘要”技能链4.1 任务场景与整体拆解假设我们要让智能体完成这样一个任务给定一篇长文章 URL提取正文、生成摘要、拆解核心观点、并且输出一份 Markdown 格式的研究笔记。如果不用技能化方案你大概会写一个又臭又长的系统提示词让模型自己处理下载、清洗、总结、整理结构。用技能化方案我会拆成三个技能web_fetch_article、summarize_content、build_research_note。拆技能的依据是能力单一、输入输出边界清晰。web_fetch_article只负责抓网页并返回清洗后的正文和标题summarize_content不管长文本从哪来只负责产出摘要和观点列表build_research_note把前面的结果按照研究笔记模板重排。三者职责单一后续任意一个环节要升级都不会牵动另两个。4.2 第一个技能抓取与清洗web_fetch_article的实现里最核心的是两手功夫一是用 HTTP 客户端抓原始 HTML二是用内容提取算法把正文从导航、广告、评论等噪声中分离出来。这里我不展开算法细节只强调一点正文提取结果必须统一成干净的文本块附带元信息否则下游摘要技能会读入大量垃圾内容。class FetchArticleExecutor(BaseExecutor): def run(self, ctx: SkillContext, url: str) - dict: html ctx.available_tools[http].get(url, timeout10) article extract_main_content(html, urlurl) return {title: article.title, content: article.text}这个技能踩过的坑主要是超长正文。有些文章正文几万字直接传给摘要模型不仅 token 超额模型还会丢失开头信息。后来我在抓取和摘要之间加了一个预处理步骤如果正文超过一万二千字先按段落做递归截断优先保留包含关键词和标题结构的段落。别小看这个处理它能显著提升下游摘要的忠实度。4.3 第二个技能摘要与观点抽取summarize_content技能的输入是前一个技能产出的标题和正文输出是一个 JSON 对象包含一句话概括、三段要点、以及三个可延伸讨论的问题。为了稳定拿到 JSON我在 prompt 模板里给出了明确的输出示例并且在执行器里做了一层 JSON 解析兜底模型偶尔会在 JSON 前后追加解释性文字解析失败时就用正则提取花括号片段再二次解析。class SummarizeExecutor(BaseExecutor): def run(self, ctx: SkillContext, content: str, title: str) - dict: prompt self.render_prompt(titletitle, contentcontent) raw_output ctx.available_tools[llm].chat(prompt) return robust_json_loads(raw_output)描述信息仍然很关键。SKILL.yaml里我会注明这个技能接受的正文应该在清洗后状态最大长度约一万字如果超过要先截断。模型看到这个限制就会在调用摘要技能之前主动考虑需不需要先做内容压缩进一步提升了链路的稳定性。4.4 第三个技能组织成研究笔记build_research_note是个相对轻的技能它把标题、摘要、要点、延伸问题整合成一份 Markdown 研究笔记。核心逻辑并不复杂但它代表了技能化架构的典型收益我可以随时替换这个技能比如改成生成幻灯片大纲或者改成生成邮件简报而不必改动摘要技能的代码。模板我用 Jinja2 写在 prompt.md 同目录下执行器把参数填进去后交给模型做文本润色和格式统一。这个阶段使用模型不是为了生成新内容而是为了让行文更自然、把要点之间的过渡补顺。对模型能力要求不高所以这里我会选用更轻量、更便宜的小模型进一步节省成本。4.5 端到端串联效果三个技能都注册到 registry 后顶层只做一次路由调用。模型看到用户发来的链接和“帮我整理一份研究笔记”的诉求会根据技能描述给出顺序列表web_fetch_article、summarize_content、build_research_note。执行器依次跑完返回最终 Markdown。整个过程有清晰的日志轨迹每个技能耗时、输入输出大小都能追踪。调试问题的时候不再是对着一大段 prompt 猜测模型意图而是看哪一步技能结果不符合预期就行了。这套结构上线后我们针对它做了回归测试集包含 URL 失效、正文超长、摘要返回 JSON 解析失败、模板渲染缺字段等场景。这些测试跑一次大概两分钟却实打实保证了每次技能改动时其他链路不被破坏。5. 常见问题与排查技巧实录5.1 高频问题速查表现象可能原因排查方法模型完全不调用某个技能技能描述里的触发条件写得模糊查看路由日志确认描述被注册表正确加载尝试在描述里补充“什么时候不要用”的排除句式技能执行返回格式无法解析输出定义不够严格或模型在 JSON 前后添加了噪音在 executor 里做鲁棒解析同时检查输出 schema 是否枚举了所有可能字段技能之间的依赖数据丢失上下文对象在不同技能间传递时被误修改排查是否用了浅拷贝检查是否有技能错误覆写了 memo 里其他技能写入的数据同一技能在相似场景表现不一致技能加载时包含无关上下文导致模型判断漂移检查 prompt 模板是否依赖了变化过大的输入或者输入字段是否标准化不足技能库加载缓慢注册表启动时执行了过长初始化逻辑将技能执行器的初始化改为惰性加载只有首次被调用时才实例化5.2 一次让我印象深刻的线上事故某个晚上收到告警智能体的“竞品信息收集”流程成功率骤降到六成。我打开日志一看问题出在web_search技能上。那天搜索 API 服务商调整了返回结构原来是数组字段的results变成了嵌套对象。第一层报错被 executor 内建的重试机制遮住重试无效后才暴露出来。由于技能边界清晰我定位速度很快。但这也暴露出一个问题我们对上游服务的返回结构缺少主动校验。修复方法是给web_search的输出层增加了一个轻量校验器如果检测结果结构与预期不符立刻降级到本地备用数据源。那次之后我养成了一个习惯任何对接外部接口的技能都必须标配一个有界缓存 降级策略不能把宝全押在一个服务商身上。5.3 排查技能问题的通用三步法第一步先看技能路由是否选对了。路由日志会记录模型挑选了哪些技能顺序是什么如果技能选择就不对问题一定出在描述文案上不是执行逻辑上。第二步看技能输入是否符合预期。重点确认传入参数是否被正确的技能间上下文规整过比如 URL 是不是走了完整清洗流程。第三步看输出是否可被下游消费。很多人习惯只看当前技能成功了没忽略了下一个技能拿到的数据是否满足它的 schema这种跨界错误往往最隐晦。按这个三步法排查百分之八十以上的技能链路故障都能在十分钟内定位。剩下一部分跑不掉的基本都是 prompt 模板里隐含的指令冲突需要特意组织对抗性用例来重现场景才能发现。5.4 关于测试的独家经验技能测试不能只测“正常路径”。我建议每个技能至少要有一组 fail-case 测试API 超时、返回空数据、加载到异常字符、参数类型不合法、极端长度文本、模板字段缺失。这些测试不用都跑完整模型调用大部分可以在 executor 单元内部用 mock 对象跑通速度快还省钱。另外维护一份“技能变更影响图”非常有用。每次改动某个底层技能跑一遍受影响的调用链条测试能避免很多昨天还能用今天突然串线的尴尬。我已经把这套测试固化成 CI 的一个步骤任何技能改动合入主干之前都必须通过全部关联用例否则直接拦截。最后再分享一点实际心得从我决定把整个智能体从提示词工程重构为 agent-skills 技能库到现在最深的感触是精力被从“不断调咒语”中解放了出来转移到了“能力沉淀”上。以前每接一个新场景我都得焦虑模型会不会漏掉某个格式细节现在只需要看看技能库里有没有现成技能没有就补一个有就做组合编排。对团队来说这种模式也让经验有了可积累的载体。每次修复线上问题时沉淀下来的不是“又改了一句 prompt”而是一段经过测试、可复用的技能版本。如果你正在做智能体应用并且开始感觉到提示词越来越难以驾驭我一定建议你尽早尝试这种思路。从一个小技能开始拆起不用一次性重构完先让系统跑起来再逐步迁移你会很快感受到技能化带来的那种“稳”的体验。