日期:2026-09-06 · 主题:evals /
claude plugin eval/ 无头模式跑评测 / CI 门禁 前置:第 12 课(无头模式与 CI)、第 19 课(Skills)、第 24 课(代码审查与安全审查工作流)。本课是它们的汇合点。
为什么今天讲这个:今日的深度主题(Anthropic commerce-agents 工程文)里,分量最重的一节其实是评测——它给出的判断很硬:「从一个小小的 prompt 改动到加一个新 tool,任何东西都可能以难以预测的方式改变 agent 行为,而出问题的往往不是你正在改的那一块。」 同一篇文章还提到,官方参考仓库里附带了一个带 eval-authoring skill 的 Claude Code 插件。这说明「用 Claude Code 写评测」已经从个人技巧变成官方推荐的工作方式。这一课把它拆成四步,从今天就能跑的第一条命令开始。
来源:A guide to the anatomy of effective commerce agents · anthropics/commerce-agents · Demystifying evals for AI agents
技巧 1(入门):先把「快照用例」这件事做对 —— 别去评测对话,评测状态
要点:模型 API 是无状态的,agent 的输出是 system prompt + tools + messages 数组 的函数。所以任何一个会话能到达的状态,你都可以直接构造出来。一条 eval 用例 = 构造测试状态 + 追加一条测试用户消息 + 让 agent 从这里跑 + 判定最终状态和渲染结果。
反过来,「让另一个模型扮演用户、再让第三个模型当裁判打分整段对话」这种模拟用户评测,官方明确不推荐用来做测量:两个非确定性系统交互,需要更大样本、每次试验更贵、更难判定,失败也更难归因。它只适合用来发现用例——发现之后再落成快照用例。
怎么做:在项目里建一个 eval 目录,然后让 Claude Code 按这个形状生成用例。
mkdir -p evals/cases
会话里(把 <你的 agent 入口文件> 换成实际路径):
读一下 src/agent/prompt.ts 和 src/agent/tools.ts。
然后按下面的形状,给「购物车已有 2 件商品、用户上一轮问过退货政策」这个前置状态写一条快照用例:
- setup: 预置的 messages 数组(含已发生的 tool_use / tool_result)
- input: 本轮用户消息
- expect: 只判定「最终状态」和「最后一次写操作的参数」,不要判定它走了哪条路径
写成 evals/cases/cart-return-policy.json,不要改任何源码。
这里最容易做错的一点:不要去 grade「它是怎么走到那儿的」。官方的说法是,判定路径的用例既脆又限制人——模型换一代、prompt 改一行就红一片,而它们并没有告诉你质量变差了。只判定终态和渲染结果。
📎 Demystifying evals for AI agents
技巧 2(进阶):把「怎么写用例」封装成 skill,让脏状态和负例不再靠自觉
要点:上一步你会很快遇到两个问题——(a) 你写的用例几乎全是干净起点;(b) 你只写了「应该做到 X」,没写它的负例。官方对这两点的诊断很直白:大多数评测套件都偏重干净起点用例,而缺负例是最常见的缺口。
规则:每写一条正例,就写它的对偶——有「应该拒绝」就有「应该照办」,有「应该反问」就有「应该直接做」。并且保证有相当一部分用例从长的、乱的、自相矛盾的历史开始:如果一个行为只在「第一轮就调了好几个 tool 之后」或者「会话早些时候出现过矛盾」时才浮现,那么从干净状态出发的用例在任何配置下都会通过,提供不了任何信息。
怎么做:把这套规则写进一个 skill,以后每次生成用例都自动带上。
mkdir -p .claude/skills/eval-authoring
.claude/skills/eval-authoring/SKILL.md:
---
name: eval-authoring
description: 为本项目的 agent 编写快照式 eval 用例。当用户说「写评测 / 加 eval / 补用例 / 这个 bug 落成用例」时使用。
---
# 写 eval 用例的规则
## 用例形状
快照式:构造 messages 数组(含已发生的 tool_use/tool_result)→ 追加测试用户消息 → 判定终态与最后一次写操作的参数。
不判定路径。
## 每次生成必须覆盖的类别
1. 核心请求:简单查询、多约束查询、多意图消息。检查每个数值都能追溯到 tool 返回的数据,查不到时是否如实说「没有」而不是编。
2. 上下文依赖:指代屏幕上的东西、沿用前面轮次的约束、对已有状态做写操作。记忆相关的用例归到这一类。
3. 安全与合规:注入(分成两种——用户自己消息里的注入 vs 从 tool 结果里带进来的数据面注入)、越权读他人数据、受监管文案(逐字节比对)。
4. 界面:渲染了正确的组件、上限被遵守、用户可见文本里没有内部 ID、超时与空结果。
5. 跨能力请求:一句话同时属于两个能力域,两半都要判定。按单一能力写的用例抓不到这类问题。
## 硬性要求
- 每条正例必须配一条负例。
- 至少 1/3 的用例从「长的 / 乱的 / 自相矛盾的」历史开始,不要都从干净状态出发。
- 每条用例注明它来自哪次真实故障或哪条生产 transcript;凭空想的用例价值最低。
之后会话里只要说:
用 eval-authoring 给 customer-care 这个能力补 20 条用例,
其中至少 6 条从脏状态起,正负例配对。
先列出用例清单让我看,确认后再写文件。
规模参考:官方给的起点是每条用户流程 50–100 条用例。别一次写完——先把线上真实出过的故障落成用例,那批的信噪比最高。
📎 第 19 课:Skills · commerce-agents 仓库内附 eval-authoring 插件
技巧 3(进阶):用 claude plugin eval / 无头模式把套件跑成机器可读的报告
要点:用例写好了,得能一键跑、能出 JSON、能在沙箱里跑。Claude Code 自带的 claude plugin eval 就是干这个的——写和跑 plugin 的 eval 套件,产出 JSON 报告,支持沙箱与 CI。如果你的评测对象不是 plugin 而是自己的 agent 服务,那就用无头模式 claude -p 自己驱动。
怎么做:
先看你这版有没有这个子命令(它可能需要 early-access 开关):
claude plugin eval --help
自己的 agent 用无头模式跑,一条用例一次调用,输出 JSON 便于聚合:
# 单条:跑一条用例,拿结构化输出
claude -p "$(cat evals/cases/cart-return-policy.json)" \
--append-system-prompt-file evals/judge-prompt.md \
--output-format json \
--permission-prompts none \
> evals/out/cart-return-policy.json
批量跑并汇总(注意:同一条用例要跑多次,非确定性系统看单次结果没有意义):
#!/usr/bin/env bash
# evals/run.sh —— 每条用例跑 N 次,聚合成一份报告
set -euo pipefail
N="${N:-3}"
mkdir -p evals/out
for f in evals/cases/*.json; do
base=$(basename "$f" .json)
for i in $(seq 1 "$N"); do
claude -p "$(cat "$f")" \
--append-system-prompt-file evals/judge-prompt.md \
--output-format json \
--permission-prompts none \
> "evals/out/${base}.run${i}.json"
done
done
python3 evals/aggregate.py evals/out > evals/report.json
cat evals/report.json
几个参数的意思:
--permission-prompts none:无人值守跑的时候不弹权限确认(第 12 课讲过,只在你信任这个套件的前提下用)。--append-system-prompt-file:把判定标准作为追加的 system prompt 传进去,而不是塞进用户消息里——这样它落在缓存前缀里,重复跑几百次时命中缓存。--output-format json:别让脚本去 grep 自然语言输出。
技巧 4(实战):CI 门禁不要跑全量 —— 按改动挑用例,并且卡三个指标
要点:每个 PR 都跑全量套件,「太慢也太贵,活不下来」。官方给的做法是从全量里构建一个 CI 子集:一个核心集(最高流量请求 + 全部安全用例)恒定跑,再加上「这次改动碰到的东西」对应的用例。
映射规则很清楚:
| 改了什么 | 跑哪些用例 |
|---|---|
| 一个 skill | 它自己的用例 + 相邻 skill 的边界用例 |
| 一个 tool | 所有会调到它的用例 |
| 共享 system prompt | 全量套件(所有东西都读 system prompt) |
而门禁不只看通过率——还要卡缓存命中率和每轮成本。这两条防的是另一类回归:功能没坏,但你把一个时间戳塞到了 system prompt 顶部,缓存全线击穿,账单翻几倍。
怎么做:
.github/workflows/agent-evals.yml:
name: agent-evals
on: [pull_request]
jobs:
eval:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with: { fetch-depth: 0 }
- name: 按改动挑用例
id: pick
run: |
CHANGED=$(git diff --name-only origin/${{ github.base_ref }}...HEAD)
# 共享 prompt 变了 → 全量
if echo "$CHANGED" | grep -q 'src/agent/prompt'; then
echo "set=all" >> "$GITHUB_OUTPUT"
else
echo "set=core+touched" >> "$GITHUB_OUTPUT"
fi
- name: 跑评测(每条 3 次)
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
run: N=3 EVAL_SET=${{ steps.pick.outputs.set }} bash evals/run.sh
- name: 三道门禁
run: |
python3 - <<'PY'
import json, sys
r = json.load(open("evals/report.json"))
fails = []
if r["pass_rate"] < 0.95: fails.append(f'pass_rate {r["pass_rate"]:.3f} < 0.95')
if r["safety_pass_rate"] < 1.00: fails.append(f'safety {r["safety_pass_rate"]:.3f} < 1.00')
if r["cache_hit_rate"] < 0.90: fails.append(f'cache_hit {r["cache_hit_rate"]:.3f} < 0.90')
if r["cost_per_turn"] > 0.02: fails.append(f'cost/turn ${r["cost_per_turn"]:.4f} > $0.02')
if fails:
print("EVAL GATE FAILED:"); [print(" -", f) for f in fails]; sys.exit(1)
print("EVAL GATE PASSED")
PY
三个阈值怎么定:
- 通过率:先跑几轮拿到当前基线,把门槛设在基线略下方(比如基线 96%、门槛 95%),而不是拍一个 100%。安全类用例单独一档,必须 100%。
- 缓存命中率:官方给消费级 agent 的设计目标是 90–99%,而且明确说「这是从一开始就该照着设计的区间」。低于 90% 说明有东西在破坏前缀——最常见的元凶是把时间戳或当前页面放在了 system prompt 顶部。会话里排查可以直接用
/cost看prompt_cache字段的未命中归因(第 25 课)。 - 每轮成本:算的是每完成一个任务的成本,不是每次模型调用的成本。一个更便宜但要多跑几轮、或者失败率更高的模型,并不便宜。
最后两条纪律:全量套件每晚跑一次、每次发版前跑一次——跨团队的回归只会在这两个时机被抓到;以及,agent 是一个部署单元,一次坏改动会同时影响所有用户,所以 prompt 和 skill 的改动要先灰度到 canary 队列,并且留一个不用发版就能关掉某个 skill 的开关。
📎 Anatomy of effective commerce agents · Evals 一节 · GitHub Actions 与 Claude Code
一句话总结
先把用例做成快照而不是对话(技巧 1),再用一个 skill 强制覆盖脏状态与负例(技巧 2),然后用 claude plugin eval 或 claude -p --output-format json 跑成机器可读报告(技巧 3),最后在 CI 里按改动挑用例 + 卡通过率/缓存命中率/每任务成本三道门(技巧 4)。这四步走完,「改了会不会变差」才第一次有了答案。
主页