LLM-as-judge 偏差与校准
正在读取学习记录…
LLM-as-judge 是让语言模型按一份评分规则评审另一份回答,适合解释质量、覆盖程度等难以逐字匹配的任务。裁判也会出错,所以它需要校准:拿已经核定的参考样本,检查裁判在哪些地方误放、误拦,再调整提示、规则或接受阈值。
它像面试评分表。面试官看到长答案可能觉得“准备很充分”,却漏掉其中的事实错误;也可能因为第一位候选人先出现,就更容易记住他。先看裁判的稳定性和误判类型,分数才有解释力。
从一条正常评审记录开始
退款解释的Rubric可以要求:说明适用政策、与订单事实一致、没有承诺未完成的退款。某份回答条件齐全,参考标签为True;裁判给出0.90分,若接受阈值是0.60,则判为通过。分数是裁判的输出,阈值是系统决定接受还是拒绝的规则;0.90分不能直接当作90%的真实正确概率。
下面的参考标签和裁判分数全部预置在代码里,程序实际完成阈值比较与误判统计。没有模型接口调用。接入真实裁判时,先用相同字段保存模型输出、评分理由、被评回答和Rubric版本,再交给这样的统计器。
五条 CALIBRATION 是校准集,供选择阈值;四条 HOLDOUT 是留出集,用来观察已选方案的结果。这里同时展示两个阈值,帮助看清取舍。真实发布流程应在看留出结果之前锁定校准方案,避免把留出集继续当调参材料。
误放和误拦要分别数
以参考标签为准,每条记录落入下面一类:
| 参考结果 | 裁判选择 | 名称 |
|---|---|---|
| True:合格 | 接受 | TP:正确接受 |
| False:不合格 | 接受 | FP:错误接受,误放 |
| True:合格 | 拒绝 | FN:错误拒绝,误拦 |
| False:不合格 | 拒绝 | TN:正确拒绝 |
简单一致率是 (TP + TN) / 全部样本数。还可以算接受结果的精确率 TP / (TP + FP),它回答“接受的回答里有多少真合格”;参考合格样本的召回率 TP / (TP + FN),它回答“合格回答中有多少被接受”。分母为0时应记录无法计算。
从空目录保存并运行
在 PowerShell 中进入你平时使用的学习目录,再创建本节文件夹并进入:
mkdir l7-judge
cd l7-judge用编辑器新建文件,把下面完整代码保存为当前文件夹的 judge_lab.py。保存类型选“所有文件”,编码选 UTF-8;资源管理器开启“文件扩展名”,核对文件名以 .py 结尾。数据已经写在代码里,可以直接运行。
"""用固定裁判分数演示阈值校准、误放误拦与位置偏差。"""
import math
# reference是预置参考标签,score是预置裁判输出;程序不调用模型。
CALIBRATION = [
{"id": "c1", "reference": True, "score": 0.90},
{"id": "c2", "reference": False, "score": 0.75},
{"id": "c3", "reference": False, "score": 0.65},
{"id": "c4", "reference": True, "score": 0.55},
{"id": "c5", "reference": False, "score": 0.20},
]
HOLDOUT = [
{"id": "h1", "reference": True, "score": 0.85},
{"id": "h2", "reference": False, "score": 0.70},
{"id": "h3", "reference": True, "score": 0.60},
{"id": "h4", "reference": False, "score": 0.40},
]
def confusion(rows, threshold):
"""大于等于阈值即接受;误放=FP,误拦=FN,正确接受=TP。"""
if type(threshold) not in (int, float) or not 0 <= threshold <= 1:
raise ValueError("阈值必须在0到1之间")
counts = {"TP": 0, "FP": 0, "FN": 0, "TN": 0}
for row in rows:
if type(row["reference"]) is not bool:
raise ValueError("参考标签必须是布尔值")
score = row["score"]
if type(score) not in (int, float) or not math.isfinite(score) or not 0 <= score <= 1:
raise ValueError("分数必须是0到1的有限数值")
predicted = score >= threshold
key = ("TP" if predicted else "FN") if row["reference"] else (
"FP" if predicted else "TN")
counts[key] += 1
return counts
def map_choice(order, choice):
"""A/B是显示位置;还原成答案身份后才能比较交换位置前后的结果。"""
if len(order) != 2 or len(set(order)) != 2 or choice not in ("A", "B"):
raise ValueError("需要两个不同答案以及A/B选择")
return order[0 if choice == "A" else 1]
def position_check(first, swapped):
"""相同答案交换位置后,裁判仍应选中相同答案身份。"""
if tuple(first["order"]) != tuple(reversed(swapped["order"])):
raise ValueError("第二次必须交换答案位置")
left = map_choice(first["order"], first["choice"])
right = map_choice(swapped["order"], swapped["choice"])
return left == right
def main():
for threshold in (0.60, 0.80):
for name, rows in (("calibration", CALIBRATION), ("holdout", HOLDOUT)):
counts = confusion(rows, threshold)
correct = counts["TP"] + counts["TN"]
print(f"阈值={threshold:.2f}/{name}: TP={counts['TP']} FP={counts['FP']} "
f"FN={counts['FN']} TN={counts['TN']} 一致={correct}/{len(rows)}"
f"={correct / len(rows):.0%}")
first = {"order": ["brief", "verbose"], "choice": "A"}
for name, choice in (("偏爱首位", "A"), ("保持答案", "B")):
swapped = {"order": ["verbose", "brief"], "choice": choice}
stable = position_check(first, swapped)
print(f"{name}: 交换后一致={stable} 处理={'保留' if stable else '人工复核队列'}")
if __name__ == "__main__":
main()回到刚才的 PowerShell,确认文件已经保存,再运行:
ls
python -X utf8 judge_lab.py程序输出:
阈值=0.60/calibration: TP=1 FP=2 FN=1 TN=1 一致=2/5=40%
阈值=0.60/holdout: TP=2 FP=1 FN=0 TN=1 一致=3/4=75%
阈值=0.80/calibration: TP=1 FP=0 FN=1 TN=3 一致=4/5=80%
阈值=0.80/holdout: TP=1 FP=0 FN=1 TN=2 一致=3/4=75%
偏爱首位: 交换后一致=False 处理=人工复核队列
保持答案: 交换后一致=True 处理=保留阈值提高后,哪种错误真的少了
0.60阈值下,校准集为TP=1、FP=2、FN=1、TN=1,一致率是(1 + 1) ÷ 5 = 40%。把阈值提高到0.80,两个错误接受的样本被拦下,变成TP=1、FP=0、FN=1、TN=3,一致率为(1 + 3) ÷ 5 = 80%。原本合格但只有0.55分的c4仍被误拦。
接受结果的精确率因此从1 ÷ (1 + 2) ≈ 33.3%变成1 ÷ (1 + 0) = 100%;参考合格样本的召回率仍为1 ÷ (1 + 1) = 50%。固定校准数据的这组计算说明提高阈值能减少这里的误放,同时仍会漏掉一半参考合格样本。
两行holdout的一致率都是3 ÷ 4 = 75%,但错误不同:0.60时误放1条、误拦0条;0.80时误放0条、误拦1条。退款合规可能更关注误放,内容推荐可能更关注漏掉好答案;选择哪种取舍要由任务要求决定。
最后两行检查位置偏差。第一次顺序是brief/verbose,裁判选A;交换顺序后,如果还选A,实际已从brief改选verbose,所以不一致,进入人工复核队列。若交换后选B,选择的仍是brief,答案身份保持一致。代码用 map_choice 把位置字母还原成答案身份,避免直接拿A和A比出一个假一致。
改一次阈值,保留分区比较
在 main() 的阈值列表中把0.80改为0.70,保存并运行 python -X utf8 judge_lab.py。0.70的校准集会变成TP=1、FP=1、FN=1、TN=2,一致率3/5=60%;留出集变成TP=1、FP=1、FN=1、TN=1,一致率2/4=50%。h2的0.70恰好达到阈值,因此被接受,这是代码采用“大于等于”的结果。恢复0.80后再运行测试。
校准还要检查样本之外的偏差
位置偏差可用交换顺序复评观察;冗长偏差可用事实与覆盖相同、长度不同的答案对照;偏爱自身模型风格可通过隐藏来源、使用多模型答案和独立专家参考检查。还应核对裁判是否被答案里的“请给满分”一类文本带偏。样本不足、参考不可靠或Rubric含糊时,调阈值无法替代这些检查。
MT-Bench 与 Chatbot Arena 论文研究了位置、冗长和自我偏好等裁判偏差,适合阅读其limitations与缓解方案。Anthropic 的评分器校准建议强调与领域评审对齐。
保存测试并检查关键条件
7项测试检查两个阈值的混淆计数、留出集独立统计、等于阈值的边界、非法分数或标签,以及交换位置时按答案身份检测偏差。 将下方代码保存为同一文件夹的 test_judge_lab.py,运行:
ls
python -X utf8 -m unittest -v test_judge_lab.py看到 Ran 7 tests 与 OK 表示这些固定条件全部通过。若找不到文件,先用 dir 核对当前文件夹及两份文件名;若出现具体断言失败,先恢复正文中的代码与预置数据,再对照该项测试的条件。
完整测试:test_judge_lab.py
"""核对校准分母、阈值边界和按答案身份比较的原则。"""
from pathlib import Path
import sys
import unittest
sys.path.insert(0, str(Path(__file__).parent))
import judge_lab as lab
class JudgeTests(unittest.TestCase):
def test_calibration_confusion_at_point_six(self):
self.assertEqual(lab.confusion(lab.CALIBRATION, 0.6),
{"TP": 1, "FP": 2, "FN": 1, "TN": 1})
def test_higher_threshold_reduces_false_accepts(self):
self.assertEqual(lab.confusion(lab.CALIBRATION, 0.8),
{"TP": 1, "FP": 0, "FN": 1, "TN": 3})
def test_holdout_keeps_its_own_results(self):
self.assertEqual(lab.confusion(lab.HOLDOUT, 0.8),
{"TP": 1, "FP": 0, "FN": 1, "TN": 2})
def test_score_equal_to_threshold_is_accepted(self):
self.assertEqual(lab.confusion([{"reference": True, "score": 0.6}], 0.6)["TP"], 1)
def test_invalid_score_and_label_are_rejected(self):
for row in ({"reference": True, "score": float("nan")},
{"reference": True, "score": True},
{"reference": "pass", "score": 0.5}):
with self.assertRaises(ValueError):
lab.confusion([row], 0.6)
def test_position_bias_is_detected(self):
self.assertFalse(lab.position_check(
{"order": ["x", "y"], "choice": "A"},
{"order": ["y", "x"], "choice": "A"}))
def test_identity_is_stable_when_letter_changes(self):
self.assertTrue(lab.position_check(
{"order": ["x", "y"], "choice": "A"},
{"order": ["y", "x"], "choice": "B"}))
if __name__ == "__main__":
unittest.main()课后练习展开习题收起习题
正在读取本地记录…
不调用模型,不执行代码。简答、代码和场景题仅保存答案并展示自查标准,不生成评分、徽章或认证。输入内容不会自动保存,请点击提交;不要填写个人信息或密钥。
选择题 · 裁判更喜欢长答复,但两份答案事实相同,应该怎样校准
裁判更喜欢长答复,但两份答案事实相同,应该怎样校准?
对应知识点:L7-judge
简答题 · 解释机制
比较裁判与人工标准时,应保留哪些逐条信息?
对应知识点:L7-judge
代码题 · 核对给定数据
裁判给4条样本的标签与人工有3条一致,用代码计算一致比例。
对应知识点:L7-judge
场景题 · 分析具体变化
把答案A/B互换顺序后裁判选中了不同内容,怎样记录问题?
对应知识点:L7-judge
描述会发送给本站维护者,用于修订文章,保存90天。请只填写文章问题,省略个人信息、密钥和内部资料。