Agent 这个词这两年被聊烂了,但真正动手给 Agent 配"技能"的人,多半会卡在一个很尴尬的位置:模型能跟你对话,却没法替你干活。我这里说的技能,不是"会写诗"那种通用能力,而是让 Agent 能稳定地调用外部工具、完成多步骤任务、并且在出错之后还能自己救回来的那套工程能力。这篇文章把我自己做 agent-skills 这套体系时的思路、踩坑和沉淀下来的方法梳理一遍,希望能给正在搭 Agent 技能库的同学一个能直接参考的框架。
先交代一下背景。我去年接手了一个内部效率项目,目标是让 Agent 能帮运营团队完成数据整理、报表生成、批量发消息这些具体工作。最开始大家觉得这很简单——把 API 一接,模型会调用不就行了。结果一跑就露馅:模型要么调用参数填错,要么中途放弃,要么生成的结果根本不能直接落库。折腾了两个月,我意识到问题的关键不在模型能力,而在"技能"本身的工程化程度。于是我把技能从 Agent 主逻辑里彻底拆出来,单独设计、单独测试、单独迭代,这才有了后面这套相对稳定的方案。
1. 为什么非要把"技能"从 Agent 主逻辑里拆出来
1.1 一次失败的对话式任务带来的启发
最早我们做的是让 Agent 自动整理周报。开发思路很简单:把公司内部的周报数据库接口暴露给模型,然后让模型自己写查询、拉数据、格式化输出。
单看每一次调用,模型表现还不错。但放到完整任务链路里就完全不是那么回事了。比如"汇总上周各团队未提交周报的人员名单",模型会先尝试查询数据库,查到一半发现接口参数不对,然后它不会换一种写法,而是开始"编"数据——把几个看起来合理的名字填进输出里,然后一本正经地告诉你任务完成了。
这不是模型的错。是我没有告诉它"这个技能有哪些参数、参数怎么填、查不到数据时该返回什么"。模型在信息不足的情况下,本能反应就是用自己训练时习得的"合理的世界模型"去补全。于是我们需要一种机制,把技能的边界、用法、异常行为全部显式地交给模型。这就是拆分的第一个理由:技能是 Agent 能力的边界,边界不清晰,模型就会开始创造事实。
第二个理由更实际:手里的任务在变,但底层的动作其实高度重合。"查询某团队成员的最近活跃时间""查询某份文档的最近修改记录""把一段结构化数据转成表格",这些动作换一个场景照样用。与其每次都让模型从零开始摸索,不如把这些动作做成标准技能模块,统一维护、统一升级。后续接新场景时,Agent 只是把这些技能重新组合一遍,成本能下降一大截。
1.2 技能、工具、插件,别让这三个词把你绕晕
我团队内部对这三个概念有一个不成文的界定,先放在这里,后面都按这个口径来:
- 工具:最小粒度的外部能力封装,比如"调用 HTTP 接口""读取本地文件""执行一段 Python 代码"。工具一般无状态,输入输出简单直接。
- 技能:一个面向任务的完整能力单元,比如"查询周报并汇总未提交名单"。一个技能内部可以调用多个工具,也可以有自己的一套状态处理逻辑(比如重试、缓存、分批拉取)。
- 插件:一组相关技能的集合,通常配套了配置信息、权限声明、依赖环境。比如"运营数据插件"包含"拉取日报""生成报表""推送通知"三个技能。
在 agent-skills 这套体系里,模型感知的最小单位是"技能",而不是"工具"。为什么?因为工具太细,模型在规划时很容易迷失。你要是给模型十个细颗粒工具,让它自己去编排"先调这个再调那个",它大概率会在第三步选错。而把任务编排好的"技能"作为一个整体暴露给模型,模型只需要回答"用哪个技能、填什么参数",成功率会高很多。
这里就引出一个所有做 Agent 的人都会纠结的问题:技能到底做多粗?太粗了,模型只能用预设好的场景,遇到新任务就傻眼;太细了,规划复杂度指数上升。我的经验是,按"一个完整业务动作"来切。比如"查周报""导报表""发通知",这都各自算一个完整动作,不要往上拆成"连接数据库""格式化时间戳""选择接收人"这种半截动作。
2. 技能定义的核心:输入输出的边界怎么画
2.1 一个技能最少要有哪几样东西
我见过很多人写技能描述只写一句话,比如"查询周报。参数:team_id, week"。然后模型用起来一塌糊涂。原因是模型需要的是完整的"使用说明书"。
在我现在的定义里,一个技能模块至少包含五个部分,缺一个都会在某个场景下翻车:
- 技能标识与版本:比如
report.query+v1.3。版本很重要,因为技能行为一变,模型的历史调用记录就得重新评估,否则会出现"文档里写的用法和实际行为不一致"的情况。 - 功能描述:用两到四句话讲清楚这个技能是干什么的、在什么场景下用、在什么场景下不要用。
- 参数 Schema:每个参数的类型、必填性、取值范围、默认值、参数之间的联动关系。
- 调用示例:至少两个完整示例,一个是常规用法,一个是边缘情况用法(比如参数缺省时怎么办)。
- 异常行为说明:什么情况会失败、失败时返回什么结构、模型拿到失败后该怎么处理。
其中第 5 点最容易被忽略,但恰恰是最重要的。模型拿到一个技能,它最担心的是"调用失败之后我该怎么办"。如果你不告诉它,它就会自己创造一个"合理的错误处理方式"——而模型自己创造的行为,通常都是给调用方回一句"抱歉,我遇到了问题",然后就没有然后了。正确的做法是在技能描述里显式写清楚:
当目标团队不存在时,返回
{status: "not_found"}。此时不要尝试编造数据,直接告知用户该团队 ID 无效并建议检查拼写。
这样模型就有了明确的"兜底路径"。
2.2 描述文本比参数 Schema 更重要
这个结论是我被现实教育出来的。早期做技能注册时,我把大量精力花在参数类型检查上,结果真正让模型学会正确调用的,不是参数定义,而是描述文本本身。
原因是现在的模型大多是靠"嵌入式指令理解"来用工具,它读你的描述文本,比读形式化的 JSON Schema 更直接。我试过同一个技能两种写法:
- 写法一:参数 Schema 非常严谨,描述只有一句"查询周报数据"。
- 写法二:Schema 保持基本类型,描述写了一段"该技能用于查询指定团队在指定日期范围内的周报提交情况。支持查看未提交名单、各人提交时间。若需要趋势分析,请使用 report.trend 技能。"
结果写法二的模型正确调用率高出差不多三成。原因很简单:模型从描述文本里能学会"什么时候该用我",而 Schema 只能告诉它"用的时候参数怎么填"。前者解决的是"选对技能"的问题,后者只解决"填对参数"的问题,前者出错造成的后果严重得多。
所以我现在的写描述标准是:开头一句话说清楚技能是干嘛的;中间说清楚适用场景和不适用场景;结尾给一个典型调用示例和对应的返回结果预期。写完之后,我会拿这个描述去问一个空上下文模型:"你有以下技能可用,用户说 X,你选哪个、参数怎么填?"跑几个典型问题,描述写得够不够清楚立刻就能看出来。
2.3 参数校验层:别全信模型,也别全不信模型
模型填参数的错误率比许多人想象的高,尤其是涉及 ID、日期格式、枚举值时。我见过模型把"上周"翻译成某个具体的错误日期范围,也见过它把公司内部团队 ID 和另一个系统里的用户 ID 混在一起用。
所以技能入口必须有一个参数校验层。这个校验层不是简单地检查"类型对不对",而是要:
- 把枚举值展开给模型看。比如
channel参数不是写"int",而是写channel: 1=企业微信, 2=钉钉, 3=邮件。 - 对 ID 类参数做存在性校验。调用真实系统之前,先查一下这个 ID 是否存在,不存在直接返回友好错误,不让请求打到下游。
- 对时间范围做合理性检查。比如"开始时间早于结束时间""跨度不超过 90 天"这类规则内置在校验逻辑里。
校验层不通过时,返回的错误信息要包含"模型该怎么修正"。比如:
{ "status": "invalid_param", "field": "team_id", "message": "未找到团队 ID: xx-123,请确认该 ID 是否来自『运营团队管理』系统,可尝试调用 team.search 技能查询正确 ID。" }这种返回方式等于给模型递了一根绳子,它顺着绳子就能自己纠正过来。比你让它干巴巴地面对一个 400 错误要强太多了。
3. 技能与 Agent 主干的三种典型连接方式
3.1 技能注册表与路由
技能和 Agent 之间需要一个"注册表"作为中介。Agent 启动时加载注册表,模型能看到的所有技能列表、描述、版本号都从注册表来。这样做的直接好处是:技能的增删改不影响 Agent 主逻辑。新增一个技能,往注册表里加一条记录,Agent 下一次请求就能感知到;下线一个技能,同样只需要改注册表。
路由逻辑我做得比较简单,就是一个两步筛选:
- 按"意图匹配"粗筛。根据用户请求的关键词和技能描述之间的语义相似度,选出候选技能集合。
- 在候选集合里,由模型做精确选择。
有人可能会问:既然最终还是模型选,为什么第一步还要做语义粗筛?原因是技能数量多了之后,把所有技能描述一次性塞进上下文会导致两个问题:一是 token 消耗大,二是模型注意力被分散,选择准确率下降。粗筛先把候选从几十个缩小到三五个,模型的选择压力小很多,准确率反而上来了。
这个粗筛逻辑对响应速度也有帮助。注册表里可以缓存技能描述的向量索引,查询一次就几十毫秒,相比把几十个技能描述全量塞给模型,成本省不少。
3.2 技能编排:串行、并行与条件分支
单个技能好做,真正难的是技能之间的编排。运营场景里最常见的任务结构是"先查数据,再加工数据,最后发出去",这至少涉及三个技能。我现在的方案支持三种编排模式:
- 串行编排:前一个技能的输出作为后一个技能的输入上下文。比如「日报查询」的输出直接注入「报表生成」的输入。
- 并行编排:多个技能同时执行,互不依赖,最后汇总结果。比如同时查「本周销售额」和「本周客诉量」,两个查询互不干扰。
- 条件分支:根据某个技能的返回结果决定后续走哪条路径。比如"检查是否有未提交人员"返回
not_found,就走"全院已提交完毕"的文案分支;返回has_missing,就走"列出名单并发送提醒"的分支。
实现上,我让每个技能的执行结果都带上一个结构化的"执行环境上下文",里面记录了本轮执行的入参、出参、状态码、耗时等。Agent 主干读取这个上下文,结合用户意图决定下一步调哪个技能。这个设计让技能之间保持低耦合,每个技能只管好自己的输入输出,组合逻辑交给主干去编排就好。
3.3 上下文注入的两种姿势
技能执行时,Agent 需要把相关的背景信息注入到模型的上下文里。我试过两种方式,各有适用场景:
- 静态注入:在模型调用前把技能描述、参数说明、历史消息摘要一次性拼进 Prompt。适合技能数量少、上下文短的场景。优点是实现简单、调试容易;缺点是上下文会越积越长,token 消耗高。
- 动态检索注入:只在需要某个技能时,才从技能描述库和对话历史库里检索出最相关的那一小段,注入到当前调用里。适合技能数量多、每个技能说明冗长的场景。缺点是检索逻辑有额外延迟,而且检索不准时会漏掉关键信息。
我现在的实现是两者结合:常用技能的描述预留在系统 Prompt 里(静态),长尾技能走动态检索。通过实验把"静态部分"控制在总 token 的 20% 以内,剩下的空间留给对话历史和工具返回结果。
4. 让技能真正"可用"的三道坎
4.1 可靠性问题:重试、降级与状态报告
模型在调用技能时,失败是常态而不是异常。我统计过一版上线初期的数据,技能调用一次成功的比例只有八成多一点,剩下的要么是参数校验不过,要么是下游系统超时,要么是模型中途自己改主意没调完。所以技能层必须具备三板斧:重试、降级、状态报告。
- 重试:对下游系统超时、瞬时 5xx 这类错误,技能内部做最多两次重试,间隔采用递增退避(比如 1s → 3s)。重试前要检查任务是否幂等,像"发送通知"这种非幂等操作,重试必须带上
request_id去重。 - 降级:在主路径不可用时,技能自动切换备选路径。比如主数据库不可用就从只读副本查询,实时计算接口超时就先拉昨天的缓存数据,并在返回结果里标注"数据时间点"和"降级原因"。
- 状态报告:技能执行超过一定阈值(我设的是 10 秒),就在结果里返回当前执行阶段的状态,比如"已拉取数据,正在生成报表,已完成 40%"。这一步主要给 Agent 一个汇报进度的机会,避免用户看到"正在执行"但半天没反应而失去耐心。
重试和降级逻辑写到技能模块内部,而不是让模型自己决策。原因很简单:模型决策不稳定,同样的错误这次它会重试,下次可能就直接放弃了。而写在代码里,行为就完全可预期。
4.2 安全边界:什么技能不该暴露给模型
做 agent-skills 最容易忽略的一点:能力越强,越要注意哪些能力不该暴露。模型拿到一个"执行代码"技能之后,理论上它能做任何代码能做的事——这既是潜力,也是风险。
我严格遵循几条原则:
- 敏感操作默认关闭。技能注册表里每个技能有独立的权限位,标注了它能接触的数据域和动作域。像"删除数据""修改配置""发送对外消息"这类技能,默认不对模型开放,只有用户显式授权后才进入模型可见列表。
- 数据域隔离。不同团队的数据技能互相隔绝,模型处理 A 团队的数据时,看不到 B 团队的技能入口。这比"靠模型自觉不去访问"靠谱得多。
- 干预审批流。某些高风险技能(比如"批量发送对外通知")在模型调用后不是直接执行,而是生成一个"待审批任务",推给用户在界面上点确认。这个审批流绕过了 Agent 本身,直接在平台层拦截,确保模型再怎么绕也绕不过人的确认。
我知道有人会觉得审批流太重,但实际运营下来,大家很快就适应了这个节奏。因为真正需要审批的技能确实没几个,而多了这一道确认,出问题时的责任边界清晰了很多。我甚至觉得,做 Agent 技能库的人越早想清楚安全边界,后面迭代的路越顺。
4.3 可观测性:日志怎么打才有用
技能层是 Agent 和外部系统的中间层,一旦出问题,排查定位往往涉及三个系统:Agent 决策日志、技能执行日志、下游系统日志。如果技能层的日志打得不好,你会在排查现场崩溃——你不知道模型到底传了什么参数,也不知道技能代码里发生了什么。
我的做法是,每个技能执行都记录一份结构化的"执行审计记录",包含这些字段:
| 字段 | 说明 | 示例 |
|---|---|---|
| call_id | 全局唯一调用 ID,关联 Agent 决策链路 | sk-20250117-001234 |
| skill_name | 技能名称与版本 | report.query@v1.3 |
| input_snapshot | 模型传入的原始参数快照 | {"team_id": "xx-123", "week": "2025-W02"} |
| validation_result | 校验层是否通过,未通过的原因 | invalid_param: team_id not found |
| execution_steps | 内部每个步骤的耗时和状态 | fetch:200ms, transform:1.2s, cache:miss |
| output_summary | 返回结果的摘要,不塞完整大对象 | {"status": "ok", "rows": 42} |
| error_stack | 错误堆栈或下游返回原始报文 | timeout on GET /api/reports |
| cost_metrics | token 消耗、调用耗时 | prompt_tokens: 312, total_ms: 3420 |
这组日志有两个重要的作用。一个是线上问题定位——拿着用户侧看到的错误信息往回反查,几分钟就能定位到是模型选错了技能、还是参数校验拦了、还是下游超时。另一个是模型行为分析——定期拉取所有执行审计记录,统计哪些技能被过度使用、哪些技能被误用、哪些技能描述导致模型反复填错参数,这些数据直接迭代技能描述和路由逻辑。
5. 评估与迭代:怎么判断一个技能真的"会了"
5.1 单测之外还要准备什么
技能模块是代码,单测当然要有,但单测只能验证"代码逻辑对不对",验证不了"模型会不会用这个技能"。我现在的做法是三层验证:
- 一层是单元测试:校验层逻辑、重试逻辑、返回值格式化,这些用常规单测框架就能覆盖。
- 二层是模型调用测试:构建一组标准任务问题,让模型在只看到技能描述的情况下调用技能,检查它选得对不对、参数填得对不对、失败后处理得对不对。这层测试不 mock 模型,直接调真实模型接口跑。
- 三层是端到端场景测试:以用户真实任务为脚本,走完整链路。比如"用户要求生成上周数据报表并发送到指定群聊",整个链路跑通,结果人工复核。
三层验证每次技能代码变更后都要全量跑一遍。刚开始大家觉得麻烦,但跑了几轮之后发现,很多问题在第三层才暴露,而且修复成本远低于上线后出了问题再救火。
5.2 用场景集而不是指标来验收
有一段时间我陷入了指标焦虑,天天看"技能调用成功率",想用一个数字来衡量技能质量。后来发现这个数字的参考价值有限——成功率低可能是技能本身有问题,也可能是用户的问题本身超出了任何技能能解决的范围,还有可能是模型抽风。
所以我现在改用场景集验收的方法。每个技能维护一份场景清单,每个场景包含:一段典型用户话术 + 期望的技能调用序列 + 期望的结果形态。改动技能后,跑一遍场景集,人工逐个看输出是否"语义正确"。语义正确是什么意思?就是"用户拿到这个结果,是否能直接用它完成自己的任务"。
这个方法听起来原始,但在 Agent 这个领域,它比任何自动指标都靠谱。因为模型输出的正确性很多时候没法用一个精确分数衡量,"用户能不能用"才是终极标准。
5.3 回归测试的一个隐蔽大坑
技能迭代最坑的一次,是我改了一个查询技能的超时时间,从 10 秒改到了 30 秒。单测过了,模型调用测试也过了,结果上线第二天,一批数据报表任务集体变慢,用户抱怨"点完没反应"。
排查发现:超时时间变长之后,技能在下游系统无响应时会多等 20 秒才报错。而 Agent 主干的等待阈值只有 15 秒,导致技能还没返回,主干已经判定超时并把任务标记为失败。两边时间参数不一致,就出现了"技能还在等,任务已经失败"的错位。
从那以后,我把回归测试的检查清单里加了一条:改动任何时间、重试、阈值相关的参数,必须检查上下游所有相关系统的对应参数。这个教训看似简单,但如果不是真实翻过车,很难意识到时间参数是横跨多个模块的隐藏耦合点。
6. 技能库的扩展方向:从一个技能到一套体系
把单个技能做好只是第一步,技能多了之后,你还需要考虑两个扩展问题——技能版本管理和技能之间的依赖关系。
技能版本管理这块,我吃过一个闷亏。有一次我更新了report.query技能的内部数据源字段名,旧版本返回team_name新版本返回team_display_name。调用新技能的 Agent 任务都正常,但那些用旧技能缓存的报表模板还是按旧字段解析,结果报表输出全是空值。所以我现在强制要求:技能的破坏性变更必须升大版本,并且注册表里保留最近两个版本,给下游系统留出迁移时间。
技能依赖关系更隐蔽。有的技能内部会调用其他技能,比如"生成周报"这个技能,内部会依次调用"查询团队列表""查询周报数据""格式化输出"三个子技能。这个调用链如果写死在代码里,未来的调整会很痛苦。我现在把这些依赖改成声明式的——技能描述里显式标注"依赖哪些基础技能、按什么顺序调用、数据如何传递"。主干加载技能时,自动解析依赖图,按拓扑序执行。这样新增一个技能只需要声明依赖,不用改主干逻辑。
7. 回看这套方案的收益和一些必须承认的局限
半年多跑下来,我手里这套 agent-skills 方案带来的收益是很直接的:新场景接入时间从原来的两周缩短到两到三天,技能复用率从最初的不到三成提升到六成以上,线上问题定位时间从小时级缩短到分钟级。这些数字不是夸张,是真的被团队内部多次复现过的效果。
但我也得必须承认它的局限。第一,技能描述和路由的调优,很大程度上还依赖人的经验,还没法做到完全自动化。每次新增技能都要手工写描述、跑场景集,这是力气活。第二,技能多了之后,注册表的管理本身是个新的维护负担——描述怎么保持与代码同步、版本怎么跟下游系统对齐,这些都需要专门的人来盯。第三,依赖模型能力的部分仍然不稳定,同一个技能描述,换一个模型版本,调用表现就可能波动。
所以我最后的建议是:如果你也要做 agent-skills 这类体系,不要一开始就追求大而全的技能框架。从三五个高复用度的技能开始,把描述规范、校验逻辑、审计日志这层地基打牢,验证模型确实能稳定调用之后,再逐步扩展。地基稳了,后面技能加得越多,体系越省力;地基不稳,技能越多,你维护起来只会越痛苦。