最近半年一直在折腾 AI Agent 的工程化落地,agent-skills这个词几乎每天都会出现在我的工作清单里。GitHub 上拿这个名字命名的仓库越开越多,行业讨论里也频繁把“技能库”“技能编排”挂在嘴边。这个词本身不算新概念,但它背后指向一个实打实的变化:AI Agent 的能力不再是一次性的提示词,也不是临时接一个函数,而是逐步沉淀成一套可复用、可测试、可组合、可版本化的能力模块。这篇文章算是我自己在这条路上的一份工程笔记,聊聊我理解的 Agent Skills 到底是什么、怎么设计、踩过哪些坑。适合正准备做 Agent 能力封装,或者已经在维护 agent 工具链的工程同学参考。
1. 先搞清楚 Agent Skills 到底在解决什么问题
1.1 从 Tools 到 Skills,中间隔了一整个工程化思路
agent-skills这类项目之所以能被单独拎出来讨论,是因为它把“Agent 会干什么”这件事从一个运行时问题,变成了一个研发管理问题。
最早大家用 Function Calling,给模型挂几个函数,比如查天气、算个算术。这个阶段本质是把“模型不会的、或者容易算错的事情”外包给确定性代码。后来发现函数越挂越多,模型选择函数的准确率开始下降,而且函数只是单一动作,没法表达“先做 A,再校验 B,失败就降级到 C”这种完整流程。
Skills 就是把这一层补齐了。一个 Skill 不再是“单个动作”,而是一个“完整的做事方法”:它包含使用说明、输入输出约定、执行脚本、校验规则、异常回退,甚至还有配套的测试用例。模型不再是简单地调用一个工具,而是像拿到一份带 SOP 的工单,按步骤把事情办完。
我理解的核心差异在于:Tools 解决的是“Agent 能调用外部能力”,Skills 解决的是“Agent 能稳定地把一件事做好”。前者解决有没有,后者解决好不好、稳不稳、能不能复用。
1.2 为什么这个时间点突然需要单独搞一套 Skill 体系
说白了,是因为 Agent 应用开始从 Demo 往生产环境走了。Demo 阶段一段提示词写死就行,生产环境不行,生产环境要回答几个非常现实的问题:
- 同一个任务,十个不同会话里做出来的结果能不能基本一致?
- 这个任务消耗多少 Token,能不能把成本压下来?
- Agent 学过的能力怎么沉淀,换一个人接手还能不能用?
- 某个能力改坏了,怎么发现?怎么回滚?
Skills 就是把这些问题变成可管理对象的载体。一个 Skill 是一个文件夹,里面有说明、有代码、有测试、有版本号。它跟普通代码库的管理方式几乎一模一样,只是运行的时候由模型来调度。我见过不少团队把 skills 目录放在独立仓库里,用常规的 Code Review 流程去审每一个 skill 的改动,效果非常好。把 Agent 的能力当成代码来治理,这才是 agent-skills 类项目真正带来的价值。
2. Skills 和 Tools 的区别,以及一个 Skill 的最小可用结构
2.1 打个比方:Tools 是刀,Skills 是菜谱
把 Tools 和 Skills 的关系想清楚,后面的设计才不会跑偏。
刀具是工具,它能切菜、能剁骨,但它不知道怎么配菜、不知道火候。菜谱是完整的方法论,它规定了你需要哪些刀具、按什么顺序处理食材、出现什么情况用什么替代方案。模型本身是厨师,它读菜谱、拿刀具、最终出菜。
这个比方能解释很多实际问题。比如为什么有的团队函数一堆,但 Agent 还是干不好活?因为只有刀,没有菜谱,厨师只能靠灵感自由发挥。为什么 Skills 体系里的单个脚本往往都很简单?因为复杂度被流程和说明吃掉了,脚本只需要把确定性的那部分算对。
我在实际项目里踩过一个大坑:一开始把所有能力都往 tool 里塞,结果模型的选择准确率越来越差,因为工具数量超过二三十个之后,光靠 description 区分彼此的边界就变得很困难。后来改成 Skills 体系,把多个工具调用、校验、降级打包进一个 skill,模型只需要在“十几个 skill”里做选择,而不是在“几十个函数”里做选择,准确率一下子就上来了。
2.2 一个 Skill 的标准解剖结构
参考社区里常见的 agent-skills 项目布局,一个标准 skill 通常是这样的目录结构:
skills/ ├── changelog_generator/ │ ├── SKILL.md │ ├── scripts/ │ │ ├── parse_diff.py │ │ └── classify_changes.py │ ├── requirements.txt │ └── tests/ │ ├── golden_001.json │ └── golden_002.json ├── document_summarizer/ │ └── SKILL.md └── registry.yaml核心文件是SKILL.md,它决定了模型能不能理解、会不会在正确时机调用这个 skill。我常用的结构是 YAML frontmatter 加正文:
--- name: changelog_generator description: 根据 git diff 生成结构化变更说明,适用于 commit message、release note 场景 version: 1.0.0 allowed-tools: [run_command, read_file, write_file] tags: [git, changelog, docs] ---正文部分才是灵魂,它告诉模型什么时候用、怎么用、不能用在哪:
- 适用场景:拿到 git diff、需要产出变更说明时使用。
- 不适用场景:用户只是想看 diff 原始内容时,不要使用。
- 执行步骤:先校验输入是否为空;再统计变更文件和增删行数;按文件类型做归类;最后按指定风格生成输出。
- 输出格式:严格输出 JSON 对象,包含 summary、categories、risk_items 三个字段。
- 失败处理:diff 超过 5000 行时先输出文件级摘要;任何步骤失败都返回文件列表而不是报错退出。
scripts 目录里放的是确定性逻辑,比如解析 diff 文本、统计行数、按文件后缀归类。这些部分绝对不能交给模型现算,不然 token 成本和出错率都压不住。tests 目录放 golden test,每个测试包含输入样本和期望输出,改完 skill 跑一遍就知道有没有破坏旧行为。
2.3 什么样的 Skill 才算合格
跟代码一样,能跑的 skill 和合格的 skill 之间差距很大。我在评审 skill 的时候会按这几条标准过:
- 描述是“给模型看的”,不是“给人看的”。不要写“强大的变更说明生成器,提升团队协作效率”,要写“当输入包含 git diff 文本、且需要生成提交信息或发布说明时使用”。判断标准是:把 description 随机塞给一个没看过代码的工程师,他也能判断什么时候该调。
- 输入输出边界清晰。每个字段有没有类型、有没有必填、有没有取值范围,都要明确。最忌讳的是“你看着办”式的字段。
- 步骤是正面指令。少用“不要做什么”,多用“应该做什么”。模型对否定指令的执行效果远差于肯定指令,这个在 skill 提示词里体现得特别明显。
- 有兜底路径。任何 skill 都必须回答一个问题:输入不符合预期时怎么办。是报错、降级、还是给默认结果,必须写死。
- 有测试用例。没有测试的 skill 一旦改坏,你往往要等到生产环境被坑了才发现。
3. 实操一个能落地的 Skill:代码变更说明生成器
光讲概念没意思,我拿最近实际整理的一个 skill 当例子,完整走一遍设计到验收的流程。选它的原因很朴素:频率高、输入清晰、确定性和大模型能力各占一半,非常适合当模板。
3.1 需求拆解:哪些交给代码,哪些交给模型
需求一句话就能说清:给一段 git diff,产出一份人话版本的结构化变更说明。
但拆开看,里面混着三种不同类型的任务:
- 纯计算:统计变更文件数、增删行数、涉及哪些目录。这类必须交给脚本,模型算不准,还费 token。
- 分类判断:这次变更是修 bug、加功能、还是改文档。这类可以用启发式规则按文件后缀和 diff 关键词粗分,再用模型做二次确认。
- 文案生成:把变更内容组织成 commit message 或 release note。这类是模型的强项,交给模型。
拆完边界,设计原则就出来了:脚本负责“算得准”,模型负责“写得好”,谁也不越界。
3.2 接口合约:先把输入输出钉死
设计 skill 的第一件事不是写提示词,而是定接口。我当时的合约长这样:
{ "input": { "diff_text": {"type": "string", "required": true}, "style": {"type": "string", "enum": ["concise", "detailed", "conventional"], "default": "concise"}, "locale": {"type": "string", "enum": ["zh", "en"], "default": "zh"} }, "output": { "summary": "string", "categories": ["feat", "fix", "docs", "refactor", "test"], "risk_items": ["string"], "stats": {"files_changed": "int", "insertions": "int", "deletions": "int"} }, "errors": ["EMPTY_DIFF", "DIFF_TOO_LARGE", "PARSE_FAILED"] }这里有几个容易被忽略的设计点:
第一,错误码一定要单独定义。不要返回“处理失败”这种笼统信息,EMPTY_DIFF和PARSE_FAILED的后续处理策略完全不同。前者说明调用方给错了输入,后者说明脚本解析逻辑有 bug 或者 diff 格式太怪。
第二,enum 字段能省掉大量解释成本。模型不用猜“style”该填什么值,三选一,出错概率直线下降。
第三,stats 字段放在 output 里很有用。模型生成文案时可以直接引用“变更了 23 个文件、新增 180 行、删除 42 行”,比自己数准确得多,而且这些数字天然适合放在 commit message 的正文里。
3.3 实现骨架:SKILL.md 加脚本怎么配合
SKILL.md 的执行步骤我写得很直白,尽量避免让模型做任何计算类工作:
- 检查 diff_text 是否为空,为空直接返回
EMPTY_DIFF。 - 用
parse_diff.py解析出文件列表、增删行数、目录分布,产出中间 JSON。 - 用
classify_changes.py按“文件后缀 + diff 内关键词”做一次粗分类,产出候选类别列表。 - 模型基于中间结果,按 style 和 locale 生成 summary 和 risk_items。
- 校验输出是否符合合约,不符合就再生成一次,最多重试两次。
- 如果 diff 超过 5000 行,跳过细节解析,只输出文件级统计和目录分布。
第 4 步是唯一需要模型发挥的地方。我在这步的提示词里刻意告诉模型:“summary 要基于 stats 字段写,不要自己从 diff 里重新数数字。”这样就把模型的幻觉空间压缩到最小。
脚本部分没什么花活,核心逻辑就是正则加统计。一个小提示:diff 文本里二进制文件、 renaming(重命名)、纯文档改动,这三类在解析时最容易出错。二进制文件没有统一的文本 diff,rename 在 git 里默认可能是 delete 加 add,文档改动则容易误判成代码重构。我的脚本里专门处理这三种情况,分类时不会把.md文件的修改归到feat里。
调用方式上,接入 agent 框架之后大概是这种感觉:
result = agent.execute_skill( "changelog_generator", input={ "diff_text": git_diff_string, "style": "conventional", "locale": "zh", }, )3.4 测试与验收:没有 golden test 等于裸奔
这个 skill 的测试集我按场景分成了几类,每个都对应一个真实踩过的坑:
| 测试场景 | 输入特征 | 期望行为 |
|---|---|---|
| 空输入 | diff_text 为空字符串 | 返回EMPTY_DIFF错误码 |
| 超大 diff | 超过 5000 行的合并请求 | 降级为文件级摘要,不卡死 |
| 纯文档改动 | 只有 .md 文件变更 | categories 只含 docs |
| 二进制文件 | diff 中出现Binary files differ | 跳过解析,计入文件数统计 |
| 重命名操作 | 文件被 git mv | 识别为 rename,不计入新增删除 |
| 乱码输入 | 非 UTF-8 编码文本 | 返回PARSE_FAILED,不抛异常 |
验收标准我也定得很量化:golden 集上成功率不低于 95%;单次调用模型 token 消耗不超过 2500;全流程耗时不超过 15 秒。这三个指标分别对应质量、成本、体验,任何一个不达标都说明设计有问题,得回去拆步骤。
4. 常见问题与排查技巧实录
4.1 模型死活不调用 Skill,问题十有八九出在 description 上
这类问题最多。模型就像个实习生,它只会根据 skill 描述判断“这个活归不归我管”。描述写得太宽泛,比如“生成高质量变更说明,为团队提供支持”,模型找个场景就硬套;写得太窄,比如“处理 git diff”,模型遇到复杂点的需求就不敢调。
我的排查习惯是先把 description 改成下面这种句式:触发条件 + 输入形式 + 输出内容 + 明确的反例。比如:
当输入包含 git diff 文本、且需要生成 commit message、发布说明或变更摘要时使用。输入必须为完整 diff 字符串,输出为结构化 JSON。如果用户只想查看 diff 原始内容,不要使用此 skill。
改完这句话,调用率直接翻倍的情况我见过不止一次。改完之后一定要抽样测试,拿历史对话记录问一遍“这里应该调用哪个 skill”,统计一下命中率再上线。
4.2 技能跑飞了,先检查确定性边界是不是被模型占了
有次我做一个统计类 skill,发现每次输出数字都对不上。查了半天发现问题出在提示词上:我让模型直接“分析 diff 中的变更规模”,模型就真的开始自由发挥估算,完全没用脚本统计的结果。
这类问题的根源都一样:把可以写成代码的部分交给了模型。排查思路很直接——把输出里所有带数字、带状态的字段列出来,问自己一句“这个值能用脚本算出来吗?”。能,就坚决写在 scripts 里,模型只能引用结果,不能参与计算。这是 skill 设计里性价比最高的一条规则。
4.3 改一行提示词,炸了一片旧用例
skills 最容易被忽视的坑是没有回归测试。上个月我调了 summary 风格的提示词,测试里没覆盖这个字段,结果线上输出的 JSON 结构变了,下游解析直接挂了一批任务。后来我把那次教训固化成流程:每次改SKILL.md,必须跑一遍tests/目录下的 golden tests;如果行为是有意变更的,先更新测试再改提示词,顺序不能反。
4.4 技能环境脏了,装个隔离层才省心
skill 的脚本依赖环境跟主 agent 环境混在一起,是最容易埋雷的地方。有的 skill 要 pandas,有的要网络请求库,装在一起出现版本冲突,跑起来就是各种 ImportError。我现在每个 skill 都自带requirements.txt,重依赖的 skill 单独建虚拟环境或者容器执行。轻量原则也很重要:能用标准库解决的,绝对不引第三方库。很多 skill 的核心逻辑就是字符串处理,一个re模块就够,硬上框架纯属给自己找麻烦。
顺手整理了一张问题速查表,日常排查基本够用:
| 现象 | 可能原因 | 排查方向 |
|---|---|---|
| 模型频繁调用错误 skill | description 边界不清 | 重写描述为“触发条件+输入+输出+反例” |
| 输出数字不准 | 模型参与了计算 | 把计算逻辑搬到脚本里 |
| 改一次旧能力就坏 | 缺少 golden test | 补测试,先改测试后改实现 |
| 脚本 ImportError | 依赖冲突 | 独立虚拟环境,按锁文件安装 |
| skill 静默失败 | 错误处理是空响应 | 定义错误码,明确降级路径 |
| 调用很慢 | 步骤里有多余的模型往返 | 精简提示词,能一步算完的别分两步 |
5. 一些个人的实操体会
做agent-skills这类项目,我最大的感受是:它看起来是在写提示词、写脚本,本质上是在做产品的流程梳理。
最开始我犯过贪多的毛病,一口气设计了二十多个 skill,结果大部分躺在仓库里吃灰。后来我改成只围绕高频任务做,一周出现三次以上的操作才值得封装成 skill。一个 skill 从设计到测试,成本不低,如果一周用不上几次,性价比根本划不来。
第二个体会是SKILL.md的文本质量,直接决定整个体系的成败。我会先用模型帮我起草一版 description 和执行步骤,然后逐字手工改。模型起草能把常见路径写全,但边界条件、反例、异常处理这些必须人肉补。倒不是模型想不到,而是它不会站在“这个 skill 会被不同人、不同上下文反复调用”的角度去写,这部分只能靠经验补。
第三个体会是把 skill 当代码管,而不是当文档管。版本号、changelog、code review、测试用例,一样都不能少。我现在的做法是每个 skill 文件夹里自带一份简短 changelog,记录行为变更,配合 git 历史,谁改了什么一目了然;上线前算三个指标:调用次数、成功率、每次成功调用的 token 成本。指标不达标就不放量,这比靠感觉判断靠得住得多。
另外一个很实用的细节:skill 命名和文件命名我也定了统一的规范。目录名用蛇形命名,name 字段跟目录保持一致,description 首句控制在 60 字以内。这些看着是小规矩,但当 skill 数量过了十个,模型描述检索的准确率就跟这些细节强相关,命名乱的体系,命中的表现一定差。
如果你正准备搭自己的 skills 能力库,我最后的建议是从两三个最高频的轻量任务开始跑通全部流程,再把经验复制到更多技能上。流程通了,后面加技能只是复制粘贴加调整的事;流程没通,技能越多,维护越痛苦。