掌握 Plugin Evals 精髓:让 Claude Code 插件质量可测量、可回归、可进 CI

在 Claude Code 插件与 Skill 开发中,最常见的困境是:你写了一个技能,自己试了几次觉得“挺好用”,上线后却发现用户根本触发不了,或者触发了但结果不稳定。claude plugin eval(Plugin Evals)正是为了解决这个问题而生。它把“主观感觉”变成可重复、可对比、可进 CI 的数字,帮助你真正掌握插件是否在起作用。

本文基于官方文档《Test plugins with evals》,从“是什么、有什么用、怎么写、怎么跑与维护、有哪些边界和陷阱”五个维度讲透它,并在最后用 15 个追问帮你检验是否真的掌握。

前置条件:Claude Code v2.1.269 及以上;git 2.31+(已安装 git 时;更旧版本会直接拒绝运行);插件根目录有 plugin.json 或 .claude-plugin/plugin.json;认证方式与日常使用一致。每次 run 和每次 judge 打分都是真实的模型调用,会计入订阅用量或 API 账单。

一、是什么:面向行为的插件测试

claude plugin eval 用真实风格的用户 prompt 去跑你的插件,再用 Grader 判断结果。它测的是“行为”,不是语法;语法/schema 检查请用 claude plugin validate。

1. 核心概念

  • Case(用例):evals/<case>/ 下的一个目录,含 prompt.md(或 case.yaml)+ 至少一个 grader。没有 grader 的 case 会加载失败。
  • Grader(评分器):对一次 run 做通过/失败判断,共 6 种,没有自定义代码 grader:
    • 免费(基于 transcript 与文件):regex、tool_used、tool_order、file_exists
    • 付费(调用 judge 模型):llm、baseline
  • Run 与评分:每个 case 默认每臂跑 3 次。一次 run 的分数 = 通过的 grader 占比(可加权),case 分数 = 各 run 的均值,case 通过 = with 臂分数 ≥ --threshold(默认 1.0)。
  • 双臂对照:默认再跑 3 次“不加载任何插件”的 without 臂。得到 WITH、W/OUT 与 Δ = WITH − W/OUT。官方原话的精神是:两臂都是 1.0,说明通过并不是插件的功劳。

2. 一次 run 发生了什么

每次 run 都会启动一个全新的、隔离的非交互会话(claude -p 子进程),只加载被测插件,从空工作目录开始,直到任务完成或触达 max_turns(默认 10)/ timeout_seconds(默认 300)。之后每个 grader 检查最终回复、transcript 或产出文件。

3. 目录结构

my-plugin/
├── .claude-plugin/plugin.json
├── skills/...
└── evals/
    ├── commit-message/
    │   ├── prompt.md          # frontmatter:用例字段;正文:prompt
    │   ├── graders/
    │   │   ├── skill-fired.md # 过程检查
    │   │   └── criteria.md    # 结果检查
    │   └── case.yaml          # 可选:仅在需要 context.* 时使用
    ├── mocks/                 # 可选:套件级 MCP mock
    └── results/               # 每次运行生成,加入 .gitignore

如果 evals/ 被占用,可在 plugin.json 里写 "experimental": {"evals": "quality/evals"},或用 --eval-dir 指定。

注意:它与 skill-creator 插件使用的 evals/evals.json 是两套互不识别的格式。skill-creator 适合在一次对话里迭代单个 skill,claude plugin eval 适合整插件、可回归、可进 CI。

二、有什么用:回答三个问题

  1. 插件会被触发吗? 最常见的失败不是“结果差”,而是 Claude 在自然表述下根本没选你的 skill。
  2. 触发后真的更好吗? 看 Δ,而不是单看 WITH。
  3. 改动或换模型后有没有变差? 同一套 suite 反复跑,捕捉回归。

官方文档特别指出:最常见的第一发现是 Δ 接近 0,且 tool_used: Skill 失败——说明 skill 的 description 在这句 prompt 的表述下没有触发。

三、怎么写:从 init 到高质量用例

1. 快速开始

claude plugin eval init     # 交互式:Claude 读插件、问你“好结果”长什么样、
                            # 提议应触发/不应触发的 prompt 与 grader、试跑后写文件
claude plugin eval .        # 跑整个 suite

手写则用 claude plugin eval init --bare first-case 生成空模板(不运行任何东西,CI 里也只能用 --bare)。

2. prompt.md

---
max_turns: 10
allowed_tools: [Read, Glob, Grep, Skill]
---

帮我给这次改动写一条 commit message:我把 getUser 重命名为 fetchUser,并更新了三处调用。

要点:

  • prompt 要像用户随口说的话,不要点名 skill,否则你测的是“被点名”而不是“被选中”。
  • 每次 run 从空目录开始,任务需要的内容要写进 prompt 本身;@path 不会被展开为附件。
  • frontmatter 不是必须,但建议至少设 allowed_tools、max_turns;未知键会报错。max_turns 触顶会记为 run error 并通常拉低分数,宁可设宽松些。
  • 其它字段:name、tags、runs(1–50)、model、timeout_seconds、append_system_prompt、env(键必须是 EVAL_*)。

3. Grader 组合:一个看结果,一个看过程

过程(graders/skill-fired.md):

---
type: tool_used
tool: Skill
input_match: '"skill"\s*:\s*"(?:[\w-]+:)?commit-message"'
---

结果(graders/criteria.md):

---
type: llm
---

PASS if 回复是单行 Conventional Commit 格式。
FAIL if 是散文、列表或多行内容。

各类型的通过条件:

类型 通过条件
regex JS 正则在 target 中命中;match: not_contains 要求不出现,match: "count:N" 要求恰好 N 次;大小写用 flags: i,不支持内联 (?i)
tool_used 工具调用次数(可用 input_match 过滤)落在 min(默认 1)与 max 之间;断言“从未调用”设 min: 0, max: 0
tool_order 两个工具都被调用,且第一个 before 调用早于第一个 after 调用
file_exists 本次 run 中 Claude 创建的文件匹配 path(或 exists: false 表示没有)
llm judge 对 rubric 三次投票中至少两次 PASS
baseline judge 认为本次 run 不差于参考 transcript(.jsonl)

regex 的 target 与 llm 的 focus 可取:last_message(默认)、trace、files(仅创建的文件路径列表,不是内容)、{ source: file, path: ... }(文件内容)、mock_calls。每个 grader 还可设 weight 与 arm。

4. 让信号稳定的四个习惯

  • 长输出(生成的文件)用 regex 查文件内容;llm 只留给短输出,rubric 写成具体的 PASS/FAIL 条件。
  • 每个 case 至少一个“结果”grader + 一个“过程”grader,才能同时知道答对没、是不是插件做对的。
  • tool_used: Skill 通过但 Δ 为负时,先怀疑 judge:小 judge 可能因格式差异误判。用 --judge-model sonnet 复跑并收紧 rubric。
  • 想验证构建/测试通过:让 Claude 跑命令并把结果写进文件,再对文件打分,同时用 tool_used 的 input_match 断言命令确实执行过。

5. 夹具与 Mock

  • case.yaml:只在需要 context.* 时才必须写——scaffold_script(造文件/git 仓库)、history_file(接续已有对话,prompt 作为下一轮用户输入)、add_dirs(只读夹具目录)。需要 schema_version: "1.1" 和 name。
  • MCP Mock:evals/mocks/<server>/<tool>.md,正文即工具返回值,支持 {{input.字段}} 替换、expect: 校验入参(违反则该 run 记 0 分并标记 aborted)、error: true、type: agent(让 judge 扮演服务端)。默认不启动真实 MCP server。
  • 权限:run 从不弹权限询问;Bash、Write、Edit、WebFetch、WebSearch 这类需要授权的工具,没授权就直接从会话中移除。用 --allow-tools Write Edit "Bash(npm test *)" 授权;授予 Bash 后命令跑在 OS 沙箱里。

四、怎么跑与维护

1. 运行目标与常用参数

claude plugin eval .                         # 整个 suite
claude plugin eval evals/x/prompt.md         # 单个 case
claude plugin eval my-plugin@marketplace     # 已安装插件
claude plugin eval . --case 'commit*' --tag smoke
参数 作用
--runs <n> 每 case 每臂的运行次数
--ablation none | with-without 是否跑无插件基线
--threshold <0..1> case 通过线(默认 1.0)
--model / --judge-model 被测模型 / judge 模型;CI 中建议固定
--max-cost-usd 成本上限(按标价估算,开跑前检查,已启动的 run 会跑完,所以可能略超)
-j, --concurrency 并发 1–8,共享你账号的限流
--json [path] 输出结构化结果(先写 target,再写 --json)
--no-publish 报告只留本地
--keep-temp 保留每次 run 的沙箱目录便于排查

成本粗算:约 case 数 × runs 次带插件 run + 同样数量的无插件 run,外加每个 llm/baseline grader 每次 run 三次短 judge 调用。

2. 日常迭代闭环

  1. 改 skill 的 description 或逻辑。
  2. 用 claude plugin eval . --case <name> --runs 1 --ablation none 快速看一眼。单次 run 噪声大,不能据此下结论。
  3. 用默认 3 次双臂重跑,观察 Δ。
  4. 打开 report.html:失败的 grader 默认展开并附解释,llm grader 还会显示 judge 的投票与它看到的证据。

3. CI 门禁

claude plugin eval . \
  --trust-plugin \
  --json results.json \
  --threshold 0.8 \
  --model claude-sonnet-5 \
  --judge-model claude-haiku-4-5 \
  --no-publish \
  --max-cost-usd 20
退出码 含义
0 所有 case ≥ 阈值,且所有 case 文件加载成功
1 有 case 低于阈值;case 文件加载失败;没找到 case;目录未受信且没传 --trust-plugin;参数非法
2 部分运行:触达成本上限,或凭证被拒绝(结果带 partial: true)
130 / 143 被中断 / 被终止(如 CI 超时)

关键:Δ 只被报告,永远不影响退出码。 CI 门禁卡的是 with 臂的绝对分数;想卡 Δ,需要自己解析 aggregate-result.json 里的 cases[].aggregates.delta。

CI 其它要点:runner 需安装 Claude Code 并在环境变量里提供凭证;把 evals/results/ 加入 .gitignore;趋势图里剔除 partial: true 和带 skippedPaidGraders 的结果;每次改动都跑的“快套件”只放免费 grader 并用 --ablation none。

4. 长期维护清单

  • 为关键 skill 同时保留“应该触发”和“不应该触发”的 case。
  • 固定 --model 与 --judge-model,让分数跨时间可比;换模型当作一次受控实验。
  • 新模型发布、插件重大改动后重跑 suite。
  • evals/ 入库,evals/results/ 忽略;mocks/.replay/ 要入库,保证 CI 可重复。
  • 优先免费 grader,llm/baseline 只在必须时使用。

五、工作原理

  1. 写用例:在插件的 evals/ 下,每个场景一个目录,包含 prompt.md(或 case.yaml)和 graders/,每个 grader 一个文件。可用 claude plugin eval init 让 Claude 帮你生成。
  2. 执行:运行 claude plugin eval .。每个 case 开全新的非交互会话,一组只加载你的插件,另一组什么都不加载,各跑 3 次(默认值),共 6 个会话。会话环境是干净的,没有 CLAUDE.md、用户设置和其他 skill。
  3. 打分:用 grader 做通过/失败判定,既有 tool_used、regex 这类确定性检查,也有 llm 评判。单次 run 的分数是通过的 grader 占比,case 分数是各次 run 的平均值。
  4. 对照:输出 WITH(有插件)、W/OUT(无插件)和 Δ(二者之差),同时生成 JSON 和 HTML 报告。
  5. 接入 CI:用 --threshold 设分数门槛。成本上限等其他参数以 --help 为准。

核心:用 Δ 衡量插件的边际贡献,而不是看绝对分数。Δ 接近 0 说明基础 Claude 本来就能做,或者 skill 没被触发。Δ 的可信度取决于 grader 设计,建议同时覆盖结果是否正确,以及是否真由插件产出(如 skill 是否被调用)。

陆、边界与陷阱

  1. Δ 才是插件贡献的度量,但它不是 CI 的退出条件。
  2. 先让触发稳定,再优化输出。 触发都不稳,后面的质量分没有意义。
  3. 双臂下部分 grader 自动不计分(scored: false):所有 tool_used: Skill、指向插件自带 MCP 的 mock_calls 检查,以及你标了 arm: with-only 的 grader,会在两臂都被排除,只在 with 臂显示 pass/fail 指示。若 case 的 grader 全被排除,则退回正常计分;arm: both 可强制两臂都计分;--ablation none 下什么都不排除,所以同一 suite 单臂与双臂的绝对分数可能不同。(因此对 tool_used: Skill 来说,arm: with-only 其实是默认行为,显式写出只是增强可读性。)
  4. 隔离会话不等于真实环境:你的用户设置、hooks、CLAUDE.md、个人 MCP、其它已装插件、memory、其它 skill 全都不加载;Artifact 工具关闭;case 定义对被测 agent 不可见;受管策略仍会生效。
  5. run 出错不等于 0 分:超时等异常 run 仍按已产出内容评分,error 字段非空只是标记;但用量/限流错误会让后续 run 大量得 0 分,且 suite 不会被标为 partial,看到整体暴跌先查 NOTES 和 error。
  6. 安全边界:评估插件等同于 --plugin-dir 的信任决策,会在你的机器上以你的身份运行其 skills/hooks/agents;scaffold_script 需 --scaffold,真实 MCP 需 --allow-real-servers/--mocks off;hooks 与真实 server 在沙箱之外。suite 通过不代表插件安全。
  7. 基线臂里找不到你的 agent 是正常的:without 臂没有加载插件,dispatch 插件 agent 会报 Agent type ... not found,预期内。
  8. 其它易踩坑:regex 默认只看 last_message;trace 是逐行 JSON,引号要写成 \";files 目标只是路径列表;file_exists 只认本次 run 新建的文件;默认阈值 1.0 会让任何不完美的 case 导致退出码 1;基线臂缺失时给 case 加 plugins: ["../.."]。

七、15 个追问:检验你是否真正掌握

说明:其中涉及官方文档明确说明的,直接引用机制;文档未覆盖的(标注“工程建议”),是基于机制的推断。

一、关于“测量什么”

Q1. WITH 与 W/OUT 都是 1.0、Δ = 0:是插件没用,还是用户本来就能做好?

这两种说法往往是同一件事的两面,区分要看“过程”grader。

  • tool_used: Skill 通过但 Δ=0:skill 被用了,却没改变结果。要么题目太简单(基础模型本来就会),要么你的结果 grader 太粗,看不出差别(比如只查了格式)。
  • tool_used: Skill 失败且 Δ=0:先是触发问题,Claude 靠自己做对了,所以没差异。

后续行动不同:题太简单 → 换成只有插件能做好的场景(团队私有规范、模型不知道的上下文、特定产出结构),收紧 grader;确认任务不简单仍无差异 → 老实问自己这个 skill 是否值得发布。注意 Δ 只衡量你写进 grader 的维度,速度、token 开销、一致性不在其中。

Q2. “没触发”与“触发了但结果差”,哪个对作者更致命?

没触发更常见,也更隐蔽:整个插件的价值为零,且后面的质量设计全未被检验。官方把触发列为第一发现,正是此意。触发了但结果差则更“显眼”,Δ 为负代表插件在拖后腿,属于主动伤害。建议的排障顺序:先修触发(description),再修结果(skill 正文)。

Q3. 去掉无插件基线会失去什么?何时单臂就够?

失去的是“这是不是插件的功劳”。WITH 高可能只是模型本来就强,也无法判断新模型是否让你的插件变得多余。单臂够用的场景:调试 grader 本身(--ablation none 可省一半成本);每次提交都跑的快速回归(已通过双臂确认过 Δ);接续历史对话的 case(目标为路径时默认单臂,因为假设录制的对话已包含插件作用)。

二、关于 Grader 设计

Q4. 为什么 tool_used: Skill 不能直接计分?

因为无插件臂里这个 skill 不存在,该 grader 必然失败。若计分,W/OUT 会被系统性拉向 0,Δ 被机械性抬高,哪怕插件毫无贡献。所以官方在双臂下自动将其排除,只在 with 臂显示指示。反过来,“不该触发”类用例要在两臂都计分,设 arm: both 并配 min: 0, max: 0。

Q5. 何时优先免费 grader?何时免费 grader 会假通过?

优先免费 grader:断言结构化事实(命令是否执行、文件是否存在、输出是否含/不含某模式)、长输出(对文件内容用 regex,每次判定一致)、CI 快套件。工程建议:免费 grader 的“假通过”常来自检查得太浅——regex 命中的是回复里无关的位置;tool_used 的 input_match 过宽;file_exists 只证明文件存在,不证明内容正确;tool_order 只比较“第一个匹配”调用;tool_used: Skill 通过只说明 skill 被调用,不说明答对。因此需要“结果”与“过程”两类 grader 配合。

Q6. grader 三次 run 里有时过有时不过,如何区分随机性与 description 不稳定?

先在 report.html 里看是哪个 grader 在翻转。

  • 翻转的是 tool_used: Skill:说明 description 处于“边缘触发”,这是真问题,要改 description,而不是多跑几次。
  • 翻转的是 llm grader:看 judge 的三次投票和它看到的证据,通常是 rubric 模糊或 judge 太小,换 --judge-model 或把 rubric 改成具体条件,必要时改用 regex。
  • 想看清分布:把该 case 的 runs 提高(上限 50)。工程建议:同一个 case 在不同表述(paraphrase)下也跑一遍,若只在个别表述翻转,说明是描述覆盖不足。

三、关于成本与迭代效率

Q7. 何时用 --runs 1 --ablation none?何时必须回到默认?

前者用于:调 grader、排查某个 case、对 description 改动做第一眼验证。后者用于:宣称“Δ 变好了”、合并前、对比模型、任何要写进结论或进入门禁的数字。官方明确提醒:单次 run 有噪声,要用默认 3 次确认。另外,单臂下不会排除 tool_used: Skill,所以绝对分数和双臂不可直接比。

Q8. --threshold 怎么定?太高/太低各有什么问题?

阈值只作用于 with 臂的绝对分数。太高(默认 1.0):任何一次随机失败都会让 CI 退出码为 1,团队很快学会忽略红灯。太低:真实回归被淹没。注意粒度:1 个 grader、3 次 run 时,case 分数只能是 0、0.33、0.67、1,此时 0.8 实际等同于 1.0;2 个 grader 时则允许某个 grader 失败一次。工程建议:在稳定版本上连跑几次,观察分数分布,把线设在分布下沿略下方;关键 case 用 tags 分层并单独收紧。

Q9. 预算超限,先砍什么?

工程建议的优先级,从“损失信号最少”到“最多”:①把能免费的 grader 换成免费的、日常迭代走 --ablation none;②用 --tag/--case 分层(smoke 常跑,全量低频跑);③改用更便宜的 judge(但小 judge 会误判,先验证稳定性);④最后才降低 runs 或删 case。关键 case 最后动,runs 降到 1 会让噪声吞掉信号。--max-cost-usd 只是保险丝,且是标价估算,不等于你的套餐用量。

四、关于与真实使用的关系

Q10. plugin-evals 不能告诉你什么?

它不能告诉你:真实对话里的触发率;多轮长期使用体验;与用户已装其它插件、CLAUDE.md、memory、个人 MCP、hooks 的相互干扰(这些在隔离会话里全都不存在);受管环境之外的行为;插件本身是否安全。它还要求你把测试题写得像真实用户的话,质量取决于你的 prompt 覆盖面。

Q11. evals 分数很高,真实用户却很少用,差了哪一层?

差“竞争”与“分布”两层(工程建议)。真实环境里你的 skill 要和用户已装的所有 skill/插件争夺选择,而 eval 是只有你自己的“净室”;你写的 prompt 也不等于用户的真实表述分布。补这一层用线上使用数据:官方的“Measure plugin cost and usage”文档讲插件给会话增加的上下文成本,以及人们是否仍在使用它。

Q12. 改完 description 后 Δ 上升,如何确认不是“适配了你的测试 prompt”?

这是过拟合问题(工程建议):①把 case 分成“调参集”和“留出集”,改动只依据前者,用后者验收;②为同一意图写多种说法(口语、简写、带噪声);③保留“不该触发”的 case,防止 description 越写越宽;④不要在 prompt 里提 skill 名字;⑤优先看触发率与负例误触,而不是只看总分。

五、关于系统边界与可靠性

Q13. 何时写 case.yaml?scaffold_script 和 mocks/ 各解决什么、引入什么风险?

只需 prompt 时不用写。需要 context.*(造文件/git、接续对话、只读夹具),或想把整个 case 写进单一文件时才用。

  • scaffold_script:解决“空目录起步”的问题。风险:它以你的身份在 agent 沙箱之外运行,需 --scaffold,只有最小环境、120 秒超时,失败则该 run 记 0;且只能造文件和 git 状态,项目级配置不会被加载。
  • mocks/:解决“不想依赖真实服务”的问题,并能用 expect: 断言插件发给服务的入参。风险:mock 与真实服务漂移(可用 _tools.json 保存真实工具 schema 缓解);type: agent 的答案随 judge 变化,需把录制结果采纳到 mocks/.replay/ 才可复现;mock 通过不代表真实服务可用;启用真实 server 则在沙箱外运行。

Q14. run 因超时/API 错误结束,但部分 grader 仍通过,怎么解读?

看 cases[].arms.with[].error。官方设计是:异常结束的 run 仍按已产出内容评分,所以非空 error 不等于 0 分。解读要分情况:基础设施问题(限流、用量耗尽、网络)是噪声,应剔除或重跑;与插件行为相关的超时(比如陷入循环、触顶 max_turns)是真信号。比较两臂:若只有 with 臂超时,更可能是插件引起。aborted(mock 的 expect 违规)记 0 分且 error 为 null;skippedPaidGraders 表示成本上限跳过了 judge,分数不可比。

Q15. 新模型发布后整体下降,是插件回归还是模型变化?

关键在于控制变量与利用双臂。官方建议在 CI 中固定 --model,避免把模型迭代误认为插件回归。做法:①同一版插件分别用旧、新模型各跑一遍;②看 W/OUT 是否同时变化——基线也变,说明是模型本身行为改变;基线不变而 with 变,才指向插件与新模型的兼容问题;③回到 tool_used: Skill:触发率下降说明新模型的选择行为变了,需要重写 description;④用 --judge-model 固定判官,避免“判官变了”被误读。设计上,你的 suite 有双臂、固定模型参数与结构化 JSON,已具备支持这种区分的条件,前提是你把 Δ 和触发指示都存下来。

八、总结:掌握的标志

当你能自然地回答下面这些问题,就基本掌握了 Plugin Evals:

  • 这个 skill 在自然表述下会被触发吗?(tool_used + 负例)
  • 触发后是否明显优于无插件?(Δ 为正且稳定)
  • 改动后是变好还是变差?(固定模型、同一 suite、3 次双臂)
  • 失败的是插件、judge、grader 还是环境?(report.html、error 字段、scored: false 指示)
  • 成本与信号怎么平衡?(免费 grader 优先、分层运行、合理阈值)

Plugin Evals 不是“再加一层测试”,而是让插件开发从“凭感觉”走向“有证据”。从今天开始,给你最重要的 skill 写第一个 case,跑一次,看一眼 Δ。