两份文档,一条完整的链
Anthropic 最近有两份关于评测的官方文档,放在一起读很有意思:
- 《定义成功标准并构建评估》(Claude 开发者平台):讲方法论。先定义什么叫「好」,再设计评估去量它。
- 《使用 evals 测试插件》(Claude Code):讲工程落地。给插件和 Skill 作者一个开箱即用的评测命令
claude plugin eval。
前者回答「评什么、怎么打分」,后者回答「怎么把评测做成可重复、可门控的工程环节」。本文按这条线拆:先拆方法论,再拆工具,最后提炼值得借鉴的设计。
第一部分:方法论——先定义成功,再谈评估
成功标准的 SMAR 四点
文档要求好的成功标准满足四点:
| 特性 | 要点 |
|---|---|
| 具体 Specific | 明确目标,不说「性能良好」,而说「准确的情感分类」 |
| 可衡量 Measurable | 用定量指标,或定义明确的定性量表 |
| 可实现 Achievable | 参考行业基准、既有实验和专家知识,不超出当前模型能力 |
| 相关 Relevant | 与应用目的、用户需求一致 |
它给的对比很直观,哪怕是「安全」这种模糊概念也能量化:
| 标准 | |
|---|---|
| 差 | 安全的输出 |
| 好 | 在 10,000 次试验中,被内容过滤器标记为有毒的输出少于 0.1% |
情感分析的例子则把四点全用上了:在 10,000 条多样化 Twitter 帖子的保留测试集(相关)上,F1 至少 0.85(具体、可衡量),比当前基线提高 5%(可实现)。
一个应用,多个维度
文档强调大多数用例需要多维标准。它列了 8 类常见维度:任务保真度、一致性、相关性和连贯性、语气和风格、隐私保护、上下文利用、延迟、价格。情感分析的「好」标准是这样组合的:
- F1 ≥ 0.85
- 99.5% 的输出无毒
- 90% 的错误只造成不便,而非严重错误
- 95% 的响应时间低于 200 ms
注意第三条:「不便」与「严重」的界限需要在实际项目里自己定义。错误要分严重程度,这一点后面还会反复出现。
构建评估的三条原则
- 针对特定任务。 测试集要反映真实任务分布,并包含边缘案例:不相关或不存在的输入、过长的输入、质量差或有害的输入、连人类都难以达成共识的模糊用例。
- 尽可能自动化。 把题目组织成可自动评分的形式。
- 数量优先于质量。 更多问题配上信号稍弱的自动评分,好过更少问题配高质量人工评分。
第三条最容易被反直觉地忽略。很多团队习惯精心打磨 20 条题、全部人工看,结果一次换模型就不敢动了。官方的立场是:覆盖面更重要,单条信号可以粗一点。
六种评分方法的选型
文档把评分方法和评估的维度一一对应,这张表几乎可以当选型速查:
| 评估维度 | 方法 | 衡量内容 | 文档中的测试集规模 |
|---|---|---|---|
| 任务保真度(情感分类) | 精确匹配 | 输出规范化后是否等于预定义答案 | 1,000 条人工标注推文 |
| 一致性(FAQ 机器人) | 余弦相似度 | 改写问题的回答在句子嵌入空间中的相似度 | 50 组问题,每组含若干改写 |
| 相关性和连贯性(摘要) | ROUGE-L | 候选与参考摘要的最长公共子序列 F1 | 200 篇带参考摘要的文章 |
| 语气和风格(客服) | LLM 李克特量表 | 让 LLM 按 1–5 给语气(同理心、专业等)打分 | 100 条带目标语气的咨询 |
| 隐私保护(医疗) | LLM 二元分类 | 响应里是否含 PHI(yes/no) | 500 条模拟患者查询 |
| 上下文利用(对话助手) | LLM 序数量表 | 引用并基于对话历史的程度,1–5 | 100 个含依赖上下文问题的多轮对话 |
规律很清楚:能用代码确定性判断的就用代码(精确匹配、余弦相似度、ROUGE-L),只有主观维度才交给 LLM(语气、隐私判断、上下文利用)。
精确匹配的实现朴素得近乎「简陋」,但这恰恰是重点:
def evaluate_exact_match(model_output, correct_answer):
return model_output.strip().lower() == correct_answer.lower()配合提示词里的「只输出标签」约束,一个分类任务的评估就做完了。
测试集里的边缘案例设计
文档示例中的边缘案例很值得抄:
- 情感分类:反讽(「航班延误 5 小时我真是太喜欢了 #bestdayever」)、混合情感(剧情糟糕但演技出色)
- FAQ 一致性:错别字(「Wut's yur retrn polcy?」)、冗长啰嗦的问题、夹带无关信息的问题
- 摘要:多主题文章、误导性标题
- 隐私:显式 PHI、假设性 PHI、隐式 PHI(「我儿子在吃和他父亲去年一样的药」)
- 客服:愤怒的客户、复杂问题、以抱怨形式出现的夸奖
这些案例的共同点是:都是模型最容易在语义上栽跟头的位置,而不是随机抽样能轻易覆盖到的。
LLM 评分的两条实操细节
代码注释里有一句不起眼但很关键的话:用 LLM 评分时,通常最佳做法是使用与生成被评估输出不同的模型来评估。另外,评分提示只要求输出「数字」或「yes/no」,便于程序解析。
这两点对应着自评偏差和解析稳定性两个真实问题。
第二部分:claude plugin eval——把方法论做成命令
方法论讲完,进入工具。claude plugin eval 针对一套测试用例运行你的插件,并给结果打分。它的核心设计可以拆成四层。
1. 用例(Case):一个目录一个场景
一个 eval 套件位于插件内的 evals/ 目录,每个用例是一个子目录:
my-plugin/
├── .claude-plugin/plugin.json
├── skills/...
└── evals/
├── first-case/
│ ├── prompt.md # frontmatter: 用例字段;正文: 提示词
│ ├── graders/
│ │ ├── criteria.md # frontmatter: 类型+选项;正文: 评分标准
│ │ └── skill-fired.md
│ └── case.yaml # 可选:仅用于 context.* 字段
└── results/ # 每次运行写入;加入 .gitignore几个值得注意的设计:
- 文件即配置。 提示词是
prompt.md的正文,运行限制(max_turns、timeout_seconds、allowed_tools等)在 frontmatter 里,没有任何数据库或平台依赖,用例天然可以进 Git、走 Code Review。 - 每个用例至少一个评分器,否则加载失败。没有评分标准的用例不允许存在。
- 提示词要写成用户真实会说的话,而不是直接点名 Skill。 文档特别强调这一点,否则测不出 Skill 的触发率。
另外有一个脚手架命令 claude plugin eval init:它会读取你的插件,询问你期望的结果,提议「应该触发」和「不应该触发」的提示词,为每个提示词设计评分器,先试运行一次确认评分器的行为,再写入文件。要点在于「不应该触发」也是一等公民,Skill 误触发和漏触发同样是缺陷。
2. 运行(Run):隔离、多次、有上限
每次用例运行,Claude Code 都会启动一个全新的、隔离的非交互式会话,仅加载你的插件,发送提示,直到完成或触及轮次/时间上限。
默认每个用例跑三次,因为「一次非确定性代理的运行告诉你很少」。运行分数是其通过的评分器的比例(可加权),用例分数是多次运行的平均值,达到 --threshold(默认 1.0)才算通过。
隔离做得相当彻底:
| 隔离项 | 行为 |
|---|---|
| 个人与项目配置 | 用户设置、hooks、CLAUDE.md、MCP 服务器、其他插件、memory、skills 全部不加载 |
| 项目级配置 | 工作区内的 .claude/、CLAUDE.md、.mcp.json 都不会读取,连脚手架脚本写入的也不读 |
| 环境变量 | 只放行允许列表和 EVAL_* 前缀变量 |
| 用例定义 | 对被测代理隐藏,Claude 看不到提示词、评分器和兄弟用例 |
| 工具权限 | 默认只读;Bash、Write、Edit、WebFetch 等必须用 --allow-tools 显式授予,且 Bash 在 OS 级沙箱下运行 |
| 权限提示 | 运行永远不会停下来请求权限,没授予的工具直接从会话里移除 |
这套隔离的价值在于可重复性:你的本机配置不会污染结果,换一台机器、换一个人跑,结论应该一致。
3. 评分器(Grader):六种类型,成本分两档
评分器是对 Claude 产出的通过/失败检查。六种类型:
| 类型 | 检查什么 | 成本 |
|---|---|---|
regex | JavaScript 正则在目标中是否匹配(可要求缺失或恰好 N 次) | 零 |
tool_used | 某个工具被调用的次数,可按输入内容过滤,min/max 控制范围 | 零 |
tool_order | 两个工具调用的先后顺序 | 零 |
file_exists | 运行期间是否创建了匹配 glob 的文件 | 零 |
llm | 评判模型对评分标准至少 3 票中 2 票 PASS | 调用模型 |
baseline | 评判模型认为运行至少与参考记录一样满足标准 | 调用模型 |
前四种从会话记录和文件计算,不产生模型成本;后两种需要评判模型,会增加成本。llm 评分器采用多票表决(三次投票取多数)来压低评判模型自身的随机性。
评分器可以查看的对象也有明确分类(target / focus):最后一条消息、完整会话记录(trace)、创建的文件路径列表、某个文件的内容,以及对 mock MCP 工具的调用记录。
一个很实用的组合是文档给出的「两个评分器」模式:
---
type: tool_used
tool: Skill
input_match: '"skill"\s*:\s*"(?:[\w-]+:)?your-skill-name"'
---一个评分器检查结果(最终消息或产物),另一个检查过程(有没有真的调用到你的 Skill)。两者结合才能回答:「答案对不对」和「是不是你的插件产出的」。
文档里的稳定性建议
这部分与我们在自己评测里踩过的坑高度重合:
- 长输出(例如生成的文件)用
regex对文件内容评分,每次都以相同方式检查整个文件;llm评分器留给短输出,并用具体的 PASS/FAIL 条件写评分标准。 - 如果
tool_used: Skill通过但Δ为负,先怀疑评判模型而不是插件——小型评判模型可能因格式与评分标准描述不同,把正确答案判错。换更强的评判模型重跑,并收紧评分标准,让格式不决定判定。 - 要验证构建或测试是否通过,让提示词要求 Claude 运行并把结果写入文件,对该文件评分,再用
tool_used加input_match断言命令确实被执行过。 - 没有自定义代码评分器,文档明确这样说。能力边界被刻意收窄。
4. 基线(Baseline):with vs without 与 Δ
这是整个设计里最漂亮的一环。
仅凭高分不能告诉你插件是否有帮助,因为 Claude 可能在没有插件的情况下也能做得很好。
所以每个用例默认跑两组:加载插件(with-arm)和不加载插件(without-arm),得到 WITH、W/OUT 两个分数,二者之差 Δ 就是插件的净贡献:
CASE WITH W/OUT Δ RUNS COST NOTES
first-case 1.00 0.33 +0.67 6 $0.41如果一个用例有无插件都是 1.0,那插件就不是它通过的原因。
这里有一个细节处理:「Skill 被调用了」这类检查在没有插件时永远不会通过,计入分数会把 without-arm 压到 0,从而夸大 Δ。因此这类评分器在两个 arm 里都不计入分数,只在 with-arm 里作为通过/失败指示器展示;需要时可以用 arm: both 强制两边都评(比如「不得调用某 Skill」这类反向检查,min: 0、max: 0)。
文档给的典型首次发现是:Δ 接近零、tool_used: Skill 失败——意味着在自然措辞下,Claude 根本没选你的 Skill。解法不是改代码,而是改 Skill 的 description,再跑一遍对比。
夹具与 Mock:让依赖外部服务的 Skill 也能被测
Skill 常常调用 MCP 工具,而真实服务不可控。plugin eval 的做法是在 evals/mocks/<server>/<tool>.md 里放 Markdown 文件,正文就是工具返回给 Claude 的内容:
---
expect:
title: string
priority: [low, medium, high]
---
Created issue #4821: {{input.title}}{{input.<field>}}把调用输入回显进结果expect:校验 Claude 传给工具的输入,违规则以 0 分中止运行并记录原因——这让你能断言「插件要求服务器做的事对不对」error: true返回工具错误,用来测错误处理路径type: agent让评判模型以服务器的身份回答,并支持把答案录制后回放(mock-recordings/→mocks/.replay/),让 CI 运行可重复且不再产生模型调用
没有 mock 文件的工具对 Claude 不可用;默认不会启动真实 MCP 服务器,除非显式 --allow-real-servers 或 --mocks off。
需要文件或 git 状态时,用 case.yaml 的 context.scaffold_script(脚本在代理沙箱之外运行,因此必须传 --scaffold 才会执行)、context.history_file(恢复一段历史对话)和 context.add_dirs(给 Claude 只读的夹具目录)。
第三部分:进 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几个参数背后的工程考虑:
--model与--judge-model分别固定:避免模型发布被误认成插件回归,评判模型固定才能保证分数跨时间可比。--max-cost-usd设成本上限:每次运行开始前检查,超限后不再启动新的运行,已开始的会完成,所以实际花费可能略超。--trust-plugin让作业不会卡在首次信任提示上。-j并发最大 8,只缩短耗时,共享账户的速率限制,不会提高吞吐上限。
退出码设计得很清晰:
| 退出码 | 含义 |
|---|---|
| 0 | 套件通过:所有用例得分达到阈值,且无其他失败原因 |
| 1 | 有用例低于阈值、用例加载失败、没找到用例、无法启动、或有运行因 API 错误而结束 |
| 2 | 部分运行:触及成本上限或凭据被拒,results.json 仍写入并带 partial: true 与原因 |
| 130 / 143 | 被中断 / 被终止(如 CI 超时) |
两个值得记住的原则:
Δ只报告,不影响退出码。 门控看的是 with-arm 分数是否达标,基线对比留给人看趋势。- API 错误不静默当作 0 分混过去。 因用量/速率限制而结束的运行会被记为 0 分且不评分,命令以非零退出,哪怕所有用例的分数都够。这避免了「限流导致的假通过/假失败」污染结论。
JSON 结果是版本化的(schemaVersion: 1),脚本应忽略不认识的字段;趋势图要把 partial: true 的文档以及有 skippedPaidGraders 的运行剔除,因为它们的分数不可比。
控制 CI 成本的建议也很具体:每次提交都跑的快速套件只用不调用评判模型的评分器,不需要 Δ 的地方用 --ablation none。
值得借鉴的设计
把两份文档对照着读,我提炼出这几点:
- 成功标准先于评分。 没有「0.85 F1、99.5% 无毒、95% 低于 200 ms」这样的数字,评分器写得再漂亮也无从判断好坏。
- 确定性评分优先,LLM 评分兜底。 官方示例里六种方法只有三种用到 LLM,而 plugin eval 的六种评分器里有四种零成本。LLM 只用在语气、隐私判断这类没法写规则的地方,并且用多票表决降低随机性。
- 一个用例同时评结果和过程。 答案对了不代表是你的 Skill 做的,反之亦然。
- 用 with vs without 基线分离「插件的贡献」和「模型自己的能力」。 分数本身不说明问题,
Δ才说明问题。 - 非确定性靠多次采样吸收。 默认三次,不是出于洁癖,而是单次运行信息量太低。
- 评测配置做成文件,进版本控制。 用例、评分标准、mock 都是 Markdown/YAML,可 Review、可 diff、可回溯。
- 把「失败了但不是被测对象的错」单独标识出来。 限流、成本上限、凭据失败、部分结果,都有独立的退出码或字段,不混进分数里。
- 评测本身也要被约束。 成本上限、沙箱、信任提示、权限最小化:评测会真实调用模型、真实执行工具,必须像生产系统一样对待。
如果你也在自建评测平台,这八条基本可以当成检查清单。比如我们自己在复测一个 Jira Skill 的时候,就遇到了「AI 评判放过转述错误」和「单 case 结论不可靠」的问题,解决方式(脚本逐字对比、100 case × 3 次采样)与官方的思路是同向的:能确定性判断的绝不交给模型,样本量要足以暴露低频致命错误。
总结
- 方法论层: SMAR 成功标准 → 覆盖边缘案例的测试集 → 按维度选评分方法(确定性优先,主观维度用 LLM)。
- 工具层:
claude plugin eval把用例、评分器、基线三者做成了文件化、可重复、可隔离的流水线,且默认就包含多次采样与 with/without 对比。 - 工程层: 固定被测模型与评判模型、设置成本上限、用明确的退出码区分「真失败」和「运行没跑完」,才能让评测成为可信的 CI 门控。
两份文档合起来传达的只有一个观点:评测不是跑完拿个分,而是一套让分数可信、可比、可追溯的工程体系。
欢迎访问 PrimeSkills —— 一个精心策划的 AI Agent 与技能市场,所有内容均经过真实企业级工作流验证。没有噱头,只有真正有效的东西。
更多实用知识和有趣产品,欢迎访问我的个人主页