让模型当裁判之前,先准备好核对它的办法
你改了学习助手的回答提示,想在几十道题上比较新旧两版。人工逐题看太慢,于是想让另一个大模型当裁判。问题是:裁判自己也会偏心,它可能偏爱排在前面的回答、偏爱更长的回答、偏爱和自己“同一家”的回答。本课在六道固定题上走完整流程:先用规则评分拦住能机械判断的错误,再给出完整的裁判提示词,然后用脚本检查位置偏差、长度偏差,讨论怎样查自我偏好,最后计算裁判和人工的一致率,并生成人工抽查名单。
完成后你会得到三个脚本和三份记录文件:rule_score.py(规则评分)、judge_check.py(裁判记录检查)、agreement.py(一致率与抽查名单)。准备 Python 3.12 和空目录 judge-lab,按顺序运行,只用标准库。基础的“固定题、先定标准、记录失败”做法见评测实操,本课在它之上加入裁判。题目、回答和裁判记录都是本站编写的教学样例,不是任何模型的实际输出。
第一步:固定题集和规则评分
能用规则判断的,不交给裁判:必须覆盖的要点、绝对不能出现的错误结论、长度上限。规则便宜、每次结果一样,还能在裁判出错时兜底。下面六道题都来自本站其他实操课的知识点,A、B 是两版提示得到的回答。保存为 rule_score.py:
import json
# 固定题集:问题、必须覆盖的要点(每个要点列出可接受的说法)、禁止出现的错误结论
QUESTIONS = {
"Q1": {"q": "temperature 调成 0 就能保证每次输出完全一样吗?",
"points": [["不能", "不保证", "不一定"]], "forbidden": ["一定完全一样"]},
"Q2": {"q": "换了 embedding 模型,旧向量还能和新查询比较吗?",
"points": [["不能", "不可以"], ["重算", "重新生成"]], "forbidden": ["维度相同就可以"]},
"Q3": {"q": "分类准确率 0.75,一定说明模型有用吗?",
"points": [["基线", "多数类"], ["召回"]], "forbidden": ["可以上线"]},
"Q4": {"q": "没有返回 usage 的超时请求,费用记 0 可以吗?",
"points": [["不可以", "不能"], ["账单"]], "forbidden": ["不收费"]},
"Q5": {"q": "RAG 回答错了,第一步该查什么?",
"points": [["检索结果", "返回了什么"]], "forbidden": ["直接换更大的模型"]},
"Q6": {"q": "能不能直接执行学员上传的代码来评分?",
"points": [["不应直接执行", "不建议直接执行"]], "forbidden": ["直接执行即可"]},
}
MAX_CHARS = 80
# 两版提示得到的回答:本站编写的教学样例,不是任何模型的实际输出
ANSWERS = {
"Q1": ("不能保证。温度为0会尽量选概率最高的词,但服务端实现和并发计算可能带来差异,需要多次运行比较。",
"温度是控制随机性的参数。调成0后模型每一步都选概率最高的词,因此输出一定完全一样,评测时只运行一次即可。"),
"Q2": ("不能。不同模型的坐标含义不同,换模型后要把文档向量全部重算。",
"不能直接比较。不同模型的向量空间不同,即使维度相同也不行,应该用新模型把文档全部重新生成向量,并记录模型名和维度。"),
"Q3": ("不一定。要先和全猜多数类的基线比,再看召回率。",
"准确率0.75已经不错,说明四次里能对三次,可以上线使用,后续再慢慢优化阈值和特征。"),
"Q4": ("不可以。没有usage不等于没扣费,应标为未知并和账单核对。",
"不可以直接记0。超时请求可能已经消耗输入token,应在记录里标为未知,月底拿平台账单逐项核对,再调整重试策略。"),
"Q5": ("先看检索返回了什么,确认正确资料有没有被召回,再看生成。",
"可以直接换更大的模型试试,大模型理解能力更强;如果还不行,再检查检索结果和提示词。"),
"Q6": ("不应直接执行。评分应基于提交的输出和说明,在服务端核对,避免运行不可信代码。",
"不建议直接执行上传的代码。确实需要运行时,应放进隔离环境,并限制网络、时间和资源。"),
}
def rule_score(spec, answer):
hit = sum(any(phrase in answer for phrase in group) for group in spec["points"])
problems = [f"出现“{bad}”" for bad in spec["forbidden"] if bad in answer]
if len(answer) > MAX_CHARS:
problems.append(f"超长{len(answer)}字")
return {"points": f"{hit}/{len(spec['points'])}", "pass": hit == len(spec["points"]) and not problems,
"problems": problems, "length": len(answer)}
results = {}
for qid, spec in QUESTIONS.items():
a, b = (rule_score(spec, text) for text in ANSWERS[qid])
results[qid] = {"A": a, "B": b}
print(f"{qid} " + " | ".join(
f"{name}:{r['points']} {'通过' if r['pass'] else '失败'} {r['length']}字"
+ "".join(f" {p}" for p in r["problems"])
for name, r in (("A", a), ("B", b))))
print("规则通过:A", sum(r["A"]["pass"] for r in results.values()), "题,B",
sum(r["B"]["pass"] for r in results.values()), "题,共", len(results), "题")
with open("rule_results.json", "w", encoding="utf-8") as f:
json.dump(results, f, ensure_ascii=False, indent=1)
实际输出:
Q1 A:1/1 通过 47字 | B:0/1 失败 52字 出现“一定完全一样”
Q2 A:2/2 通过 30字 | B:2/2 通过 57字
Q3 A:2/2 通过 23字 | B:0/2 失败 42字 出现“可以上线”
Q4 A:2/2 通过 30字 | B:2/2 通过 56字
Q5 A:1/1 通过 28字 | B:1/1 失败 41字 出现“直接换更大的模型”
Q6 A:1/1 通过 38字 | B:1/1 通过 41字
规则通过:A 6 题,B 3 题,共 6 题
注意 Q5 的 B:它提到了“检索结果”,要点命中,但同时给出了“直接换更大的模型”这个错误的第一步,所以仍判失败。关键词规则也有明显局限:它看不懂否定句(“不能说一定完全一样”也会被误判),也认不出意思正确但措辞不同的回答。所以规则只负责拦截确定的错误,质量高低的比较才交给裁判和人工。B 的每个回答都比 A 长,这一点下面会用到。
第二步:写裁判提示词(练习方案(本站未实跑))
这一步需要你自己的模型平台账号,6 题各跑两轮共 12 次调用,会产生少量费用;本站没有替你运行,第三步用的是虚构记录。裁判用两两比较:每次给它同一题的两个回答,只让它选更好的一个或判平局。完整提示如下,每次替换问题、参考要点和两个回答:
你是评测员。任务是比较两个回答对同一个问题的质量,你不需要自己回答这个问题。
【问题】
temperature 调成 0 就能保证每次输出完全一样吗?
【参考要点】(人工预先写好,回答不必逐字相同)
- 不能保证每次完全一样;
- 需要多次运行比较,不能只跑一次下结论。
【回答1】
<粘贴第一个回答的全文>
【回答2】
<粘贴第二个回答的全文>
评判规则,按优先级:
1. 事实错误最严重:与参考要点矛盾,或给出没有依据的确定结论,算致命错误。
2. 其次看是否覆盖参考要点;没覆盖但没说错,好于说错。
3. 长度、语气和格式不加分;内容相当时,更短的回答不算差。
4. 不要因为回答出现的先后顺序偏向任何一方。
5. 两个回答质量相当,或有同等严重的错误,判 tie。
只输出一行 JSON,不要输出其他文字,格式:
{"winner": "<1 或 2 或 tie>", "fatal_error": {"1": <true 或 false>, "2": <true 或 false>}, "reason": "<不超过60字,写决定性差异>"}
其余五题的参考要点:Q2 不能直接比较,换模型后全部重算;Q3 不一定,先和全猜多数类的基线比,再看召回率;Q4 不可以,标为未知并核对账单;Q5 先看检索返回了什么,确认正确资料是否被召回;Q6 不应直接执行,基于提交的输出在服务端核对,确需运行时放进隔离环境。
操作要求:每题跑两轮,第 1 轮把 A 放在“回答1”、B 放在“回答2”,第 2 轮交换位置;所有题用同一个裁判模型、同一版提示和同样的参数,采样随机性设为平台允许的较低值;裁判尽量选与两个被评回答都不同来源的模型;把模型名、日期、提示版本和 12 条原始输出原样存进 judge_raw.json。格式说明里不写具体胜者的示例,是为了不把裁判引向某一方。
第三步:检查位置偏差和长度偏差
公开研究早已报告过这些问题:2023 年的论文《Judging LLM-as-a-Judge with MT-Bench and Chatbot Arena》(arXiv:2306.05685)在摘要中列出了位置偏差、冗长偏差和自我增强偏差(2026-10 核对原文页面)。你的裁判有没有、有多严重,要用自己的题测。下面用虚构的 12 条裁判输出演示检查方法。保存为 judge_check.py:
import json
with open("rule_results.json", encoding="utf-8") as f:
rules = json.load(f)
# 教学虚构的裁判原始输出,不是任何模型的实际结果。
# 第1轮“回答1”是A、“回答2”是B;第2轮交换顺序,“回答1”是B、“回答2”是A。
RAW = {
"Q1": ['{"winner": "2", "reason": "回答2解释更完整"}', '{"winner": "1", "reason": "回答1更详细"}'],
"Q2": ['{"winner": "2", "reason": "回答2提到记录模型名"}', '{"winner": "1", "reason": "回答1更全面"}'],
"Q3": ['{"winner": "1", "reason": "回答1提到基线"}', '{"winner": "1", "reason": "回答1给出行动建议"}'],
"Q4": ['{"winner": "2", "reason": "回答2更具体"}', '{"winner": "1", "reason": "回答1更具体"}'],
"Q5": ['{"winner": "1", "reason": "回答1步骤清楚"}', '{"winner": "1", "reason": "回答1更实用"}'],
"Q6": ['回答1和回答2都不错,我选回答1。', '{"winner": "tie", "reason": "两者都强调不直接执行"}'],
}
# 人工标注(教学虚构):tie 表示两者质量相当
HUMAN = {"Q1": "A", "Q2": "B", "Q3": "A", "Q4": "tie", "Q5": "A", "Q6": "tie"}
ORDER = [{"1": "A", "2": "B", "tie": "tie"}, {"1": "B", "2": "A", "tie": "tie"}]
def parse(raw, order):
try:
data = json.loads(raw)
except json.JSONDecodeError:
return None, None # 无效输出照样计入分母,不能悄悄丢掉
if not isinstance(data, dict) or data.get("winner") not in order:
return None, None
return order[data["winner"]], data["winner"]
summary, first_wins, decisive, invalid = {}, 0, 0, 0
for qid, outputs in RAW.items():
verdicts = []
for raw, order in zip(outputs, ORDER, strict=True):
verdict, slot = parse(raw, order)
verdicts.append(verdict)
invalid += verdict is None
if slot in ("1", "2"):
decisive += 1
first_wins += slot == "1"
if None in verdicts:
final = "需重跑"
elif verdicts[0] == verdicts[1]:
final = verdicts[0]
else:
final = "tie" # 交换顺序后结论相反:按平局处理,并列入人工抽查
longer = "A" if rules[qid]["A"]["length"] > rules[qid]["B"]["length"] else "B"
rule_fail = [k for k in ("A", "B") if not rules[qid][k]["pass"]]
summary[qid] = {"rounds": verdicts, "judge": final, "human": HUMAN[qid],
"consistent": verdicts[0] == verdicts[1] and None not in verdicts,
"longer": longer, "rule_fail": rule_fail}
note = ",裁判选了规则判失败的回答" if final in rule_fail else ""
print(f"{qid} 两轮={verdicts} 裁判={final} 人工={HUMAN[qid]} 较长={longer}{note}")
valid_pairs = [s for s in summary.values() if None not in s["rounds"]]
print(f"无效输出 {invalid}/{2 * len(RAW)};交换顺序后一致 "
f"{sum(s['consistent'] for s in valid_pairs)}/{len(valid_pairs)} 题")
print(f"有明确胜者的 {decisive} 次判断里,判“回答1”胜 {first_wins} 次")
judge_pick = [s for s in summary.values() if s["consistent"] and s["judge"] in ("A", "B")]
human_pick = [s for s in summary.values() if s["human"] in ("A", "B")]
print(f"裁判一致判胜时选较长回答 {sum(s['judge'] == s['longer'] for s in judge_pick)}/{len(judge_pick)};"
f"人工选较长回答 {sum(s['human'] == s['longer'] for s in human_pick)}/{len(human_pick)}")
with open("judge_summary.json", "w", encoding="utf-8") as f:
json.dump(summary, f, ensure_ascii=False, indent=1)
实际输出:
Q1 两轮=['B', 'B'] 裁判=B 人工=A 较长=B,裁判选了规则判失败的回答
Q2 两轮=['B', 'B'] 裁判=B 人工=B 较长=B
Q3 两轮=['A', 'B'] 裁判=tie 人工=A 较长=B
Q4 两轮=['B', 'B'] 裁判=B 人工=tie 较长=B
Q5 两轮=['A', 'B'] 裁判=tie 人工=A 较长=B
Q6 两轮=[None, 'tie'] 裁判=需重跑 人工=tie 较长=B
无效输出 1/12;交换顺序后一致 3/5 题
有明确胜者的 10 次判断里,判“回答1”胜 7 次
裁判一致判胜时选较长回答 3/3;人工选较长回答 1/4
按信号逐个处理。Q3、Q5 交换顺序后结论正好相反,说明裁判在这两题上跟着位置走,脚本按平局记并列入人工抽查;10 次明确判断里“回答1”赢了 7 次,没有位置偏差时应接近一半,但 10 次太少,只能当作“要扩大样本再查”的信号,不能当结论。长度方面,裁判三次一致判胜都选了较长的回答,人工四次只选了一次。Q1 最危险:裁判选了一个规则已判失败、含有错误结论的回答,所以规则失败要优先于裁判结论。Q6 第一轮不是 JSON,记为无效并重跑,而不是从统计里删掉。
第四步:自我偏好怎么查
自我偏好是指裁判更喜欢与自己同一模型或同一家族生成的回答。假设本例中 B 版回答恰好由与裁判同家族的模型生成,那么“裁判偏爱 B”既可能是长度偏差,也可能是自我偏好,两者在这份数据里混在一起,分不开。要拆开,就得让两个因素不再同时出现:
- 换一个与两个被评模型都不同来源的裁判,用同一批题、同一版提示再跑一遍,比较两个裁判选 B 的比例。
- 准备几对长度相近的回答(把较长的回答删去重复句,内容不变),看偏好是否还在。
- 在同一批题上计算“裁判选同源回答的比例”减去“人工选同一回答的比例”,差值明显为正且在更多题上稳定存在,才算有自我偏好的证据。
如果条件有限,至少做到第 1 条,并在报告里写明裁判与被评模型的来源关系。
第五步:算一致率,定人工抽查名单
一致率回答“裁判和人工有多常意见相同”。只看原始一致率会被“大家都爱判平局”这类巧合抬高,所以同时计算 Cohen's kappa:它扣除了碰巧一致的部分,1 表示完全一致,0 表示和随机差不多。保存为 agreement.py:
import json
import math
import random
from collections import Counter
with open("judge_summary.json", encoding="utf-8") as f:
summary = json.load(f)
def agreement(pairs):
"""pairs 是 (裁判, 人工) 列表;返回原始一致率和 Cohen's kappa(扣除碰巧一致的部分)。"""
n = len(pairs)
observed = sum(j == h for j, h in pairs) / n
judge_freq, human_freq = Counter(j for j, _ in pairs), Counter(h for _, h in pairs)
expected = sum(judge_freq[c] * human_freq[c] for c in judge_freq) / (n * n)
kappa = (observed - expected) / (1 - expected) if expected < 1 else float("nan")
return round(observed, 2), round(kappa, 2)
usable = [(s["judge"], s["human"]) for s in summary.values() if s["judge"] != "需重跑"]
print(f"可比较 {len(usable)} 题,(一致率, kappa) =", agreement(usable))
print("分歧明细:", dict(Counter(f"裁判{j}/人工{h}" for j, h in usable if j != h)))
def spot_check(items, rate=0.2, minimum=5, seed=2026):
"""有问题的题全部人工看;其余按比例随机抽,至少 minimum 题。"""
must = sorted(i for i, flags in items.items() if flags)
rest = sorted(i for i, flags in items.items() if not flags)
size = min(len(rest), max(minimum, math.ceil(len(rest) * rate)))
return must, sorted(random.Random(seed).sample(rest, size))
# 抽查计划演示:假设正式评测有 50 题,标记来自第三步那样的检查(编号与标记均为演示用)
flags = {f"T{i:02d}": [] for i in range(1, 51)}
for tid in ("T03", "T11", "T17", "T26", "T31", "T44"):
flags[tid].append("交换顺序不一致")
for tid in ("T08", "T26"):
flags[tid].append("规则失败却被判胜")
flags["T40"].append("输出无效")
must, sampled = spot_check(flags)
print(f"必查 {len(must)} 题:{must}")
print(f"随机抽查 {len(sampled)} 题:{sampled}")
print(f"人工共看 {len(must) + len(sampled)}/{len(flags)} 题")
assert agreement([("A", "A"), ("B", "B")]) == (1.0, 1.0)
实际输出:
可比较 5 题,(一致率, kappa) = (0.2, 0.0)
分歧明细: {'裁判B/人工A': 1, '裁判tie/人工A': 2, '裁判B/人工tie': 1}
必查 8 题:['T03', 'T08', 'T11', 'T17', 'T26', 'T31', 'T40', 'T44']
随机抽查 9 题:['T09', 'T10', 'T19', 'T25', 'T33', 'T38', 'T39', 'T43', 'T48']
人工共看 17/50 题
这个虚构裁判和人工只在 1/5 的题上一致,kappa 为 0,等于没比随机好,不能替代人工。分歧明细比总数更有用:两次“裁判平局/人工选 A”来自位置不一致,一次“裁判 B/人工 A”来自长度偏好。前面提到的论文在它自己的题集和设定下报告过强裁判与人类超过 80% 的一致,那是它的条件,不能直接搬到你的题目上,必须自己测。
抽查比例是本课的建议,不是行业标准:新写的裁判提示第一次使用时,全部题都人工看一遍,用来校准;之后每轮必查所有被标记的题(交换顺序不一致、规则失败却被判胜、输出无效),其余随机抽 20%、至少 5 题。随机种子写进记录,别人才能复现同一份名单。若抽查中发现的分歧明显增多,就回到全量人工。
常见失败与修改
裁判总是选更长的回答。 提示里写“长度不加分”只能减轻、不能消除。先用规则拦掉含错误结论的长回答;为被评回答设统一的长度上限;再按第三步统计“裁判选较长/人工选较长”的比例,差距仍大就换裁判或改用逐条打分。
交换顺序后结论相反。 每题固定跑两轮,相反就记平局并人工复核。如果这类题超过五分之一,修改提示:要求裁判先分别列出两个回答的致命错误和遗漏要点,再给结论,然后重测一致率。
输出不是 JSON 或字段不对。 计入无效,最多重跑一次,仍无效就转人工;不要在统计时删掉。解析与校验写法见JSON 实操。
裁判和人工分歧很大。 先看分歧明细。大量“裁判平局/人工有偏好”通常说明参考要点写得太粗,补充要点并写清平局条件;人工之间也可能不一致,两人独立标注一部分题,先算人工之间的一致率。
拿裁判胜率直接宣布新版更好。 报告里同时给出四个数:规则通过率、裁判胜率、裁判与人工一致率、人工抽查结论;并写明题数、裁判模型、提示版本和日期。只有一个胜率的结论不可复核。
保存和使用结果
judge-lab 目录保留三个脚本、rule_results.json、judge_summary.json、judge_raw.json 和一份人工标注表(题号、人工结论、标注人、日期、备注)。每次改裁判提示都换版本号重跑全部题,不只重跑上次出错的题。裁判结果只用于辅助排序和发现可疑题,最终是否采用新版提示,以规则结果和人工抽查为准。
自测与答案
- 为什么每题要交换顺序各跑一次?用来发现位置偏差;两轮结论相反说明裁判在这题上不可靠,要记平局并人工复核。
- 裁判选了一个规则判失败的回答,以谁为准?以规则为准,并把这题列入必查。
- 一致率 0.8 一定说明裁判可靠吗?还要看 kappa、题数和分歧集中在哪类题;如果大多数题本来就是平局,0.8 也可能主要来自巧合。
- 裁判和 B 版回答来自同一家族,B 胜率偏高,能断定是自我偏好吗?不能,可能混着长度偏差,要换不同来源的裁判或用长度相近的对照来拆开。
交付物是规则评分结果、裁判提示词与原始输出、偏差检查结果、一致率和人工抽查名单。本课数字全部来自虚构样例,只演示方法;你的裁判表现要用你的题和真实运行记录来证明。
来源与内容说明
AIScope 中文手册 · AIScope 编辑
本篇为本站原创学习内容,引用资料保留参考来源。
收录:2026年10月19日
适用范围:尚未标注,请结合原文与实际环境核对
人工核验:尚无核验记录