1. 从"agent-skills"说起:一个被低估的工程化命题
第一次看到agent-skills这个词,很多人会下意识地把它理解成"给 AI 智能体写提示词"。这个理解不算错,但太浅了。真正在项目里折腾过 AI coding agents 的人会明白,agent-skills本质上是一套可复用、可组合、可测试的能力封装体系——它把"让 AI 干某件事"从一次性的对话,变成了一份可以进版本库、可以被 review、可以被回归测试的工程资产。
我最初接触这个概念,是在给团队搭 Claude Code 工作流的时候。当时我们面临一个很现实的问题:同一个"生成单元测试"的需求,张三写一段提示词,李四写一段提示词,王五又抄了一份改改,结果三个人产出的测试风格、覆盖率、边界处理完全不一样。更麻烦的是,模型一升级,之前调好的提示词可能就失效了,但没人知道是哪一条失效了,因为根本没有测试。
agent-skills要解决的就是这个问题。它把每一个"技能"(skill)定义成一个独立的、有明确输入输出的单元,配上描述、示例、约束条件,甚至配上测试用例。这样做的直接好处是:技能可以被版本管理,可以被 CI 跑测试,可以在不同 agent 之间迁移,也可以被非原作者维护。
这篇文章适合三类人看:第一类是把 Claude Code 当日常工具、但还没系统化组织自己提示词的开发者;第二类是团队里负责搭建 AI 编码基础设施的工程师;第三类是对 test-driven-development 在 AI 场景下如何落地感兴趣的技术负责人。我会从设计思路讲到实操细节,再讲到踩过的坑,尽量把"为什么这么设计"讲透,而不是只丢一堆配置让你抄。
需要提前说明的是,下面涉及的具体目录结构、CLI 用法、测试组织方式,一部分来自公开的工程实践惯例,一部分是我在实际项目里验证过的方案。不同团队的工具链不一样,你可以按自己的情况裁剪,但底层的设计逻辑是通用的。
2. agent-skills 的整体设计与思路拆解
2.1 为什么不是"提示词库"而是"技能库"
很多人第一反应是搞一个提示词仓库,按目录分类,用的时候复制粘贴。这个方案在个人使用阶段没问题,但一旦进入团队协作就会崩。原因有三个。
第一,提示词是文本,技能是契约。一段提示词只描述了"我希望 AI 做什么",但没有描述"什么算做对了"。技能必须包含验收标准,否则你无法判断一次调用是成功还是失败。这就是为什么agent-skills和 test-driven-development 天然绑定——TDD 的核心不是"先写测试",而是"先把验收标准显式化"。
第二,提示词是扁平的,技能是可组合的。一个真实的编码任务往往需要多个能力串联:先读代码理解上下文,再定位要改的文件,再生成补丁,再跑测试,再根据失败信息修复。如果每个环节都是一段独立提示词,串联逻辑就散落在调用方代码里,无法复用。技能库的设计要求每个技能声明自己的输入输出类型,这样组合才有依据。
第三,提示词无法回归测试。模型升级、上下文长度变化、工具调用格式调整,都会影响输出。没有测试,你只能靠人肉抽查。技能库把每个技能配上测试用例,就能在模型或工具链变更时快速定位回归点。
所以agent-skills的第一个设计决策就是:每个技能是一个带元数据的目录,而不是一段文本。
2.2 技能的最小构成单元
一个合格的技能目录,我通常会包含这几个部分:
SKILL.md:技能的主描述文件,包含名称、用途、适用场景、输入输出说明、约束条件。examples/:至少两个正例和一个反例。反例特别重要,它告诉 agent"什么情况下不要用这个技能"。tests/:测试用例,可以是输入输出对,也可以是断言脚本。scripts/(可选):如果技能需要执行确定性操作(比如格式化、文件扫描),把脚本放这里,让 agent 调用脚本而不是自己生成代码。metadata.json:机器可读的元数据,包含版本、依赖、兼容的 agent 类型等。
这个结构看起来有点像 npm 包或者 Python 包,这不是巧合。技能本质上就是给 agent 用的库,只不过调用方从人类程序员变成了 AI。既然是库,就得有版本、有依赖、有测试、有文档。
我见过一些团队把技能写成单个 markdown 文件,几百行堆在一起。这种写法在技能数量少于 5 个时还能忍,超过 10 个就彻底失控了。因为 agent 在检索技能时,需要的是精准匹配,而不是读一篇长文。目录化 + 元数据化,才能让检索变得可靠。
2.3 与 Claude Code 等 agent 的集成思路
Claude Code 这类工具的核心能力是"在终端里读写文件、执行命令、根据反馈迭代"。agent-skills要做的,是给它一套结构化的能力清单,让它在面对任务时先检索技能,再决定调用哪个。
集成方式通常有两种。一种是显式调用:用户在提示里写明"使用 xxx 技能",agent 直接加载对应目录。这种方式可控性高,适合流程固定的场景。另一种是隐式检索:agent 根据任务描述,从技能库里匹配最相关的几个技能,再组合使用。这种方式灵活,但对技能的描述质量要求极高——描述写得含糊,检索就会跑偏。
我的建议是先显式后隐式。团队刚起步时,技能数量少,显式调用足够,而且能快速发现哪些技能描述不清楚。等技能库稳定了,再引入检索层。一上来就搞自动检索,往往会因为技能描述质量参差不齐而效果很差,最后大家还是回去手动指定。
2.4 为什么 test-driven-development 是这套体系的地基
TDD 在传统开发里的流程是:先写一个失败的测试,再写代码让它通过,再重构。放到agent-skills场景下,这个流程变成:
- 先写清楚"这个技能被正确执行时,输出应该满足什么条件"。
- 用当前技能描述跑一遍,看是否满足。大概率不满足。
- 调整技能描述、示例、约束,直到满足。
- 把这次的条件固化成测试用例。
这个流程的价值在于,它把"调提示词"从玄学变成了工程。你不再是"感觉这样写效果好一点",而是"这条测试从红变绿了"。模型升级后,跑一遍测试,哪些技能退化了立刻可见。
我踩过的一个坑是:早期技能没有测试,全靠人工抽查。结果有一次模型小版本升级,某个"生成数据库迁移脚本"的技能开始漏掉回滚逻辑,但没人发现,直到上线前才被 DBA 拦下来。从那以后,我们规定任何进入共享库的技能必须有至少一个测试用例,没有测试的技能只能放在个人草稿区。
3. 核心细节解析与实操要点
3.1 SKILL.md 到底该写什么
SKILL.md是技能的门面,也是 agent 检索时主要读的文件。它不需要长,但必须精准。我通常按这个模板写:
# 技能名称:generate-unit-test ## 用途 为指定的函数或类生成单元测试,覆盖正常路径、边界条件和异常路径。 ## 适用场景 - 目标代码有明确的输入输出 - 项目已有测试框架(pytest / jest / go test 等) - 需要快速补齐测试覆盖率 ## 不适用场景 - 目标代码依赖大量外部服务且无 mock 方案 - 目标代码是纯 UI 渲染逻辑,断言成本高于收益 ## 输入 - 文件路径 - 函数或类名 - 测试框架类型 ## 输出 - 测试文件内容 - 覆盖的场景清单 ## 约束 - 不修改被测代码 - 测试文件命名遵循项目现有约定 - 每个测试用例只断言一个行为注意"不适用场景"这一节。很多人写技能只写"能干什么",不写"不能干什么",结果 agent 在错误场景下强行调用,产出垃圾。明确边界,比扩大能力更重要。
3.2 示例的写法:正例要具体,反例要典型
示例部分是最容易被敷衍的。我见过有人写"示例:输入一个函数,输出测试代码",这等于没写。好的示例应该是可以直接复制去跑的真实案例。
正例至少两个,覆盖不同复杂度。比如一个简单函数的测试生成,一个带依赖注入的类的测试生成。反例一个就够,但要典型——比如"目标函数有 200 行且嵌套 5 层,此时应该先重构再生成测试,而不是硬生成"。
反例的作用是给 agent 一个"刹车信号"。没有反例,agent 会倾向于在所有情况下都尝试执行技能,因为它不知道什么时候该停。
3.3 测试用例的组织方式
技能的测试和普通代码测试不太一样。普通测试断言的是"函数返回值等于 X",技能测试断言的是"agent 执行技能后的产出满足某些条件"。条件可以是:
- 输出文件存在且语法正确
- 输出中包含特定关键词或结构
- 输出通过了某个校验脚本
- 输出与预期输出的相似度超过阈值
最后一种要慎用。相似度阈值很难调,太松没意义,太紧容易误报。我倾向于用结构化断言:比如生成的测试文件必须能被测试框架解析,必须包含至少 N 个测试函数,必须覆盖指定的边界值。这些是确定性的,不依赖模型输出的措辞。
测试目录我通常这样组织:
tests/ cases/ case-001-simple-function/ input.json expected-assertions.json case-002-class-with-deps/ input.json expected-assertions.json run-tests.shrun-tests.sh负责遍历 cases,调用 agent 执行技能,然后用断言脚本校验输出。这个脚本可以接进 CI,每次技能变更都跑一遍。
3.4 元数据与版本管理
metadata.json里我至少放这些字段:
| 字段 | 说明 | 示例 |
|---|---|---|
| name | 技能唯一标识 | generate-unit-test |
| version | 语义化版本 | 1.2.0 |
| agent_compat | 兼容的 agent 类型 | claude-code, generic |
| dependencies | 依赖的其他技能 | read-code-context |
| tags | 检索标签 | testing, python, pytest |
| maintainer | 维护者 | team-backend |
版本号很重要。技能描述改了,版本号要升。这样当某个任务失败时,你可以回溯"上次成功用的是哪个版本"。我建议用语义化版本:描述微调升 patch,输入输出契约变化升 minor,不兼容变更升 major。
dependencies字段容易被忽略,但它决定了技能能否被正确组合。如果generate-unit-test依赖read-code-context,那么调用方必须先确保上下文读取技能可用。没有这个声明,组合调用就会在运行时才报错。
3.5 实操心得:描述要写给"检索"看,不只是写给"人"看
这是我最想强调的一点。技能描述有两个读者:人类维护者和 agent 检索器。很多人只考虑前者,把描述写得像文档,结果检索效果很差。
给检索看的描述,要包含任务动词 + 对象 + 场景限定词。比如"生成单元测试"就比"测试相关能力"好得多,因为前者包含了动词"生成"和对象"单元测试"。再比如"为 Python 函数生成 pytest 单元测试"就比"生成单元测试"更精准,因为它限定了语言和框架。
我通常会在SKILL.md顶部放一行"一句话描述",专门给检索用,格式是:[动词] [对象] [限定条件]。这行描述会进metadata.json的 tags,也会被检索层优先匹配。
4. 实操过程与核心环节实现
4.1 从零搭建一个技能库的完整流程
假设你现在什么都没有,想给团队的 Claude Code 工作流搭一套技能库。我建议按这个顺序来。
第一步:盘点高频任务。别一上来就设计架构。先花一周时间,记录团队里大家用 agent 最常干的 10 件事。通常是:读代码理解逻辑、定位 bug、生成测试、写迁移脚本、生成 API 文档、重构函数、写 commit message、review 代码、生成 mock 数据、解释报错。
第二步:选 3 个最高频的做成技能。不要贪多。3 个技能足够验证整套流程是否可行。我一般选"生成单元测试""解释报错""生成 commit message"这三个,因为它们输入输出清晰,容易写测试。
第三步:为每个技能建目录。按 2.2 节的结构建。先写SKILL.md,再补示例,再写测试。
第四步:写一个最简的调用脚本。不需要复杂的检索层,先支持显式调用。比如:
#!/bin/bash # run-skill.sh SKILL_NAME=$1 INPUT_FILE=$2 SKILL_DIR="./skills/$SKILL_NAME" if [ ! -d "$SKILL_DIR" ]; then echo "技能不存在: $SKILL_NAME" exit 1 fi cat "$SKILL_DIR/SKILL.md" > /tmp/prompt.txt echo "---INPUT---" >> /tmp/prompt.txt cat "$INPUT_FILE" >> /tmp/prompt.txt # 这里调用你的 agent CLI claude-code --prompt-file /tmp/prompt.txt这个脚本很粗糙,但能跑通流程。先跑通,再优化。
第五步:跑测试,修描述。用tests/run-tests.sh跑一遍,看哪些 case 失败。失败的原因通常是描述不够精确,或者示例不够典型。改描述,再跑,直到全绿。
第六步:接入 CI。把run-tests.sh加到 CI 里,每次技能目录有变更就触发。这一步做完,技能库才算真正进入工程化。
4.2 一个完整的技能实现示例
拿"生成单元测试"这个技能举例,完整实现如下。
skills/generate-unit-test/SKILL.md:
# generate-unit-test 一句话描述:为 Python 函数生成 pytest 单元测试 ## 用途 读取指定 Python 文件中的目标函数,生成符合项目规范的 pytest 测试。 ## 输入 - file_path: 目标文件路径 - function_name: 目标函数名 - framework: 测试框架,默认 pytest ## 输出 - 测试文件路径 - 测试文件内容 - 覆盖场景清单 ## 约束 - 不修改被测文件 - 测试文件放在 tests/ 目录下,命名 test_<原文件名>.py - 每个测试函数只断言一个行为 - 必须覆盖:正常输入、边界值、异常输入 ## 不适用场景 - 函数超过 100 行 - 函数直接依赖数据库或网络且无 mock 接口skills/generate-unit-test/examples/good-01.md:
输入: file_path: src/calculator.py function_name: divide 输出: tests/test_calculator.py def test_divide_normal(): assert divide(10, 2) == 5 def test_divide_by_zero(): with pytest.raises(ZeroDivisionError): divide(10, 0) def test_divide_negative(): assert divide(-10, 2) == -5skills/generate-unit-test/tests/cases/case-001/input.json:
{ "file_path": "fixtures/simple_math.py", "function_name": "add", "framework": "pytest" }skills/generate-unit-test/tests/cases/case-001/expected-assertions.json:
{ "file_exists": true, "parseable_as_python": true, "min_test_functions": 3, "must_cover": ["normal", "boundary", "exception"] }tests/run-tests.sh的核心逻辑:
#!/bin/bash set -e SKILL="generate-unit-test" PASS=0 FAIL=0 for case_dir in skills/$SKILL/tests/cases/*/; do echo "运行: $case_dir" input="$case_dir/input.json" expected="$case_dir/expected-assertions.json" output=$(./run-skill.sh "$SKILL" "$input") if python3 scripts/assert_output.py "$output" "$expected"; then echo " 通过" PASS=$((PASS+1)) else echo " 失败" FAIL=$((FAIL+1)) fi done echo "通过: $PASS, 失败: $FAIL" [ $FAIL -eq 0 ]assert_output.py负责解析 agent 输出,检查文件是否存在、是否可解析、测试函数数量是否达标、是否覆盖指定场景。这个脚本是确定性的,不依赖模型措辞。
4.3 参数选择与阈值设定
测试里涉及几个需要调的参数,我分享一下我的取值逻辑。
min_test_functions:我一般设 3。一个正常路径、一个边界、一个异常。设太高会逼着 agent 生成冗余测试,设太低覆盖不足。如果目标函数逻辑复杂,可以在 case 级别覆盖这个值。
相似度阈值:如果非要用相似度,我建议 0.75 起步,但只在没有结构化断言可用时才用。实测下来,0.75 能过滤掉大部分跑偏输出,同时不会因为措辞差异误报。
超时时间:单个技能执行超时我设 60 秒。超过这个时间,要么是 agent 卡住了,要么是任务本身不适合这个技能。超时后直接判失败,不要重试,因为重试往往还是超时。
重试次数:技能执行失败时,我允许重试 1 次。重试时会在提示里加上"上次失败原因",让 agent 有机会修正。但重试超过 1 次就没意义了,说明技能本身有问题,应该去改技能而不是反复重试。
4.4 与 Claude Code 的具体集成
Claude Code 支持通过项目根目录的配置文件声明可用工具和上下文。把技能库接进去,通常有两种做法。
一种是把技能目录作为上下文的一部分,让 Claude Code 在需要时自己读取。这种方式简单,但技能多了会占用大量上下文窗口。我的做法是只把metadata.json里的摘要注入上下文,完整SKILL.md在确定调用某个技能时再加载。
另一种是写一个 wrapper 脚本,把技能调用包装成 Claude Code 可以执行的命令。比如:
# 在 Claude Code 里执行 ./skills-cli run generate-unit-test --file src/calculator.py --function divideskills-cli是你自己写的 CLI,负责加载技能、组装提示、调用模型、校验输出。这样 Claude Code 只需要知道"有个命令叫 skills-cli",不需要理解技能库的内部结构。
我倾向于第二种,因为职责清晰。Claude Code 负责交互和文件操作,skills-cli 负责技能管理。两者通过命令行接口解耦,任何一方升级都不影响另一方。
4.5 实操现场记录:第一次跑通全流程
我第一次把整套流程跑通,大概花了两个下午。第一天下午搭目录、写第一个技能、写测试脚本,跑起来发现 agent 输出格式不稳定,有时候返回 markdown 代码块,有时候返回纯文本。第二天上午加了输出解析层,统一提取代码块内容。下午把第二个、第三个技能补上,跑 CI,全绿。
中间最大的坑是输出格式不稳定。解决办法是在SKILL.md的约束里明确写"输出必须是纯代码块,不要包含解释文字",同时在解析层做容错——如果检测到 markdown 代码块就提取内容,否则按纯文本处理。这个容错层后来证明非常必要,因为不同模型对格式约束的遵守程度不一样。
另一个坑是测试用例的输入文件路径。一开始用相对路径,CI 里跑就找不到文件。后来统一改成相对于技能目录的路径,由run-tests.sh负责转换成绝对路径。这个改动很小,但省了很多调试时间。
5. 常见问题与排查技巧实录
5.1 技能检索不准怎么办
这是最常见的问题。表现是:明明有对应技能,agent 却没用,或者用了不相关的技能。
排查顺序是这样的。先看metadata.json里的 tags 是否覆盖了任务描述里的关键词。比如任务是"给这个函数补测试",tags 里应该有"测试""单元测试""test"这些词。如果 tags 太窄,检索就匹配不上。
再看"一句话描述"是否包含动词。检索层通常对动词敏感,"生成单元测试"比"单元测试能力"更容易被匹配到。
如果前两步都没问题,那就是检索算法的问题。这时候不要急着改算法,先加一个显式调用的 fallback:当检索置信度低于阈值时,提示用户手动指定技能。这个 fallback 能救急,也能帮你收集"哪些任务检索失败"的数据,用于后续优化。
5.2 技能执行结果不稳定
同一个技能,同样的输入,两次执行结果差异很大。原因通常有三个。
第一,提示里包含了不确定信息。比如"参考项目现有风格",但项目里有多种风格。解决办法是把风格约束显式化,写成具体的规则。
第二,模型温度参数太高。技能执行场景下,温度应该设低,0.1 到 0.3 之间。温度高适合创意任务,不适合确定性任务。
第三,上下文里混入了无关内容。技能执行时,上下文应该只包含技能描述、示例、输入,不要混入其他技能的描述或历史对话。上下文越干净,输出越稳定。
5.3 测试用例维护成本高
技能一多,测试用例就多,维护起来很累。我的应对策略是分层测试。
核心技能(被多个任务依赖的)写完整测试,覆盖所有边界。边缘技能只写冒烟测试,确保能跑通就行。这样 80% 的维护精力花在 20% 的核心技能上。
另外,测试用例要定期清理。半年没跑过的 case,要么删掉,要么标记为"已知失效"。留着不跑比删掉更危险,因为它会给人虚假的安全感。
5.4 常见问题速查表
| 问题现象 | 可能原因 | 排查动作 | 解决方向 |
|---|---|---|---|
| 技能检索不到 | tags 不全 / 描述缺动词 | 检查 metadata.json | 补 tags,改描述 |
| 输出格式混乱 | 约束不明确 | 看 SKILL.md 约束节 | 加格式约束,加解析容错 |
| 执行超时 | 任务不适合该技能 | 看输入规模 | 拆分任务或换技能 |
| 测试误报 | 断言依赖措辞 | 看断言脚本 | 改结构化断言 |
| 模型升级后退化 | 无回归测试 | 跑测试套件 | 补测试,修描述 |
| 技能组合失败 | 依赖未声明 | 看 dependencies | 补依赖声明 |
5.5 独家避坑技巧
技巧一:技能描述里加"反触发词"。除了正向的 tags,再加一组"什么情况下不要用这个技能"的关键词。检索层匹配到反触发词时,直接排除该技能。这个技巧能显著降低误调用率。
技巧二:给每个技能配一个"最小可运行示例"。这个示例要短到能塞进上下文窗口,又要完整到能跑通。当 agent 不确定怎么调用时,先看这个示例。实测下来,有最小示例的技能,首次调用成功率比没有的高不少。
技巧三:技能版本升级时,保留旧版本至少一个迭代周期。不要直接覆盖。旧版本放在versions/目录下,新版本跑测试通过后再切换默认版本。这样出问题时能快速回滚。
技巧四:把技能执行日志结构化存储。每次执行记录:技能名、版本、输入摘要、输出摘要、耗时、是否通过测试。这些日志是后续优化的金矿。比如你会发现某个技能在特定输入类型下失败率特别高,那就是描述需要补充的场景。
技巧五:定期做"技能审计"。每季度过一遍技能库,问三个问题:这个技能还在用吗?描述还准确吗?测试还跑得通吗?三个问题有一个答不上来,就标记待处理。技能库和代码库一样,不维护就会腐烂。
6. 技能库的扩展方向与个人体会
技能库跑顺之后,可以往几个方向扩展。一个是技能市场:团队之间共享技能,A 团队写的"生成 GraphQL resolver"技能,B 团队可以直接引用。这需要统一的元数据规范和版本兼容策略。另一个是技能自动生成:从历史对话里挖掘高频任务,自动生成技能草稿,人工审核后入库。这个方向能大幅降低技能库的维护成本,但对挖掘算法的准确率要求很高。
还有一个方向是技能与 CI/CD 深度集成。比如 PR 提交时,自动跑相关技能检查代码规范、生成缺失的测试、检查文档更新。这相当于把 agent 从"辅助工具"变成"流程节点"。我试过在 pre-commit hook 里跑"生成 commit message"技能,效果不错,但要注意别让 hook 太慢,否则开发者会绕过它。
我个人在实际操作中的体会是:agent-skills这套东西,技术难度不高,难的是纪律。难的是坚持给每个技能写测试,难的是描述改了就升版本,难的是定期审计。我见过太多团队一开始热情很高,建了二十个技能,三个月后一半失效没人管。所以我的建议是:宁可少建,不可不维护。五个维护良好的技能,价值远大于五十个没人管的技能。
最后再分享一个小技巧:技能库的 README 里,放一个"当前可用技能清单",包含技能名、一句话描述、维护者、最近一次测试通过时间。这个清单每周自动更新。它能让团队里所有人一眼看到技能库的健康状况,也能让维护者有点压力——毕竟自己的技能显示"三个月前测试失败",总归不太好看。