Skill 也需要 CI:我给 23 个 Skill 写了回归测试

Claude 中文知识站 Lv4

先说件真事,到现在我还有心理阴影。

上个月某个周四,我改了一个叫 code-summarizer 的 Skill,把输出格式从 markdown 改成了带前缀的结构化格式,想让下游更好解析。本地测了两个 case 觉得没问题,推了。

周五早上四个团队的人在群里炸了。他们有的工作流依赖这个 Skill 的 markdown 格式做正则提取,我一改,全挂。

回滚都来不及,因为有人已经基于新格式跑了半天了。那个下午我花了六小时一边帮他们临时适配一边自我怀疑。

回来之后下定决心:Skill 也是代码,也要 CI

Skill 被忽视的测试需求

大家对 prompt 测试这事儿其实已经有共识了,anthropic 的 evals 工具也挺成熟。但 Skill 这块,我观察下来没几个团队在系统测——包括我之前。

为什么?我想了想有几个原因。

一是 Skill 看起来就是一段文字,大家觉得「不会出什么大问题」。这是错觉。
二是 Skill 的触发是隐式的,测起来比 slash command 麻烦——slash 至少能直接敲。
三是 Skill 的输出往往是自然语言,不像代码有明确的对错。

但这些理由都不是「不测」的借口。Skill 被上线后,改一次影响面很大,测试不足就是定时炸弹。

我给 Skill 测哪些维度

归纳我现在做的 CI 测试,一共五个维度:

第一,触发准确性。给一堆测试 prompt,看 Skill 有没有在该触发的时候触发、不该触发的时候不触发。这是基础,之前那篇 Skill 元信息与触发准确率 讲过怎么用 llm-as-judge 做。

第二,输出格式。核心字段在不在、JSON 解析成不成、markdown 结构对不对。这是那次炸锅事件教我的——格式变更等于 API 破坏

第三,长度。输出不能超过某个 token 数。有些 Skill 被训得越来越啰嗦,早发现早治理。

第四,关键字段语义。比如「风险评估」Skill,分数应该在 0-100 之间;「单元测试生成」Skill,输出应该包含 assert 语句。用 assertions 卡住这些。

第五,成本。每次调用的 token 消耗。设一个 budget,超了告警。防止某次改动让 Skill 变贵而没人察觉。

仓库布局

一个生产 Skill 项目现在的目录大概是这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
.
├── skills/
│ ├── code-summarizer/
│ │ ├── SKILL.md
│ │ └── templates/
│ ├── pr-diff-analyze/
│ └── ...(23 个)
├── evals/
│ ├── skill_code_summarizer.yaml
│ ├── skill_pr_diff_analyze.yaml
│ └── shared/
│ └── fixtures/ # 共享测试数据
├── .github/
│ └── workflows/
│ └── skill-ci.yml
└── scripts/
└── eval-runner.py

evals/ 下每个 Skill 一份 YAML,约束它的行为。scripts/eval-runner.py 负责拉起 Claude 跑这些 eval。GitHub Actions 在 PR 时自动触发。

一个 eval YAML 的样子

code-summarizer 举例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
skill: code-summarizer
version: 2.3.1

test_cases:
- name: typescript_function
input:
code: |
function foo(x: number) {
return x * 2 + 1;
}
assertions:
- format: markdown
- contains_section: "## Purpose"
- contains_section: "## Inputs"
- contains_section: "## Outputs"
- max_length_tokens: 300
- semantic_match:
prompt: "Does the summary correctly identify this is a doubling function with offset?"
expected: yes

- name: trigger_negative_docs
input:
user_message: "帮我写一篇 blog post"
assertions:
- should_trigger: false

budget:
max_tokens_per_call: 450
max_cost_per_call_usd: 0.0021

几个重点:

  • version 字段很关键,我待会儿讲。
  • assertions 里既有格式类(contains_section)也有语义类(semantic_match)。语义类通过 llm-as-judge 实现。
  • should_trigger: false 是负样本,测这个 Skill 不应该被拉起来的场景。
  • budget 卡住成本和 token,防止退化。

跑一次完整 eval,23 个 Skill 总共花了 $1.34,跑了 11 分钟。放在 PR CI 里完全能接受。

GitHub Actions 跑起来

workflow 文件大概这样(精简版):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
name: Skill CI
on:
pull_request:
paths:
- 'skills/**'
- 'evals/**'

jobs:
eval:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: pip install -r scripts/requirements.txt
- name: Detect changed skills
id: changes
run: python scripts/detect-changed-skills.py
- name: Run evals
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
run: python scripts/eval-runner.py --skills ${{ steps.changes.outputs.skills }}
- name: Comment results
if: always()
run: python scripts/comment-pr.py

有几个细节我踩过坑:

只跑改动涉及的 Skill。全量跑 23 个 eval 太贵也太慢,我写了 detect-changed-skills.py 解析 diff,只挑受影响的 Skill 跑。对改公共 fixture 的 PR 还是全跑。

失败要显示到 PR 里。光 CI 红没用,得把具体哪个 assertion 失败、expected 是啥、actual 是啥评论到 PR 上。comment-pr.py 干这个。

API key 权限最小化。给 CI 用的 API key 限制了 spend cap,每月上限 $20,跑飞了也烧不了多少。

版本化:frontmatter 加 version 字段

Skill frontmatter 里加一个 version: 2.3.1,遵循 semver。

  • major:破坏性变更(输出格式变、触发范围缩)
  • minor:兼容新功能(加个新选项、优化输出质量)
  • patch:bug fix、文案优化

版本号不是为了好看。我的调度层(slash command 或另一个 Skill)可以显式依赖某个 Skill 的 major 版本,避免被破坏性变更波及。

breaking change 走 deprecate 流程:

  1. 新 Skill(比如 code-summarizer-v3)并行上线,老的 code-summarizer 标记 deprecated,description 里加「deprecated,将在 X 月下线,请迁移到 code-summarizer-v3」。
  2. 通知下游团队迁移,给两周到四周时间。
  3. 时间到了,老 Skill 文件挪到 skills/_archived/,不再被加载。

这套流程走了三次,没再出过那次炸锅的事。

影子模式 + 灰度的故事

讲一个完整的灰度案例。

3 月份我要大改 pr-risk-score 这个 Skill,从纯启发式打分换成更结构化的规则引擎。输出字段变了,评分分布也会变——算是典型的破坏性变更。

我的灰度步骤:

阶段 1:影子模式(5 天)。新老 Skill 都加载,用户请求只跑老的返回结果,但同时异步调用新的,把新老输出都记录下来对比。5 天跑了 1,247 次,统计发现新版本在高风险场景判定更准(老版本把 8.3% 的高风险误判为中风险),但在低风险场景偶尔过度紧张(2.1% 过度标红)。根据数据调了新版的一个阈值。

阶段 2:5% 灰度(3 天)。按用户 ID hash 分流,5% 流量走新版。这阶段收反馈。有一个团队说新版本的建议更有用,没人说变差。

阶段 3:25% 灰度(4 天)。扩大。监控误报率、用户 override 率、满意度打分。一切正常。

阶段 4:100%(持续监控)。老版本降级为 fallback,新版 30 天稳定后删除老版本。

这个节奏对我来说是 sweet spot——太快容易翻车,太慢没意义。整个周期大约两周半。

context 预算的管控 在灰度期间也是一个观测点,我会盯着新版 Skill 对 context 的占用有没有异常膨胀。

不值得测的东西

为了公平再说几条「别测」的:

别测完全主观的东西。比如「summary 读起来自然不自然」,这种没有稳定 ground truth,你每次跑都可能不一样,CI 会变成噪音。
别测极端边缘 case。99% 用户永远不会触发的输入,加进去只是拖慢 CI。
别在 CI 里做耗资金的昂贵 eval。比如每次跑 500 个 prompt 的大 eval 套件。这种放 nightly 或者手动触发,PR CI 保持轻量。

CI 的目标是抓住「明显不对」的改动,不是抓所有 bug。

小结

Skill 越来越像代码,工程实践也得跟上。五个维度的 assertions、一份合理的 eval YAML、一个轻量的 GitHub Actions,再加上版本化和灰度,基本能挡住 90% 的「改崩了没发现」事故。

你不需要一开始就搭完这套。从最关键的那几个 Skill 开始,先给它们加 eval,发现收益了再推广。我是第一周给 5 个关键 Skill 加了 eval,两个月之后扩到 23 个。

延伸
CI 搞定之后,如果你所在的是大公司,下一步挑战就是怎么把 Skill 做成公司级的知识资产。我跟一个 42 人研发团队合作 11 个月沉淀了 67 个 Skill,讲讲这条路怎么走:把公司知识库做成 Skill 合集:42 人研发团队的 11 个月实战
  • 标题: Skill 也需要 CI:我给 23 个 Skill 写了回归测试
  • 作者: Claude 中文知识站
  • 创建于 : 2026-04-18 16:35:00
  • 更新于 : 2026-04-19 14:09:00
  • 链接: https://claude.cocoloop.cn/posts/skill-testing-versioning/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
评论