DAY 06 / PHASE 1 · ENGINEERING

Eval 工程

Golden Set · LLM-as-Judge 去偏 · Prompt Regression · Anthropic Evals

2026-05-24 · BigCat

没 eval 的 prompt = 玄学;改 prompt 凭感觉 = 用 vibes 做工程。

前置概念 → ai-ml-daily Day 15: 评估与基准(MMLU, HumanEval, MT-Bench)

// WHY THIS MATTERS

问 100 个写 prompt 的人「你怎么知道改完比改前好」,99 个会说「我跑了几个例子感觉不错」。这就是为什么大多数 AI 产品上线后越调越糟——没有 eval 的 prompt 工程不是工程,是巫术。Hamel Husain 那句被工业界引爆的话——「Your AI product needs evals」——背后的真实意思是:模型只是 commodity,你的 eval suite 才是护城河。这一期不讲 MMLU / HumanEval 那种公开基准(基本和你的真实场景无关),讲资深用户该会的四件事:怎么从 0 搭一个能跑的最小 eval、为什么 LLM-as-judge 必须做去偏才能信、怎么把 prompt 当代码做 regression test,以及 Anthropic 自己怎么用 evals 框架开发 Claude。读完你应该能在 1 小时内给手头任意 prompt 建一个真能挡 regression 的评测系统。

// 01

Minimum Viable Eval:20 个例子 + 一个 scorer 起步

论断:第一版 eval 不需要框架、不需要 LLM judge、不需要 1000 条数据——20 条真实场景 + 一个 Python 函数就足以挡住 80% 的 regression。

背景与原理

大多数人不做 eval 的真实理由不是「不知道重要」,而是「以为要先搭一套基础设施」。这是被 Weights & Biases / Braintrust / Langfuse 这些工具页面误导的结果。Hamel Husain 在 Your AI Product Needs Evals 里给出了 dead simple 的起手式:挑 20 条覆盖典型 + 边缘的真实输入,写一段 Python 跑 prompt 拿输出,再写一段 Python 给输出打分。这就是 MVP eval,多一个文件都不要。

Eval 的核心不是工具,是数据集质量。20 条精心挑过的例子(10 条 happy path + 5 条 edge case + 5 条 adversarial),价值远大于 10000 条从生产 log 里随手抓的。Andrej Karpathy 在多次 talk 里强调过同样的事——「dataset is the new code」,你的 golden set 就是 eval 的 source of truth,要像维护核心代码一样维护它(diff / review / version)。

Scorer 分三档,先用最便宜的:

Eval 的工程化层级(从下到上越贵越慢越不稳) ┌────────────────────────────────────────────────┐ │ Human eval · 真人打分 · 黄金标准但不可扩展 │ ├────────────────────────────────────────────────┤ │ LLM-as-judge · 主观任务兜底 · 必须去偏与校准 │ ├────────────────────────────────────────────────┤ │ Heuristic · 长度/格式/关键词 · 几乎免费 │ ├────────────────────────────────────────────────┤ │ Code-based · regex/schema/unit test · 首选 │ └────────────────────────────────────────────────┘ 原则:能往下落一级就别用上一级

实战示例

给一个「从邮件里抽取会议时间」的 prompt 做 MVP eval,全部不到 60 行:

# eval_meeting_extractor.py — 真能跑的最小 eval
import json, re, anthropic
from dataclasses import dataclass
from datetime import datetime

client = anthropic.Anthropic()

# ① Golden Set —— 20 条手挑数据;每条都是「输入 + 期望」
GOLDEN = [
    {"id":"happy_01", "input":"明天下午 3 点会议室 A 开周会",
     "expect":{"time":"15:00", "location":"会议室 A"}},
    {"id":"edge_tz",  "input":"Mon 9am PT sync",
     "expect":{"time":"09:00", "tz":"PT"}},
    {"id":"adv_noop", "input":"今天天气不错",
     "expect":{"time":None}},  # 不该幻觉
    # ... 17 条更
]

PROMPT = """You are a meeting time extractor. Output strict JSON:
{"time": "HH:MM" or null, "location": str or null, "tz": str or null}
Input: {text}"""

def run(text):
    r = client.messages.create(model="claude-opus-4-7", max_tokens=200,
        messages=[{"role":"user","content":PROMPT.format(text=text)}])
    return json.loads(r.content[0].text)

# ② Scorer —— 全是 code-based,便宜稳定
def score(pred, expect):
    for k, v in expect.items():
        if pred.get(k) != v: return 0
    return 1

# ③ Runner —— 一行 for loop
def evaluate():
    fails = []
    for case in GOLDEN:
        try:
            pred = run(case["input"])
            if score(pred, case["expect"]) == 0:
                fails.append((case["id"], pred, case["expect"]))
        except Exception as e:
            fails.append((case["id"], "ERROR", str(e)))
    pass_rate = 1 - len(fails) / len(GOLDEN)
    print(f"PASS {pass_rate:.0%} ({len(GOLDEN)-len(fails)}/{len(GOLDEN)})")
    for f in fails: print("  FAIL", f)
    return pass_rate

if __name__ == "__main__": evaluate()

这就够上线了。每次改 PROMPT 跑一遍,从 17/20 跌到 14/20 就是 regression。三个月后你会拥有 200 条 golden set,到那时再考虑 Braintrust / Langfuse。

失败模式:(1)从生产 log 随机抽 1000 条做 golden set——里面 80% 是简单 case,覆盖度反而比手挑的 20 条差;要按 cluster / 难度分层抽。(2)pass rate 100%——说明你的 golden set 太简单了,没有 fail 的 eval 不是 eval。(3)只看 pass rate 不看 fail 分布——哪几个 fail 是同一类?是 edge case 还是 adversarial 还是 prompt 没覆盖?分层报告比单一数字重要十倍。
进阶资源 · Hamel Husain Your AI Product Needs Evals, hamel.dev/blog/posts/evals · Eugene Yan Task-Specific LLM Evals, eugeneyan.com/writing/evals · Anthropic Create strong empirical evaluations, docs.anthropic.com/.../develop-tests
// 02

LLM-as-Judge 的偏见与去偏

论断:未校准的 LLM judge 不是 eval,是放大器——它会把你 prompt 的 bias 放大成 90% 的虚假 pass rate。

背景与原理

当任务是开放式生成(摘要质量、回答相关性、改写忠实度),code-based 失效,多数人转向 LLM-as-judge:给 judge 一段 instruction 让它给生成结果打分或挑赢家。问题是 LLM judge 本身带着系统性偏见,不去偏直接用,你测出来的是 judge 的偏好,不是模型的能力

Zheng 等人 2023 的 Judging LLM-as-a-Judge(MT-Bench 论文,NeurIPS Datasets & Benchmarks)系统性证明了四类偏见,几乎所有后续研究都建立在这之上:

这四个 bias 不是 prompt 问题,是 RLHF 训练目标的副产物,无法靠「我提醒 judge 别偏」消除——必须用结构性方法去偏。

实战示例:5 个可直接套用的去偏技巧

# —— 1. 位置交换(Position Swap)——
# Pairwise 永远跑两次:(A,B) 和 (B,A),两次都赢才算赢;不一致记 TIE
score_ab = judge(q, ans_a, ans_b)   # A 在前
score_ba = judge(q, ans_b, ans_a)   # B 在前
winner = "A" if score_ab == "A" and score_ba == "B" else \
         "B" if score_ab == "B" and score_ba == "A" else "TIE"

# —— 2. 长度归一(Verbosity Control)——
# 在 judge prompt 里硬约束:长度差异 > 30% 时优先看质量
JUDGE_PROMPT = """Compare A and B. CRITICAL: do not reward verbosity.
If lengths differ by >30%, explicitly check whether the longer one
adds substance or just padding. Penalize padding."""

# —— 3. 异家族交叉评 (Cross-family Judging) ——
# 测 Claude 输出用 GPT 当 judge,测 GPT 输出用 Claude 当 judge
# 或者拿 3 个不同模型 judge 取多数
judges = ["claude-opus-4-7", "gpt-5", "gemini-2.5-pro"]
votes = [judge_with(m, q, a, b) for m in judges]
winner = max(set(votes), key=votes.count)  # majority

# —— 4. Rubric 化(替代单一分数)——
# 不让 judge 给「整体分」,让它按维度打 0/1
RUBRIC = """Score each independently (0 or 1):
- factual_correct: all stated facts verifiable from source?
- completeness:   covers all parts of the question?
- format_valid:   matches required JSON schema?
- no_hallucination: no info absent from source?
Return JSON: {factual_correct: 0/1, ...}"""

# —— 5. 人工校准(Calibration Set)——
# 50 条人工已标注的样本,每次跑 judge 先在这 50 条上测一致率
# judge ↔ human 一致率 < 80% → judge prompt 不可信,回去改
human_labels = load("calibration_50.json")
judge_labels = [judge(c) for c in human_labels]
agreement = sum(h == j for h, j in zip(human_labels, judge_labels)) / 50
assert agreement > 0.80, f"Judge unreliable: {agreement:.0%}"

关键直觉:把 judge 当成「你雇来打分的实习生」。你不会让一个实习生只看完一段就拍一个总分——你给他 rubric、让他写理由、抽样复核他的打分。LLM judge 同理。

失败模式:(1)单次 pairwise + 单个 judge——位置 bias 直接吃掉 5-10% 真实差异;(2)用同家族 judge(Claude 评 Claude 改写)——self-preference 让你的 prompt 看起来「越调越好」其实没动;(3)只让 judge 输出「A 好 / B 好」不要 reasoning——judge 几乎全在 pattern match,要求它先写理由再下判断,准确率提升 5-15 个百分点(Zheng 2023);(4)calibration set 用 LLM 生成——校准集必须是人工标注,否则等于自己证明自己。
进阶资源 · Zheng et al. Judging LLM-as-a-Judge with MT-Bench and Chatbot Arena (NeurIPS 2023), arxiv.org/abs/2306.05685 · Panickssery et al. LLM Evaluators Recognize and Favor Their Own Generations, arxiv.org/abs/2404.13076 · Anthropic Reducing bias in LLM-graded evaluations, docs.anthropic.com/.../test-and-evaluate
// 03

Prompt as Code:Regression Test 与版本化

论断:prompt 是 source code 的一种,必须 diff、必须 review、必须在 CI 跑 regression——把 prompt 散在 notebook 里 copy-paste 是 2023 年的卫生水平。

背景与原理

Prompt 改一行,行为可以彻底变。这不是缺点,是 LLM 的基本属性。问题是大多数团队对待 prompt 的态度还停留在「散落在 Slack 截图和 Jupyter 单元格里」,结果就是:上周改的某个 prompt 让某个边缘 case 从 pass 变 fail,没有人发现,直到客户投诉。

资深做法是把 prompt 当代码全栈对待:

核心心智模型——prompt 不是配置,是行为代码。配置改了顶多业务参数变,prompt 改了模型决策可能整个翻转。所以应该用比代码更严格的 review 流程,而不是更松。

实战示例:CI 跑 prompt regression 的最小配置

# —— 仓库结构 ——
prompts/
  meeting_extractor.md       # prompt 文本,用 {{var}} 占位
  meeting_extractor.meta.yml # 模型、温度、max_tokens、min_pass_rate
tests/
  golden_meeting.jsonl       # golden set
  test_meeting.py            # pytest + 调 §1 的 evaluate()
.github/workflows/
  eval.yml                   # CI

# —— prompts/meeting_extractor.meta.yml ——
model: claude-opus-4-7
temperature: 0
max_tokens: 200
min_pass_rate: 0.90       # 跌破这个 CI 直接红
allow_regression: []      # 显式声明允许 fail 的 case_id

# —— tests/test_meeting.py ——
import pytest, yaml
from evaluator import evaluate

def test_meeting_regression():
    meta = yaml.safe_load(open("prompts/meeting_extractor.meta.yml"))
    result = evaluate("meeting_extractor")
    assert result.pass_rate >= meta["min_pass_rate"], \
        f"Pass rate dropped: {result.pass_rate:.0%} < {meta['min_pass_rate']:.0%}"
    # 检查没有新 fail 进入
    new_fails = set(result.failed_ids) - set(meta["allow_regression"])
    assert not new_fails, f"New regressions: {new_fails}"

# —— .github/workflows/eval.yml ——
name: prompt-eval
on: [pull_request]
jobs:
  eval:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: pip install -r requirements.txt
      - run: pytest tests/ -v
        env: { ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} }

有了这个最小集,下次有人在 PR 里改 prompt,CI 会自动跑 20 条 golden 给出 pass rate 和 diff。一周内你的团队就再也回不到「凭感觉改 prompt」的旧时代。

失败模式:(1)temperature ≠ 0 跑 eval——结果不稳,每次跑数字飘,没人信;regression test 必须 temperature=0 且固定 seed/model_version;(2)只对比 pass rate 数字,不看 fail diff——pass rate 一样可能换了一批 fail;diff 必须列出「新挂的 case_id / 新过的 case_id」;(3)allow_regression 越列越长——这是技术债告警,每个允许 fail 的 case 必须配 issue 跟踪;(4)golden set 和模型一起进化——你优化了模型却用模型生成的数据测自己,循环论证。
进阶资源 · Eugene Yan What We've Learned From A Year of Building with LLMs (Part I: Evals & Monitoring), applied-llms.org · Shreya Shankar Operationalizing ML Tests, shreya-shankar.com · OpenAI Evals 框架源码, github.com/openai/evals
// 04

Anthropic Evals 框架与「测能力 vs 测产品」之分

论断:Anthropic 自己开发 Claude 用的 evals 框架不是 MMLU,是几千条能力切片(capability eval)+ 产品级 task eval 的组合——你也该这么分。

背景与原理

Anthropic 在多篇博客和文档里透露了内部 eval 体系的轮廓(Claude's ConstitutionCore views on AI safety、Anthropic Cookbook 中的 evals 例子)。关键洞察是把 eval 分两层

普通团队的错误是只做其中一种:

正确做法是双层 + 可追溯:task eval 失败时,能下钻到具体哪个 capability 挂了。比如客服 agent 漏单——是 instruction following 挂(没按 SOP 走)、还是 entity extraction 挂(没抽到订单号)、还是 refusal 挂(错误拒答)?三种都要分别有 capability eval。

实战示例:双层 eval 结构

evals/
├── capability/                   # 原子能力,模型/prompt 改时跑
│   ├── instruction_following.jsonl   # 200 条「按格式输出」
│   ├── entity_extraction.jsonl       # 150 条「抽 JSON」
│   ├── refusal_accuracy.jsonl        # 80 条「该拒就拒」
│   ├── long_context_recall.jsonl     # 100 条「needle in haystack」
│   └── tool_selection.jsonl          # 120 条「选对 tool」
└── task/                         # 端到端业务,每次发布前跑
    ├── customer_support.jsonl        # 50 条真实工单 → 期望分类 + 回复
    └── code_review.jsonl             # 30 个 PR → 期望发现的 bug 列表

# —— 双向追溯:task fail 时关联到 capability ——
def diagnose(task_failure):
    """task eval fail → 自动跑相关 capability 看哪个低于阈值"""
    related = TASK_TO_CAPABILITY[task_failure.task_name]
    # e.g. customer_support 关联 [instruction_following, entity_extraction]
    diag = {}
    for cap in related:
        diag[cap] = run_capability_eval(cap).pass_rate
    return sorted(diag.items(), key=lambda x: x[1])  # 最低的最可疑

# —— 用法 ——
if task_eval.pass_rate < 0.85:
    print("Task regressed. Capability diagnosis:")
    for cap, rate in diagnose(task_eval): print(f"  {cap}: {rate:.0%}")
# Output:
#   instruction_following: 0.62  ← 罪魁祸首
#   entity_extraction:     0.94
双层 Eval 拓扑(Anthropic 风格简化版) ┌─────────────── Task Eval(产品级) ───────────────┐ │ customer_support · code_review · research_agent │ │ 20-200 条 · 真实业务结局 · 每次发布跑 │ └────────────────────┬─────────────────────────────┘ │ 失败时下钻 ┌────────────────────▼─────────────────────────────┐ │ Capability Eval(原子能力切片) │ │ instruction_following · entity_extraction │ │ refusal · long_context · tool_selection · math │ │ 100-1000 条/类 · prompt/model 改时跑 │ └──────────────────────────────────────────────────┘ 规则:task fail 一定能映射到一个或多个 capability 退化 capability 全绿但 task 红 → 你的能力切片没覆盖到真实需求

这个结构还有一个隐藏好处——换模型时可解释。Claude Opus 4.7 升 4.8、或者切到 Sonnet,你能看到「instruction_following +2%,entity_extraction -5%」,而不是只有一个糊涂的「整体好像差不多」。模型选型不再凭感觉。

失败模式:(1)只跑公开 benchmark——MMLU / HumanEval / GSM8K 这些和你的业务几乎无关,且大概率被训练污染;(2)capability eval 太宽——一个 capability 涵盖 5 种子能力,挂了不知道哪个挂;切片要原子,宁可分得碎;(3)task 和 capability 不关联——出问题时下钻不到根因,eval 退化成「报数仪表」;(4)忽略 cost / latency 维度——产品 eval 不只是 quality,必须同时记录 token / 延迟 / cache hit;上线决定看的是 Pareto 曲线不是单点。
进阶资源 · Anthropic Cookbook · Evals 例子, github.com/anthropics/anthropic-cookbook · Anthropic Develop empirical tests, docs.anthropic.com/.../develop-tests · Liang et al. HELM: Holistic Evaluation of Language Models, crfm.stanford.edu/helm

// 综合实战 · 给一个 prompt 从 0 搭完整 eval(90 分钟)

把 4 节串成一个能交付的工作流。挑你手头跑得最多的一个 prompt——比如「邮件摘要 + 提取 todo」——按以下 7 步走:

  1. 分层(§4,10 min):写下任务是什么,列出涉及的 2-4 个 capability(如:长 context 检索、entity extraction、format compliance)。每个 capability 决定要不要单独切片。
  2. 建 golden set(§1,30 min):手挑 20 条真实 input。10 条 happy、5 条 edge、5 条 adversarial。每条标注期望输出(JSON)。存为 tests/golden_xxx.jsonl
  3. 写 code-based scorer(§1,10 min):能用 JSON 字段相等 / regex / schema 解决的,绝不上 LLM judge。给每条 case 写出 scorer 逻辑。
  4. 跑 baseline(§1,5 min):在当前 prompt 上跑一遍,记录 pass rate 和 fail 分布。这是基线,禁止再跌破。
  5. 如有开放式部分加 judge(§2,20 min):摘要质量这种打不了字符串等价的,加 LLM judge:rubric 化(拆 3-4 个 0/1 维度)+ 位置交换 + 异家族 judge。在 20 条校准集上对齐到 ≥ 80% 人工一致率。
  6. 接 CI(§3,10 min):把 prompt 文件、meta.yml、pytest、GitHub Actions 串起来。在 PR 上自动跑 regression。
  7. 跑一次「故意改坏」(5 min):把 prompt 改掉一句关键 instruction,看 CI 是否变红。如果没红,你的 golden set 还不够锐——补 case,直到改坏能挡住。

90 分钟后你拥有的不是「一个 eval 脚本」,而是这个 prompt 未来一年的护城河。下次模型升级、prompt 改写、团队成员换人,这套 eval 都在那里替你守门。这就是 Anthropic / OpenAI / Cursor 工程团队相对普通团队最大的能力差距——不是模型用得好,是 eval 做得早。

// ENGLISH GLOSSARY

Eval / Evaluation
对 LLM 或 prompt 输出系统性测分以判定是否达到要求的工程实践。
Golden Set
人工挑选并标注的高质量评测数据集,是 eval 的 source of truth。
Code-based Scorer
用代码(regex / schema / unit test)判定输出是否正确,便宜稳定首选。
LLM-as-Judge
用另一个 LLM 给被测模型输出打分或挑赢家的评测方法。
Position Bias
pairwise judge 中 A/B 位置影响打分的系统性偏差。
Verbosity Bias
LLM judge 偏爱更长答案的倾向,与正确性无关。
Self-preference Bias
judge 偏爱与自己同家族模型输出的倾向。
Calibration Set
带人工标签的小样本集合,用来校准 LLM judge 是否可靠。
Rubric
把质量拆成多个独立 0/1 维度的评分标准。
Regression Test
每次修改 prompt 跑 golden set 确认没让任何 case 从 pass 变 fail。
Capability Eval
针对模型某一原子能力(如 instruction following)的切片评测。
Task Eval
针对完整业务 workflow 的端到端评测。
Goodhart's Law
「指标一旦成为目标就不再是好指标」——eval 也会被 hack。

// 深入思考

20 个例子的 eval 显然不够 production,但够提多少改进信号?什么时候必须扩到 200+?
20 例够 detect ±20% 的质量变化(粗粒度,挡 regression 大部分够用)。扩到 200+ 的场景:1) Prompt iteration 进入小幅 tuning(要 detect ±5%);2) 覆盖多语言/多场景的产品(每个 sub-pattern 至少 30 例);3) 做 A/B 实验需要统计显著性(一般 ≥ 100 per arm)。Anthropic 内部 eval 通常 100-1000 例分层。
LLM-as-Judge 的 bias 中,「位置偏见」和「verbose bias」哪个更难去除?
Verbose bias 更难。位置偏见可以用 swap-and-average(每对 A/B 跑两次交换顺序)抵消,几乎完全消除。Verbose bias 需要:1) 显式 prompt judge 'do not prefer longer answers'(效果有限);2) length normalization 后处理;3) 多 judge ensemble。难根除是因为 GPT/Claude 训练数据里「详细=好」是强 prior。
把 prompt 当 source code 做 regression test——但 LLM 输出 non-deterministic,怎么处理 false positive?
三层防御:1) 固定 temperature=0 + 固定 seed(减少大部分 nondeterminism);2) 用 fuzzy match 而非 exact match(如 'contains key concept' 而不是 byte equal);3) 允许 N-of-M(10 例允许 1 失败,超过才报警)。剩余 false positive 用人工 triage。Anthropic Evals 框架内置这些机制。
「Capability eval vs Product eval」怎么分?给一个具体例子。
Capability eval 测模型本身能力,与产品无关(如 Claude 能不能识别 SQL 注入)。Product eval 测端到端体验。例:客服 agent 的 capability eval = 'Claude 在给定 ticket 下能不能找到知识库正确条目'(test retrieval);product eval = '用户发完问题 30 秒内拿到的回答能否解决问题'(包括 retrieval + ranking + 回答风格 + latency)。两者都要——capability 帮 debug,product 帮 align 业务。
为什么 Anthropic 不用 MMLU 评 Claude?业界还用 MMLU 的原因是什么?
不用因为:1) MMLU 已被 fit 到训练集(contamination 严重);2) 只测 multiple choice 不测生成质量;3) 题目 outdated。业界还用因为:1) Publication 需要标准 benchmark 才能对比;2) 市场宣传需要 single number;3) 做 model selection 时还有点弱信号。Anthropic 内部用几千条手工设计的能力切片 + 真实产品 task。

// 延伸阅读