Mingyu's Library主页
2026年9月6日

第 26 课:评测驱动开发 —— 让「改了会不会变差」有个答案

日期: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

几个参数的意思:

📎 Claude Code CLI 参考 · 无头模式


技巧 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

三个阈值怎么定:

最后两条纪律:全量套件每晚跑一次、每次发版前跑一次——跨团队的回归只会在这两个时机被抓到;以及,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)。这四步走完,「改了会不会变差」才第一次有了答案。