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

人工智能 Oct 19, 2026

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

掌握 Plugin Evals 精髓:让 Claude Code 插件质量可测量、可回归、可进 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 是否被调用)。

claude_code-plugin_evals_execution_flow

陆、边界与陷阱

  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,跑一次,看一眼 Δ。

Tags

轩帅

轩帅,字琼璞,逍遥自在轩城主,晚晴幽草轩轩主,静轩之别苑阁主,悠然宜想亭主持。

Great! You've successfully subscribed.
Great! Next, complete checkout for full access.
Welcome back! You've successfully signed in.
Success! Your account is fully activated, you now have access to all content.