给 Cursor、Codex、Claude Code 用的 AI Skill,到底该怎么测
Posted on 一 20 4月 2026 in Journal
| Abstract | 给 coding agent 用的 AI Skill 测试与评估指南 |
|---|---|
| Authors | Walter Fan |
| Category | Journal |
| Status | v1.0 |
| Updated | 2026-04-20 |
| License | CC-BY-NC-ND 4.0 |
给 Cursor、Codex、Claude Code 用的 AI Skill,到底该怎么测
短大纲
- 这种 skill 为什么和 promptfoo 那种 skill 不一样
- 测试金字塔:结构 lint → 触发 eval → 行为 eval → 回归
- 用 Claude Code 的 headless 模式做 CI 测试
- Anthropic 官方
skill-creator的 eval / benchmark / comparator 思路 - 一个最小可运行的测试样例
- 用魔法打败魔法:把这套流程封装成一个
skill-testerskill,让 agent 自己跑 - 常见坑和落地建议
一、先把问题说清楚:这种 skill 长什么样
上一篇我聊过用 promptfoo 测 AI skill。那种场景假设挺干净:你有个 HTTP endpoint,输入是 prompt,输出是 JSON,promptfoo 当个外部测试执行器,喂数据、收结果、判分,齐活。
可咱们日常写得更多的,其实是另一种 skill:
my-skill/
├── SKILL.md # 给 agent 看的指令 + 工作流
├── references/ # 按需加载的深度参考
│ └── api-spec.md
├── scripts/ # 可执行脚本
│ └── validate.sh
└── assets/ # 模板、字体、样例
它没有 endpoint,没有自己的 prompt,触发方式是用户对 Cursor 说一句"帮我把这份 PDF 填一下",然后 Cursor 在已安装的 skill 里挑一个最像的,把 SKILL.md 加载进 context,再按里面写的步骤干活。
这就和 promptfoo 那种"我自己控制输入输出"的场景完全不是一回事了。这里的调用方是另一个 agent,而且这个 agent 的行为还会随模型更新、context window、并发任务、用户说话方式飘来飘去。你想测它,第一反应是:从哪儿下手?
我自己踩了一阵子坑之后,得出一个结论:别想着用一种工具把所有事都干了,把测试拆成金字塔,每层用最合适的工具。
二、测试金字塔:四层从下往上
我把这种 skill 的测试拆成四层。从下往上,越靠下越便宜越快,越靠上越接近真实但越贵。和 Java 工程的测试金字塔同构:
| 层 | 类比 | 测什么 | 一次跑多久 | 工具 |
|---|---|---|---|---|
| 第 1 层 结构 lint | 编译期检查 | YAML frontmatter、字段格式、文件存在 | 毫秒级 | pulser eval、自写脚本 |
| 第 2 层 触发 eval | 路由 / dispatch 测试 | 该用的时候触发了吗,不该用的时候没乱触发吗 | 秒级 | LLM 当判官,跑 description |
| 第 3 层 行为 eval | 集成测试 | 真的拉起 agent 跑一遍,看步骤、工具、产出 | 分钟级 | Claude Code -p headless 模式 |
| 第 4 层 回归 / benchmark | 性能 + 稳定性 | 改完之后是变好了还是变差了 | 小时级 | A/B comparator,多次跑取均值 |
为什么要分这么细?因为这四层失败的原因完全不同,混在一起测,调试时就只能两眼一抹黑。
举个真实例子。我之前有个 skill 上线第二天就有人吐槽"不工作"。我第一反应是去改 prompt。结果排查半天发现,根本不是 prompt 的事——是 description 写得太泛,被另一个更具体的 skill 抢走了触发。换句话说,"不工作"的原因不在第 3 层,而在第 2 层。如果我有第 2 层的测试,5 分钟就能定位。
下面分别讲。
三、第 1 层:结构 lint,先把低级错误挡在编译期
这一层最便宜,也最容易被忽视。它就是把 SKILL.md 当成一个有 schema 的配置文件来检查:
- YAML frontmatter 必填字段在不在(
name、description) - description 长度有没有超限(很多 agent 对 description 有截断)
- 引用的
references/xxx.md、scripts/xxx.sh文件存在吗 - 内嵌的 bash 命令有没有明显的语法错误
- 有没有写了 "TODO"、"FIXME" 就提交上来
社区里有个叫 pulser eval 的小工具,零依赖,200ms 内跑完,专门干这个。GitHub Action 一接,PR 就有红绿灯。原理上没什么神秘——就是 YAML 解析 + 一堆正则。你完全可以自己写一个,几十行 Python 就够:
import yaml, re, sys
from pathlib import Path
def lint_skill(skill_dir: Path) -> list[str]:
errors = []
skill_md = skill_dir / "SKILL.md"
if not skill_md.exists():
return [f"missing SKILL.md in {skill_dir}"]
text = skill_md.read_text()
m = re.match(r"^---\n(.*?)\n---", text, re.S)
if not m:
errors.append("missing YAML frontmatter")
return errors
fm = yaml.safe_load(m.group(1))
for field in ("name", "description"):
if not fm.get(field):
errors.append(f"frontmatter.{field} is empty")
if len(fm.get("description", "")) > 1024:
errors.append("description too long (>1024 chars)")
for ref in re.findall(r"references/([\w\-./]+\.md)", text):
if not (skill_dir / "references" / ref).exists():
errors.append(f"broken reference: {ref}")
return errors
这种 lint 听起来太朴素,但它能挡掉至少 30% 的"我以为没事"型 bug。最贵的不是 bug 本身,是 bug 在第 3 层、第 4 层才被发现——那时候你已经烧了一堆 token,还没找到问题在哪。
四、第 2 层:触发 eval,agent 该不该用你的 skill
这一层是大多数人不会想到的,但其实是 coding agent 时代最关键的测试。
逻辑很简单:你写了一个 skill,能力再强,agent 不在该用的时候触发它,等于零。这件事 Anthropic 官方的 skill-creator 团队最近也专门强调过——他们把这件事叫"触发精度"(triggering precision):太宽就误触发,太窄就根本不开火。
测法也直白:准备一批"应该触发"和"不应该触发"的用户 prompt,跑一遍模型,看 description 能不能正确路由。
trigger_eval:
skill: pdf-form-filler
description: |
Use when filling PDF forms with structured data. Supports both
AcroForm fields and non-fillable PDFs (places text by coordinates).
cases:
- prompt: "帮我把这份报销 PDF 填一下"
expect: trigger
- prompt: "我有个 W-2 表格要填字段"
expect: trigger
- prompt: "帮我读一下这份 PDF 里的合同条款"
expect: no-trigger # 这是阅读不是填写
- prompt: "把这份 Word 文档转成 PDF"
expect: no-trigger # 这是转换不是填写
跑这个 eval 不一定要真起 agent,可以省一点:把所有已安装 skill 的 description 拼成一个列表,让一个便宜的模型当 router,对每条 prompt 选 0 个或 1 个 skill,再和 expect 对一下。Recall 和 Precision 都看,跌破阈值就 fail CI。
我自己的经验是:写 skill 的时候在 description 上偷的懒,最后都会在触发 eval 上还回来。 一个 description 既要让 agent "看一眼就知道我能干嘛",又要"看一眼就知道我不能干嘛"——后半句往往被人忘掉。
五、第 3 层:行为 eval,真起一个 agent 跑一遍
这一层是肉。前两层是"白盒检查",这一层才是"端到端集成测试"。
核心思路是:用 Claude Code、Codex 这类 agent 的 headless 模式,把它当成一个可脚本化的进程跑起来,喂任务,收结果,判分。
Claude Code 的 -p 模式正是为这个设计的:
claude --bare -p "帮我把 sample/expense.pdf 填好,金额 1234.56" \
--allowedTools "Read,Edit,Bash" \
--max-turns 8 \
--output-format json \
--max-budget-usd 0.10 \
> result.json
几个开关都很关键:
--bare:禁用 hook、plugin、MCP 自动发现,保证机器之间结果一致--allowedTools:把要用的工具显式列出来,不用每次手动确认--max-turns 8:硬性步数上限,防止 agent 跑飞--output-format json:拿到结构化输出,包含每一步动作、消耗、最终结果--max-budget-usd:钱包上限,免得测试本身把账单跑爆
跑完之后,result.json 里能拿到:
- 最终 assistant 输出
- 每一步的 tool call(哪个工具、什么参数、返回什么)
- 总 token 和总费用
- 总耗时
有了这些,你就能写出和单元测试同构的断言:
def test_pdf_skill_fills_acroform():
result = run_claude_headless(
prompt="填好 sample/expense.pdf,金额 1234.56",
allowed_tools=["Read", "Edit", "Bash"],
max_turns=8,
)
assert "pdf-form-filler" in result["skills_used"]
tool_calls = [t["name"] for t in result["tool_calls"]]
assert "Read" in tool_calls
assert "Bash" in tool_calls
output_pdf = Path("sample/expense_filled.pdf")
assert output_pdf.exists()
text = extract_pdf_text(output_pdf)
assert "1234.56" in text
assert result["total_cost_usd"] < 0.05
assert result["duration_seconds"] < 60
是不是很眼熟?这就是 JUnit 风格的集成测试,只不过被测对象是"一个 agent + 一个 skill"。三种断言一样不少:
- 结果断言:PDF 真的填对了金额
- 路径断言:触发了对的 skill,调了对的工具
- 代价断言:成本和时长在阈值内
到这一步,给 coding agent 用的 skill 终于有了和后端服务一样的"可测性"。
六、第 4 层:回归与 benchmark,改完到底是变好了还是变差了
到这里其实已经能交差了。但还有一件事更要命:改完 skill 之后,你怎么知道它是真的变好了,不是只在你昨晚那个用例上变好了?
这就是 benchmark 和 A/B comparator 要解决的事。
Anthropic 在 2026 年 3 月给官方 skill-creator 加了一套 eval 框架,里面就有四个子 agent,分工很有意思:
- Executor:在干净的 context 里并行跑 eval
- Grader:对照预期判分
- Comparator:拿到 v1 和 v2 的输出,盲评谁赢
- Analyzer:跨样本找规律,看 v2 比 v1 强在哪、弱在哪
为啥要 comparator?因为很多 skill 改动看不出绝对优劣——v1 跑出来 78 分,v2 跑出来 81 分,你怎么知道这是真改进还是模型当天的随机波动?盲对盲让另一个 agent 比,是个挺干净的解法。
工程上你不一定要复刻这一整套,最小可用的版本是这样:
for i in 1 2 3 4 5; do
claude --bare -p "$(cat tests/case_001.txt)" \
--skills v1/pdf-form-filler \
--output-format json > runs/v1_run_$i.json
claude --bare -p "$(cat tests/case_001.txt)" \
--skills v2/pdf-form-filler \
--output-format json > runs/v2_run_$i.json
done
python compare.py runs/v1_*.json runs/v2_*.json
跑 5 次取均值,比较 pass rate、平均 token、平均时长。再让另一个模型对每对输出做盲评,统计 v2 胜率。如果 v2 胜率显著高于 50%,再合并。
这套机制最大的价值不是某次发版,而是模型升级时。Claude 4.5 升到 4.6,你的 skill 还能用吗?跑一遍 benchmark 立刻见分晓。Anthropic 官方还提到一个更微妙的发现:有时候底模升级之后,你的 capability uplift 类 skill 反而不再需要了——base model 自己就能干。这种"skill 退役"信号,只能靠持续 benchmark 才能捕捉到。
七、把四层串起来:一个最小可跑的样例
光讲方法论容易飘。我给一个我自己在用的精简版项目结构,你可以直接套:
my-skill/
├── SKILL.md
├── references/
├── scripts/
└── tests/
├── lint.py # 第 1 层
├── trigger_cases.yaml # 第 2 层
├── behavior/ # 第 3 层
│ ├── case_001_acroform.yaml
│ ├── case_002_non_fillable.yaml
│ └── conftest.py
├── benchmark/ # 第 4 层
│ ├── runner.sh
│ └── compare.py
└── Makefile
Makefile 长这样:
.PHONY: lint trigger behavior benchmark all
lint:
python tests/lint.py .
trigger:
python tests/run_trigger_eval.py tests/trigger_cases.yaml
behavior:
pytest tests/behavior -v
benchmark:
bash tests/benchmark/runner.sh
python tests/benchmark/compare.py
all: lint trigger behavior
然后 CI 配置就一句话的事:
# .github/workflows/skill-ci.yml
on: [pull_request]
jobs:
test:
steps:
- run: make lint # 必跑,秒级
- run: make trigger # 必跑,分钟级,便宜
- run: make behavior # 必跑,但只跑核心 5 个 case
- run: make benchmark # 只在 nightly 跑
PR 阶段挡 lint 和触发回归,nightly 跑完整 benchmark,模型升级时手动触发一次 A/B。每一档花的钱和时间都对得上风险等级。
八、几个常见坑
坑一:把 lint 当成"测试已经做了"。 通过结构 lint 只能说明你的 SKILL.md 没语法错,不代表它工作。我见过有人配了一堆 lint 规则就发版,等于把 typo 检查当 QA。
坑二:行为 eval 不固定 seed、不固定 model 版本。 agent 本身就抖,再加上模型悄悄换版本,你的"回归测试"会变成"占卜"。至少要把 model 版本写死,温度调低,并发跑 N 次取均值。
坑三:触发 eval 只测正样本。 只看"该用的时候用了",不看"不该用的时候没乱用"。后者更要命——一个抢了别人触发的 skill,会污染整个 skill 库的体验。
坑四:用同一个模型既写 skill 又评 skill。 这个我在 上一篇写 skill 方法论的文章 里说过,这里再强调一次:让 GPT 评 Claude 写的、让 Claude 评 GPT 写的,盲区不一样,互补效果更好。
坑五:benchmark 不存历史。 跑完看一眼通过率就关掉,等于没跑。最起码要把 runs/*.json 存成版本化的 artifact,能在三个月后回头看趋势——是模型变弱了,还是 skill 真退化了。
九、用魔法打败魔法:写一个 skill 来测 skill
写到这儿,有读者私信问我:这一整套 lint + trigger + behavior + benchmark,能不能封装成一个 skill,让 Cursor / Claude Code 自己就能调?
能。而且这事挺顺——因为测试 skill 本身,也是一套可反复使用的工作流,正好长成 skill 的样子。
这不就是"用魔法打败魔法"么。
我把这个 skill 叫 skill-tester,放在仓库的 .claude/skills/skill-tester/、.cursor/skills/skill-tester/、.agents/skills/skill-tester/ 三份(Cursor、Claude Code、通用 agent 各一份),目录结构很朴素:
skill-tester/
├── SKILL.md # 给 agent 看的四层流程
├── scripts/
│ ├── lint.py # L1:结构 lint
│ ├── trigger_eval.py # L2:触发精度(judge 模型问 YES/NO)
│ ├── behavior_run.sh # L3:起 headless agent 跑 case
│ ├── evaluate_runs.py # L3:对 run 输出做断言
│ └── benchmark.py # L4:A/B 盲评 + 多轮平均
├── references/
│ ├── trigger-cases-template.yaml
│ └── behavior-case-template.yaml
└── assets/
└── report-template.md
用法是纯自然语言。你在 Claude Code 里说:
帮我用 skill-tester 测一下
.claude/skills/my-pdf-skill/,只跑前三层就行。
Agent 读到 SKILL.md 的描述之后,就会按顺序调 lint.py → trigger_eval.py → behavior_run.sh + evaluate_runs.py,一层 fail 就停下来告诉你原因,全过就出一份报告。
几个设计上值得聊的取舍:
为什么 lint 是真 Python 脚本,不是让 agent"自己看看"? 因为结构 lint 这种东西,确定性要求最高。agent 读 SKILL.md 可能漏看 description 长度,漏看引用文件是否存在。用 re + yaml.safe_load 只要 30 行代码,稳定得多。低层确定性强,高层语义灵活,这是分层测试的基本思路。
为什么 trigger eval 要单独起 LLM 判 YES/NO,不让 agent 自己判? 因为被测 skill 的 description 一旦进了当前 agent 的 context,它就被"污染"了——它已经知道这个 skill 的意图。要测"一个干净的 agent 在路由时会不会选它",必须另起一个新 session。这个细节我踩过坑,调试了两天才想明白。
为什么 behavior 跑 claude --bare? 这句我重点说一下。--bare 会跳过 hook、plugin、MCP 自动发现。不加它,你今天跑的测试和明天跑的测试结果可能不一样——因为同事合了一个新 plugin 进来。测试必须可复现,--bare 是"可复现"的门闸。
为什么 benchmark 的判官要做盲评 + 随机 swap? LLM 作为 judge 有 position bias(偏好第一个或第二个答案)。benchmark.py 里每个 case 都随机决定把 v1 放 A 还是放 B,事后再把结果反推回来。这是 LLM-as-Judge 研究里最基本的一个防偏见手法,不做不行。
我自己第一次跑的时候发现了一件挺尴尬的事。 我用 skill-tester 去测另一个 skill,lint 立刻报"broken reference"——SKILL.md 里引用的 references/xxx.md 其实文件名拼错了,小写大写不一致。这个 bug 之前一直没被发现,因为 agent 能"猜"到正确文件,我肉眼看也看不出来。lint 半秒就抓住了。
还有更尴尬的:我用 skill-tester 测 skill-tester 自己,第一次运行报错:
[L1 lint] FAIL
ERROR broken reference: scripts/trigger_eval.py not found
ERROR broken reference: scripts/behavior_run.sh not found
原因是我写 SKILL.md 时把未来要写的脚本都引用上了,但还没真写。这就是 lint 该干的事——强迫你把承诺兑现。改到 PASS 才算合格。
这种 dogfooding 还顺手揭示了一个生态问题:Claude 栈(.claude/skills/)的 skill 很多是没有 YAML frontmatter 的自由格式,而 Cursor 栈(.cursor/skills/)严格要求 name + description。同一个 lint 脚本跑过去,两边结果完全不同。这说明写跨栈 skill 时,最好往最严格的一方对齐,否则搬家的时候一片红。
最后一句实话:skill-tester 本身也是被测对象。它的每一次改动,都应该再用它自己跑一遍。一个测试工具如果没法测自己,多半也测不好别人。
九·一、写完一版之后,又踩到的四个真问题
第一版 skill-tester 发出去之后,我很快发现它有四个软肋,正好也是这类 meta skill 最容易犯的通病。顺手把它们都修了,算是这篇文章的"第二版说明"。
1) L3 只吃 Claude Code 的 JSON。 原版 evaluate_runs.py 假设输出是 {"messages":[...], "result": "..."} 这种 Claude 格式,拿到 Codex 的 JSONL 流或 Cursor 的 {"type":"result", ...} 就抓不到工具名。我把解析拆成了独立模块 scripts/agent_runs.py,三种 agent 各写一个 parser,再加一个嗅探函数 detect_agent(),靠 payload 的 schema 特征(thread.started / type=result + session_id / messages 字段)自动识别。然后写了 19 条单元测试固定行为,TDD 途中还真抓到两个 bug:一个是 Claude fixture 被误判成 Cursor(因为我的嗅探顺序搞错了),一个是我在解 Claude messages 时提前 return,把工具调用给漏了。这就是为什么任何跨格式 parser 都必须配 fixture 测试——你肉眼看不出 schema 的细微差异。
2) judge 模型只能用 Anthropic。 原版 trigger_eval.py 硬编码 Anthropic Messages API;benchmark.py 里的盲评也是。但"判官和被测不要同家"才是 LLM-as-Judge 的基本防偏见手法,只能用 Claude 判 Claude 显然不行。我把 judge 抽成 scripts/judge.py,根据模型名自动推 vendor(claude-* → Anthropic,gpt-* / o* → OpenAI),API 失败回落到对应 CLI(claude --bare -p 或 codex exec),还加了 SKILL_TESTER_JUDGE_VENDOR 环境变量兜底。现在你可以拿 gpt-4o 判 Claude skill,或拿 claude-haiku 判 Codex skill,偏见互相抵消。
3) 四个脚本散着放,CI 里调用麻烦。 我加了一个 scripts/skilltest.py 做统一 entrypoint,子命令 lint / trigger / behavior / benchmark / all / gen / selftest。底层脚本没动——CLI 就是个薄调度层,想绕过它直接调 lint.py 也完全可以。最重要的是 selftest 子命令:它会把 test_agent_runs.py / test_judge.py / test_evaluate_runs.py 三套单元测试都跑一遍,确保"工具链没坏"。CI 的第一步就该跑它,之后才敢去跑真 agent。
4) 没有 pytest 入口。 很多团队已经有完善的 pytest 基建,再给他们一个独立 CLI 是负担。我加了 scripts/pytest_adapter.py:一行 register_skill(...) 就能把任意 skill 目录接进 pytest,产出 test_skill_lint[x] / test_skill_trigger[x] / test_skill_behavior[x] 三个参数化节点。关键设计取舍是:pytest 跑 behavior 时不会spawn 真 agent——而是读取 runs_dir 下上游 CI 步骤提前抓的 JSON 做断言。这样 pytest 保持确定性、离线、秒级;真正贵的 skilltest behavior 只在上游跑一次。完整示例见 references/pytest-example.md。
再加上一个隐藏好处:这四个改动让 scripts/ 目录自身变成了被 self-test 覆盖的 Python 包——不是只有 lint 被自己测了,parse_run 和 judge 也有单元测试护着。再改一版不用手动回归,skilltest selftest 一条命令就知道没回归。
一个经验教训:meta skill 的第一个版本一定是"只服务自己那家 agent 的"。 你只在 Claude Code 里写、只在 Claude Code 里调,天然就会把假设烙进代码。等别的 agent 来用一次,假设就破了。所以一开始就按"如果明天换 Codex/Cursor 还能跑吗"来写,成本其实更低——晚一点改会把你的测试用例、fixture、CI 配置全拖着走。
十、和上一篇 promptfoo 测试的关系
可能你看到这里会问:那 promptfoo 还有用吗?
有用,而且应该叠着用:
| 测什么 | 工具 |
|---|---|
| 你的 skill 在被 agent 调用时表现 | Claude Code headless / Codex CLI 直接跑 |
| 你的 skill 内部的 LLM 子调用(比如某个 prompt 模板) | promptfoo |
| 你的 MCP server 工具的安全性 | promptfoo redteam + MCP plugin |
| 你的 skill 写出来的 RAG 模块 | Ragas |
简单说:外层用 agent CLI 跑端到端,内层关键 prompt 用 promptfoo 跑单元,安全用 promptfoo redteam 兜底。 三个工具不打架,各管一段。
总结
- 给 Cursor / Codex / Claude Code 用的 skill 没有 endpoint,调用方是另一个 agent,所以测试方式必须从"测 API"换成"测 agent + skill 的组合行为"。
- 拆成四层:结构 lint、触发 eval、行为 eval、回归 benchmark。每层用最合适的工具,别想拿一个工具搞定全部。
- Claude Code
-p --bare --max-turns --output-format json是这套测试的事实基础设施,能让 agent 像普通 CLI 一样进 CI。 - Anthropic 官方
skill-creator的 executor / grader / comparator / analyzer 思路值得借鉴,尤其是 A/B 盲评,是判断 skill 改动好坏的最干净办法。 - 把这套流程封装成一个
skill-testerskill,是 dogfooding 的绝佳机会——一个连自己都测不了的测试工具,多半也测不好别人。 - 一句话:你 skill 的稳定性,等于你测试的颗粒度。 没有第 2 层触发 eval,你永远不知道 skill 为啥不响应;没有第 4 层 benchmark,你永远不知道改完到底是好是坏。
思维导图

@startmindmap
* coding agent 的 AI Skill 测试
** 这种 skill 长什么样
*** SKILL.md + references + scripts
*** 没有 endpoint
*** 调用方是另一个 agent
*** 触发依赖 description
** 测试金字塔(四层)
*** L1 结构 lint
**** YAML 字段
**** description 长度
**** 引用文件存在
**** 工具:pulser / 自写脚本
*** L2 触发 eval
**** 正样本:该触发
**** 负样本:不该触发
**** 看 precision + recall
*** L3 行为 eval
**** Claude Code -p --bare
**** --output-format json
**** 结果 / 路径 / 代价 三种断言
**** pytest 集成
*** L4 回归 / benchmark
**** Executor 并行跑
**** Grader 判分
**** Comparator 盲评 A/B
**** Analyzer 找规律
**** 模型升级时必跑
** 工具组合
*** Claude Code headless
*** Codex CLI
*** promptfoo(内层 prompt + 红队)
*** Ragas(RAG 子模块)
*** GitHub Actions / Jenkins
** skill-tester(自举)
*** 用一个 skill 测别的 skill
*** lint.py / trigger_eval.py
*** behavior_run.sh / evaluate_runs.py
*** benchmark.py(A/B 盲评)
*** 能自测自己
** skill-tester v1.1 迭代
*** agent_runs.py:claude/codex/cursor 三家 parser
*** judge.py:Anthropic + OpenAI 双厂商 judge
*** skilltest.py:统一 CLI(含 gen + selftest)
*** pytest_adapter.py:接入已有 pytest 基建
*** 单元测试护航(19+ 条)
** 常见坑
*** 把 lint 当 QA
*** 行为 eval 不固定 model
*** 触发 eval 只测正样本
*** 同模型自评自
*** benchmark 不存历史
@endmindmap
可执行 CheckList
- 给每个 skill 的
tests/目录补上lint.py,把 frontmatter 字段、description 长度、引用文件这些低级错挡在 PR 之外。 - 至少写 5 条触发正样本和 5 条触发负样本,跑触发 eval。description 改一次,跑一次。
- 行为 eval 固定 model 版本和温度,至少跑 3 次取均值,别用单次结果做合并依据。
- CI 分两档:PR 跑 lint + trigger + behavior 核心 5 case;nightly 跑全量 benchmark。
- 模型升级、infra 升级、SKILL.md 大改之后,手动跑一次 A/B comparator,确认没有静默退化。
- 把每次 benchmark 的
runs/*.json当 artifact 存起来,趋势图比单点结果重要得多。
扩展阅读
- Anthropic Skill Creator 公告(带 eval / benchmark / comparator 介绍):https://claude.com/blog/improving-skill-creator-test-measure-and-refine-agent-skills
- skill-creator 源码与示例:https://github.com/anthropics/skills/tree/main/skills/skill-creator
- Claude Code Headless Mode 文档:https://docs.claude.com/en/docs/claude-code/headless
- Headless Claude in CI 实战:https://agentpatterns.ai/workflows/headless-claude-ci/
- pulser eval(skill 结构 lint):https://dev.to/thestack_ai/testing-claude-code-skills-in-ci-pulser-eval-github-action-3na9
- 本文配套的
skill-testerskill(仓库内):.claude/skills/skill-tester/、.cursor/skills/skill-tester/、.agents/skills/skill-tester/ - Codex 非交互模式(
codex exec --json):https://developers.openai.com/codex/noninteractive/ - Cursor Agent CLI 的 JSON / stream-json 输出格式:https://cursor.com/docs/cli/reference/output-format
- 上一篇:用 Promptfoo 给 AI skill 做体检
- 前置篇:如何写好一个 AI Skill
- 入门篇:Agent Skills:给 AI 助手装上"技能包"
本作品采用知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议进行许可。