今年做Agent相关的项目时,我遇到一个特别尴尬的阶段:工具函数写了一堆,Agent反而开始“挑三拣四”,不是调用错工具,就是在无关场景里硬套某个函数。后来我把重心从“堆工具”转到一个我自己命名为 agent-skills 的技能库方案上,情况才慢慢好转。这个方案的核心思路很简单——把Agent能用到的能力封装成目录化、文档化、可测试的技能包,而不是继续往系统提示词里塞操作说明。这篇文章就聊聊这个方案到底解决什么问题、技能包怎么写、调度机制怎么设计,以及我在实测中踩过的坑,适合正在做Agent应用、工具链设计、或者被“工具一多就失控”困扰的朋友参考。
1. 为什么要单独建一个“技能层”:工具调用的三个典型失效场景
先说我遇到的问题。最初做Agent功能时,我按教科书的方式把所有能力都做成函数调用(Function Calling),每个函数配一段简短描述,让模型自己选择。开始时确实好用,但随着业务需求扩展,函数数量从5个涨到20多个,问题开始集中爆发。
1.1 Function Calling沦为“函数农场”的三个信号
第一个信号是误调用。有一次我写了一个“获取天气”的函数,又写了一个“查询节假日”的函数,这两者的描述里都含有“日期”这个词。结果用户问“明天适合开会吗”,Agent居然同时调用了天气和节假日接口,返回的信息七零八落。这不是模型笨,而是函数描述太薄,模型根本没有足够信息判断哪个才是真正相关的。
第二个信号是上下文污染。为了让模型知道每个函数怎么用、什么时候不要用,我不得不把更长的说明写进系统提示词。结果每轮对话都要携带一大段工具说明,Token消耗肉眼可见地上涨,而真正留给业务数据的空间却越来越少。
第三个信号是经验无法沉淀。函数是一段代码,它只能“执行”,不能“思考”,更无法承载一整套方法论。比如“做一轮竞品调研”,这不是单个API调用能搞定的,它涉及信息源选择、筛选标准、报告结构、异常处理等多个步骤。这些步骤用函数表达很别扭,用Prompt表达又容易超出窗口。
1.2 技能与工具的边界:菜谱与锅碗的关系
我后来想明白一个类比:函数是锅碗瓢盆,技能才是菜谱。
锅碗瓢盆是单次动作,切菜、翻炒、装盘,你让Agent调用它们没问题,但Agent不知道“今天要做一盘鱼香肉丝”意味着什么。菜谱则不同,它包含目标、步骤、关键火候、常见翻车点、以及最后怎么判断菜品成功。技能就是给Agent的一份菜谱。
所以在 agent-skills 体系里,我对“技能”的定义是:一段面向特定任务的方法论文本,加上配套的资源文件和可执行脚本,它们共同构成一个可复用的能力单元。技能可以调用函数,但函数不是技能。
1.3 技能层能否成立的验证标准
我在建技能库之前,先定了三条验证标准,免得自己把技能库做成另一个“函数农场”:
- 可解释性:一个技能是否能让模型明白“什么时候该用、什么时候坚决不用”。
- 可复用性:同一个技能能否在不同项目、不同Agent之间迁移,而不是绑死在某段代码里。
- 可演进性:技能的改进是否可以脱离代码发版独立进行,比如修改方法论文档后立刻生效。
如果这三条不满足,那就说明还是在用旧思路做新容器,不如回去继续堆函数。
2. agent-skills 的技能文件标准:目录、SKILL.md 与资源拆分
确定要建技能层之后,第一个要解决的就是技能包的文件结构。没有一个统一结构,技能之间就会长得各不一样,Agent无法从异构格式里稳定提取信息。
2.1 技能目录的最小可用结构
我的 agent-skills 仓库里,每个技能都遵循同一个最小目录结构:
skill-name/ ├── SKILL.md # 技能主描述,包含元信息与核心方法论 ├── scripts/ # 可执行脚本,用于稳定输出格式 │ └── main.py ├── references/ # 长文档、参考资料,按需加载 │ └── api_notes.md ├── templates/ # 输出模板,保证生成内容结构统一 │ └── report_template.md └── examples/ # 输入输出样例,作为few-shot参考 └── expected_output.json这个结构不复杂,但每层都有它的用处。SKILL.md 是入口,scripts 负责模型无法稳定复现的确定性计算,references 存放不该灌进上下文的冗长材料,templates 让输出保持一致,examples 给模型一个“标准答案”做参照。
2.2 SKILL.md 怎么写才真正被模型读懂
SKILL.md 是整个技能包的心脏。目前各家主流做法大同小异,核心是YAML格式的元信息块加上Markdown正文。我常用的模板长这样:
--- name: meeting_notes_skill description: 用于将会议原始记录整理成结构化纪要。 when_to_use: 输入包含会议转写文本、聊天记录,或需要输出结论、待办与负责人时。 when_not_to_use: 输入不包含任何会议信息,或只是一个简单问答时。 version: 1.2.0 --- # 任务目标 将原始语音转写或文本记录,转换为带结论、待办、风险与负责人的结构化纪要。 # 执行步骤 1. 提取参会人与讨论主题。 2. 按话题聚类原始内容,保留关键决策。 3. 为每个决策标记负责人与截止时间。 4. 无法确认的信息标记为“待确认”,不要自行补全。 # 注意事项 - 原始文本中经常有语气词,直接忽略。 - 如果同一个结论出现两次,合并表达,不重复记录。 - 截止时间没有明确日期时,写成“未明确”,不要用“尽快”这类模糊词。 # 输出格式 详见 templates/report_template.md,并严格按模板字段输出。这里有两个容易被忽略的细节。第一,when_not_to_use 一定要写。很多技能描述只写“能做什么”,不写“不该做什么”,Agent就会在模棱两可时倾向于误调用。第二,关于“不要自行补全”这类约束,我建议用肯定句加否定句双保险,单纯说“不要补全”效果有限,最好同时写明“标记为待确认”,模型才有替代动作。
2.3 资源文件是拉开体验差距的地方
我在接手项目时,别人给的技能只有一页说明,没有任何资源文件。看起来简洁,结果Agent每次输出格式都不一样,今天用表格,明天用列表,后天直接给我一段散文。
后来我补了几个资源文件,效果立刻不同。references 目录放的是详细的领域背景,比如复杂规则说明,这些内容只有在Agent认为必要时才被纳入上下文,平时不占窗口。templates 目录更关键,它让模型有锚点可依,输出结构至少能保持在同一个坐标系内。examples 目录则承担few-shot职责,给一个输入输出的成对样例,比在描述里写十句“要工整”都管用。
资源文件要不要拆分,取决于一个原则:高频信息留在 SKILL.md 里,低频信息移到 references 里,稳定格式交给 templates,正确示范放进 examples。这四类信息的加载成本不同,拆开之后,Agent才能按需取用。
3. 从零实现一个“调研纪要技能”:完整实操记录
空谈结构没有说服力,我拿一个实际技能举例:调研纪要整理技能。这个技能在我内部叫 research_notes_summary_skill,它的任务是把散落的访谈、网页摘录、数据片段整合成一份轻量调研报告。
3.1 先写描述再写步骤,描述决定一切
第一次写这个技能时,我直接开始写执行步骤。结果用起来很别扭,Agent经常在用户问“帮我查一下竞品定价”时,把整个调研纪要技能搬出来跑一遍,输出一堆不相关的框架。
后来我调整了写技能的顺序:先写 description,再写步骤。描述部分我拟了三个维度:
- 做什么:把多个来源的信息片段综合成一份调研纪要。
- 什么时候用:用户提供多条来源信息,或要求“汇总”“对比”“整理调研结果”。
- 什么时候不用:用户只是提出一个问题,不附带任何信息源;或者信息量极小,单独一句就能回答。
写完描述再回头看步骤,整个技能就清晰了。
3.2 方法论文本与可执行脚本的配合
调研纪要里有一项工作让我很头疼:为每条观点标注信息源的可信度。模型本身做不到精确计算,它不知道一个网站是官方渠道还是个人博客。这块我选择用脚本辅助,写一个 SourceRanker 的小工具,接收来源URL和域名,返回一个1到5的可信度评分,判断规则写死在脚本里。
#!/usr/bin/env python3 import sys import re def rank_source(url: str) -> int: domain = re.sub(r"^https?://", "", url).split("/")[0] official_domains = {"stats.gov.example", "who.int", "openai.com"} if domain in official_domains: return 5 if "edu" in domain or "ac." in domain: return 4 if "github" in domain or "arxiv.org" in domain: return 3 return 2 if __name__ == "__main__": url = sys.argv[1] if len(sys.argv) > 1 else "" print(rank_source(url))方法论文本负责告诉模型“判断维度”和“什么时候调用脚本”,脚本负责给出确定性的分数。一条信息既给了编号,又给了分数,模型再综合排序就稳定很多。
3.3 第一次实测暴露的问题:技能被调错与输出漂移
实测过程中翻车经历很有价值。第一次跑通后,我用一个包含访谈记录和网页摘要的测试集去验证,出现了两个问题。
第一个问题是技能被调错。输入里包含“请你比较一下A和B两个产品”——我的本意是用普通问答处理,但Agent把调研技能拉了出来。看日志发现,我的 description 里写了“比较”“对比”作为触发词,导致模型认为任何比较类问题都属于调研。
修正方案是给触发词加上边界条件:只有当“提供了多个信息源”时才适合本技能,并以最核心的使用条件开头。我把 description 改成“用于将多条已采集信息综合成结构化的对比纪要。适用于用户已经在输入中提供原始素材的场景”,触发词从句子开头移到了句子中段。改完之后,误调用明显减少。
第二个问题是输出漂移。同一个测试输入,第一次输出的是表格,第二次输出的是编号列表。我检查 Skill 的正文,发现里面有一句“尽量采用结构化形式输出”,这个“尽量”给了模型太多自由。我直接删掉这句,换成明确指令“输出必须使用 templates/report_template.md 规定的表格字段”。
# 输出格式 严格遵循 templates/report_template.md 中的字段顺序: 简介、主要发现、证据来源、待确认问题、下一步动作。换成“必须”之后,输出稳定性肉眼可见地提升。
4. 技能调度与多技能协作:我在组合调用中踩过的坑
技能文件写好了,技能数量一旦上去,调度问题就会浮出水面。Agent怎么从几十个技能里选中正确的那个?多个技能一起用时怎么防止上下文互相污染?这些坑,我一个个说。
4.1 技能选择的底层逻辑:不是精确匹配,而是语义相近度
先明确一件事:Agent选择技能本质上不是“查字典”式的精确匹配,而是一次语义相似度计算。模型把用户请求向量化,与所有技能的 name 和 description 做对比,然后挑相似度最高的候选。这意味着 description 里的措辞质量,直接影响技能是否被命中。
我的实测经验是,描述里动词的粒度很关键。比如“清洗数据”和“修正数据格式”,语义距离看着近,但适用场景差很多。前者更适合用统计方法处理缺失值,后者更适合统一字段格式。我会尽量把描述里的动词写具体,避免用“处理”“分析”这类万能动词。
还有一个容易被忽视的信号是技能自身的前置条件。比如我有个代码审查技能,要求用户提供代码上下文。如果描述里写明“需要项目路径或代码片段作为输入”,模型在用户没给具体代码时就会拒绝执行并追问补充信息,而不是硬着头皮把无关代码翻出来。
4.2 上下文污染:长技能文本在轮次对话里的隐形消耗
技能调度一旦启动,SKILL.md 就会被注入上下文。如果一个技能文件超过2000字,还在对话里被反复用到,那每轮对话都要携带这2000字的“固定成本”。
我自己定的规则是这样的:SKILL.md 正文控制在800字以内,凡是超过的内容都放进 references。比如技能涉及一套完整的企业内部规范,正文里只写“按 references/company_rules.md 执行”,把完整规范放到外部文件里,由脚本或者二次检索按需塞入上下文。这样既保留了必要的信息约束,又没把整个规范文本常驻在上下文里。
另外,我会把“输出格式模板”的维护频率降低,尽量不每轮都重新解释。模板在状态机里属于稳定部分,频繁变动的模板会让模型无所适从,反而增加输出随机性。
4.3 技能间依赖与串联:不要指望Agent自由发挥
技能数量多了以后,组合调用的需求自然出现。比如“周报生成”这个技能,基础信息来自“会议纪要技能”的输出,还可能需要“工时统计技能”的数据。
一开始我允许Agent自行串联,结果出现了几类问题:会议纪要技能输出的标题,周报技能无法解析;工时统计技能的日期口径两边不一致;两个技能同时工作时,上下文里的角色设定互相干扰。
解决方式是为技能声明依赖关系。在 SKILL.md 元信息里增加 requires 字段:
--- name: weekly_report_skill description: 根据会议纪要、工时数据生成周报草稿。 requires: - meeting_notes_skill - timesheet_stats_skill ---然后在主技能的方法论文本里写明“先获取 meeting_notes_skill 的结构化输出,再汇总 timesheet_stats_skill 的数据,最后套周报模板”。依赖关系显式化之后,Agent 的行为预期变得可控,不会再出现“先把会议纪要整理了一遍,又基于整理结果再整理一遍”这样的无效循环。
5. 技能仓库的治理:版本、冲突与跨项目复用
技能数量到二三十个之后,光靠脑子记已经不现实了。这里我讲一下在 agent-skills 仓库里怎么治理这些技能资产。
5.1 用Git管理技能资产:版本号与变更日志
我把每个技能当作一个可独立发版的小型组件。技能目录下的 SKILL.md 头部带 version 字段,只有当描述、步骤、模板发生实质变化时,我才会递增版本号。用语义化版本规范:主版本号在方法论彻底改变时递增,次版本号在新增场景时递增,修订号用于修正错别字或补充案例。
配套维护一个 CHANGELOG.md,每次改技能都记录“改了什么、为什么改、影响了哪些场景”。看历史记录时,价值非常大。有次我发现某技能一周前还是好用的,两个小时后失效了,翻 CHANGELOG 才发现中途改过描述里的触发词,把一个敏感条件删掉了。
5.2 技能命名冲突与仓库分层
多个技能之间最容易出现的冲突是描述重叠。比如“文档总结”和“会议总结”,看起来能共存,但模型经常搞混。我实践下来的做法是给技能名称加领域前缀,让命名空间结构化:meeting_xxx_skill、doc_xxx_skill、news_xxx_skill。这样一来,即使描述里出现重叠词,名称本身也能提供额外的区分信号。
仓库内部我还分了三个层级:
- core:经过多轮测试、稳定通用的技能,比如基础格式转换、日志解析。
- community:单项目里验证过、但还没经过跨场景测试的技能。
- personal:个人偏好或一次性场景专用的技能,不在跨项目场景里共享。
这种分层有点类似代码仓库里的主分支和功能分支,个人技能可以先在 personal 里快速试错,稳定后合入 core。
5.3 跨Agent跨项目复用的关键细节
我遇到过最典型的失败是把技能放在某个项目专用目录里,收到“代码路径写死”的错误。技能的复用性其实取决于它对项目细节的抽象程度。
跨项目复用的硬性要求有三条:
- 技能内部不得出现具体项目的绝对路径。
- 技能引用的资源文件必须都在技能目录内部,不能散落在项目根目录。
- 技能的输出模板必须自包含,不依赖项目特定的表结构。
如果某个技能在项目A里依赖了A系统的接口,我会在 references/adapters 目录下写适配器说明,把A系统的差异隔离到单独文件里,而不是直接改 SKILL.md。
实际效果是,我把“日志异常检测技能”从A项目迁到B项目时,只改了一个 adapter 文件,其余部分原样沿用。这种“一次迁移只动一个地方”的体验,让技能库真正变成了跨项目资产。
6. 给新手的入坑路线与我的长期体会
如果你现在正准备用 agent-skills 的思路搭建自己的技能库,我给几条实操建议,都是我实际走过来觉得最划算的方向。
6.1 从“先有5个技能而不是50个”开始
刚开始最忌讳求多。技能库的维护成本体现在:每一个技能都要写描述、测触发、看输出,数量上去了,维护负担是线性增长的,但调度复杂度会指数上升。我的建议是先从日常最高频的5个任务入手,比如会议纪要、信息提取、周报生成、代码审阅、日志分析。把这些做到“不需要思考就能稳定命中”的程度,再拓展数量。
6.2 我的技能迭代循环:写-测-记-删
每个技能都要经历四步循环:
- 写:先写 SKILL.md 的 description 与执行步骤。
- 测:准备2到3个正例和2个负例,验证误调用和输出稳定性。
- 记:把失败现象记进 REmark 文件,或者在 CHANGELOG 里记录调整点。
- 删:如果一个技能连续两周使用频率极低,或者总产生无关输出,就考虑删除或合并。技能不在多,在精。
这套循环看起来麻烦,但长期看特别有效。我删掉过两个触发频繁但输出无用的技能,去掉之后主Agent的响应准确率反而上升了。
6.3 三个小技巧
最后分享三个平时不太会写进文档、但实测特别有用的小技巧。
第一个,description 的开头第一句决定命中概率。模型在做语义匹配时,句子开头的权重往往更大。所以把最核心的使用场景写进第一句,不要在开头写“这是一个用于……的工具”这类废话。
第二个,给技能加“验收样例”。在 examples 目录里放一份标准的“输入输出对”,当技能行为出现漂移时,拿样例做回归测试,马上能看出是哪一步变了。我通常把这份样例命名为 gold_test_case.json,每个技能必须保留。
第三个,把失败案例写进 SKILL.md 的注意事项。Agent运行中出现的典型错误,比如“日期缺失时不要默认填今天”,直接写进正文。这样系统提示词没覆盖到的边界情况,会在技能层补齐。文本只会在该技能被调用时进入上下文,也不会挤占主线对话窗口。
我自己的体会是,模型能力越来越强的今天,很多人把Agent项目的成败押在提示词工程上,却忽略了更底层的“能力资产化”问题。你对付出的每一次技能整理,本质上都是在给Agent沉淀一套可迁移、可测试、可演进的操作手册。我踩过工具一多就失控的坑,也体会过技能库稳定命中带来的踏实感。如果你也正被“工具函数一堆但效果稀碎”困扰,不妨从三五个技能包开始,给自己建一个 agent-skills 体系,这条路值得走。