自动化评测流水线
CI 集成:PR 修改 Skill 时自动跑 eval
手动跑 eval 能解决问题,但靠不住。忙起来就忘了,或者”改了个措辞应该没事吧”然后直接合并。
和代码测试一样——自动化才是唯一可靠的方案。
GitHub Actions 配置
# .github/workflows/skill-eval.yml
name: Skill Evaluation
on:
pull_request:
paths:
- '.claude/skills/**'
jobs:
eval:
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0 # 需要完整历史来对比 baseline
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Detect changed skills
id: changed
run: |
SKILLS=$(git diff --name-only origin/main...HEAD \
| grep '^\.claude/skills/' \
| cut -d'/' -f3 \
| sort -u)
echo "skills=$SKILLS" >> "$GITHUB_OUTPUT"
echo "Changed skills: $SKILLS"
- name: Run evals for changed skills
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
run: |
for SKILL in ${{ steps.changed.outputs.skills }}; do
SKILL_DIR=".claude/skills/$SKILL"
EVAL_FILE="$SKILL_DIR/evals.json"
if [ ! -f "$EVAL_FILE" ]; then
echo "::warning::No evals.json found for $SKILL"
continue
fi
echo "Running eval for $SKILL..."
# 注意:以下命令是概念演示。目前没有官方的 skill-eval CLI 工具。
# 实际实现有两种方式:
# 1. 用 skill-creator 的 scripts/run_eval.py(需要 Python 环境)
# 2. 自己写一个简单的评测脚本,核心逻辑是:
# - 启动两个 Claude Code 会话(一个加载 Skill,一个不加载)
# - 把 evals.json 中的 prompt 分别发给两个会话
# - 收集输出,对比断言通过率
# 用 skill-creator 的方式(如果已安装):
# python -m scripts.run_eval --skill "$SKILL_DIR" --evals "$EVAL_FILE"
#
# 或者用自定义脚本(见 examples/ch21-ci-pipeline/eval-runner.ts):
npx tsx .claude/scripts/eval-runner.ts \
--skill "$SKILL_DIR" \
--evals "$EVAL_FILE" \
--output "eval-results/$SKILL/summary.json"
done
- name: Compare with baseline
id: compare
run: |
REPORT=""
EXIT_CODE=0
for SKILL in ${{ steps.changed.outputs.skills }}; do
SUMMARY="eval-results/$SKILL/summary.json"
BASELINE=".claude/skills/$SKILL/benchmark.json"
if [ ! -f "$SUMMARY" ]; then
continue
fi
NEW_RATE=$(jq -r '.results.with_skill.pass_rate' "$SUMMARY")
DELTA=$(jq -r '.results.delta' "$SUMMARY")
if [ -f "$BASELINE" ]; then
OLD_RATE=$(jq -r '.pass_rate' "$BASELINE")
DIFF=$(echo "$NEW_RATE - $OLD_RATE" | bc)
if (( $(echo "$DIFF < -0.05" | bc -l) )); then
REPORT="$REPORT\n❌ **$SKILL**: pass_rate $OLD_RATE → $NEW_RATE (退化 $DIFF)"
EXIT_CODE=1
else
REPORT="$REPORT\n✅ **$SKILL**: pass_rate $OLD_RATE → $NEW_RATE (delta: $DELTA)"
fi
else
REPORT="$REPORT\n⚠️ **$SKILL**: pass_rate $NEW_RATE (无 baseline,首次评测)"
fi
done
echo "report<<EOF" >> "$GITHUB_OUTPUT"
echo -e "$REPORT" >> "$GITHUB_OUTPUT"
echo "EOF" >> "$GITHUB_OUTPUT"
exit $EXIT_CODE
- name: Comment on PR
if: always()
uses: actions/github-script@v7
with:
script: |
const report = `${{ steps.compare.outputs.report }}`;
const body = `## Skill Eval Results\n\n${report}\n\n` +
`_Auto-generated by skill-eval workflow_`;
// 查找已有评论并更新,避免重复评论
const comments = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
});
const existing = comments.data.find(c =>
c.body.includes('Skill Eval Results'));
if (existing) {
await github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: existing.id,
body,
});
} else {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body,
});
}这个 workflow 做四件事:
- 检测哪些 Skill 被改了
- 对每个改了的 Skill 跑 eval(with_skill + without_skill)
- 和 baseline 对比,检查有没有退化
- 把结果评论到 PR 上
触发条件是 .claude/skills/** 路径有变更。你改业务代码不会触发这个 workflow,只有动了 Skill 才跑。
退化阈值
配置里的关键数字是 -0.05。pass_rate 下降超过 5% 就判定为退化,CI 报错。
为什么是 5% 而不是 0?因为 LLM 的输出有随机性。同一个 Skill、同一个 prompt,跑两次的结果不完全一样。允许 5% 的波动是务实的。
如果你的测试用例够多(10 条以上断言),可以把阈值收紧到 3%。用例太少的话,单条断言的随机波动就会导致 pass_rate 剧烈变化。
基线管理
基线是评测的锚点。没有基线,你只知道”这次 pass_rate 是 0.85”,不知道这是进步还是退步。
benchmark.json 版本化提交
每个 Skill 目录下放一个 benchmark.json:
{
"version": "2025-04-01",
"skill_commit": "abc1234",
"pass_rate": 0.85,
"delta": 0.40,
"total_assertions": 14,
"passed_assertions": 12,
"eval_details": [
{ "name": "simple-function", "pass_rate": 1.0 },
{ "name": "complex-pr", "pass_rate": 0.75 },
{ "name": "security-vuln", "pass_rate": 1.0 },
{ "name": "react-component", "pass_rate": 0.67 }
]
}这个文件提交到 git,和 Skill 一起版本管理。CI 跑 eval 时就拿它做对比。
基线更新流程
不是每次 eval 跑完都更新基线。更新基线意味着”我认可当前的表现水平”。
流程:
- 跑 eval,确认 pass_rate 有提升
- 人工 review eval 结果,确认提升是真实的(不是因为断言变弱了)
- 更新 benchmark.json
- 在 commit message 中说明为什么更新基线
git add .claude/skills/code-review/benchmark.json
git commit -m "chore(skill): update code-review baseline to 0.88
Added performance check rules, eval pass_rate improved from 0.85 to 0.88.
Reviewed eval outputs manually, improvement is genuine."反模式:CI 自动更新基线。这等于取消了基线的意义——永远和上一次比,永远不退化,但也永远不知道绝对水平在哪。
eval-viewer 可视化
eval 的原始数据是 JSON,不够直观。用 generate_review.py 生成交互式 HTML,可以直观地对比两次 eval 的结果。
生成方式:
python scripts/generate_review.py \
--input eval-results/code-review/ \
--output eval-results/code-review/review.html生成的 HTML 包含:
- 输出对比视图:左右分栏,with_skill 和 without_skill 的输出并排显示,差异高亮
- 断言通过率:每个 eval 用例的通过率,用颜色编码(绿/黄/红)
- 评分详情:每条断言的判定结果和理由
- 趋势图:多个 iteration 的 pass_rate 变化曲线
这个 HTML 可以作为 CI 的 artifact 上传,reviewer 在 PR 页面直接下载查看。
在 GitHub Actions 中添加 artifact 上传:
- name: Upload eval report
if: always()
uses: actions/upload-artifact@v4
with:
name: eval-report
path: eval-results/
retention-days: 30团队评测流程
把前面的所有环节串起来,完整流程如下:
改 Skill
│
▼
本地跑 eval
│
├─ pass_rate 退化 → 回去改 Skill
│
▼
提 PR
│
▼
CI 自动跑 eval ──→ 评论 PR(pass_rate 对比)
│
├─ CI 失败 → 回去改 Skill
│
▼
Owner review
│
├─ 看 eval report(下载 artifact)
├─ 看 Skill 变更的合理性
├─ 对照 checklist(第 19 章)
│
▼
合并到 main
│
▼
更新 baseline(如果 pass_rate 提升了)
│
▼
全团队生效几个注意点:
本地 eval 是可选但强烈推荐的。 CI 跑一次 eval 可能需要几分钟(取决于测试用例数量和 API 延迟)。如果你改了 Skill 就直接提 PR 等 CI 告诉你结果,来回几次就浪费半天。在本地先跑一遍,确认没问题再提 PR。
CI eval 是强制的。 本地 eval 可能因为环境差异(不同的模型版本、不同的上下文状态)得到不同结果。CI 跑的 eval 是标准化的,所有人用同一个环境、同一个模型配置。
Owner review 不只是看 eval 数据。 eval 通过只说明客观指标没退化。Owner 还需要判断:这个变更有没有必要?会不会让 Skill 膨胀(第 17 章)?新规则的措辞够不够精确(第 7 章)?
基线更新是显式操作。 合并 PR 之后,如果 pass_rate 提升了,Owner 手动更新 benchmark.json 并提交。这个操作本身就是一个 commit,有记录可查。
最后一点
评测流水线的成本不低——每次 CI 要调 API、要等结果、要上传 artifact。但这是值得付的成本。
没有自动化评测的 Skill,就像没有自动化测试的代码。刚开始写的时候觉得”我心里有数”,三个月后就是一坨不敢动的东西。
你团队的第一个 CI eval 不需要很完善。从最关键的那一两个 Skill 开始,跑起来再逐步完善。等团队尝到甜头——某次 PR 的 CI 评论提示”pass_rate 退化了 0.12”,reviewer 一看,果然是新规则和旧规则冲突了——以后就没人会质疑”这玩意有必要吗”。