一个绕不开的问题:「这个模型比那个好」凭什么说?软件工程里你有测试套件——红绿一目了然。但 LLM 的输出是开放文本,没有唯一正确答案。今天拆解四个核心评估机制,它们恰好对应你熟悉的测试金字塔:MMLU 是单元断言,HumanEval 是跑得过测试才算对,MT-Bench 是多轮集成测试,LLM-as-Judge 是自动化裁判——以及裁判自己的 bug。
MMLU 就是给模型出的「标准化高考」——57 个学科(数学、法律、医学、历史…)的多选题题库。在你的世界里它对应回归测试套件(regression suite):一组固定不变的断言,每发一个新模型版本就跑一遍,看分数有没有退化。每道题相当于一条 assert。
Pain point:早期 NLP 每个任务一个 benchmark,碎片化得像一百个互不兼容的测试框架,没人能回答「这模型到底懂多少世界知识」。Hendrycks 等人 2021 年把 57 个领域的多选题汇成一个分数,覆盖从初等数学到专业法律。
机制的关键不是让模型「自由作答」(那没法自动判分),而是把开放生成问题降维成分类问题:给题干 + 4 个选项 A/B/C/D,看模型对哪个选项赋予的对数似然(log-likelihood,即模型认为这串字出现的可能性)最高。哪个选项概率最高就算选哪个,对了 +1。这一步「降维」是 benchmark 能规模化、零人工的根本原因。
诚实地说它有个致命软肋:污染(contamination)——题目原文泄漏进了训练数据,模型不是「会做」而是「背过」,分数虚高。这就像测试用例被偷偷写进了被测代码。
# MMLU 的判分本质:比较每个选项的对数似然,不是让模型自由生成 from datasets import load_dataset ds = load_dataset("cais/mmlu", "high_school_physics", split="test") q = ds[0] # q["question"], q["choices"] = ["A选项",...], q["answer"] = 正确索引 prompt = f"{q['question']}\nA. {q['choices'][0]}\nB. {q['choices'][1]}\n..." # 对每个候选答案算 log P(选项 | prompt),取最大的作为模型的选择 def score_choice(prompt, letter): # 真实评估框架(lm-eval-harness)会取该 token 的 logprob return model.logprob(prompt + " " + letter) pred = max("ABCD", key=lambda L: score_choice(prompt, L)) correct = ("ABCD"[q["answer"]] == pred) # 自动判分,零人工
HumanEval 是给模型的「TDD 验收测试」——164 道 Python 题,每道带一组隐藏单元测试,生成的代码跑过测试才算对,长得漂不漂亮无所谓。而 pass@k 就是你最熟的那个东西:给不稳定的下游服务设重试,重试 k 次至少成功一次的概率。
Pain point:代码不能用字符串匹配判分。两段代码可以一字不差地不同却都对,也可以长得几乎一样一个对一个错。所以传统的 BLEU、编辑距离全失效。Chen 等人 2021 年(Codex 论文)的答案简单粗暴:functional correctness——跑单元测试,过了就对。
真正有数学含量的是 pass@k。它要回答:「采样 k 个解,至少一个正确的概率多少?」最朴素的写法是 1−(1−p)^k,但单题样本少时方差极大、估计不准。Codex 论文用了一个无偏估计器:每题先采样 n 个解(n 远大于 k),数出其中 c 个正确,然后:
逐符号拆开看就很直觉:
· C(n, k):从 n 个解里随机抽 k 个,一共多少种抽法(组合数)。
· C(n−c, k):只从那 c 个错解……不对,是从 n−c 个错解里抽 k 个的抽法数——即「k 个全抽到错解」的情况数。
· 两者相除 = 抽 k 个全错的概率;1 减去它 = 至少抽到一个对的概率。
用大 n 估出真实正确率,再算这个组合式,就比直接重试 k 次稳得多——本质是用更多采样换更低的估计方差。
# pass@k 无偏估计器(Codex 论文给出的标准实现) import numpy as np def pass_at_k(n, c, k): # n=总采样数, c=正确数, k=想评估的 k if n - c < k: # 错解不足 k 个 → 必然抽到对的 return 1.0 # 用连乘算 C(n-c,k)/C(n,k),避免大数阶乘溢出 return 1.0 - np.prod(1.0 - k / np.arange(n - c + 1, n + 1)) # 评测一道题:采样 n 个解,各跑隐藏单元测试 samples = [model.generate(problem["prompt"]) for _ in range(8)] c = sum(run_unit_tests(s, problem["test"]) for s in samples) print(pass_at_k(n=8, c=c, k=1)) # 一次就对的概率 print(pass_at_k(n=8, c=c, k=4)) # 4 次内至少一次对
如果 MMLU/HumanEval 是单元测试,MT-Bench 就是集成测试——它测多轮(2 轮)对话里模型能不能保持上下文一致、follow-up 不掉链子。就像测一个有状态会话服务:第一个请求没问题不算赢,要看跨多个请求后会话状态还对不对。
Pain point:多选题和代码题测不了「开放对话质量」——写作、角色扮演、解释推理、改写。这些没有唯一正确答案,没法 assert。MT-Bench(出自 Zheng 等人 2023 的 MT-Bench / Chatbot Arena 论文)用 80 道高质量两轮问题,覆盖写作、推理、数学、编码等 8 类,专门压力测试「第二轮还跟得上吗」。
核心难题随之而来:开放回答怎么自动判分?MT-Bench 的答案是把判分外包给一个强模型——LLM-as-Judge,给每个回答打 1–10 分(single-answer grading),或两个模型的回答两两对比(pairwise)。这就引出了今天最关键、也最危险的机制 → 下一张卡。
# MT-Bench 风格的单答打分:让强模型按 rubric 打 1-10 分 import anthropic client = anthropic.Anthropic(api_key="sk-ant-...") # 占位 JUDGE = """你是公正的评审。请就【有用性、相关性、准确性、深度】 给下面的回答打 1-10 分。先简述理由,最后单独一行输出 "Rating: [[分数]]"。 [问题] {q} [第一轮回答] {a1} [追问] {q2} [第二轮回答] {a2}""" msg = client.messages.create( model="claude-opus-4-8", max_tokens=512, messages=[{"role":"user", "content": JUDGE.format(q=q, a1=a1, q2=q2, a2=a2)}]) # 用正则从 "Rating: [[8]]" 抽出分数——结构化输出便于自动解析
用一个强模型给另一个模型的输出打分,就像「用一个服务监控另一个服务」,或者自动化的 code review bot。但关键反转:裁判自己有系统性偏差——就像你的监控系统本身也可能有 bug,会谎报或漏报。不校准裁判,就等于信任一个没测过的探针。
Pain point:人工标注又贵又慢,规模化不了。Zheng 等人 2023 的发现给了 LLM-as-Judge 合法性:GPT-4 当裁判与人类偏好的一致率超过 80%——和人类彼此之间的一致率是同一水平。换句话说,强裁判已逼近「人类天花板」。
但同一篇论文也点名了三大系统性偏差,必须知道:
最实用的缓解是治位置偏差:交换顺序(swap)跑两遍——A/B 各当一次先手,只有两次都赢才判赢,否则算平。这把「位置」这个干扰变量消掉了。
一致性怎么量化?用一致率(agreement rate),更严谨的用 Cohen's kappa——它扣掉了「瞎猜也会蒙对」的随机一致部分,只留下真实一致。另一条路是 Chatbot Arena:让真人对两个匿名模型的回答投票,再用 Elo / Bradley-Terry 模型把海量两两胜负转换成一个排名分(和象棋天梯分同一套数学)。
# 成对评判 + 位置交换,消除 position bias def judge_pair(q, ans_a, ans_b): def ask(first, second): p = f"问题:{q}\n回答1:{first}\n回答2:{second}\n" \ "哪个更好?只回 '1' 或 '2'。" return client.messages.create( model="claude-opus-4-8", max_tokens=5, messages=[{"role":"user","content":p}]).content[0].text.strip() r1 = ask(ans_a, ans_b) # A 先手 r2 = ask(ans_b, ans_a) # B 先手(交换) # 只有两轮都选 A 才算 A 赢,否则平局——抵消位置偏差 if r1 == "1" and r2 == "2": return "A" if r1 == "2" and r2 == "1": return "B" return "tie"
1 − C(n−c,k)/C(n,k) 解析地算出 pass@k 的期望。这等于「用更多采样买更稳的估计」。统计直觉:你不是在「模拟一次 k 重试」,而是在「估计这道题的难度参数,再算出任意 k 下的理论通过率」。类比你做压测:与其手动重放 5 次请求看成功率,不如先精确测出单请求成功率 p,再解析推导任意重试次数的可用性——后者样本利用率高得多,结论也稳得多。