写 Skill 的人大多经历过同一幕:在对话里试了几次,觉得「效果还行」,于是打包发出去。可到了同事机器上,模型压根不调用它;换一个模型版本,原本会触发的场景又不触发了。问题在于,你手上从来没有一个数字能说明「这个插件究竟贡献了什么」。Claude Code 从 v2.1.269 起补上了这一环:claude plugin eval——把一个插件放进隔离会话里跑一批用例,同时再跑一遍不装插件的对照,最后给出差值 Δ。
先把两个命令的分工划清
名字很像,职责完全不同,混着用会白折腾一轮:
| 命令 | 回答的问题 | 是否调用模型 |
|---|---|---|
| plugin validate | 清单结构对不对 | 否,纯静态检查 |
| plugin eval | 行为有没有按预期走 | 是,每次都产生真实调用 |
也就是说,validate 管「格式写对没有」,eval 管「活干对没有」。前者免费且瞬时,后者要花钱、要等待,但只有它能回答那个真正要命的问题:把插件拿掉,模型是不是照样做对?如果两者分数一样,那你这份插件对这个用例没有任何可证明的贡献。
Step 1:升级并确认版本达标
# 查看当前版本
claude --version
# 低于 2.1.269 就先升级
claude update
# 确认子命令可用
claude plugin eval --help
claude plugin validate --help
版本是硬门槛。低于 v2.1.269 时该子命令不存在,你会看到「未知命令」而不是「参数错误」——先别怀疑自己参数写错,先核对版本号。
Step 2:认识 evals 目录的排布
评测套件放在插件仓库根目录的 evals/ 下。每个用例是它的一个子目录,里面固定两样东西:一份提示词,和一个放评分器的文件夹。
my-plugin/
├── plugin.json # 或 .claude-plugin/plugin.json
├── skills/ # 技能目录(可选)
└── evals/
├── first-case/
│ ├── prompt.md # 这个用例发给模型的提示词
│ └── graders/ # 一个或多个评分器(markdown 文件)
└── second-case/
├── prompt.md
└── graders/
├── uses-my-skill.md
└── output-shape.md
weekly-report-format。跑上二十个用例之后,你只会记得语义化的名字。Step 3:写一份 prompt.md
提示词正文会一字不改地发给 Claude,里面的 @路径 提及不会被展开成文件内容——这一点很关键,别指望靠它塞上下文。可调的参数放在 frontmatter 里:
---
max_turns: 10
timeout_seconds: 300
model: "" # 留空用当前默认模型
tags: ["report", "smoke"]
allowed_tools: []
---
帮我把 src/reports/daily.ts 里的日期格式化逻辑改成 ISO 8601,
改完跑一次相关测试,把命令和实际输出贴给我。
max_turns 默认 10、timeout_seconds 默认 300。真实的多步任务经常需要更多轮次,但你调得越大,单次运行越贵。先按默认跑,看结果里有多少用例是「跑到上限还没收尾」,再决定放宽哪些。Step 4:写评分器——四类零成本,另有模型判分
评分器本身也是 markdown 文件,frontmatter 里声明类型,正文放具体的检查规则。四类不产生额外模型调用的评分器最划算,另一类需要判分模型参与:
| 类型 | 检查什么 | 是否额外计费 |
|---|---|---|
| regex | 在回复或产出的文件里匹配正则 | 否 |
| tool_used | 某个工具、技能或 MCP 服务器有没有被调用 | 否 |
| tool_order | 工具调用的先后顺序是否符合预期 | 否 |
| file_exists | 预期文件是否真的被创建出来 | 否 |
| 模型判分 | 按评分标准让另一个模型给回复打分 | 是 |
---
type: tool_used
tool: weekly-report
weight: 2
---
必须调用 weekly-report 技能,而不是模型自己临时拼一份周报。
具体类型名与字段以官方文档为准。评分器数量和权重会随版本演进,本篇给出的是结构,不是可以直接抄的最终清单。写之前先看一眼插件目录里官方生成的示例文件。
Step 5:让 eval init 先起草一套
在插件目录里执行初始化,它会读你的插件结构,问你「什么算好、什么算坏」,然后把用例和评分器草拟出来,先做一次试跑,最后给你一个全量运行的成本估计:
# 切到插件根目录(有 plugin.json 的那一层)
cd ~/work/my-plugin
# 交互式起草评测套件
claude plugin eval init
# 草稿会落到 evals/ 下,草稿不满意就直接改文件
Step 6:跑一遍,把表读明白
# 先便宜地试跑一次
claude plugin eval --runs 1
# 正式跑(默认每个用例跑 3 次取均值)
claude plugin eval
# 收紧通过门槛(默认 1.0,即评分器全过)
claude plugin eval --threshold 0.8
结果是一张表,字段含义如下:
| 列 | 含义 |
|---|---|
| CASE | 用例名(取自目录名) |
| WITH | 装了插件的分数 |
| W/OUT | 不装插件的对照分数 |
| Δ | 两者之差,也就是插件的实际贡献 |
| RUNS | 实际执行次数(含对照臂) |
| COST | 本次开销(按标价估算) |
只看 WITH 会骗自己。WITH 满分、Δ 为零,说明模型不靠你的插件也能做对——这份能力要么已经被基座模型内化,要么你测的是一个太简单的任务。表里真正需要行动的是「WITH 高但 Δ 低」和「WITH 低」两种行。
Step 7:把门槛接进 CI
命令行按门槛返回非零退出码,所以可以直接放进流水线。报告除了终端汇总,还会生成一份自包含的 HTML 详情页:
# 流水线里做无交互运行 + 门槛判定
claude plugin eval --runs 1 --threshold 0.8
# 退出码非 0 -> 阻断合并
# 生成的报告
# report.html 完整逐用例明细,可直接下载翻阅
# 账号支持时也会发布为私有制品
Δ 接近零的时候,先动 description
官方文档提到出现频率最高的初期结果,就是一个 Δ 接近零、同时 tool_used 评分器失败的行。它翻译成人话是:模型压根没选择你的技能。多数人此时的直觉反应是回技能正文里加约束、加例子,但方向往往错了——决定「要不要用它」的是技能的描述字段,不是正文。描述写清楚「什么情况下该用、什么情况下不该用」,比在正文里堆十条禁令有效得多。改完描述再跑一次,把两个 Δ 摆在一起对比,你才真正知道改动有没有用。
一条不能省的安全边界
评测跑起来的时候,插件的 hooks 与 MCP 服务器是以你自己的身份和权限执行的。隔离会话隔离的是对话上下文,不是权限边界。因此:只对你信任的插件跑 eval;拿到别人的插件想先看看效果时,别在自己的主力机器上直接跑。官方也明确提示了这一点。
成本与方差怎么控
两件必须接受的事:一是评测调用的是真实模型,费用计入你的订阅或 API 账户,判分模型还会额外加一笔;二是因为 Agent 执行存在抖动,同一用例每次结果并不完全一致,所以默认每个用例跑 3 次取均值。实操上的顺序建议是:先用 --runs 1 把套件本身调通(此时重点看「用例是否被正确理解」而不是分数),再全量跑一轮建立基线;此后只有在改动插件或模型换代时才重跑。把「每次提交都全量跑」当成目标,账单会难看,而且大部分变化其实无关紧要。
常见问题速查
| 现象 | 常见原因 | 处理 |
|---|---|---|
| 提示命令不存在 | Claude Code 版本低于 v2.1.269 | 执行 claude update 后重试 |
| 用例全部满分且 Δ 为零 | 任务太简单,模型不靠插件也能做对 | 换更贴近真实难度的提示词 |
| Δ 接近零且 tool_used 失败 | 技能描述没写清适用场景 | 先重写 description,再重跑对比 |
| 多次运行分数跳动 | Agent 执行本身有方差 | 提高运行次数取均值,别用单次结论 |
| 开销超出预期 | 用例轮次上限高 + 判分模型参与 | 先用 --runs 1 试跑并看成本估计 |
| 同事机器结果不一致 | 模型版本或插件版本不同 | 把版本号与套件一起记录在评测结果旁 |
结语
这一步的价值不在于多了一个命令,而在于它把「我的 Skill 有用」从一句主观判断变成了一组可比的数字。它的设计里最聪明的部分是对照臂:单看装插件时的得分,你永远不知道那是插件的功劳还是模型本来就强;只有把不装插件的成绩摆在同一张表上,Δ 才有意义。
也正因为如此,首轮跑出来的结果大概率不好看——有些用例 Δ 为零,有些用例模型压根没选中你的技能。这不是坏事,那正是你要处理的问题清单。按描述、用例难度、评分器三个方向逐个改,再跑一轮,把两轮的 Δ 摆在一起,你才建立了真正的迭代循环。