我在给AI编程助手调教自定义Skill时发现,很多人把Skill理解成“一段提示词”,结果做出来的东西换个项目就废。尤其是测试类Skill,如果你只告诉它“帮我写单元测试”,它大概率会写出只覆盖Happy Path的用例,甚至把mock写错。而真正好用的测试Skill,核心就三样东西:SKILL.md、scripts、references。这篇文章就把这三步走的方法、代码、坑一次讲清楚,适合正在折腾Agent自定义技能、想把重复测试工作交给AI的同学。
1. Skill开发前必须想清楚的事
1.1 Skill到底是什么?它和普通提示词、Agent的区别
很多人第一次接触Skill时,会把它当成“大号提示词”。这不能说错,但会限制你对它的设计。Skill本質上是一个“能力包”,它通过一个结构化的目录,把任务说明、可执行脚本和参考资料打包在一起,让AI Agent在特定场景下能调用。Skill不是Agent,Agent是能自主决策、多步执行的主体;Skill更像是Agent的“职业资格证书”,告诉它“你在测试这件事上应该按什么标准干活”。
以Claude Code、Codex或OpenClaw这类工具为例,它们都开始支持自定义Skill。你给一个Skill起好名字、写好描述,Agent就会在遇到相关任务时自动加载它。如果你只是把一段测试规范贴在系统提示词里,那每次换项目、换模型都要重新调;但如果你把规范放进Skill的references,把重复判断逻辑写进scripts,就能在不同项目间复用,而且Agent的执行稳定性能明显提升。
我一开始也走过弯路:把测试方法全写在提示词里,结果上下文被占掉一大截,Agent还经常“忘记”用覆盖率工具。后来改成SKILL.md + scripts + references的结构,问题才真正解决。
1.2 为什么测试类Skill特别适合“三步走”
测试类任务和写文案、写邮件这类纯文本任务最大的区别是:它需要确定性。你不能让模型“猜”一个测试用例是否通过了,必须让它真正跑一遍pytest、看覆盖率报告、解析失败日志。这就需要scripts发挥作用。同时,测试又强依赖项目规范和工具用法,比如你们的测试文件放哪、mock必须怎么写、覆盖率阈值是多少,这些知识库内容正好放到references里。
SKILL.md则负责“决策”:什么时候触发、先做什么后做什么、哪些情况下要停下来征求用户意见。三层各司其职,比什么都塞进提示词要清晰得多。
如果你准备开发一个测试Skill,我的建议是先别急着写代码,花半小时想清楚下面几个问题:
- 这个Skill要服务什么语言和测试框架?(Python/pytest、JavaScript/vitest等)
- 它需要执行哪些操作?(生成用例、跑测试、查覆盖率、做静态检查)
- 团队有没有必须遵守的测试规范?(命名、目录、mock规则、阈值)
想清楚这三个问题,后面三步走就是填内容而已。
1.3 选定场景:我们这次要做一个什么样的测试Skill
为了让教程不悬空,我以Python项目为例,做一个名为pytest-qa的测试Skill。它能做四件事:
- 分析项目源码结构,列出待测模块和函数清单。
- 根据团队规范生成或补全pytest测试用例。
- 自动执行测试与覆盖率检查,并生成可读报告。
- 在覆盖率不达标时,明确指出缺口代码位置。
这个Skill麻雀虽小,但SKILL.md、scripts、references三部分都会用到,足够覆盖大部分自定义Skill的开发套路。你完全可以照着它改成JavaScript、Go或者其他语言版本。
2. 第一步:SKILL.md是技能的大脑
2.1 SKILL.md文件结构与元信息写法
SKILL.md是整个Skill的入口文件,Agent会优先读取它。它通常由两部分组成:YAML格式的frontmatter和正文Markdown。frontmatter里最重要的是name和description,前者是Skill的唯一标识,后者决定了Agent在什么场景下会触发它。
description的写法很有讲究。我见过很多人写“用于测试”,这太模糊了。正确的是写清楚“什么情况下用、能解决什么问题”,比如:
--- name: pytest-qa description: 用于Python项目的测试分析、用例生成与质量检查。当用户要求写测试、补测试、分析测试覆盖率或检查测试质量时使用。 ---这样Agent在决策时,能通过语义匹配把“帮我看看为什么测试覆盖率这么低”归类到这个Skill。如果你的Skill只负责特定框架,也要在description里写清楚,避免被误触发。
正文部分不需要长篇大论。SKILL.md不是技术文档,更像是一份“操作手册摘要”,它告诉Agent“按什么流程做、遵守什么原则”。核心信息包括:适用场景、工作流程、执行规范、输出格式、脚本调用方式和references索引。
2.2 如何描述测试任务,才能让Agent执行不跑偏
很多Skill失败,问题不在模型能力,而在SKILL.md写得太像“需求文档”,没有形成可执行的约束。我总结了一个比较实用的写法:用步骤+约束+输出格式来控制行为。
步骤要足够具体,比如:
- 先调用
scripts/analyze.py扫描src/目录,获得待测函数清单。 - 读取
references/testing_guidelines.md,确认项目测试规范。 - 按规范生成测试文件到
tests/目录。 - 调用
scripts/run_checks.py执行pytest和覆盖率检查。 - 如果覆盖率低于阈值,返回具体未覆盖行号,并给出补充建议。
约束要比步骤更重要。没有约束的Agent会自作主张,典型问题包括:不读规范直接生成测试、乱改业务代码、把整个项目日志打印到输出里。所以我会在SKILL.md里单独写一节“执行约束”,明确禁止哪些行为:
注意:不要跳过分析脚本直接凭经验写测试;不要在未经用户确认时修改业务代码;不要在执行结果中展示大段原始日志;如果覆盖率不达标,不要只写“建议补充测试”,要列出具体缺口。
输出格式也要提前定义好。我的习惯是要求Agent最终输出包括:本次执行的测试数量、通过/失败数量、覆盖率变化、未覆盖文件/函数清单、风险分级。这样结果才能直接用于团队评审。
2.3 一个可直接参考的SKILL.md示例
下面是我实际在用的SKILL.md简化版本,你可以直接复制改:
--- name: pytest-qa description: 用于Python项目的测试分析、用例生成与质量检查。当用户要求写测试、补测试、分析测试覆盖率或检查测试质量时使用。 --- # pytest-qa ## 适用场景 - 新模块开发后需要补第一轮单元测试 - 已有测试覆盖不足,需要定位并补全 - 需要执行pytest并输出覆盖率报告 - 需要检查测试质量,识别脆弱测试和无效断言 ## 工作流程 1. 读取项目根目录,确认源码位置通常是`src/`或项目同名目录。 2. 读取`references/project_context.md`获取项目结构摘要。 3. 调用`scripts/check_env.py`检查pytest和pytest-cov是否已安装。 4. 调用`scripts/analyze.py`提取待测模块的类、函数、异常分支。 5. 参照`references/testing_guidelines.md`生成或补全测试。 6. 调用`scripts/run_checks.py`执行测试并收集覆盖率。 7. 汇总结果,给出风险清单和下一步建议。 ## 执行约束 - 必须先运行分析脚本,再决定写哪些测试。 - 不修改业务代码,除非用户明确要求。 - 测试代码必须遵循`references/testing_guidelines.md`的命名和结构规范。 - 外部HTTP调用必须mock,不允许测试访问真实网络。 - 覆盖率低于阈值时,必须列出未覆盖的具体文件和行号。 ## 输出格式 - 测试总数、通过数、失败数、跳过数。 - 覆盖率:总覆盖率、核心文件逐一覆盖率。 - 未覆盖风险:按“高/中/低”分级列出。 - 建议动作:每个风险点给出可执行的补充测试方案。这份文档的关键不是“写得好”,而是能让Agent在有限上下文里快速形成正确的行动路径。SKILL.md本身不需要把所有细节写进去,细节交给references,执行交给scripts。
3. 第二步:scripts是技能的双手
3.1 scripts里该放什么,不该放什么
scripts目录放的是可以被Agent调用的可执行脚本。测试Skill里最常见的脚本包括环境检测、源码分析、测试生成、测试执行、覆盖率统计和质量门禁。脚本的价值在于,它把模型不擅长的确定性计算和精确判断接管过来。
但也要注意,不应该把整个业务逻辑写进scripts。scripts只是辅助Agent的“工具”,它应该保持小而专。大而全的脚本反而会让Agent不知道怎么用,也会增加维护成本。我一般控制在4个脚本以内,每个脚本只做一件事,并且支持--help参数,因为Agent会尝试用--help来理解脚本功能。
3.2 测试Skill常用脚本拆解:环境检测、用例生成、质量门禁
先看环境检测脚本check_env.py。它的作用不是装依赖,而是快速告诉Agent当前环境缺什么、版本够不够。下面是一个简化但可用的版本:
#!/usr/bin/env python3 """检查当前Python项目运行pytest所需依赖是否就绪。""" import importlib.util import sys from pathlib import Path REQUIRED = [ ("pytest", "pytest"), ("pytest_cov", "pytest-cov"), ] def main(): project_dir = Path.cwd() req_file = project_dir / "requirements.txt" if not req_file.exists(): print("WARN: 未找到requirements.txt,建议补全依赖声明后交付。") missing = [] for module, package in REQUIRED: if importlib.util.find_spec(module) is None: missing.append(package) if missing: print(f"MISSING: {', '.join(missing)}") sys.exit(1) print("ENV_OK: pytest与pytest-cov均已安装。") if __name__ == "__main__": main()这个脚本的逻辑很简单,但效果很好。Agent拿到ENV_OK或MISSING结果后,就知道是继续跑测试还是先装依赖,而不是盲目执行pytest然后报错。
源码分析脚本analyze.py用Python自带的AST实现,用来扫描待测函数:
#!/usr/bin/env python3 """扫描指定源码目录,输出候选待测函数清单。""" import ast import sys from pathlib import Path def extract_functions(path: Path): tree = ast.parse(path.read_text(encoding="utf-8")) result = [] for node in ast.walk(tree): if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)): # 跳过魔法方法 if node.name.startswith("__") and node.name.endswith("__"): continue result.append({ "file": str(path.relative_to(Path.cwd())), "name": node.name, "line": node.lineno, "args": [arg.arg for arg in node.args.args], "is_async": isinstance(node, ast.AsyncFunctionDef), }) return result def main(): root = Path(sys.argv[1] if len(sys.argv) > 1 else "src") if not root.exists(): print("ERROR: 源码目录不存在。") sys.exit(1) for py_file in sorted(root.rglob("*.py")): for info in extract_functions(py_file): print(f"{info['file']}:{info['line']} {info['name']}({', '.join(info['args'])}) {'async' if info['is_async'] else ''}") if __name__ == "__main__": main()这个脚本输出的是纯文本行,Agent不需要额外解析JSON,直接读就能定位待测函数。我故意用这种简单格式,因为大模型读纯文本比读复杂结构更不容易出错。
执行检查脚本run_checks.py才是质量门禁的核心:
#!/usr/bin/env python3 """执行pytest并输出覆盖率摘要。""" import subprocess import sys from pathlib import Path COVERAGE_THRESHOLD = 70 def main(): project = Path.cwd() cov_cmd = [ sys.executable, "-m", "pytest", "--cov=src", "--cov-report=term-missing", "--tb=short", "-q", ] result = subprocess.run(cov_cmd, cwd=project, capture_output=True, text=True) print(result.stdout[-3000:]) if "passed" not in result.stdout and "failed" not in result.stdout: print("ERROR: 无法获取pytest结果,请检查测试文件是否存在。") sys.exit(2) # 这里简单从输出中提取总覆盖率 try: percent_line = [line for line in result.stdout.splitlines() if "TOTAL" in line][0] total_percent = float(percent_line.split()[-1].replace("%", "")) if total_percent < COVERAGE_THRESHOLD: print(f"\nQUALITY_GATE_FAILED: 总覆盖率{total_percent:.1f}% < {COVERAGE_THRESHOLD}%") sys.exit(3) except Exception: pass sys.exit(result.returncode) if __name__ == "__main__": main()注意这个脚本里有几个“巧思”:--tb=short能减少日志长度,result.stdout[-3000:]只保留最后3000字符,避免把上下文撑爆;覆盖率低于阈值时返回非0退出码,Agent通过退出码就能判断是否触发质量门禁,而不是靠“读文本猜”。
3.3 脚本依赖与“requirements.txt”的坑
很多人在开发Skill脚本时会忽略依赖声明,导致换一台机器Skill就废。尤其是热词里出现过的“python skill 缺少 requirements.txt 或依赖声明”,这是实际高频踩坑点。
我的建议是:Skill目录内单独放一个scripts/requirements.txt,把脚本自身依赖和项目依赖分开。比如:
pytest>=7.0 pytest-cov>=4.0然后在SKILL.md的工作流程里加上一条:如果check_env.py提示缺失依赖,先安装scripts/requirements.txt中的包,再继续执行。这样既不会污染项目主依赖,也能保证Skill在干净环境里跑起来。
另一个坑是路径问题。脚本执行时的工作目录不一定是Skill目录,所以脚本里所有路径都要基于Path.cwd()或显式传入项目根目录。我的习惯是要求Agent统一在项目根目录执行脚本,并把这一条写进SKILL.md。
4. 第三步:references是技能的弹药库
4.1 references目录的选材与组织
references目录的作用是给Agent提供“背景知识”,相当于给新员工看的团队文档。它可以是Markdown、TXT、PDF甚至JSON,但为了Agent解析方便,我强烈建议统一用Markdown,并控制单个文件体积。
哪些内容适合放references?我总结了三类:
- 规范类:团队的测试命名规范、目录结构、mock规则、覆盖率阈值。
- 工具类:pytest常用写法、fixture示例、参数化用例、异常测试技巧。
- 上下文类:当前项目的结构摘要、历史测试分析结论、已知风险模块。
这些内容如果写进SKILL.md,会让决策路径变得臃肿;如果写进scripts,又没法让Agent理解。放在references里,让Agent按需读取,是最合适的。
4.2 测试规范、pytest手册与项目上下文的落地写法
我通常会在references目录放这三个文件:
references/testing_guidelines.md写团队规范,内容要具体到能直接执行:
# 测试规范 - 测试文件统一放在`tests/`目录,文件名以`test_`开头。 - 每个待测模块对应一个测试文件,例如`src/user.py` -> `tests/test_user.py`。 - 公共fixture统一放在`tests/conftest.py`中,不在测试文件里重复定义。 - 对外部HTTP请求必须mock,禁止在测试中访问真实网络。 - 纯函数优先使用参数化测试,覆盖正常值、边界值、异常输入。 - 测试函数命名格式:`test_<被测函数>_<场景>_<期望结果>`。references/pytest_cookbook.md写工具技巧,类似“代码块字典”:
# pytest常用写法 ## 参数化 @pytest.mark.parametrize("value,expected", [(1, 2), (0, 0), (-1, -2)]) def test_double(value, expected): assert double(value) == expected ## 异常断言 with pytest.raises(ValueError): parse_input("bad") ## 临时目录 tmp_path是pytest内置fixture,可直接使用,无需自行创建临时文件夹。 ## mock外部调用 from unittest.mock import patch with patch("project.service.requests.get") as mock_get: mock_get.return_value.status_code = 200references/project_context.md则建议由脚本自动生成,每次运行Skill时更新。它不需要很复杂,只要记录模块路径、关键函数、已知的坏味道位置就行。比如:
# 当前项目上下文 - 项目类型:Python 3.11 FastAPI服务 - 源码目录:src/app/ - 核心模块: - src/app/services/payment.py:支付逻辑,高风险,覆盖率缺口集中在退款流程。 - src/app/utils/validator.py:入参校验,近两周改动频繁。 - 已有测试目录:tests/ - 最近一次覆盖率:68%,距阈值70%还差2个百分点。4.3 控制references体积,避免上下文爆炸
references也不是越多越好。模型上下文是有限资源,尤其是大项目,如果你把几千页文档全塞进去,Agent反而抓不住重点。我有三个控制原则:
第一,单个文件控制在100行以内,超过就拆成多个小文件。第二,在最需要的时候才读取,SKILL.md里明确写“仅在生成测试前读取testing_guidelines.md”,而不是让Agent一开始就加载全部references。第三,能够动态生成的内容不要静态维护,比如项目上下文,用脚本在Skill运行时自动生成,免得每次手动更新。
实际上,很多Agent工具支持在Skill内部通过相对路径引用references文件,你可以让Agent在需要时自行查看。这样同一个Skill既不会占满上下文,又能保证信息不过时。
5. 完整实战:三步拼装一个可用的“pytest-qa”测试Skill
5.1 目录结构与安装方式
现在把前三步的内容组合起来。最终目录结构是这样的:
pytest-qa/ ├── SKILL.md ├── scripts/ │ ├── check_env.py │ ├── analyze.py │ ├── run_checks.py │ └── requirements.txt └── references/ ├── testing_guidelines.md ├── pytest_cookbook.md └── project_context.md安装方式取决于你用的Agent工具。以Claude Code为例,通常是把整个目录放到~/.claude/skills/下;Codex系列工具有些放在~/.codex/skills/或项目级.codex/skills/;OpenClaw这类插件化工具则可能有自己的导入流程。不管路径怎么变,核心目录结构是一致的,SKILL.md必须在一级目录下,scripts和references保持同名。
不同工具对Skill的发现机制略有差异,最稳妥的办法是查看Agent输出日志,看它是否成功索引到了SKILL.md。如果没有,多半是路径放错了,或者description里没有触发关键词。
5.2 从SKILL.md到scripts的调用链路
整个调用链路是这样的:用户提出“帮我把payment模块的测试补一下”→ Agent读取SKILL.md,判断适用→ 按流程先读references/project_context.md,了解背景→ 调用scripts/check_env.py检查环境→ 调用scripts/analyze.py src/app/services/payment.py获取函数清单→ 读取references/testing_guidelines.md确认规范→ 生成测试文件→ 调用scripts/run_checks.py执行质量门禁→ 输出报告。
这里最容易被忽略的是“环境检查”这一步。很多测试Skill一上来就写测试、跑pytest,结果环境里连pytest都没装,回头还得找用户问。有了check_env.py,Agent可以自己在脚本输出里看到缺失依赖,然后提示用户安装,整个流程就顺了。
scripts的输出格式也要为Agent设计。比如analyze.py输出“文件:行号 函数名(参数)”,Agent可以直接把行号对应到源码;run_checks.py输出最后3000字符,包含TOTAL覆盖率和通过/失败统计。Agent不需要理解大量日志,只需要抓住末尾关键行。
5.3 在Claude Code / Codex / OpenClaw中的接入说明
接入前,先确认你的Agent工具支持自定义Skill或类似扩展。现在主流Agent都在做这个方向,但命名和目录规则有差异。我的建议是:先查官方文档确定目录,再把Skill目录原样放进去,然后在一个小项目上验证触发效果。
验证时不要直接测完整流程,先问一个简单问题:“我的测试覆盖率是多少?”看Agent是否主动加载pytest-qa。如果它没反应,检查两点:一是description里是否包含“测试”“覆盖率”这些关键语义;二是Skill目录是否被工具正确识别。
如果用的是OpenClaw这类更偏自动化流程的工具,可能还涉及权限配置或依赖安装。不过没关系,只要SKILL.md+scripts+references的结构清楚,迁移到任何工具都只是目录和配置文件的差异。
6. 常见问题与排查技巧实录
6.1 Skill文件格式正确却没有被加载
这个问题我遇到好几次。最常见的原因是SKILL.md的frontmatter写错,比如YAML里冒号后没有空格、description为空、或者文件编码不是UTF-8。另一个原因是Agent工具只扫描特定目录,没有把目录放到正确路径。还有一种情况是文件名大小写不一致,比如把SKILL.md写成了skill.md,有些工具能兼容,有些不能。
排查时优先看工具日志,确认它扫描到了哪个目录。然后检查SKILL.md的YAML块,确保name和description都正常。最后用一个非常直白的问题触发它,比如“请读取pytest-qa skill并给出它的工作流程”,如果Agent能正确总结,说明加载正常。
6.2 脚本执行报错:python环境、依赖缺失、路径错误
脚本报错大多集中在三处:第一,系统里没有安装python或者sys.executable不是虚拟环境,导致子进程调用pytest失败;第二,src目录路径不对,项目用的可能是app/或lib/;第三,缺少依赖。我的处理方法是让check_env.py把环境信息一次打印清楚,包括Python版本、当前目录、pytest是否可用。这样Agent不用猜,脚本也能给出明确退出码。
路径问题尤其隐蔽。比如脚本里写Path("src"),但Agent执行时的工作目录不是项目根目录,就会报“目录不存在”。所以我在SKILL.md里明确要求:所有脚本统一在项目根目录下执行,并且脚本里使用Path.cwd()推导路径,不要写死绝对路径。
6.3 references不生效或上下文被撑爆
references不生效,通常是因为SKILL.md里没有明确告诉Agent“什么时候去读哪个文件”。有些工具会在Skill加载时自动把所有references内容读进来,但更常见的是按需读取。你需要在SKILL.md中加入类似“生成测试前,先读取references/testing_guidelines.md”的指令,否则Agent不知道这些文件的存在。
反过来,上下文被撑爆是因为把大文件放进了references。我的经验是:单个Markdown文件超过100行后,Agent读取时消耗的token会明显增加,而且容易丢失重点。解决办法是把长文档拆成小模块,并且在SKILL.md里规定“只读取需要的部分,不要把整个文件内容重复输出”。
6.4 常见问题速查表
| 问题 | 可能原因 | 快速解法 |
|---|---|---|
| Skill未被加载 | 目录路径错误/文件名大小写/frontmatter异常 | 检查工具日志,确认SKILL.md在正确目录,YAML格式规范 |
| 脚本能跑但退出码无意义 | 子进程异常未被捕获 | 在脚本中显式sys.exit非0值,并输出ERROR:前缀 |
| pytest找不到测试文件 | 测试文件命名或目录不符 | 检查tests/下文件是否以test_开头,确认run_checks.py覆盖路径 |
| 覆盖率始终为0 | --cov=src路径与源码路径不符 | 修改cov_cmd中的src为实际源码目录 |
| Agent输出冗长日志 | 未限制stdout输出长度 | 脚本中截取末尾字符,并提示Agent只提取关键行 |
| references内容与项目过期 | 项目结构变动后未更新 | 用脚本自动刷新project_context.md,不用手工维护 |
这个表我每次开发Skill都会复用。排查时先按“加载—资源—退出码—输出”四个维度定位,基本能在五分钟内找到问题。
7. 最后分享一个我自己的习惯
每次做完一个测试Skill,我不会立刻拿到真实项目上用,而是先造一个“故意留了3个bug、2个未覆盖函数”的小项目,跑一遍完整流程。这样能快速校验SKILL.md里的流程是否通、scripts的退出码是否合理、references规范是不是真被Agent遵守了。你会发现很多问题在“测试Skill”自己身上:要么Agent跳过了分析脚本,要么环境检测没有返回期望的退出码,要么输出里漏掉了覆盖率门槛。这些问题在干净小项目里暴露出来,比在真实项目里Debug要高效得多。测试Skill本质上也在被测试,把这条原则内化到开发流程里,比任何模板都管用。