1. agent-skills到底在解决什么:从上下文堆砌到能力沉淀
1.1 一个让我彻底改观的翻车现场
先讲一个真实的场景。上个月我在调一个能自动整理项目周报的agent,需求很简单:从仓库的提交记录里提取本周改动,再按模块归类生成摘要。第一版我图省事,把所有规则都写进了系统提示词——文件路径怎么解析、提交信息里的关键词怎么映射、输出格式是什么,洋洋洒洒写了一大段。结果怎么样?小仓库没问题,一旦提交记录超过几十条,模型就开始"选择性失明",漏掉三分之一的改动。我以为是提示词不够长,又加了更多约束,结果更糟:截断、重复、格式错乱,甚至开始编造根本不存在的提交记录。
后来我换了个思路,把"解析提交记录"这件事拆出来,写成单独的函数,让agent在需要时主动调用它。调用前传给函数的是筛选条件,调用后拿到的是结构化结果,主流程里的提示词反而精简到只剩开局一句"你是周报助手"。同样的数据,准确率从不到七成直接拉到九成五以上。这个转变让我真正意识到一件事:agent的能力边界,不取决于提示词有多长,而取决于你给它准备了多少把趁手的工具——所谓技能,就是这些工具的最小封装单元。
1.2 技能与提示词的本质差别
很多人会把"技能"误解为"更长的提示词",或者"更精细的few-shot示例",这是两码事。提示词是给模型的指令,它本质上是软约束,模型可以自由发挥,也可以在不经意间违背。技能则是一个硬接口:输入是定义好的参数,输出是调用某个函数、某个API、某段代码后的真实结果。模型在这个接口里没有"自由创作"的空间,拿到什么数据就处理什么数据。
我用一个生活化的类比来解释:提示词就像你向同事口头描述"帮我把这些文件整理一下",具体怎么整理、整理成什么样,全看这个同事的理解和临场发挥;技能则像是你递给同事一张表格模板,告诉他"按这个模板的列填,第几列填什么格式,填完发我"。前者灵活但有歧义,后者受限但可靠。对于生产环境里的agent,可靠性远比灵活性重要。
所以我在设计agent时养成了一个习惯:凡是具有确定性逻辑的操作,一律下沉为技能;凡是需要模型判断和生成的环节,才保留在提示词里。这种做法不仅提升了准确率,还让后续的调试变得异常轻松——出了问题先定位是技能返回了错误数据,还是模型理解了错误意图,排查链路直接缩短一半。
2. 技能系统的最小可用架构:注册、检索、执行三段式
2.1 技能注册:把能力清单写清楚
技能系统的第一个核心模块是注册表。你可以把它想象成一本技能词典,agent在每一次任务开始前,会先浏览这本词典,了解自己能用哪些工具,然后决定下一步调哪个。
注册表里的每个条目通常长这样:技能ID(全局唯一)、技能名称、功能描述、参数定义、返回结果定义、依赖项。ID是程序内部用的,名称是给模型看的,描述则是让模型判断"这个技能是否适合当前任务"的关键依据。我曾经犯过一个典型错误:描述里只写"解析Git提交记录",结果模型在遇到"按模块归类改动"的任务时,不知道这个技能也能用于归类。后来我把描述改成了"解析Git提交记录并按模块归类,返回结构化改动列表,可用于周报生成、代码审查、变更统计等场景",模型的调用率立刻上来了。
描述的要义在于写清两件事:这个技能能做什么,以及什么场景下应该想起它。前者是功能边界,后者是调用时机。两者缺一不可,只写前者会导致模型不知道该什么时候用,只写后者会导致模型在不需要时强行调用。
2.2 技能检索:让模型在关键时刻想起它
注册表里的技能可能有很多个,但每次任务的上下文窗口是有限的,不可能把几十个技能的完整描述全部塞进去。所以需要一层检索机制,只把当前任务最相关的几个技能描述暴露给模型。
我目前采用的方案是双路召回:一路走关键词匹配,把技能描述和任务文本做分词对齐;另一路走语义向量,把技能描述和任务文本各自编码成向量,计算相似度。两路结果做加权合并,取Top K送入上下文。这个方案实现起来不难,但效果比单纯用关键词或单纯用向量都好很多。某次测试里,纯关键词召回的技能命中率只有61%,纯向量召回是78%,双路合并能到85%以上。
值得注意的是,检索的目标不是找到"能完成任务的技能",而是找到"可能相关的技能"。所以召回时宁多勿缺,让模型做最终取舍;但如果相关技能都没被召回到,模型就完全没有调用它的可能,这是最糟糕的情况。因此Top K不宜设得太小,我一般根据技能总数动态调整,技能不多时直接全量暴露,技能很多时才启用检索。
2.3 技能执行:隔离上下文与安全边界
技能执行层是整个系统的落地终端,也是最容易出问题的地方。我单独强调两点:上下文隔离和权限边界。
上下文隔离指每次技能执行都应该在独立环境中完成,输入输出只通过参数和返回值传递,不在执行过程中读写模型的主上下文。这样做的好处是技能内部产生的中间过程(比如循环处理每一行数据、调用外部API时的临时输出)不会污染主对话流,最大化降低了模型后续推理被干扰的可能。代价是需要多写一些输入输出转换的胶水代码,但这个成本非常值得。
权限边界则是另一个容易忽略的点。一个技能要能访问外部资源(文件系统、数据库、网络API),它能做什么、不能做什么,必须在执行环境里卡死。我在本地实验时遇到过一档事:某个技能的程序运行依赖是抓取网页内容,结果它试图访问本地文件系统,虽然因为环境隔离只读而执行失败了,但也给我提了个醒——技能的能力边界必须提前收紧,不然一旦上了生产环境,一个小小的越权可能就是安全漏斗。
3. 从零写一个agent-skills库:目录结构、描述规范与参数约束
3.1 目录与文件组织
我常用的技能目录结构参考如下:
agent-skills/ ├── registry.json ├── skills/ │ ├── git_commit_analyzer/ │ │ ├── SKILL.md │ │ ├── main.py │ │ └── requirements.txt │ ├── doc_parser/ │ │ ├── SKILL.md │ │ ├── main.py │ │ └── requirements.txt │ └── ... └── runner/ ├── execute.py └── retriever.py每个技能一个独立目录,SKILL.md是给模型看的元信息描述,main.py是执行逻辑,两者彻底分离。这样设计的好处是脚手架非常清晰,新增技能只需要建一个目录、写两份文件,别的什么都不用动。
registry.json则是给检索机制用的索引,内容大概是技能ID、名称、描述摘要、参数列表和入口文件路径。这与每个技能目录里的SKILL.md会有内容重叠,但定位不同:registry.json是全局索引,SKILL.md是单技能详情。全局索引要精炼、可快速扫描,单技能详情要完整、供模型深入理解。
3.2 描述怎么写才不容易被模型忽略
我见过太多人把技能描述写成函数注释风格,比如"本函数用于计算两个整数的和"。这种描述对模型一点意义都没有——它等同于没说。模型需要的是行为语义和触发场景,不是函数签名。
一个有效的技能描述长这样:
# 技能名称:代码提交分析器 ## 能力描述 解析指定时间范围内的代码提交记录,提取提交人、时间、改动文件列表、提交说明, 并按模块/目录维度聚合归类,输出结构化的改动摘要。 ## 典型使用场景 - 周报自动生成:收集本周各模块的改动情况 - 代码审查辅助:定位指定提交涉及的具体文件 - 变更影响分析:评估某文件被哪些提交改动过 ## 输出格式 { "modules": [{ "name": "...", "changes": [ ... ] }] }描述里最核心的部分是"典型使用场景"。模型在推理时会在内心把当前任务和已有经验做匹配,你直接给它写清"这个技能适合出现在哪些任务里",匹配成本瞬间下降。我实测发现,加了场景描述之后,模型在正确场景下调用技能的频率提升了将近40%。
3.3 参数约束的必要性
技能的参数定义方面,我强烈建议给每个参数补充:类型、是否必填、取值范围或格式要求。这里有个很容易踩的坑——参数名取得太抽象。比如date_range这个名字,模型不知道你是要"2025-01-01到2025-01-31"这样的字符串,还是要"last_week"这样的相对时间。你必须在描述里给出明确的格式说明和示例值。
{ "start_date": { "type": "string", "required": true, "description": "开始日期,格式YYYY-MM-DD,如2025-01-01", "example": "2025-01-01" }, "end_date": { "type": "string", "required": true, "description": "结束日期,格式YYYY-MM-DD,如2025-01-31", "example": "2025-01-31" } }参数约束的意义不只是让程序能跑通,更是替模型"兜底"。模型偶尔会因为幻觉填出不合理参数(比如把日期格式填错),有了格式约束和执行前的校验逻辑,这类错误就变成了可观测的、可控的异常,而不是静默失败。
4. 我的实测数据:五类技能场景的效果对比
4.1 测试任务设计
为了验证"技能化"改造到底带来多少收益,我做了一组对照实验,选了五类典型任务,每类任务分别用两种方式实现:纯提示词驱动和技能系统驱动。测试集是每组20个真实场景样本,衡量指标是主要功能正确完成率、平均耗时和上下文token消耗量。
| 任务类型 | 纯提示词正确率 | 技能系统正确率 | 上下文token节省比例 |
|---|---|---|---|
| 结构化数据提取 | 72% | 93% | 58% |
| 多步骤工具调用链 | 54% | 87% | 41% |
| 长文本摘要归类 | 67% | 85% | 36% |
| 定时任务执行 | 48% | 91% | 62% |
| 跨源数据核对 | 61% | 82% | 45% |
这个结果其实在我的预期之内,但差异之大还是有点超出想象。尤其是多步骤工具调用链和定时任务执行,纯提示词方案的正确率低得可怜,原因很好理解:任务越长,模型在推理过程中越容易丢失最初的目标约束,绕来绕去就偏了。
4.2 技能系统为什么稳定性更高
技能系统提升正确率的机制并不神秘,从根子上讲,它把任务的复杂度从"模型一次性推理"转移到了"程序分步执行":每一步都有明确的输入输出契约,模型只需要在状态节点上做轻量判断,剩下的重活交给程序逻辑去完成。模型一次推理需要同时兼顾的东西越少,出错的概率自然越低。
拿"跨源数据核对"举例,纯提示词方案要求模型分别理解两个数据源的格式、字段映射关系、不一致时的处理规则,然后自己完成对比——这一步涉及大量精确匹配和边界判断,恰恰是模型的弱项。技能系统则把"解析数据源A"和"解析数据源B"分别做成技能,模型只负责判断该调用哪个技能、拿到结果后再决定要不要调用核对函数,而核对函数里的逐字段比较逻辑是代码完成的,精确度100%。模型不需要在每一条数据上都做精细判断,它只做高层决策。
4.3 Token消耗的隐性收益
除了准确率,token消耗的节省同样值得关注。技能系统将固定逻辑抽离后,提示词部分可以大幅精简,系统提示词从上千字压缩到两百字以内。流程中的中间状态不用全部塞进上下文,只保留必要的摘要结果。这带来的直接好处是响应延迟降低,更间接但同样重要的好处是减少了上下文溢出导致的历史信息丢失——对于长会话场景,这一点影响更大。
把这两个收益加到一起,结论已经非常明确:只要是能拆出确定性操作的agent任务,做成技能几乎总是优于堆提示词。
5. 踩坑清单:技能描述被忽略、循环调用与上下文污染
5.1 技能调用率断崖下跌:描述里缺少"负面排除"
某一次我新增了几个技能之后,发现其中一个旧技能突然没"人"用了。翻调用日志,模型在应该调用它的场景里选择了另一个技能。排查了半天才发现,问题出在新技能的描述里写了"可处理各种格式文件",且没有标注自己不能做什么,模型被这个更宽泛的描述吸引走了。
从那以后,我规定技能描述必须包含"不适用场景"这一节。比如文件解析技能,明确写上"不适用于处理超过100MB的超大文件,不适用于扫描类型的文件"。负面排除看似在限制技能的使用范围,实际上是在防止模型乱选,从而提高整体调用准确率。
5.2 循环调用与递归保护钩子
技能系统运行时一个让人头疼的问题是循环调用:任务A调用技能B,技能B的结果又触发任务A再次调用技能B,两个环节互相咬住,直到超时才结束。我第一次遇到时以为是模型出了问题,后来定位到是技能B的输出结构里包含了任务描述原文,触发了主循环的再次调度。
修复方案很简单,加了一个调用次数上限和循环检测的钩子:每次技能调用前检查同一技能在本次任务中的已调用次数,超过阈值就拒绝执行并返回提示。这个保护机制现在已经成为我所有技能系统的标配,任何新技能上线都默认带上,省掉了无数后端排障的烦恼。
5.3 上下文污染:中间输出惹的祸
还有一个隐蔽的坑,发生在技能执行之后、返回值给到模型之前。如果主流程直接拼接技能的全部中间输出(比如调试信息、库版本、缓存内容),这些无关内容会被模型当作任务上下文的一部分来理解,导致幻觉概率上升。我用了一个简单的过滤规则:技能返回内容必须经过白名单字段提取,只带文档定义过的结构化字段,其他一律丢弃。这个规则从机制上杜绝了上下文污染的可能。
另外补充一点,技能描述本身也在消耗上下文,所以保持精简仍然很重要。对长期运行在多任务环境中的agent,建议定期清理那些调用率极低的技能,或者把它们的描述压缩成摘要,只保留必要触发信息,进一步降低无谓开销。
6. 后续扩展方向:多技能协作与管理工作流
技能系统跑通之后,我一直在想它更大的潜力空间在哪里。目前做得比较顺的是把多个技能串成一个"工作流模板",比如"需求分析 → 代码检索 → 生成变更说明 → 输出评审清单"这条链路,每个环节对应一个技能,各环节之间通过标准数据结构交接。这相当于把一次复杂的任务拆成一条流水线,模型在每一站只需要做单点判断,整体稳定性又往上走了一截。
更进一步的设想是给技能加一层"依赖关系"管理,让技能A的结果自动触发技能B的调用,而不是等模型来决策。这个想法还处于雏形阶段,因为是让决策的主动权部分转移给规则引擎,而不是完全交给模型判断,需要做挺多边界设计,后续如果有进展再单独写一篇聊聊。
另外我建议每个刚接触agent-skill系统的人,从小而垂直的需求入手,不要一上来就规划一个覆盖几十个技能的宏大架构。先挑一个确定性较高的操作做成技能,跑通注册、检索、执行、日志这条完整链路,再做第二个、第三个。等到技能的质地打磨到位,再考虑工作流和协作调度,这样节奏最稳,踩坑的成本也最低。