我拿到一张「11 种 LLM 评估方法」的信息图,每个方法一条五步流水线,画得很漂亮。我没有照着讲,而是造了三句话喂进去:一句正确的改写、一句只改了否定词的错句、一句被截断的复制。人的排序毫无争议是「改写 > 截断 > 错句」。三代指标给出的答案是:ROUGE-L 排 截断 > 错句 > 改写,BERTScore 排 错句 > 改写 > 截断。三句话、三个指标,没有一个排对。
这不是指标没调好。这 11 种方法不是 11 个工具,而是同一道题的 7 个答案——那道题叫「参照物从哪来」。
名词速查
| 术语 | 一句话解释 |
|---|---|
| n-gram | 连续 n 个词的片段。“the cat sat” 是一个 3-gram |
| clipped precision | 候选里命中参考的 n-gram 数 ÷ 候选 n-gram 总数,且同一个 n-gram 的命中次数不能超过参考里的出现次数 |
| brevity penalty (BP) | BLEU 的长度惩罚项,候选比参考短时按 打折,用来堵「只输出最有把握的几个词」这条捷径 |
| 上下文向量(contextual embedding) | 同一个词在不同句子里拿到不同向量的那种词向量,来自 BERT 这类模型的中间层 |
| logprob | 模型给某个候选 token 的概率的自然对数。指数化回去就是概率,一次前向就能拿到整个词表上的分布 |
| argmax | 在分布里取概率最大的那一项。「模型打了 3 分」通常指的是分数 token 的 argmax |
| Spearman 相关 | 只看排序不看数值的相关系数。评测指标的好坏一般用它跟人类打分对齐程度来衡量 |
| 标准误(SEM) | 「均值这个估计量本身有多抖」。k 个独立裁判取平均,标准误缩小 倍 |
| 过程监督(process supervision) | 不只给最终结果反馈,而是给每一个中间步骤反馈 |
读这篇需要先有的三件事,站内都有专文,不必现在补:判据本身会怎么撒谎见LLM 评测的 25 个坑;分数的置信区间与判分者一致性见评测分数的统计地基;裁判的 position / verbosity / self-enhancement 三块偏差见LLM-as-a-Judge 学习笔记。本文不重讲这些,只讲指标内部的机械结构——那是上面三篇都没拆开的部分。
一句根本约束:可接受输出的集合不可枚举
先把本质写成一句可反驳的话:
LLM 评估之所以分裂成十几种方法,是因为生成任务的「可接受输出集合」不可枚举。
怎么反驳它?很简单——把这个约束去掉,看这 11 种方法会不会全部塌成一种。会。MMLU 是四选一,GSM8K 的答案是一个数,这两类任务的可接受输出集合分别是 4 个元素和 1 个元素,于是没有任何人在 MMLU 上跑 BLEU,也没有人给 GSM8K 配一个裁判模型——大家用的都是最朴素的 exact-match accuracy。当正确答案可以被穷举时,所有评估方法退化成同一个:字符串相等。 这就是这条约束的证据:它一消失,整棵树就消失。
约束不消失,就只剩一个办法:花代价买一个参照物。11 种方法的差别不在「先进不先进」,而在去哪买、买到的是什么、买错了会怎么错。
flowchart LR
Q["可接受输出<br>不可枚举"] --> R{"参照物<br>从哪来?"}
R -->|"人写的一份答案<br>比字面"| L1["BLEU<br>ROUGE"]
R -->|"人写的一份答案<br>比语义"| L2["BERTScore"]
R -->|"一个模型的先验"| L3["LLM-as-Judge<br>G-Eval"]
R -->|"多个模型的先验"| L4["LLM Juries"]
R -->|"直接买人的判断"| L5["Human Eval"]
R -->|"不要参照物<br>改要判据"| L6["DAG<br>Safety Eval"]
R -->|"不看终态<br>看路径"| L7["Trajectory<br>Multi-turn"]
下面七族,每族只回答三个问题:参照物是什么、机械上怎么算、坏掉会怎样。 前四族我都跑了数字。
第一族:从一份人写的答案里抠字面(BLEU / ROUGE)
参照物是一份人写的 reference。BLEU 问「候选里的片段有多少被参考支持」(precision),ROUGE 问「参考里的内容有多少被候选覆盖」(recall)。我的三句话:
- 参考 R:
The cat sat on the mat. - A 正确改写:
A feline rested upon the rug. - B 否定句(只多了 did not):
The cat did not sit on the mat. - C 截断复制:
The cat sat on the
先手算 B 的 1-gram。B 的 8 个 unigram 里,the 出现 2 次、参考也有 2 个 the,截断后记 2;cat、on、mat 各记 1;did、not、sit 参考里没有,记 0。命中 5,总数 8,所以 。同样数下去:、、。
这一项是全篇第一个机械真相。 BLEU 取的是 n-gram 精度的几何平均,一项为 0 整个乘积就是 0:
所以 A 和 B 的 BLEU-4 都是 0.0000。不是 A 得 0.02、B 得 0.31 这种可比的小数,是两个一模一样的零。在单句尺度上,BLEU-4 的默认值就是 0——它设计时的计算单位是整个语料库,不是一句话。这一点在「BLEU 衡量精度」这种教科书式的一句话概括里完全看不出来。
C 的四档精度是 5/5、4/4、3/3、2/2,几何平均正好是 1,所以它的 BLEU-4 等于 BP 本身。BP 也能手算:、,。一个被砍掉最后一个词、语义都不完整的句子,拿到 0.8187。
我用一个 40 行脚本把三句话的全部档位算齐(纯 Python,无依赖,命中数都能对着上面的手算核):
| 指标 | A 正确改写 | B 否定句(错) | C 截断(不完整) |
|---|---|---|---|
| BLEU-4 | 0.0000 | 0.0000 | 0.8187 |
| BLEU-4(加一平滑) | 0.1930 | 0.2937 | 0.8187 |
| BLEU-2 | 0.0000 | 0.5175 | 0.8187 |
| ROUGE-1 F1 | 0.1667 | 0.7143 | 0.9091 |
| ROUGE-2 F1 | 0.0000 | 0.5000 | 0.8889 |
| ROUGE-L F1 | 0.1667 | 0.7143 | 0.9091 |
(平滑那一档是我脚本里的「0 命中改记 」,不是 NLTK 任何一个标准 method,只用来说明一件事:平滑把零打开之后,排序变得更坏,不是更好——B 从「和 A 并列为零」变成了「比 A 高 1.52 倍」。)
崩点在这里,而且是符号翻转不是精度下降。 真实质量排序 A > C > B。ROUGE-L 给出的排序是 C > B > A:正确答案从第一名掉到最后一名,错句比它高 4.29 倍,截断句比它高 5.45 倍。ROUGE-1 里参考的 6 个词,C 覆盖了 5 个,recall 0.8333;A 只覆盖 1 个(the),recall 0.1667。字面参照物根本不知道 rug 和 mat 是一回事,也根本不知道 not 改变了一切。
顺带说清一个常被误解的点:BP 不是为了惩罚「答得不完整」,它是为了堵一个非常具体的作弊路径——只输出最有把握的那几个词能把 precision 刷到 1.0。C 的 0.8187 恰好说明这道堤坝有多矮:砍掉全句 1/6 的内容,只扣 18%。
第二族:从表示空间里借语义(BERTScore)
BERTScore 换了参照物的读法:不再比字符串,而是把两句话都送进一个语言模型,取中间层的 token 向量,然后做贪心最大匹配。整个算法就这几行(伪代码,对应论文 arXiv 1904.09675 第 3 节;真实现还有 IDF 加权和 baseline rescaling):
# 伪代码:BERTScore 主干
cand_vecs = normalize(encode(candidate)) # |cand| x d,L2 归一化
ref_vecs = normalize(encode(reference)) # |ref| x d
sim = cand_vecs @ ref_vecs.T # 余弦相似度矩阵
precision = sim.max(axis=1).mean() # 每个候选 token 找最像的参考 token
recall = sim.max(axis=0).mean() # 每个参考 token 找最像的候选 token
f1 = 2 * precision * recall / (precision + recall)
# 省略:IDF 加权、baseline rescaling、多层选择
我用 roberta-large 第 17 层(bert-score 官方对该模型的默认层)跑了同样三句话:
| 指标 | A 正确改写 | B 否定句(错) | C 截断 |
|---|---|---|---|
| BERTScore P | 0.9415 | 0.9627 | 0.9597 |
| BERTScore R | 0.9490 | 0.9799 | 0.9243 |
| BERTScore F1 | 0.9452 | 0.9712 | 0.9417 |
好消息:A 从 ROUGE 的 0.1667 涨到 0.9452。语义参照物确实看懂了 feline≈cat、rug≈mat——这是第二族存在的全部理由。坏消息:B 还是第一名。换了一整代技术,错句依然排在正确答案前面。
为什么?打印出贪心匹配的对齐就一目了然了(这是我实测的原始输出,Ġ 是 RoBERTa 的词首空格标记):
--- B 否定句
'The' -> 'The' cos=0.9810
'Ġcat' -> 'Ġcat' cos=0.9943
'Ġdid' -> 'The' cos=0.9235 <- 参考里没有对应物
'Ġnot' -> 'The' cos=0.8818 <- 语义反转的那个词
'Ġsit' -> 'Ġsat' cos=0.9372
...
did 和 not 在参考里没有任何对应物,但它们不是拿到 0 分,而是拿到 0.9235 和 0.8818。原因是 precision 那一行写的是 sim.max(axis=1):每个候选 token 都被强制配一个最像的参考 token,配不上也要配。 贪心最大匹配里没有「未匹配惩罚」这一项,插入一个反转语义的词,代价只是把那个位置的分数从 0.99 降到 0.88。
那这个 0.88 的地板有多高?我拿 6 句和参考完全无关的句子测了一遍:
F1=0.8413 Quarterly revenue in the Asia-Pacific segment declined by four percent.
F1=0.8643 Install the dependency and restart the daemon to apply the new configuration.
F1=0.8839 She has never been to Lisbon in the winter.
F1=0.8827 The compiler emits a warning for unreachable branches.
F1=0.8457 Photosynthesis converts light energy into chemical energy in plant cells.
F1=0.8772 He bought two kilograms of unsalted butter.
n=6 mean=0.8659 min=0.8413 max=0.8839
「完全无关」的分数是 0.8659。 这就是为什么 BERTScore 原始分几乎没有可读性:它的实际动态范围只有 0.87 到 1.00 这 13 个点,而不是看起来的 0 到 1。一个 0.94 听着像「94 分」,其实只是在 13 点的区间里走了一半。论文自己也知道,所以官方实现提供 baseline rescaling——用随机句对的分数 把区间拉回来:。拿我实测的 代进去:
| A 正确改写 | B 否定句 | C 截断 | |
|---|---|---|---|
| 原始 F1 | 0.9452 | 0.9712 | 0.9417 |
| rescale 后 | 0.5917 | 0.7854 | 0.5652 |
rescaling 改的是刻度,不是次序。 B 还是第一。这一点我得说清楚,因为「加了 rescale 就好了」是我自己动手前的默认假设——实测之后才明白,rescale 解决的是「0.94 看起来太高」这个观感问题,解决不了「贪心匹配对否定盲视」这个结构问题。
五分钟复现这个地板
这是本篇最便宜的一手实验,环境干净的话五分钟:
uv run --with torch --with transformers python floor.py
floor.py 里只要做三件事:用 AutoModel.from_pretrained("roberta-large", output_hidden_states=True) 取 hidden_states[17]、丢掉 <s>/</s> 两个特殊 token、按上面伪代码算 F1。要记录的数字就一个:你自己造的一批无关句对的 F1 均值。我的是 0.8659;换模型或换层这个数会变,但它一定远远高于 0——这个「远远高于 0」才是要带走的结论。首次运行会下载 roberta-large 的权重(约 1.4 GB),这是唯一的时间成本。
第三族:从一个模型的先验里借标准(LLM-as-Judge / G-Eval)
前两族的参照物都是「一份人写的答案」。第三族换了个买法:不要答案,要标准——把评分准则写进 prompt,让模型直接打分。参照物从一份文本变成了一个模型的先验。
信息图上 G-Eval 那一格的第四步写着 “Probability-Weighted Score”,这五个字是 G-Eval 和普通 judge 的唯一实质区别,也是它值得单开一格的原因。机制是这样:模型输出「3」的时候,它内部其实有一个 1 到 5 上的完整分布,argmax 把这个分布压成了一个整数。G-Eval 论文(arXiv 2303.16634)的做法是不压,直接算期望:
这件事本地就能验。我用 llama.cpp 起了一个 Qwen2.5-7B-Instruct(Q4_K_M)当裁判,照 G-Eval 论文的 form-filling 格式写 prompt(评分准则 + 三步 CoT evaluation steps + 原文 + 待评摘要),只生成 1 个 token,把 n_probs 开到 20,拿分数 token 的完整分布。待评的是同一篇新闻的三份摘要:S1 忠实通顺、S2 忠实但语序打散、S3 有事实错误。
| 摘要 | argmax | p(1) | p(2) | p(3) | p(4) | p(5) | G-Eval 加权分 |
|---|---|---|---|---|---|---|---|
| S1 忠实通顺 | 3 | 0.000 | 0.115 | 0.876 | 0.009 | 0.000 | 2.8934 |
| S2 语序打散 | 3 | 0.000 | 0.137 | 0.863 | 0.000 | 0.000 | 2.8630 |
| S3 有事实错误 | 1 | 0.545 | 0.446 | 0.009 | 0.000 | 0.000 | 1.4640 |
看 argmax 那一列:S1 和 S2 都是 3,完全平手。 一个通顺、一个被我故意打散成「Burnley 0-0 draw. / Townsend was brought on… / Merson has restarted his row. / Tottenham midfielder.」的碎片堆,整数分数给不出任何区别。而概率加权把这个平手打开了:2.8934 vs 2.8630。
为什么这很重要?因为评测指标的好坏是用 Spearman 相关衡量的,而 Spearman 吃的是排序。一个只会输出整数的裁判,在几百条样本上会制造大量并列,并列本身就是相关系数的直接损失。 概率加权不是为了让分数「更准」,是为了让分数有分辨率。G-Eval 论文报告的 GPT-4 版在摘要任务上 Spearman 达到 0.514(论文原文数字,我没复现),大幅超过之前的方法——这个提升里有相当一部分就来自把整数换成实数。
但 0.0304 这个差值不要当真。 我只跑了一条样本、7B 模型、单次调用,方差比这个差值大得多(下一节就会量出来)。方向对,量级不可信——这是我实测的,不是论文的。另外这个本地 7B 明显低估了 S1(该给 4-5 分,它给 3),小裁判不等于 GPT-4,这也是我实测里最该被警惕的一点。
我在这里踩的坑:空格 token 吃掉了全部概率质量
第一次跑的时候我没套 chat template,prompt 直接以 A: 结尾。返回的第一个 token 是 " "(单个空格),logprob ——概率 0.9998 全在空格上,1 到 5 的数字 token 挤在后面,我如果照旧对数字做归一化再算期望,算出来的是一个「在 0.02% 的残余质量上加权」的数,看起来照样是个漂亮的 2.87。
这就是 G-Eval 概率加权最危险的地方:它永远能算出一个数,而那个数可能算在了错误的 token 位置上。 修法有两步:套模型自己的 chat template(Qwen2.5 是 <|im_start|>assistant\n),并在指令末尾明确写 Respond with the single digit score only.。修好之后我加了一行校验——把 1 到 5 的原始概率求和再打印:
sum = 1.0000
质量全在数字上,才能往下算。任何实现 G-Eval 的人都应该打这一行:如果这个和不接近 1,你加权的不是分数分布,是噪声。
裁判自身的 position / verbosity / self-enhancement 三块偏差不在本文范围,LLM-as-a-Judge 学习笔记和25 个坑已经拆过。信息图上的 LLM-as-Judge 那一格画的是 pairwise + win rate ranking,和 G-Eval 的 pointwise 打分是两种不同形态,pairwise 恰恰是为了绕开「绝对分数不可靠」才存在的——同一个模型说不清「这答案值几分」,但说得清「A 和 B 哪个好」。
第四族:多个裁判(LLM Juries)——治方差,不治偏差
信息图上 LLM Juries 那一格写着 “Individual Scores → Average Scores → Ensemble Score”,配的说明是「独立裁判并行打分,取平均得到最终裁决」。听起来像「三个裁判比一个准」。我想量一下准在哪,于是把同一条 prompt 在 temperature=1.0 下跑了 30 次(grep -c '^sample' jury.log = 30,其中非数字输出 0 次):
直方图: {2: 5, 3: 25}
n=30 mean=2.8333 sd=0.3790
sem(1 个裁判)=0.3790 sem(3 个平均)=0.2188 sem(5 个平均)=0.1695
那部分是教科书结论,读者可以直接核:。取平均确实在缩小抖动。 但把这组数字和上一节的表并排放,会掉出一个更有意思的东西:
- 30 次采样的均值:2.8333(就是 ,可以用上面的直方图自己除一遍)
- 同一个 prompt 的概率加权期望:2.8934
两个数差 0.0601,而 30 次采样的标准误是 ——差距不到一个标准误。这不是巧合:采样分布就是那个 token 分布本身,所以「让同一个模型当 30 个裁判再取平均」在数学上只是在用蒙特卡洛去估计那个一次调用就能直接读出来的期望。30 次调用买到的东西,G-Eval 用 1 次调用免费拿到了,而且没有采样噪声。
这就推出了我认为这一族最该带走的判断(这是我的观点,论据是上面这组数):一个由同一模型克隆体组成的合议庭,只是那个模型单一分布的一台噪声采样器。它把方差磨平,却把偏差原封不动地保留下来。 我这个 7B 裁判系统性地低估 S1,克隆 30 个、300 个,平均值仍然停在 2.83,不会向 4 靠近一分。
所以 PoLL 论文(arXiv 2404.18796)的标题里那个词是 diverse,不是 multiple——它明确要求评审团由 disjoint model families 组成。论文报告:由更多小模型组成的评审团胜过单个大裁判,因为模型家族不重叠所以 intra-model bias 更小,同时成本低七倍以上(论文摘要原文数字,我未复现其实验)。方差靠数量治,偏差只能靠换血统治。这跟集成学习里偏差-方差分解的老结论是同一件事:bagging 同构模型降方差,想降偏差必须换假设空间。
关于「多少个裁判才够」「分数差多少才算真的差」,是评测分数的统计地基那篇的主场,这里不展开。
第五族:直接买人的判断(Human Eval)
这一族只有一句话是关键的,但它是整棵树的根:前四族都在近似参照物,只有人评在定义参照物。
所以它的评价方式也不一样。你不能问「Human Eval 准不准」——它就是「准」的定义。你只能问它自己稳不稳,于是信息图上那一格的第四步是 “Check Annotator Agreement”。判分者一致性(Cohen’s kappa 及其亲戚)是人评唯一的自评指标,也是它唯一可能失败的地方:如果两个标注员的 kappa 只有 0.3,那这个「金标准」本身的信噪比就配不上后面所有小数点。kappa 的机械细节在统计地基那篇里;怎么设计一条值得标注的样本在什么是一个好的评测样本那篇里。
值得多说一句的是它的代价结构:人评贵在「每条样本都要重新买」,而前四族的代价是「一次性写好脚本,之后近乎免费」。这个差别决定了工程上的分工——人评用来校准自动指标(几百条),自动指标用来扫描回归(几万条)。把人评当日常回归跑,和把自动指标当发布闸门用,是同一个错误的两个方向。
第六族:不要参照物,改要判据(DAG / Safety Eval)
前五族都在回答「像不像参考」。第六族换了个问题:「有没有违反某条规则」。 参照物被换成了一串可判定的是非题。
信息图把它标成 “DAG (Deep Acyclic Graph)“。我一开始以为这是图作者把 Directed 写错了,去查了 DeepEval 官方文档——它自己就叫 Deep Acyclic Graph,Python 类名也确实是 DeepAcyclicGraph。所以这是 DeepEval 的命名怪癖,不是信息图的错。有意思的是,名字里唯一被代码强制的是 acyclic 那部分:deepeval/metrics/dag/graph.py:124-137 的 visit() 用一个 stack 集合做环检测,发现环直接 raise ValueError("Cycle detected in DAG graph.")。
读源码读出来三条文档里没写、但直接影响你怎么用它的结论:
一、分支判断仍然是 LLM 做的。 BinaryJudgementNode._execute()(nodes.py:268-287)做的事就是拼一个 generate_binary_verdict prompt、带 BinaryJudgementVerdict schema 去调模型。DAG 没有把 LLM 从回路里拿掉,它只是把 LLM 的工作从「给个 1-5 分」换成了「回答一个是非题」。变确定的只是分数映射——分数是你在 add_verdict(verdict=True, score=10) 里手写的常量,不是模型生成的。官方文档的对比表其实承认了这点(“Branch decisions remain LLM-based, but score mapping is controlled”),但这句话太容易被「deterministic decision trees」那个宣传语盖过去。
二、它的 LLM 调用次数通常比 G-Eval 多,不是少。 G-Eval 是一次调用出一个分数;DAG 走一条根到叶的路径,路径上每个判断节点都是一次调用。用 DAG 换来的确定性,是用调用次数付的账。 想省钱的人选 DAG 会失望。
三、二元节点被强制恰好一个 True 分支和一个 False 分支。 nodes.py:250-266 的 _validate() 连查三道:子节点必须正好 2 个、必须都是 VerdictNode、verdicts.count(True) != 1 or verdicts.count(False) != 1 就报错。也就是说写不出「只有 True 分支」的节点——你不能只声明「合格就给 10 分」而对不合格的情况保持沉默,必须把两条路都显式写出来。这个约束我认为是对的:评测规则里的隐式默认分支,正是线上最常见的评分口子。
还有一处细节能看出它是个真的图执行器,不是一串 if-else:graph.py:48-82 的 _collapse_exclusive_verdict_edges,docstring 解释得很清楚——判断节点的多个 verdict 子节点里永远只有一个会触发,如果它们同时是某个下游节点的父节点,按常规给每条边记一次入度,那个下游节点就永远到不了入度 0,因为「落选的那些 verdict 永远不会到达」。所以这些互斥边要被折叠成一条。互斥边会卡死普通拓扑排序,这是把评测规则做成 DAG 之后必然撞上的工程后果,值得记住。
Safety Eval 那一格其实是这一族的一个固定形状:把若干分类器并联(bias / toxicity / PII),任一命中就 flag。它不是打分,是闸门——参照物是一份策略文档,判据是「有没有违反」。这条路线做到工业级的样子可以看Llama Guard 的策略运行时。
第七族:不看终态,看路径(Trajectory Accuracy / Multi-turn Eval)
最后一族改的不是参照物的来源,而是参照物挂在哪个对象上:从「最终输出」挪到「执行路径」。Agent 任务里终态往往对不上——同一个订单退款,走三步和走七步都能退成,而错误的中间步骤(查了不该查的表、改了不该改的字段)在终态里看不见。
这个转向不新。它就是过程监督(arXiv 2305.20050 明确把 outcome supervision 和 process supervision 对立起来)在评测侧的再实例化:既然只看结果的反馈信号太稀疏、太容易被歪路蒙对,那就给每一步反馈。Trajectory Accuracy 是这个想法最朴素的形式——把 agent 的完整步骤序列和一条期望路径比对。
坏掉会怎样,这一族有一个很尖的失败模式:把路径唯一化。 如果期望路径只有一条,那么一个用四步解决了七步问题的 agent 会被判错——指标在惩罚更优解。这正是目标函数即命运那条老原理在评测上的复现:你度量步骤序列,就会得到「照着标准答案走流程」的 agent,而不是「把事办成」的 agent。τ-bench(arXiv 2406.12045)绕开这一点的办法是不比路径,比最终数据库状态 + 是否违反领域策略——即状态等价而非路径等价。同一个思路在工具调用上的具体落法见BFCL v3 的多轮状态判据;用可执行测试彻底替掉参照物的极端形态见SWE-bench。
Multi-turn Eval 是同一族的对话版:参照物是整段会话而不是单轮回复,考察角色一致性、知识保持、前后连贯。它和 Trajectory 共享同一个机械难点——怎么给一条「路径」定义等价类。定义得太窄就惩罚更优解,定义得太宽就什么都通过。
共同祖先:这 11 种方法是 AI 史的一次重播
把七族按「参照物从哪来」排好之后,它们的时间顺序会自己浮上来——这 11 种方法基本就是整个 AI 史按原顺序又走了一遍:
| 时代 | 那个时代的核心手法 | 在评测里的化身 |
|---|---|---|
| 符号主义 | 人手写规则、决策树 | DAG、Safety Eval 的分类器级联 |
| 统计学习 | 计数、n-gram、信息检索的 precision/recall | BLEU、ROUGE |
| 表示学习 | 分布语义——词由它的上下文定义 | BERTScore |
| 学出来的评估器 | 把评估函数本身交给模型(同 RLHF 的 reward model) | LLM-as-Judge、G-Eval |
| 集成 | bagging 降方差,异构成员降偏差 | LLM Juries / PoLL |
| 过程监督 | 结果反馈太稀疏,改给每步反馈 | Trajectory、Multi-turn |
一个反直觉的地方:符号时代的手法(DAG)不是被淘汰的那一族,它是最近才重新流行起来的那一族。 原因也在那条根本约束上——当你能把判据写成有限条是非题时,可接受输出集合就局部地重新可枚举了,于是最老的方法重新变成最好的方法。这是「第五条原理:从规则到数据」的一次反向运动,值得单独记住。
总表:七族 × 参照物 × 代价 × 坏掉会怎样
| 族 | 方法 | 参照物是什么 | 边际代价 | 坏掉会怎样(本文实测/源码) |
|---|---|---|---|---|
| 1 | BLEU / ROUGE | 一份人写答案的字面 | 近乎 0 | 正确改写掉到最后一名,截断句拿 0.9091 |
| 2 | BERTScore | 一份人写答案的语义 | 一次编码 | 否定句拿 0.9712 排第一;无关句地板 0.8659 |
| 3 | LLM-as-Judge / G-Eval | 一个模型的先验 | 1 次调用 | argmax 制造并列;概率质量可能落在空格 token 上 |
| 4 | LLM Juries | 多个模型的先验 | k 次调用 | 同族克隆只降方差(),偏差一分不动 |
| 5 | Human Eval | 人的判断本身 | 每条都要重买 | kappa 太低时金标准自己就是噪声 |
| 6 | DAG / Safety Eval | 一串可判定的是非题 | 路径长度次调用 | 判据写不全就有隐式默认分支(源码强制两条都写) |
| 7 | Trajectory / Multi-turn | 一条期望路径 | 全程记录 | 路径唯一化会惩罚更短的正确解 |
把这七族压成三句话
- 根本约束:生成任务的可接受输出集合不可枚举,所以必须花代价买一个参照物;这 11 种方法就是 7 种买法。约束一旦消失(MMLU、GSM8K),全部塌回 exact-match。
- 崩点:最朴素的买法(字面比对)不是不够准,是排序符号翻转——真实质量 A>C>B,ROUGE-L 给 C>B>A,正确答案从第一掉到最后,差 4.29 倍。换到语义比对(BERTScore)修好了改写,没修好否定,错句仍然第一。
- 可带走的动作:选指标时不要问「哪个更先进」,问「我的参照物从哪来、买错了会朝哪个方向错」。字面指标朝「抄得多」错,语义指标朝「词面像」错,单裁判朝「模型自己的偏好」错,合议庭只改方差不改方向,DAG 朝「判据没写全」错,轨迹指标朝「不许走捷径」错。
换成一句不带术语、能直接讲给同事听的话:评估一篇作文,你可以对着标准答案数重合的词(便宜但只认字面)、请一个人看(贵但算数)、或者请一个读过很多作文的人凭经验打分(中间价但有他自己的口味)。 这 11 种方法没有一种跳出这三条路,它们只是在这三条路上换了不同的交通工具——所以先问「我这次买的是哪条路的票」,再问票价。
误解 → 纠正表
我动手之前,下面这五条里有三条是我自己的默认假设。每一条右边都对应本文一个具体数字或源码位置。
| 常见误读 | 实际情况 |
|---|---|
| BERTScore 是 BLEU 的升级版,能替掉它 | 它修好的是改写(0.1667 → 0.9452),没修好否定(错句仍以 0.9712 排第一)。贪心最大匹配没有未匹配惩罚 |
| BERTScore 0.94 说明质量很高 | 完全无关的句对就有 0.8659(n=6 实测)。原始分的动态范围只有 13 个点,不做 rescale 基本不可读 |
| baseline rescaling 能修好 BERTScore 的问题 | 它改刻度不改次序。rescale 后 A=0.5917、B=0.7854,错句还是第一 |
| 三个裁判比一个裁判更准 | 同族克隆只把标准误缩小 倍(0.3790 → 0.2188),偏差一分不动。PoLL 要的是 disjoint model families,不是数量 |
| DAG 让评测变确定,还能省钱 | 分支判断仍是 LLM 调用(nodes.py:268-287),确定的只有分数映射;路径上每个节点一次调用,通常比 G-Eval 更贵 |
| BLEU 的 brevity penalty 能挡住答不完整 | 砍掉全句 1/6 的内容只扣 18%(),截断句的 ROUGE-1 F1 反而是全场最高的 0.9091 |
最后留一个我还没做的:这篇里所有数字都是单句、单样本尺度的。我想验但没验的是——把这三类扰动(改写 / 否定 / 截断)扩到几百条样本,算一遍每个指标与人工排序的 Spearman,看否定句那一类的排序错误能不能被样本量摊掉。我的猜测是不能,因为它是结构性的而不是随机的;如果跑出来是能,那说明我这篇的结论只在单句尺度上成立,会更新回这里。
参考来源
工程实践
- DeepEval
DAGMetric文档:deepeval.com/docs/metrics-dag(“DAG (Deep Acyclic Graph)” 这个叫法出自这里) - DeepEval 源码:
deepeval/metrics/dag/nodes.py(BinaryJudgementNode._validate/_execute)、deepeval/metrics/dag/graph.py(DeepAcyclicGraph._build_graph、_collapse_exclusive_verdict_edges) - 本文所有本地实测:llama.cpp
llama-server+ Qwen2.5-7B-Instruct-Q4_K_M(n_probs=20取分数 token 分布);BERTScore 用transformers的roberta-large第 17 层自行实现贪心匹配 - 信息图原作:DailyDoseofDS《11 LLM Evaluation Methods》
arXiv 论文
- BERTScore:arXiv 1904.09675(2019-04-21)
- G-Eval:arXiv 2303.16634(2023-03-29,GPT-4 版摘要任务 Spearman 0.514)
- Replacing Judges with Juries (PoLL):arXiv 2404.18796(2024-04-29,disjoint model families,成本低七倍以上)
- Let’s Verify Step by Step(过程监督 vs 结果监督):arXiv 2305.20050(2023-05-31)
- τ-bench(状态等价而非路径等价):arXiv 2406.12045(2024-06-17)
- BLEU:Papineni et al., BLEU: a Method for Automatic Evaluation of Machine Translation, ACL 2002
- ROUGE:Lin, ROUGE: A Package for Automatic Evaluation of Summaries, ACL 2004 Workshop