Skill 也需要 CI:我给 23 个 Skill 写了回归测试
先说件真事,到现在我还有心理阴影。
上个月某个周四,我改了一个叫 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 | . |
evals/ 下每个 Skill 一份 YAML,约束它的行为。scripts/eval-runner.py 负责拉起 Claude 跑这些 eval。GitHub Actions 在 PR 时自动触发。
一个 eval YAML 的样子
拿 code-summarizer 举例:
1 | skill: code-summarizer |
几个重点:
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 | name: Skill CI |
有几个细节我踩过坑:
只跑改动涉及的 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 流程:
- 新 Skill(比如
code-summarizer-v3)并行上线,老的code-summarizer标记 deprecated,description 里加「deprecated,将在 X 月下线,请迁移到 code-summarizer-v3」。 - 通知下游团队迁移,给两周到四周时间。
- 时间到了,老 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 进行许可。