过去一年我一直在跟 Agent 打交道,反复被同一个问题折磨:同一个模型,有些人调出来的智能体特别“听话”,换个人来做就完全不是一回事。后来我意识到,差的不是模型,而是你有没有把“技能”当作一个正经的工程对象来管理。“agent-skills”想解决的问题,说到底就是这一件:让 Agent 的能力不再是零散的提示词片段,而是一套可以被设计、被复用、被评测、被组合的结构化技能库。这篇文章我会从技能体系的价值讲起,把手上的拆解方法、定义规范、落地实现和踩坑实录全部摊开来讲,内容偏工程实践,适合正在做 Agent 应用、或者准备把 Agent 产品化的朋友参考。
1. 为什么 Agent 需要一套“技能”体系
1.1 技能和提示词,本质上不是一回事
我见过很多团队的代码仓库里躺着一堆.md文件,里面写满了“你是一个优秀的文案专家”“请帮我润色这段文字”这种话。它们通常被叫做提示词模板,偶尔也被叫做“技能”。但严格说,这不是技能,这只是技能的一小部分。
技能是一个包含输入输出协议、执行逻辑、工具调用、容错处理和评测标准的完整工程单元。提示词只是其中“和模型沟通”的那一层壳。举个例子,一个“查询天气”的技能,如果只是提示词,大概长这样:
请查询一下北京的天气。
但一个真正能跑起来的技能至少要包含:城市参数怎么传、日期格式是什么、调用哪个天气接口、接口超时了怎么办、找不到城市时是否给出模糊匹配候选、返回结果用 JSON 还是自然语言。这些内容提示词管不了,必须在技能定义层解决。
这一点想清楚之后,很多 Agent 效果不稳定的问题就找到了根因——你以为缺的是提示词技巧,其实缺的是技能封装。提示词能决定模型在某一次对话里表现得好不好,技能体系能决定 Agent 在长期运行中能不能稳定、可控、可持续迭代。
1.2 技能体系重点解决三类核心痛点
第一类痛点是可复用性极差。我早期做 Agent 项目时,“总结文档”这个能力在三个不同场景里写了三份提示词,每次改动都要同步改三处,漏改一处就会出现两个 Agent 行为不一致的问题。技能化之后,只需要一个“文档总结”技能,不同场景通过参数控制风格和长度,改一处全局生效。
第二类痛点是无法评测。提示词写得好不好,全凭感觉,线上效果只能通过用户反馈间接判断。但技能不一样,技能有固定的输入输出 schema,我可以为每个技能准备一组评测用例,每次改动都能跑回归测试。比如“信息抽取”这个技能,我准备了 100 条测试样本,精确率、召回率全部量化,改模型版本或者改技能描述时,分数一对比就知道改动到底值不值。
第三类痛点是不可组合。复杂任务很少由一个技能完成,往往是“理解用户意图 → 检索知识库 → 生成回答”这样多个步骤。没有技能化的时候,这些步骤都混在一个大逻辑里,根本无法独立演进。技能化之后,检索归检索、生成归生成,我可以单独优化某一步,再用编排层把它们组装起来。这让整个系统具备了一种“搭积木”的能力,越到后期优势越明显。
1.3 什么时候你就该认真建技能库了
如果你的项目满足下面任意一条,我就建议你别再靠堆提示词了:一是同一个能力要在两个以上场景使用;二是你开始记录“这段提示词在某某场景下表现好”;三是你发现自己反复调整提示词却总是按下葫芦浮起瓢;四是你的 Agent 要面向外部用户,行为稳定性有硬要求。
技能体系本质上是用结构换确定性。它确实比随手写提示词多了一些定义成本,但这个成本是值得的。尤其是团队协作时,技能库就是团队共同维护的“能力资产”,新人来了看技能库里有什么,就知道这个 Agent 能做什么,不需要再翻聊天记录找提示词。
2. 技能拆解与设计:从一个需求到一组技能
2.1 技能拆解的三层漏斗法
把业务需求拆成技能,我通常用三层漏斗:需求层 → 任务层 → 技能层。
假设需求是“做一个能帮用户写周报的助手”。需求层很清楚:用户给一堆零散工作记录,Agent 输出结构化周报。到了任务层,需要拆解成四个任务:第一,把原始材料按项目归类;第二,提取每类任务的关键进展;第三,结合上期周报找出进度差异;第四,按照周报模板生成最终文本。每个任务再往下一层映射,就得到了技能:你要一个“文本分类”技能,一个“信息抽取”技能,一个“差异对比”技能,一个“模板渲染”技能。
这层拆解很多人容易犯急性子,一上来就想找一个“写周报”的大技能。这样做不是不行,但复用性会很差。“写周报”只能写给周报,换个场景变成“写项目总结”,又得重新写一个。拆成基础技能之后,“信息抽取”既能用在周报场景,也能用在简历解析、订单信息提取等完全不一样的场景。
拆解时还有一条判断标准:如果一个子任务在其他场景中有可能以同样的方式被使用,那它就应该被独立成一个技能;如果一个子任务只属于当前业务场景,那它可以保留在场景内部的私有技能列表里。按这个标准拆出来的技能库,基础技能占比会越来越高,业务技能越收越窄,整个体系的可迁移性就会很强。
2.2 技能定义的五大核心要素
一个可落地的技能定义,在我这里必须包含五个要素。
要素一,技能名称。名称要短、要精确、要有区分度。比如“web_search”和“knowledge_base_retrieval”,一看就知道是外部搜索还是内部知识库检索。不要去起“helper”“processor”这种含糊的名字,技能多了之后,路由全靠名称和描述,含糊等于没法用。
要素二,技能描述。描述是给模型看的路由提示。它决定了模型在什么情况下会调用这个技能。描述不能只写功能,还要写清楚适用范围和边界。比如“文本分类”技能的描述,我会写成:对给定文本按指定类别体系进行分类,返回各分类标签及置信度分数,适用于意图识别、内容归类、主题标注等场景,不适合需要主观判断的评价任务。
要素三,输入参数 Schema。这是技能的外部契约。参数名、类型、是否必填、默认值、取值范围,全部要明确定义。参数 Schema 是 Agent 与技能之间的界面,界面不稳定,上层编排和下层实现都会跟着动荡。
要素四,执行逻辑。这是技能内部真正干活的部分,可能是一段 Python 代码调用一个第三方 API,也可能是一段模板提示词加模型调用,还可能是两者的组合。执行逻辑需要把参数映射到真实操作,并处理各种异常情况。
要素五,输出格式。输出必须结构化,至少是统一封装的 JSON。这样上层编排层才能拿到结果继续做后续处理。输出格式还包括对输出内容的约定,比如字段含义、单位、精度,尽量避免让上一层去猜测。
这五个要素缺一个,技能在长期运行中都会出问题。尤其是输出格式,有很多人图省事让技能直接返回一段话,当时很爽,后面编排复杂任务时就会吃苦头,因为你想从一段自然语言里再提取结构化信息,等于重复造了一次轮子,而且更容易出错。
2.3 技能描述怎么写,模型才“认”
技能描述是生产环境里最容易被忽视但影响最大的部分。模型在大段技能清单里做路由选择,本质上是在做语义匹配,描述写得好不好,直接决定路由准确率。
我总结了几条经验。第一,描述要以动词开头,准确表达动作。“搜索、检索、提取、总结、生成、对比、分类”,每个动词背后代表一类操作,模型对动词的语义是敏感的,含糊的描述会让模型难以判断。
第二,要写清楚触发场景,用“当用户需要……时调用”,给模型一个明显的“信号”。比如“当用户需要查询最新资讯或网页内容时调用”,模型一眼就能对上号。
第三,要写反例边界。描述里明确“不适用于什么”非常有用。比如“本技能只用于事实性信息检索,不适用于情感分析或主观评价”。这能显著降低模型乱调技能的概率。
第四,同一个技能的描述,要为不同时机的路由分别维护。如果技能列表很长,模型第一轮只需要判断大类,详情描述可以在选定大类后再暴露给模型。我做过一次实验,把 40 个技能的一次性列表改成先分 6 个大类再二次路由,路由准确率从 82% 提升到 95%。这个提升不是模型变强了,而是决策难度降低了。
2.4 技能粒度:拆多细才合适
技能粒度是最难平衡的问题。拆得太粗,复用性差;拆得太细,技能数量爆炸,路由困难,维护成本也高。我个人的经验是:一个技能的输入输出如果超过 7 个参数,说明它太粗了,两个技能如果总是成对出现,说明它们可能该合并成一个。
举个例子,“生成销售周报”本质上包含“数据汇总”和“报告生成”两个技能,如果它们每次都在一起调用,不如合并成一个“销售周报生成”技能,把内部流程封装起来。反过来,“获取客户信息”这个技能如果既要查客户基本资料、又要查客户订单、又要查客户售后记录,那它其实该拆成三个。判断标准就是看“变化概率”和“复用面”:如果一部分逻辑变化频繁、另一部分趋于稳定,拆开更明智;如果某一段逻辑可能在多个场景中复用,也应该独立出来。
3. 技能落地实现:构建一个可直接调用的技能库
3.1 技能库的目录结构与元信息设计
我在项目里通常用一个skills目录来组织所有技能,每个技能占一个子目录,目录名就是技能名。一个典型的结构长这样:
skills/ web_search/ SKILL.md schema.json run.py tests/ cases.json info_extract/ SKILL.md schema.json run.py tests/ cases.json doc_summarize/ SKILL.md schema.json run.py tests/ cases.jsonSKILL.md 是技能的“人读”文档,描述技能的用途、适用场景、使用注意事项。schema.json 是技能的“机读”说明,定义输入输出参数。run.py 是技能的“执行”入口。tests 目录存放技能级评测用例。
这套结构的好处是:每个技能都有独立生命周期,可以被 Git 单独追踪。我可以用代码评审的方式审技能改动,也可以在技能出现回归时单独回滚它,而不会影响其他技能。对于一个几十技能规模的项目来说,这种结构上的独立性会让迭代效率有很大提升。
3.2 技能描述的元信息:不只写给人看
很多人在 SKILL.md 里写作用、写注意事项,但很少写成结构化的元信息。我的做法是给 SKILL.md 增加一个 YAML front-matter,里面写好路由信息。这样技能既能被人类阅读,也能被程序解析。一个典型的示例:
--- name: doc_summarize description: 对输入文档进行要点提炼,生成结构化摘要,适用于会议纪要、报告、论文等长文本,不适用于代码审查或数据统计。 input: document: string, 必填, 原始文档内容 max_points: integer, 选填, 摘要要点数上限, 默认5 language: string, 选填, 输出语言, 默认zh output: summary: string, 摘要正文 points: array, 要点列表 word_count: integer, 摘要字数 ---这个 front-matter 可以用程序直接读出来,拼装成模型路由时看到的技能列表。我在实际项目中,技能列表不是手写死在某段代码里的,而是启动时扫目录自动生成。新增技能只需要在skills目录下加一个子文件夹,Agent 能力就能扩一份,这套机制让技能库扩展变得非常轻。
3.3 技能的执行封装:把工具调用和模型调用统一起来
技能的执行逻辑不能五花八门,要有一个统一的执行接口。我的实现思路是让run.py暴露一个run(params: dict) -> dict函数,所有技能都遵守这个签名。好处是上层编排层可以无差别调用任何技能,传入一个参数字典,拿到一个结果字典。
在 run.py 内部,我建议把工具调用和模型调用分开。比如 doc_summarize 这个技能,本质上需要先对文本做分段,再逐段调用模型提炼要点,最后合并生成摘要。工具调用的部分可以用普通 Python 代码实现,模型调用的部分单独封装一个call_llm的公共模块,方便统一管理模型版本、温度参数和 token 上限。
还有一个容易被忽略的细节:给每个技能加上 execute 的超时控制。Agent 跑起来之后,很多技能会因为外部接口变慢或者模型推理超时而拖住整个链路,没有超时控制会让用户感受到“卡死”。我习惯把超时拆成两层:工具调用超时和模型调用超时,默认分别 15 秒和 60 秒,超时后技能返回一个统一的timeout_error结果,由上层决定重试还是降级。
3.4 技能编排:把原子技能串成工作流
有了基础技能,下一步就是编排。编排层的目标是:根据用户请求,决定调用哪些技能、按什么顺序调用、如何处理中间结果。我试过两种主流方案。
第一种是代码编排,用 Python 写死工作流。比如“处理用户周报请求”这个流程,在代码里就是先调用classify_task,再调用info_extract,最后调用doc_summarize。这种方案优点是完全可控、可调试,缺点是不灵活,流程变化需要改代码。
第二种是模型编排,让模型基于技能元信息动态决定调用顺序。这种方案灵活,但可控性差,容易出现模型用错技能、跳步、死循环等问题。我的实践是:尽量用代码编排固定主流程,在固定的主流程之间允许模型做局部选择。混合编排既保证了关键路径的稳定性,又保留了模型选择的弹性。
我还给编排层设计了一个简单的任务状态对象,记录每个技能调用的输入输出、耗时、成功/失败状态。这相当于给 Agent 加了“操作日志”,排障时能清晰看到是哪个技能出了问题、哪一步耗时超标、哪一步产生了错误输出。这套日志极大地缩短了我的排障时间,强烈建议大家在早期就把埋点做好,不要等出了问题再补。
3.5 技能评测与回归:技能库质量的生命线
技能库建起来之后,最怕的就是“改坏了”。模型升级、技能描述修改、参数调整,都可能让某个技能表现退化。没有评测,这些问题只能靠线上用户抱怨来发现,代价太大。所以我坚持为每个技能建立评测集,技能越基础,评测集要越完善。
评测集至少包括三类样本:常规样本、边界样本、错误样本。常规样本覆盖典型输入;边界样本覆盖空输入、超长输入、异常格式;错误样本覆盖那些必须拒绝或给出明确错误信息的场景。以“信息抽取”为例,常规样本是正常的订单信息文本,边界样本是一个超长合同文本,错误样本是只有一句“你好”却没有可抽取信息的情况。
我每两周跑一次全量技能评测回归,模型或技能库有改动时立即触发一次。跑完自动生成一份报告,对比每个技能的精确率、召回率和耗时。有了这份报告,“最近 Agent 变笨了”这类模糊反馈就能迅速定位到具体技能。评测是 Agent 工程质量的地基,这门课偷懒不了。
4. 常见问题与排查技巧实录
4.1 技能调用失败的典型根因
我踩得最多的坑,是输入参数类型不匹配。技能的 schema 写了max_points是 integer,但模型传了一个字符串 "5"。小项目里这种情况能侥幸跑通,技能多了以后必然炸。解决方法是所有参数进入技能前先做一层严格校验,类型不对就抛错,而不是在技能内部进行隐式转换。隐式转换的毛病在于它会把问题掩盖住,等数据流到深层逻辑里才爆炸,排障成本成倍增加。
第二类高频问题是技能内部调外部 API 的鉴权失败。这个问题比较隐蔽,因为不是每次都会出问题。我后来做了一个统一的credentials管理模块,所有技能都不能自己存密钥,必须从统一模块里取,并且定一个规则:凡是涉及到密钥变更,必须立刻跑一次相关技能的评测集,避免“改完配置后某个功能悄悄失灵”的尴尬局面。
第三类问题是技能输出不符合 schema 约定。尤其是模型直接输出结果的技能,经常多给字段、少给字段、嵌套结构不对。解决方法是:模型输出后加一个协议校验层,校验不通过就自动重试一次,重试还不通过就返回错误,不要硬着头皮往下走。协议校验的成本很低,但能挡住大量脏数据。
4.2 技能路由混乱与描述冲突
技能多了之后,路由冲突是必然的。最典型的情况是:两个技能都能处理用户的同一类请求,模型一会儿选这个一会儿选那个,用户就感觉 Agent 行为不稳定。我给技能列表做了一次“互斥性审查”,专门找那些描述存在重叠的技能。
比如早期我的技能库里有“文档总结”和“会议纪要生成”两个技能,描述都出现了“对会议内容进行整理”。模型经常混用。后来我把边界重新划清:会议纪要生成只负责“把原始会议记录转成格式化纪要”,文档总结负责“对任意文档提取要点”。描述里互相加了排除语:“会议纪要生成”写明“不适用于非会议类文档”,“文档总结”写明“不适用于需要保留原始结构的格式转换”。改完之后,路由准确率明显上升。
冲突排查还有一个技巧:在技能评测集里加上“路由测试”,专门测试一段请求会被模型路由到哪个技能。如果两个技能都频繁出现在结果里,就要及时调整描述或技能边界。路由测试应该成为技能库的常态化检查项,等上线后再发现路由混乱,代价就大了。
4.3 技能膨胀:什么时候该合并或拆分
用技能库的时间越久,技能数量越容易膨胀。我见过一个项目从 20 个技能膨胀到 80 多个,看上去很丰富,实际上有一半技能调用率极低,还有一部分功能重叠。技能数量多未必是好事,因为路由空间越大会决策越困难,维护成本也越高。
我每季度会做一次技能盘点,看两个数据:调用次数和成功次数。调用次数低于阈值的技能,要么下掉,要么合并到通用技能里。功能重叠的技能,直接合并。比如“PDF 内容提取”和“Word 内容提取”两个技能,本质上都是在做“文档解析”,我合并成一个单参数file_type控制的“文档解析”技能,减少一个技能,就少一份维护和路由负担。
还有一个值得警惕的信号:如果某个技能的描述里出现了大量条件分支句式,说明这个技能已经在承担超出它职责的能力了。比如描述里出现“当输入是 A 类型时,执行 X;当输入是 B 类型时,执行 Y”,这通常是要拆成两个技能的信号。技能职责单一,才能保证描述简单、路由准确、行为稳定。
4.4 三个最值得记住的实操坑
第一个坑:不要完全相信模型编的技能调用参数。早期我让模型自己决定技能入参,结果经常出现模型编造城市名、编造日期的情况。后来我在编排层强制要求:凡是技能入参必须来自上游可信数据,优先从前一个技能的结构化输出里提取,不直接采信模型从用户话里猜测的值。实在需要猜测,那就额外加一个“参数确认”环节,让用户确认后再调用技能。
第二个坑:别在技能描述里写“如果...就...”这类规则。描述是给模型做语义匹配用的,不是给模型做逻辑推理用的。你把复杂规则写进描述,模型大概率不会严格执行,反而会影响路由。复杂的判断逻辑应该放到编排层代码里,让代码来做决策,模型只负责做它擅长的事情。
第三个坑:技能评测集不是一次性资产。评测集要跟着业务演进而不断扩充。我每次在线上发现问题,都会把出问题的输入样本追加到评测集里,形成“线上回流”机制。这样每个被修复的 bug 都会变成一次永久回归保护,技能库的质量就会像滚雪球一样越滚越好。不回流,评测集就会慢慢陈旧,最后沦为一堆没有意义的数字。
我在实际项目中还有一个小习惯:每当技能系统做了较大改动,我会重新读一遍全部技能的描述,站在一个完全不了解技能库的模型视角去检查:如果只给我这些描述,我能准确选出合适的技能吗?这种做法虽然耗时,却总能发现很多平时注意不到的歧义和边界问题。技能库维护是一场持续投入的工程活,但它的每一分投入,最终都会反映在 Agent 的稳定性和用户体验上。