1. 从"agent-skills"这个命名说起:它到底指什么
第一次看到"agent-skills"这个词,很多人会下意识地把它当成某个具体框架或库的名字。但如果你在工程实践里摸爬滚打过一段时间就会发现,它更像是一个概念容器——用来描述"智能体(agent)具备哪些可被调用的能力"这件事本身。换句话说,它不是一个现成的工具,而是一套关于"如何给智能体装配技能"的设计思路和组织方式。
我在实际项目里接触过不少类似命名的东西,有的团队把它做成一个技能注册中心,有的把它做成一套提示词模板库,还有的干脆把它当成一个插件目录。命名相同,落地形态却千差万别。这恰恰说明"agent-skills"这个词的核心不在实现,而在抽象层次:它站在"能力"这一层,往上承接具体的业务任务,往下对接模型调用、工具执行、状态管理等底层机制。
那它到底解决什么问题?简单说,就是让一个智能体不再只会"聊天",而是能真正"干活"。一个只会对话的模型,你问它天气它只能凭记忆瞎猜;但如果你给它装配一个"查询天气"的技能,它就能调用真实接口拿到数据。技能就是智能体从"会说"到"会做"之间的那座桥。
这篇文章适合谁看?如果你正在做智能体相关的应用开发,或者你手头有一个需要让模型调用外部能力的场景,再或者你只是好奇"智能体到底怎么被组织起来干活",那这篇内容都能给你一些可以直接参考的东西。我会尽量把抽象的东西讲具体,把"为什么这么设计"讲透,而不是只丢一堆名词。
需要先说明一点:由于原始输入里项目正文、关键词、摘要都是空的,下面所有关于实现细节、目录结构、参数配置的内容,都是基于我在类似项目中的常见实践做的合理补全。这些不是唯一答案,但都是经过验证、能跑通的方案,你可以直接拿去改。
2. 技能的本质:把"能力"从"模型"里剥离出来
2.1 为什么不能把所有逻辑都塞进提示词
很多人做智能体应用的第一反应是:把所有规则、所有知识、所有操作步骤全写进系统提示词里。刚开始确实能跑,但很快就会撞墙。
第一个问题是上下文膨胀。你每加一个能力,提示词就长一截。加到十几个能力之后,光系统提示词就几千字,模型每次调用都要重新读一遍,token 成本飙升不说,模型对每条规则的注意力还被稀释了。我实测过一个案例,当系统提示词从 800 字涨到 4000 字时,模型对其中某条关键约束的遵守率从 95% 掉到了 70% 左右。这不是模型变笨了,是信息过载。
第二个问题是无法执行真实操作。提示词再详细,模型也只能"描述"怎么查数据库,不能真的去查。它没有手,只有嘴。
第三个问题是不可维护。业务逻辑散落在几千字的自然语言里,改一个参数要在文本里大海捞针,还没法做单元测试。
"agent-skills"这套思路的核心价值,就是把这些能力从提示词里剥离出来,变成一个个独立、可注册、可调用、可测试的单元。模型只负责"决定调用哪个技能、传什么参数",具体怎么执行交给技能本身。
2.2 一个技能应该包含哪些要素
一个设计良好的技能,通常包含四个部分,我用一个"查询订单状态"的技能来举例说明:
| 要素 | 作用 | 示例 |
|---|---|---|
| 名称与描述 | 让模型知道这个技能是干什么的 | query_order_status:根据订单号查询当前物流与支付状态 |
| 参数定义 | 告诉模型需要提供哪些输入 | order_id(字符串,必填)、detail_level(枚举,可选) |
| 执行逻辑 | 真正干活的代码 | 调用内部订单服务接口,解析返回结果 |
| 返回结构 | 统一输出格式,方便模型理解 | 标准化的 JSON,包含状态码、消息、数据体 |
这里最关键的是描述和参数定义。模型不是靠读你的代码来决定调不调这个技能的,它是靠读描述。所以描述写得好不好,直接决定技能被正确调用的概率。我见过太多人把描述写成"查询订单",结果模型在用户问"我买的东西到哪了"的时候根本想不起来调它。正确的写法应该覆盖用户可能的表达方式,比如"查询订单的物流进度、支付状态、预计送达时间"。
参数定义同样重要。每个参数的类型、是否必填、取值范围、默认值都要写清楚。模型在生成调用参数时,这些信息就是它的"说明书"。参数定义模糊,模型就会瞎填。
2.3 技能与工具、插件、函数的区别
这几个词经常被混用,我在不同团队里见过不同的叫法。大致可以这样区分:
- 函数(function):最底层的代码单元,纯粹的执行逻辑,不包含任何给模型看的元信息。
- 工具(tool):函数加上给模型看的描述和参数 schema,是模型可以直接"看到"并调用的东西。
- 插件(plugin):一组相关工具的集合,通常还带有生命周期管理、配置注入等能力。
- 技能(skill):语义上更强调"能力"这个层面,一个技能可能对应一个工具,也可能是一组工具的编排。
在实际落地时,这些概念经常是重叠的。你叫它技能也好,工具也好,本质都是"模型可调用的能力单元"。我在文章里统一用"技能"这个词,但你要知道它和工具在多数场景下是可以互换的。
理解这一层区别的意义在于:当你设计系统时,要清楚自己是在定义"执行逻辑"还是在定义"给模型看的接口"。这两件事应该分开,混在一起就会导致技能既难测试又难被模型正确调用。
3. 技能注册与发现:让智能体知道自己会什么
3.1 注册表是整个体系的中枢
技能不会凭空被模型知道。你需要一个注册表(registry),把所有可用技能集中管理起来。模型在每次决策前,会拿到一份"当前可用技能清单",然后从中选择。
这个注册表最简单的形态就是一个字典或者列表,每个条目包含技能的名称、描述、参数 schema 和执行入口。复杂一点的会带上权限控制、版本管理、启用开关等。
我建议注册表至少支持这几个操作:
register(skill):注册一个技能unregister(name):注销一个技能list_available(context):根据当前上下文返回可用技能列表get(name):获取某个技能的完整定义
其中list_available是精髓所在。不是所有技能在任何场景下都该暴露给模型。比如一个"删除数据"的技能,在只读会话里就不该出现。根据用户身份、会话状态、任务阶段动态筛选可用技能,能大幅降低模型选错技能的概率。
3.2 技能描述怎么写才能被模型正确选中
这是整个体系里最容易被低估的环节。我踩过的坑几乎都集中在这里。
先说一个反直觉的结论:技能描述不是写给人看的文档,是写给模型看的"广告"。它的目标不是准确,而是"在正确的时机被正确选中"。
具体来说,好的技能描述要满足三点:
第一,覆盖同义表达。用户说"查快递""看物流""我的包裹呢""订单到哪了",这些都应该能命中同一个技能。你可以在描述里把这些说法都列出来,或者用更抽象的方式概括:"查询订单相关的物流、配送、签收信息"。
第二,明确边界。如果一个技能只处理"已支付订单",描述里就要写清楚。否则模型会在用户问未支付订单时也调它,然后执行失败。边界写清楚,模型才知道什么时候不该用。
第三,避免描述重叠。如果你有两个技能描述都是"查询订单信息",模型就会随机选一个。这时候要么合并,要么把差异写清楚,比如一个查"支付状态",一个查"物流状态"。
我一般会用一个简单的自测方法:把技能描述单独拿出来,想象用户可能说的 20 句话,看这个描述能不能让人(或模型)判断出该不该调它。如果判断不了,描述就得改。
3.3 动态技能加载的两种策略
技能不一定一开始就全部加载。有两种常见策略:
全量加载:启动时把所有技能注册进去,每次决策都带上完整清单。优点是简单,缺点是技能多了之后,光技能清单就占掉大量上下文,模型选择难度也上升。适合技能数量在 20 个以内的场景。
按需加载:根据当前任务阶段或用户意图,只加载相关技能。比如用户刚进入"下单"流程,就只加载下单相关的技能;进入"售后"流程,再切换到售后技能。优点是上下文精简、选择准确,缺点是需要一套意图识别或流程管理机制来驱动加载。
我在实际项目里更倾向按需加载,尤其是技能超过 30 个之后,全量加载的准确率下降非常明显。一个折中方案是"分组加载":把技能按业务域分成若干组,先让模型选组,再在组内选技能。这样每层的选择空间都小,准确率能稳住。
4. 技能执行链路:从模型决策到真实动作
4.1 一次完整的技能调用经历了什么
很多人以为模型"调用技能"是一步完成的,其实中间有好几个环节。理解这条链路,是排查问题的关键。
一次典型的技能调用大致经过这些步骤:
- 模型根据当前对话和可用技能清单,输出一个结构化的调用意图,包含技能名和参数。
- 系统解析这个意图,校验技能是否存在、参数是否合法。
- 如果校验通过,执行技能逻辑;如果不通过,把错误信息返回给模型。
- 技能执行结果被序列化成模型能理解的格式,回填到对话上下文。
- 模型基于结果继续生成回复,或者决定调用下一个技能。
这条链路里,第 2 步的校验最容易被忽略,但恰恰是稳定性的关键。模型生成的参数经常有各种小毛病:数字传成字符串、必填项漏填、枚举值拼错。如果不校验直接执行,轻则报错,重则产生脏数据。
我的做法是在校验层做三件事:类型转换(能转就转,比如字符串数字转数字)、必填检查、枚举白名单。校验失败时,不要直接抛异常给用户,而是把具体的错误信息返回给模型,让它自己修正参数重试。实测下来,模型在拿到明确错误提示后,重试成功率很高。
4.2 参数校验与错误回传的实操细节
这里展开说一下错误回传的写法,因为这是很多人做不好的地方。
假设模型调用query_order_status时漏了order_id。如果你返回一句"参数错误",模型大概率会再瞎试一次。但如果你返回:
调用失败:缺少必填参数 order_id。请从用户消息中提取订单号后重新调用。模型就能明白问题在哪,并且知道下一步该干什么。错误信息里带上"怎么修"的提示,比单纯说"错了"有用得多。
再比如枚举值错误。模型传了detail_level: "full",但你的白名单只有basic和detailed。返回:
调用失败:detail_level 的值 "full" 无效,可选值为 basic 或 detailed。模型下次就会从白名单里选。这种"带选项的错误提示"能显著减少来回次数。
还有一个细节:错误信息不要暴露内部实现。比如不要返回堆栈信息、数据库字段名、内部服务地址。这些对模型没用,还可能泄露敏感信息。错误信息应该面向"如何修正调用",而不是"系统哪里崩了"。
4.3 超时、重试与幂等性
技能执行是真实操作,就会有超时和失败。这块处理不好,整个智能体就会卡死或者产生重复操作。
超时:每个技能都应该有超时设置。查询类技能可以短一点,比如 5 秒;涉及外部服务的可以长一点,比如 30 秒。超时后要返回明确的超时信息,让模型知道"这次没成功,可以稍后重试或换方式"。
重试:不是所有失败都该重试。网络抖动导致的失败可以重试,参数错误导致的失败重试也没用。我一般只对"超时"和"明确的临时性错误"做自动重试,重试次数控制在 2 次以内,且要加退避间隔。
幂等性:这是最容易被忽视的。如果模型因为超时重试了一次"创建订单"的技能,结果创建了两个订单,那就是事故。所以对于写操作类技能,必须做幂等设计。常见做法是让调用方传一个唯一请求 ID,服务端根据这个 ID 去重。模型生成请求 ID 不太可靠,所以通常由系统在执行层自动注入。
提示:写操作类技能一定要做幂等,读操作类技能可以不做。判断标准是"重复执行会不会产生副作用"。
5. 多技能编排:当一件事需要好几个技能配合
5.1 串行编排与并行编排的取舍
现实任务很少能靠单个技能完成。"帮我查一下上周的订单,把没发货的挑出来,然后给这些订单的客户发提醒"——这一句话里至少涉及查询、筛选、通知三个技能。
编排方式主要有两种:
串行:一个技能的输出作为下一个技能的输入,按顺序执行。适合有依赖关系的任务。上面那个例子就是串行:先查订单,再筛选,再通知。
并行:多个技能同时执行,结果汇总。适合相互独立的任务。比如"同时查一下天气、汇率和股价",这三个互不依赖,并行能省时间。
选择哪种,取决于技能之间有没有数据依赖。有依赖就必须串行,没依赖就尽量并行。但要注意,并行执行会带来结果合并的问题,多个技能同时返回,模型需要能正确区分和处理。
我在实际项目里的经验是:默认串行,确认无依赖再并行。因为串行的调试和错误定位简单得多,而并行的性能收益在技能本身耗时不高时并不明显。
5.2 让模型自己编排还是用固定流程
这是一个架构层面的选择。
模型自主编排:把可用技能都给模型,让它自己决定调用顺序。灵活,能处理没预设过的任务组合。但不可控,模型可能绕远路,也可能在中间步骤出错后不知道怎么回退。
固定流程编排:针对特定任务预先定义好技能调用顺序,模型只在每个步骤内做参数填充。可控,稳定,但只能处理预设过的任务。
我的建议是混合:对高频、关键的任务用固定流程,保证稳定;对长尾、探索性的任务用模型自主编排,保证灵活。判断标准是"这个任务出错的代价大不大"。代价大的走固定流程,代价小的放开让模型试。
具体实现上,固定流程可以用状态机或者工作流引擎来描述,每个节点绑定一个技能。模型自主编排则依赖模型的规划能力,通常需要配合"任务分解"的提示策略。
5.3 中间结果的传递与状态管理
多技能编排时,中间结果怎么传是个实际问题。
最简单的方式是全部塞回对话上下文,让模型自己记住。技能少、步骤短的时候没问题,但步骤一多,上下文里全是中间结果,模型容易被干扰。
更稳妥的方式是引入一个会话状态存储。每个技能的输出除了回填给模型,还存一份到状态里,用键值对管理。后续技能需要前面的结果时,从状态里取,而不是从对话历史里翻。这样上下文保持精简,状态也清晰可查。
状态存储要注意生命周期。一次任务结束后,相关状态应该清理掉,避免污染下一次任务。我一般会给状态加上"任务 ID"作为命名空间,任务结束就整体清除。
6. 实测中踩过的坑与排查思路
6.1 技能被"雪藏":模型总是不调用某个技能
这是最常见的问题。你明明注册了一个技能,描述也写了,但模型就是不用它,宁可自己瞎编答案。
排查思路按顺序来:
第一步,确认技能真的在可用清单里。有时候是动态加载逻辑把它过滤掉了,模型根本看不到。打印一下每次决策时传给模型的技能清单,一眼就能看出来。
第二步,检查描述是否覆盖了用户的表达。如果用户说"我的包裹呢",而你的技能描述是"查询订单物流信息",模型可能匹配不上。把用户可能的说法补进描述里。
第三步,看是不是被其他技能抢了。如果有个描述更宽泛的技能,模型可能优先选了它。这时候要调整描述,让目标技能更"显眼"。
第四步,检查系统提示词有没有压制。有时候系统提示词里写了"优先使用内置知识回答",模型就会倾向于不调技能。这种冲突要排查掉。
我遇到过一次特别隐蔽的:技能描述里有个错别字,导致语义偏移,模型一直匹配不上。改掉错别字就好了。所以描述写完一定要通读一遍。
6.2 参数总是填错:类型、格式、枚举的坑
模型填参数出错,通常集中在三类:
类型错误:把数字填成字符串,把布尔填成"true"字符串。解决办法是在参数 schema 里写清楚类型,并在校验层做自动转换。能转的就转,转不了的返回明确错误。
格式错误:日期格式、手机号格式、ID 格式。这类问题靠 schema 里的pattern约束,加上校验层的正则检查。错误信息里要给出正确格式的示例。
枚举错误:填了不在白名单里的值。这个前面说过,返回带选项的错误提示,让模型重选。
还有一个高频问题是参数来源错误。模型有时候会把上一轮对话里的值填到这一轮,或者把用户没提到的值自己编一个。解决办法是在参数描述里强调"必须从用户当前消息中提取,不得编造"。实测这句话能减少不少幻觉参数。
6.3 执行成功但模型理解错了结果
这个坑比较隐蔽:技能明明执行成功了,返回了正确数据,但模型基于这个数据给出了错误回复。
原因通常是返回结构对模型不友好。比如你返回一个嵌套很深的 JSON,模型可能读错层级。或者返回字段名是缩写,模型猜错含义。
解决办法是把返回结果也当成"给模型看的接口"来设计。字段名用完整单词,结构尽量扁平,关键信息放在显眼位置。如果数据量大,做一层摘要,只把模型需要的关键字段给它,细节数据放在状态里备用。
我一般会在返回结构里加一个summary字段,用一句自然语言概括结果,比如"订单 A123 已发货,预计明天送达"。模型读这句话比读一堆字段快得多,也不容易错。
6.4 技能之间的相互干扰
当技能多了之后,会出现一些奇怪的现象:调了 A 技能之后,B 技能的行为变了。这通常是共享状态被污染了。
比如两个技能都读写同一个全局缓存,A 写进去的数据被 B 读到了。或者两个技能用了同一个临时文件路径,互相覆盖。
排查这类问题,关键是给每个技能的执行环境做隔离。共享状态要么加命名空间,要么干脆每个技能独立。临时资源用完即清。如果确实需要共享,要明确约定读写规则,不能各写各的。
7. 技能体系的扩展与长期维护
7.1 技能版本管理怎么做
技能会迭代。今天query_order_status返回三个字段,明天可能要加两个。如果直接改,正在依赖旧结构的流程就会崩。
我的做法是给技能加版本号,注册表里同一个技能名可以存在多个版本。新流程用新版本,老流程继续用老版本,等老流程都迁移完了再下线旧版本。
版本号不一定要暴露给模型。模型看到的还是同一个技能名,系统在背后根据流程配置决定用哪个版本。这样对模型透明,对开发者可控。
7.2 技能的可观测性:日志、指标、追踪
技能跑在背后,出问题时如果没日志,排查就是盲人摸象。我建议每个技能执行都记录这几样:
- 调用时间、技能名、参数(脱敏后)
- 执行耗时
- 执行结果状态(成功/失败/超时)
- 失败时的错误类型
这些数据积累起来,能回答很多问题:哪个技能最常失败?哪个技能最慢?哪个技能被调用的频率异常?有了这些,优化就有方向。
如果技能之间有编排关系,还要做链路追踪。给一次任务分配一个追踪 ID,所有相关技能调用都带上这个 ID,这样能把一次任务的完整执行链路串起来看。
7.3 技能数量增长后的治理
技能从 10 个涨到 100 个,管理方式要变。
分类:按业务域分组,注册表支持按组查询。模型决策时先选组再选技能。
去重:定期审查技能描述,合并功能重叠的。两个技能做同一件事,只会让模型困惑。
下线:不再使用的技能要及时注销。留着不仅占上下文,还可能被误调用。
权限:不同技能对应不同权限级别。敏感技能只对特定角色开放,注册表的list_available里做过滤。
我见过一个项目,技能堆到两百多个,没人清理,结果模型选择准确率惨不忍睹。后来做了一轮治理,砍到八十个,准确率立刻回升。技能不是越多越好,够用且清晰才是目标。
8. 我在这套体系里最看重的几个实践
说了这么多,最后分享几个我个人在实际操作中体会最深的点,都是踩过坑之后总结出来的。
第一,技能描述值得花大力气打磨。很多人把描述当附属品,随便写两句。但实际上,描述的质量直接决定整个体系的可用性。我现在的习惯是,每写一个技能描述,都拿二十句真实用户可能说的话去测,看能不能命中。命中率不到八成,就继续改。
第二,错误信息要面向"修正"而不是"报告"。同样是失败,返回"参数错误"和返回"缺少 order_id,请从用户消息中提取后重试",效果天差地别。前者让模型卡住,后者让模型自愈。这个细节能省掉大量人工干预。
第三,写操作技能必须幂等。这是底线,不是优化项。我见过因为重试导致重复下单的真实案例,修复成本远高于一开始就做好幂等。
第四,技能不是越多越好。每加一个技能,模型的决策空间就大一分,选错的概率就高一分。加技能之前先问:这个能力能不能合并到已有技能里?能不能用参数区分而不是新建技能?能合就合。
第五,给技能体系留观测口子。日志和指标不是上线后才补的,是一开始就要设计的。没有观测,你根本不知道体系在真实环境里表现如何,优化也就无从谈起。
这套东西说起来不复杂,但真正落地时,细节决定成败。技能注册、描述打磨、参数校验、错误回传、幂等设计、可观测性,每一环都有讲究。把这几环做扎实,智能体才能真正从"会说"变成"会做",而且做得稳、做得可维护。