我接到这个项目标题的时候,心里的第一反应是:终于有人把 agent 的“手和脚”当成正经工程来对待了。过去一年里我做了不少 agent 原型,身边朋友也常问同一个问题:为什么智能助手接了七八个工具之后,还是经常答非所问、用错工具、传错参数?市面上讲 prompt 的文章很多,讲 function calling 的教程也不少,但很少有人把“工具”这套东西再往上抽象一层,做成一套可维护、可复用、可评估的技能资产。agent-skills这个项目就是想补齐这一层。如果你正在做生产级 agent,或者被工具调用不稳定折磨过,这篇文章会告诉你问题到底出在哪一步。
1. 为什么需要独立的技能层
先抛结论:function calling 只是“模型能调用函数”,而技能是“模型能正确完成一件业务动作”。两者差得很远。你给模型注册一个 API,它能调,但调得好不好、该不该调、调完结果怎么接,全得靠 prompt 里的连蒙带猜。当一个 agent 只有两三个工具时,这个差距不明显;一旦工具数量超过十个,业务场景开始复杂,问题就全冒出来了。
1.1 工具调用暴露出的三个老大难
第一个是选择困难。模型要在几十个工具的列表里找出唯一正确的那个,很像让一个新人从厚厚的工作手册里翻流程。工具描述稍微含糊,或者多个工具语义相近,模型就会犹豫、选错、甚至反复试错。我翻过一次线上日志,一个查询订单的需求,模型连续调用了查询库存、查询商品、查询物流三个工具,最后才找到查询订单的能力。每一次误调用都在消耗 token 和时间,在生产环境里还意味着一次错误的外部副作用,比如发了不该发的通知、改了不该改的数据。
第二个是上下文隔离。裸工具调用通常是扁平结构,每个函数只知道自己收到什么参数、返回什么结果。它不知道这个结果是谁来用、下一步要做什么、失败之后有没有别的路径。模型在对话里需要自己把多次工具调用的结果拼起来,逻辑一长就会忘。某团队做过一次测试,让 agent 完成“查上季度某区域销售额并生成图表”这个需求,如果不做技能编排,只靠裸工具调用,成功率始终过不了四成。
第三个是无法复用。做 A 项目时写好的“发送企业微信通知”函数,到 B 项目往往要复制粘贴改一遍,因为参数模型、错误码、重试逻辑、权限检查全跟 A 项目的业务耦合在一起。工具列表看起来繁荣,实际上全是散装零件,根本组不成流水线。所以很多 agent 项目做到一半,工具越来越多,模型却越来越笨,最后只能不断加规则去堵漏洞。
1.2 技能层到底在做什么
技能层的思路并不复杂:把工具从一个函数升级成一个能力单元。它不只封装一个 API,还要在 API 外面补上业务上下文、使用场景、参数语义、前置条件、输入输出约定,甚至把多个工具串成一个标准流程。模型看到的不再是几十个孤立函数,而是一组带业务含义的动作,每个动作知道自己解决什么问题、什么时候能用、怎么传参、怎么处理失败。
我习惯把技能层理解成给模型配了一套完整的工作手册,而不是一堆零散按钮。按钮谁都会做,但手册告诉模型哪些按钮可以一起按、按错了怎么撤、什么情况下不要按。agent-skills要的就是这套手册化资产。它的核心产物是一个可被检索、可被校验、可被编排的技能库,不依赖某一个特定模型,也不绑定某一种业务框架。下面这张表可以看清差异:
| 维度 | 裸工具调用 | 技能层 |
|---|---|---|
| 选择依据 | 函数名加一句短描述 | 场景化描述加语义检索 |
| 组合能力 | 基本没有,靠模型自由发挥 | 复合技能显式编排依赖 |
| 可观测性 | 只有函数日志 | 技能链路加副作用审计 |
| 复用成本 | 跨项目复制粘贴 | 目录化资产加版本管理 |
| 失败处理 | 模型反复试错 | 重试、降级、熔断策略 |
2. 技能库设计:描述、参数和版本
做技能层最容易起步的地方是定义技能格式。我见过很多团队一上来就写 class、写接口,结果三个月后接口重构,前面全白干。我的建议是先用一个通用 JSON 结构把技能描述清楚,再考虑代码层面的封装。格式稳定了,后面所有工具才能都往这一层上挂。
2.1 一个技能描述就是一个搜索问题
模型选择技能的过程,本质上是在技能库里做检索。谁被检索出来,取决于技能描述和用户意图之间的语义距离。所以描述不能写成空话,“创建订单”四个字就没有信息量,模型不知道这个订单是线上还是线下、要不要校验库存、要不要触发支付、创建之后返回什么。
我在实际项目里踩过坑,后来总结出一套描述模板:动作对象加业务场景加关键约束。举例来说,一条创建任务的技能描述,我会写成“在项目管理系统中创建一条新的任务记录,适用于用户提出待办、缺陷报告或需求登记等场景;创建时将自动校验负责人是否存在,必要字段缺失会直接报错,不会创建半截数据”。模型在检索时能落到明确场景里,误用率显著下降。
另一个容易被忽视的点是技能名称。名称要稳定,语义要唯一。同一个动作不要让 create_task、newTask、addTodo 三个名称并存,模型会困惑。技能名称我建议用“领域.动作”的命名风格,比如task.create、report.generate、notify.send。参数命名同样要保持人类直觉,拒绝缩写,别为了省 token 把recipient_list写成rl,模型真的看不懂。
2.2 参数契约与校验
参数契约是技能层最枯燥但最重要的部分。每个技能都必须声明参数名、类型、是否必填、默认值、取值范围,以及参数之间的依赖关系。依赖关系的处理尤其关键,比如发送周报这个技能,如果没传收件人列表,就需要一个默认收件人,这些业务默认值要写进技能配置,而不是靠模型现场猜。
我用的参数 schema 参考了 JSON Schema 的子集,但额外强调两个字段:一个是side_effect,说明技能执行后会产生什么副作用,比如会发邮件、会改数据库;另一个是purpose,说明模型在何种意图下应该调用本技能。副作用声明在生产环境特别重要,因为模型经常在不确定的时候乱试技能。看清副作用后,可以在策略层加一道闸门,比如只读请求绝对不能触发写操作。
参数校验在服务端分两层做。第一层是结构校验,防止模型传来缺失字段、类型错误、多余字段。第二层是业务校验,比如日期范围、权限、库存是否满足。这里有个心得:模型传参的错误大多不是格式错,而是把另一个技能的字段带了过来。结构校验只能挡掉一半,真正的拦路虎是业务校验,这也是为什么技能层必须和业务系统打通,不能只是一个空壳转发层。
2.3 技能目录结构与版本管理
技能库不该是丢在一个文件夹里的散文件。我建议目录按领域分包,并且每个技能自带一个元数据文件。目录结构大致是这样:
skills/ notification/ send_message/ SKILL.md schema.json executor.py get_template/ report/ generate_daily/ export_excel/ task/ create/ update_status/SKILL.md是给人看的说明,也是给 prompt 检索用的描述来源。schema.json管参数契约。executor.py是执行逻辑。这套目录解决两个问题:人可以快速浏览业务能力,技能可以独立做单元测试,改一个技能不牵连其他技能。
版本管理上,最忌讳的是把技能版本和代码版本绑死。技能的接口语义、描述措辞、执行逻辑,三者都可能独立变化。我采用技能级别的版本号,格式类似skills.task.create@1.3。agent 在启动时锁定技能版本,这样模型看到的技能描述不会因为别人改了一句话就突然跳变。升级时走灰度流程,让一部分流量先跑新版本,观察成功率之后再全量切过去。
3. 技能拆解:从原子能力到复合流程
技能库稳定之后,真正发挥威力的是组合。单个技能解决的业务动作有限,但多个技能串起来就能覆盖完整流程。这一步最容易过度设计,我在前几个版本里吃过亏,把技能拆得太碎,结果模型每一步都要做选择题,串联成功率反而更低。
3.1 原子技能与复合技能的划分标准
我的划分标准是看能不能独立完成一个有业务边界的动作。比如发送一条通知算原子技能,根据报表数据向指定负责人发送周报摘要就算复合技能,因为它内部要先取数、再算摘要、再发通知。这里有个朴素判断:如果一个动作离开业务上下文就没法说清边界,它就应该下沉或者组合。
原子的粒度不能太小。我犯过一个典型的错:把“读取用户信息”拆成“读取用户基本信息”和“读取用户联系方式”。结果模型在需要查邮箱时只调了基本信息,漏掉了联系方式。后来我把它合并成一个技能,参数里加include_contact字段,错误率立刻下降。拆技能的目的是降低模型理解成本,不是制造更多选项。
复合技能则是把一条多步骤流程固定下来,模型只需要发出一个意图,剩下的编排由技能层自己完成。这样做的好处很明显:减少模型在中间步骤上的决策次数,也减少模型忘步骤导致的失败。坏处是灵活性下降,如果用户临时想换其中一个环节的输出格式,复合技能就得重新配置。
3.2 技能依赖怎么声明和解析
复合技能会依赖原子技能,这种依赖关系必须在技能元数据里显式声明,而不是在 executor 里偷偷调用别的 executor。不声明的后果是技能库的静态分析失效,你无法知道哪些技能改动会影响哪些复合流程。
我在 schema 里增加了一个dependencies数组,每一项写的是被依赖技能 ID 和最小版本号。加载技能库时会做一次依赖解析,发现循环依赖直接报错。解析顺序按拓扑排序,保证被依赖的技能先加载、先注册。这套做法是从依赖注入容器里借来的思路,放到技能库里同样适用。
依赖解析还有一个额外收益:可观测性。复合技能执行时,我可以把一条调用链完整展开,看到它用了哪几个原子技能、每个技能耗时多少、在哪一步失败。做问题排查时,这比看模型吐出来的一长串工具调用日志痛快得多。
3.3 复合技能实战:从数据拉到报表生成
讲一个实际案例。某跨平台系统里有个需求:让运营每天用自然语言查询“某区域昨天的实时营业额,并生成 PDF 报表发给几个固定邮箱”。如果用裸工具让模型自由编排,它至少要走四步,每步都可能出错。用复合技能后,我只需要向技能库注册一条流程。
步骤一是查询区域配置,确认区域 ID 和报表模板;步骤二是从数据仓库拉取按小时聚合的营业额数据;步骤三是把数据渲染进模板并导出 PDF;步骤四是调用通知技能群发邮件。整条流程的产物是一份报表,对外参数只需要一个区域名称和一个日期,其余全部由内部逻辑决定。
这四步里我只保留“查询区域配置”作为第一步,因为区域名称必须映射成内部 ID,这个映射不适合让模型猜。后三步固定下来之后,成功率和执行速度都明显提升。实测下来,原来模型自由编排时成功率不到五成,换成复合技能后稳定在九成以上。
这个案例说明一件事:复合技能不是把模型的活全包了,而是把业务上可以固定的部分固定下来,把真正需要用户意图决定的部分暴露成参数。模型仍然负责理解需求和选择正确技能,但不再负责在中间步骤上做选择。
4. 实操接入:注册、调试与上线
理论讲完,落地的时候其实不算复杂。真正麻烦的是接入细节,很多坑都藏在注册、调试和上线的缝里。下面这部分是我踩过坑之后整理出来的流程,可以直接参考。
4.1 技能注册的关键代码结构
技能注册的核心是做一个注册中心,让模型执行侧只依赖注册中心的接口。我先定义了一个最简技能接口:
class BaseSkill: name: str version: str description: str parameters: dict def execute(self, params: dict) -> dict: raise NotImplementedError def validate(self, params: dict) -> dict: # 返回清洗后的参数,或抛出校验异常 raise NotImplementedError每个技能继承这个类,实现validate和execute。注册中心启动时自动扫描技能目录,读schema.json和SKILL.md,把技能加载进内存。加载完成后,注册中心对外暴露两个方法:一个是list_skills(),给模型召回模块用;另一个是run_skill(skill_id, params),给执行模块用。
这里有个细节值得分享:execute方法里不要直接执行业务 API 调用,而是先经过一个中介器。中介器负责参数清洗、权限检查、限流和副作用记录。我早期直接把业务 SDK 写进execute,后来发现根本没法做统一审计,谁调了什么技能、传了什么参数、有没有产生副作用,全部一团黑。中介器加进来之后,审计日志自然就齐了。
上线前还有一个动作:把技能描述编译成模型侧使用的格式。不同模型对工具描述的支持格式不一样,注册中心可以维护适配器,把统一技能定义转成当前模型的工具 JSON。这样一来,切换模型时不需要重写技能库,这是技能层最划算的部分。
4.2 新技能的四步验证法
我每开发一个技能,不会直接让它进入正式 agent 上下文,而是先跑四步验证。第一步是单技能调用测试,用真实的用户 query 去调,确认模型能正确命中该技能而不是选了其他相似技能。第二步是参数极端测试,故意缺字段、传错类型、传不存在的枚举值,看校验层是否都能拦住。第三步是描述干扰测试,把几个相似技能同时挂上线,反复用同一批 query 去跑,确认模型不会受干扰。第四步是组合回归测试,如果这个技能会被某个复合技能依赖,就要跑一遍复合流程全链路。
四步验证看起来不复杂,但它挡住了我项目里至少三分之一的线上事故。有一次我新增了“导出月度明细”技能,描述里带了“导出”两个字,结果原本该用“导出报表”的技能被大量误命中。回归测试跑出的误用率接近三成,最后我把两个技能的名称和描述全部调整,才把误用率压下去。
验证过程最好自动化。技能库会持续增长,模型也会升级,任何一环变化都可能改变召回结果。建议把每轮验证的 query 集合和预期技能 ID 存成测试用例,每次技能库变更后自动跑一遍,而不是上线后靠用户反馈补锅。
4.3 性能开销与并发控制
技能层不是免费的,它给模型侧增加了描述成本,也给服务侧增加了注册和编排成本。最大的开销是上下文里的技能描述,几十个技能的 JSON 全部塞进模型上下文,token 消耗不可忽略。我做过一次统计,30 个技能的描述大约要占 2000 到 3000 个 token。有些团队把几百个工具全放进上下文,上下文直接爆掉。
解决办法是召回剪枝。模型不必看到全量技能列表,只需要先根据用户 query 做一次检索,把最相关的十个技能放进上下文。这样 token 开销大幅下降,模型的选择范围也缩小,准确率反而提升。检索可以用向量召回加关键词加权,也可以用规则过滤,小规模场景一个简单的倒排索引就够用。
并发控制上,技能执行侧必须做限流和熔断。原因是模型可能在同一轮对话里连续触发多个技能,而每个技能背后的 API 能力不同。我给每个技能配置了每秒最大调用次数和重试策略,超过限流直接返回可读的错误信息,不让模型继续硬试。熔断逻辑放在中介器里,如果一个技能失败率连续五分钟超过阈值,就临时摘掉它,避免整个 agent 被拖垮。
5. 常见问题与排查技巧实录
最后这部分是真正的实战区。我在跟不同团队协作时,收到最多的问题就是“模型为什么不听使唤”。这里整理几个高频问题的排查思路和解决办法。
5.1 模型总是不调用该调用的技能
这个问题的原因往往不在模型,而在技能描述本身。排查顺序是:先看用户 query 里有没有明显的意图词,再看技能名称和描述里是否覆盖了这些词。一个常见原因是描述里写的全是内部术语,比如 SKU、ROI,而用户 query 说的是“商品”“利润率”,语义对不上,模型自然不选。
另一个原因是候选列表过长。模型面对几十个选项时,注意力会被相似描述分散。此前我给某个 agent 同时挂了十二个带“查询”字样的技能,模型在非查询任务里还是倾向于尝试其中几个。后来我把所有只读查询类技能统一加标记,在 prompt 里明确“查询类技能必须等系统确认后再执行”,误用明显减少。也可以在上层加一个意图分类器,先判断用户是不是要做查询,再决定是否放行查询技能。
还有一种情况是技能描述和实际副作用不一致。模型判断出这个技能会改数据,而用户只是想简单问一句,它就会保守地不调用。这种时候要么调整描述,明确说明它是安全的只读操作,要么在系统层面给只读标记。
5.2 模型传参时出现幻觉字段
模型生成的参数里多出一些 schema 里不存在的字段,这是高频问题。比如技能设计的参数明明是area_id和date,模型却传了area_name和time_range。出现这种问题,通常是两个原因:一个是上下文中还有其他系统的技能描述,字段前缀相似被带偏;另一个是 schema 里的参数命名不够贴近用户语言。
我的处理方式有三步。第一,schema 在注册时做严格校验,多余字段直接丢弃并警告,绝不会透传给执行器。第二,参数校验时做一次同义词映射,比如模型传了date而 schema 期待report_date,可以通过配置把report_date重命名为date来缓解。第三,重要参数写在技能描述里,明确说“此技能只接受area_id,不接受area_name”。别指望模型记住复杂 schema,它只会记住描述里顺带写过的东西。
如果幻觉字段已经导致执行失败,还需要在错误信息上做文章。错误信息要具体,最好带上合法参数示例,让模型看到之后不需要再猜。我见过很多后端的报错就是 “param error”,模型根本没法自我纠正,只能反复用错误参数重试好几轮。
5.3 技能执行失败后的重试和降级
技能执行失败不可避免,问题是怎么让它失败得优雅。最先要考虑的是重试策略:不同的错误码要区别对待。网络超时可以自动重试一两次,业务参数错误重试没有意义,权限错误更不该重试。我给每个技能的错误码打上retryable标记,重试逻辑只对可重试的错误生效。
降级方案要在设计技能时就想好。一个复合技能如果中间某一步挂了,是整体失败,还是用默认值继续?我的经验是,如果最终产物允许近似结果,就配置降级路径。例如报表生成时,实时数据库慢了,可以用前一天的全量快照替代。用户拿到的是稍旧但仍可用的报表,而不是一张失败报错。
最后是失败信息的可观测性。技能库的日志必须能还原执行链路,包括模型输入、清洗后的参数、每一步依赖的执行状态、耗时和错误码。没有这些信息,排查线上问题只能靠猜。我在注册中心把每次技能执行都落成结构化日志,带trace_id关联到对话,用户说“昨天那个报表没生成”时,我们可以很快定位到是哪一步出了问题。
5.4 问题速查表
| 现象 | 可能原因 | 解决思路 |
|---|---|---|
| 模型不调用目标技能 | 描述缺少业务场景词;候选列表过长 | 重写描述;做召回剪枝;增加只读/写标记 |
| 模型误用相似技能 | 技能名称或描述语义重叠 | 拉开命名差异;补充触发条件;跑描述干扰测试 |
| 传参出现多余字段 | 其他技能描述干扰;命名不贴近用户 | 严格 schema 校验;同义词映射;描述里写清合法字段 |
| 传参字段类型错误 | 模型对枚举理解不足 | 在描述中给出示例;校验层做类型转换 |
| 技能执行失败后反复重试 | 错误码未分类 | 给错误码打可重试标记;不可重试直接降级 |
| 复合技能整体失败 | 某中间步骤不稳定 | 配置降级路径;用快照或默认值替代 |
| 技能库里技能越多准确率越低 | 无召回机制,全量塞入上下文 | 用向量/关键词做召回,只把最相关技能放进去 |
6. 技能运营:评估、迭代与治理
技能库不是上线之后就完事的。它需要像业务系统一样运营,要观察使用情况,要知道哪个技能经常出错,哪个技能几乎没被调用过,哪个技能描述改一次效果就翻天。
6.1 核心指标怎么定
我给技能库定的核心指标有三项。第一是命中率,也就是用户 query 里该用某个技能时,模型是否正确选择了它。第二是执行成功率,也就是技能调用之后,是否在限定期限内返回了业务上可用的结果。第三是副作用异常率,比如发送通知类技能发出去了,但用户根本没有要求发送,这类事件必须单独统计。
命中率反映的是描述质量。执行成功率反映的是执行链路的稳定性,包括依赖 API 的可用性和参数校验的合理性。副作用异常率比较隐蔽,但它是安全底线,一旦异常率上升,就要立刻回滚技能版本或者摘除技能。
运营上我建议按周复盘。每周看技能库的调用排行、失败排行和误用排行。失败排行前几个技能往往不是业务本身难,而是错误码没归类、降级策略缺失、或者参数约束写得不够清楚。这类问题修起来很快,但如果不复盘,永远没人知道。
6.2 版本灰度与回滚机制
技能版本上线不能一刀切。我采用两阶段灰度:先在测试环境用仿真流量跑一遍回归,再在生产环境放量百分之五,观察命中率和执行成功率。如果指标下降明显,自动回滚到旧版本。
灰度期间要特别注意描述变更的影响。模型侧对描述变化的敏感度很高,可能只是改了一句话,召回结果就完全变化。所以我要求任何描述改动都要生成一次对比测试,至少用五十条真实历史 query 跑新旧两版,比较命中率差异。没有明显提升,宁可保留旧描述。
回滚机制要能在运行期生效,不能依赖重新部署整个 agent。注册中心应该支持热更新技能,把某个技能从最新版切到上一版,几秒钟内完成。这样即使误上线了一个坏版本,也能快速止血。
6.3 技能治理的几条红线
最后分享几条我在技能治理上立的红线。第一,任何技能上线前必须有 schema 校验和业务校验覆盖;没有校验的技能不允许存在。第二,写操作技能必须声明副作用,并且默认不允许模型在单轮对话里连续执行超过三次写操作。第三,技能库的变更必须记录审计日志,包括谁改的、改了哪个字段、有没有通过回归测试。
这些红线看起来都是流程上的约束,但支撑起了整套技能层的可靠性。脱离治理的技能库,随时可能因为一次小改动毁掉整个 agent 的稳定性。把红线定下来之后,新技能合入的流程就变得清晰,每个人都知道该做什么检查,而不是靠一个人的临场判断。
做agent-skills这套东西,我最大的体会是:模型能力决定了 agent 的上限,但技能层决定了 agent 的下限。很多人把精力都花在调 prompt 和选模型上,却舍不得花时间把工具调用做成资产。几十个散装工具看起来灵活,用起来全是累赘。如果你正在做 agent,不妨先退一步,把技能分成原子和复合两层,把描述当成搜索问题去打磨,再把执行侧的中介器补上。这三件事做完,你会发现 agent 突然变“懂事”了很多。这也是我认为agent-skills这类实践最值得推广的原因。