数据截至 (上游 commit 8eac7a7de521)
03 · 模型当裁判(LLM-as-judge)
本章讲 Evals 最有借鉴价值的一块:当答案是开放式(摘要、解释、翻译)、没法字符串比对时,怎么"让另一个模型来打分",并且把它的打分逼成机器可解析的形式。
1. 这一章要解决的问题
问"法国首都是?"可以字符串比对;但问"总结这段话"或"这个回答事实对不对?"——答案有无穷种合法写法,startswith 完全失效。
思路:用模型评模型。 把"原始问题 + 被测模型的回答 + 标准答案"包进一段评分 prompt,交给一个裁判模型,让它输出一个可枚举的选项(如 A/B/C/D/E 或 Yes/No),再把选项解析成分数。裁判模型和被测模型可以是同一个,也可以不同。
2. 数据流:一题怎么被裁判
test_sample
{input, ideal}
│
▼
① 被测模型答题(若样本里没带现成 completion)
completion = completion_fn(input)
│
▼
② 填进评分模板(把 {input}{ideal}{completion} 代入)
prompt = mg.prompt.format(...)
│
▼
③ 追加"答题格式"指令(eval_type 决定:先推理后选项 / 只给选项)
prompt += ANSWER_PROMPTS[eval_type]
│
▼
④ 裁判模型打分
evaluation = eval_completion_fn(prompt)
│
▼
⑤ 解析出选项 → 映射成分数
choice = get_choice(evaluation, ...) # 取不到 → "__invalid__"
score = choice_scores[choice]
对应 ModelBasedClassify.eval_sample(evals/elsuite/modelgraded/classify.py:53-102)和 classify(classify_utils.py:51-87)。
3. 裁判规格:ModelGradedSpec
一个模型裁判由 YAML 声明,加载成 ModelGradedSpec(evals/elsuite/modelgraded/base.py:11-26)。真实例子 evals/registry/modelgraded/fact.yaml(事实一致性判定):
fact:
prompt: |-
[Question]: {input}
[Expert]: {ideal}
[Submission]: {completion}
... 比较 submission 和 expert 的事实内容,选一个:
(A) submission 是 expert 的子集且一致
(B) submission 是 expert 的超集且一致
(C) 细节完全一致
(D) 二者冲突
(E) 有差异但不影响事实性
choice_strings: ABCDE # 期望裁判输出的可枚举选项
input_outputs:
input: completion # 用 input 这个字段去生成 completion
关键字段:
| 字段 | 作用 |
|---|---|
prompt | 评分 prompt,{key} 由样本字段或额外 args 填充 |
choice_strings | 裁判应输出的选项集合(如 "ABCDE" 或 ["Yes","No"]) |
input_outputs | 映射:用哪个输入字段生成哪个 completion 字段 |
choice_scores(可选) | 每个选项 → 分数,用于汇总平均分 |
eval_type(可选) | 期望的答题格式(见下节) |
4. 关键技巧一:eval_type 把"怎么答"标准化
裁判 模型不会自动用可解析格式回答,所以框架自动追加一句格式指令。三种 eval_type 对应三种 ANSWER_PROMPTS(classify_utils.py:13-28):
| eval_type | 含义 | 追加的指令(大意) |
|---|---|---|
cot_classify | 先推理,后选项(推荐) | 先一步步推理,然后单独一行打印 {choices} 之一,最后再重复一遍答案 |
classify_cot | 先选项,后推理 | 先打印选项,再从下一行解释理由 |
classify | 只给选项 | 只打印 {choices} 之一,别的都不要 |
为什么默认推荐 cot_classify?因为让模型先推理再下结论通常更准(避免"开口就拍脑袋")。文档明确把它列为默认(docs/eval-templates.md:37)。
指令是 append_answer_prompt(classify_utils.py:131-149)拼到 prompt 末尾的,{choices} 被替换成 "A" or "B" or ...(choice_to_str,classify_utils.py:105-107)。
5. 关键技巧二:解析选项时"反向扫行"
裁判输出一大段推理 + 选项,怎么稳健地抠出那个选项?get_choice(classify_utils.py:110-128)的做法很巧:
# 示意,非源码:从裁判的多行输出里抠出选项
lines = text.strip().split("\n")
if eval_type.startswith("cot_classify"):
lines = lines[::-1] # 关键:CoT 模式下反转,从最后往前找
for line in lines:
line = strip_punctuation(line) # 去标点,避免 "(A)." 干扰
for choice in choice_strings:
if match_fn(line, choice): # 默认 starts_or_endswith
return choice
return "__invalid__" # 一个都没匹配上
两处设计值得记:
- CoT 模式反向扫行(
classify_utils.py:117-118):因为cot_classify让模型"最后再重复一遍答案",最终选项在末尾,反转后第一个命中的就是它,避开了推理正文里偶然出现的选项字母。 - 解析失败不崩,记
__invalid__(常量INVALID_STR,classify_utils.py:10):裁判跑偏时这一题被标记为无效,而不是让整轮评测挂掉。
匹配函数可换(MATCH_FNS,classify_utils.py:29-34:include/exact/endswith/starts_or_endswith),默认 starts_or_endswith。