判官不稳定时,先别换判官——一次 LLM-as-Judge 的完整排查实录

58 阅读21分钟

30 秒判断:如果你在用 LLM-as-Judge 评测 RAG / Agent,遇到"同一份输出、判官时 pass 时 fail,或者判定和依据自相矛盾",本文给的是一条完整排查路径。核心结论只有一句:判官不稳定时,第一反应不是加提示词、换判官或上确定性规则,而是回头检查"判定任务本身有没有被构造得可判定" 。本文是一次真实排查的全过程记录:两个异常 → 三次无效的直觉反应 → 变量分离实验 → 根因(居然在任务定义、金标准和输出 schema)→ 修复。所有实验数据、判官提示词、金标准都在开源仓库,可独立审计。

适合谁读:正在搭 LLM 评测体系、被 LLM 判官的"随机性"折磨的工程师。不需要 RAG 背景,但最好写过至少一个 judge prompt。

阅读成本:约 25 分钟。第 1、2 节是故事,第 3–8 节是干货,赶时间可以直接跳第 3 节。

0. 背景:一套会被判官骗过的评测体系

我在做一个 RAG 学习助手(检索多篇文章、回答跨文档对比类问题的小系统),项目不大,但评测体系建得比较认真:每条用例有金标准,跑批用 LLM 判官打 pass/fail,repeat 20 次看分布。判官跑在温度 0 下。

跑通 12 条基准用例之后,评测体系本身开始出问题。两个异常几乎同时浮出水面。

异常一:判官不稳定。

同一条输出、同一个缺失主题,repeat 20 次,判官给出 15 次 pass、5 次 fail。

【术语锚 · judge consistency】 业界对"同一被评对象 × 同一判定任务 × repeat N 次送判,统计判官翻转率"的通用名称是 judge consistency / self-consistency(Microsoft Foundry 把它列为判官工程三大可靠性支柱之一——Human Alignment / Self-Consistency / Inter-Model Agreement),Anthropic judge cookbook 同义称 judge stability,Orq.ai 操作化为 flip-rate(同输入 N 次送判里判定翻转的比例)。学界亦称 intra-rater reliability(配 Krippendorff's α)。

测量目标始终是判官本身,不是被评输出——一个对"同一缺失主题"都能给出 15 pass / 5 fail 的判官,它给出的任何覆盖率裁决都不再可信,任何下游统计都失效。上文"15 pass / 5 fail"即这一指标的最小观测点;后文"任务拆解 + 修金标准到 1600/1600 零翻转"也是同一指标的收口。

延伸阅读:《Rating Roulette》(arXiv:2510.27106)实测 Llama 3.1 / Deepseek-R1 / Qwen 3 三款主流模型的 Krippendorff's α 区间为 0.33–0.79——自我不一致不是孤例,是同类工程共性。

异常二:判官自相矛盾。 单次判定内部,结论说 covered,给出的依据里写的却是不覆盖的理由——判定和依据当面打架。

两个异常各有各的致命处。异常一意味着温度 0 也压不住随机抖动:如果你的判官有 25% 的概率放水,那你所有"这次改动让通过率从 16/20 提升到 19/20"的结论都是噪声。异常二意味着你连"它为什么这么判"都无法归因——该信结论还是该信依据?判官不可信,整条评测流水线报废。

下文按时间线记录整个排查过程。先剧透结论:两个异常的根因都不在判官模型身上——判官不稳定,一个根因在判定任务的范围(附带任务叠加),一个在金标准的写法;自相矛盾,根因在判官输出的字段顺序。判官模型本身,自始至终是够用的。 修异常一用了两刀(第 2、3 节),修异常二只需要把字段顺序对调(第 4 节)。

1. 第一反应:修判官——然后连续撞墙

面对这两个异常,工程师的直觉反应大概就三种:加约束、换结构、上确定性。我全试了,记录一下撞墙过程,因为撞墙的方式本身有信息量。

撞墙一:加提示词约束。 当时的判官提示词里已经写了三重约束:①"标 covered 必须在 evidence 中引用与探针要素对应的输出原句,无法指认则标 not_covered";②"禁止出现'证据说未覆盖却标 covered'的自相矛盾";③点表里每一点自带覆盖探针。结果同一份输出让判官判 3 次(温度 0),问题点仍是 2 次 covered / 1 次 not_covered,且两次 covered 的 evidence 自己写着缺关键组件——两个异常,一个都没压住。

结论很硬:if-then 形态的约束,LLM 不执行。 写得再明白也没用——约束是给"先读完全文再决策的智能体"准备的,而 LLM 是逐 token 生成的,它读到约束时并不能保证回头修改已经生成的 token。这一点后面会成为重要伏笔。

撞墙二:换结构,做结构化裁决。 把自由文本判官改成"清单型":金标准不再是散文描述,而是拆成 15 个显式的点(核心 8 个 + 扩展 7 个),每个点带覆盖探针;判官只做逐点标注(covered / not_covered / equivocal),最终 pass/fail 由程序规则聚合。

这步方向是对的(后面会讲为什么),但它把异常从"裁决漂移"变成了"点级标注漂移":15 个点里 14 个点 100% 稳定,就那几个点反复横跳。确定性聚合只能保证"给定相同标注、结论相同",保证不了标注本身稳定。

撞墙三:扩大单次调用的职责。 中间还走过一段弯路:把覆盖标注、遗漏透明、格式评判塞进同一次判官调用。后来证明这恰恰是早期大面积不稳的重要来源之一(第 2 节的变量分离实验会给出数据)。

三次撞墙的共同点:全部动作都在"压判官",没有一次停下来问"任务出给判官的时候,是不是就没出对"。

2. 转折:把问题换一个问法

撞墙之后,问题被重新表述为:

判官不稳定,到底是判官能力不行,还是判定任务本身没被构造得可判定?

这个区分至关重要,因为两者的修法完全不同:前者要换模型、加确定性闸门、堆基础设施;后者只需要改任务定义——便宜得多。

怎么区分?变量分离实验。把判定任务一层层拆开,使用同一个Agent的一次输出(0号输出)作为输入,控制变量重放:

实验任务构造次数结果
单点只判 1 个问题点(完整上下文 / 极简上下文两个版本)×6全部 not_covered—判官本身判得对
5 点纯覆盖5 个点纯覆盖标注,去掉遗漏透明 / 格式等附带任务×1010/10 稳定
15 点纯覆盖15 个点,纯覆盖标注,去掉遗漏透明 / 格式等附带任务×10149/150,仅 1 个问题点出现 1 次假阳性
15 点 + 附带任务线上原版(叠加 transparency + format + verdict)×3问题点 2 次假阳性
15 点纯覆盖同上 ×10 版本×1001493/1500——14 个点 100/100,唯问题点 7/100

这组实验给了两个发现:

发现一:点数不是过载主因,附带任务叠加才是。 15 个点纯标注稳如磐石,一叠上"顺便判遗漏透明、顺便判格式"就开始翻。单次调用里混多种认知任务,是判官不稳的第一大来源。后来的架构就按这个结论改:覆盖标注、事实边界、格式三路并行独立调用,程序聚合。

发现二:翻转不是随机的,是系统性的。 那个 7/100 的问题点(下称"角色重定义"点),7 次 covered 里的假阳性 evidence 全部引用同一句话——那句话其实是另一个点("核心命题"点)的覆盖证据。同一份证据被判官拿去给两个点圆场,这不是噪声,是串位。噪声要靠统计吸收,串位要靠改任务——两者的修法完全不同,混在一起就会瞎修。

3. 根因居然在金标准:给判官出难题的人是我

顺着"串位"这条线回查原文,真相出来了:

"角色重定义"点当初是这样写的(合并版):

自主层级提升 / 工程师角色重定义

而查原文发现,这是两个独立的章节:"Redefining the role of the engineer"(第 2 节,讲任务拆块、prompt 驱动、评审自动化)和"Increasing levels of autonomy"(第 9 节,讲端到端驱动、任务链、升级人类)。更糟的是,"核心命题"点里已经概括过角色转变("从写代码转向设计环境、表达意图、构建反馈回路"),于是判官面对的是这样一道题:

  • 点 A(核心命题):角色转变的概括
  • 点 B(角色重定义):角色转变的细节,外加另一个主题(自主层级)

判官被迫做粒度判别——"这句话算概括还是算细节?"这是一个尺度题,不是事实题。 而金标准里没有任何信息告诉它边界在哪。它拿 A 的证据去 cover B,在它的世界观里甚至不算错。

修法:修金标准,不是再给判官加压。 具体三刀:

  1. 把合并点拆成两个正交的点(由此15个覆盖点变为16个覆盖点);
  2. 每个点把探针实例化——不再说"覆盖须含细节",而是列出生什么才算("任务分解为可交付块、以提示词驱动 agent、评审由 agent 之间完成……任一");
  3. 写反例——"仅复述'设计环境、表达意图、构建反馈回路'的概括不算"。

修后长这样:

工程师角色重定义的细节(覆盖探针:任务分解为可交付块、以提示词驱动 agent、评审由 agent 之间完成、人类转向设计/验收/反馈等任一;仅复述"设计环境、表达意图、构建反馈回路"概括不算)

自主层级提升的细节(覆盖探针/端到端驱动新功能里程碑 / agent 完整任务链 / 仅在需要判断时升级人类;泛化表述不算)

收口数据(使用上一节的0号输出重复让判官判定):

修复后的16个点纯覆盖次数结果
初步验证×20320/320 零翻转
加大repeat次数确认×1001600/1600 零翻转

(注:零翻转统计的是判定结果 v 的稳定性;单次判定内 v 与 evidence 的自相矛盾不在此口径内——那是异常二,第 4 节处理。×100 复测由 AI 结对执行,原始 run 记录可审计,作者抽查数十条复核通过。)

顺带把这次沉淀成了一份清单型金标准写作规范,五条:

  1. 点正交:两两检查,同一母题的概括与细节不拆成两个点;相似类别显式归属到源;
  2. 正面探针实例化:列出点名时必须出现的关键概念/术语,让判官有词可查;
  3. 反例必备:明确"什么不算"——相关点内容不算、背景/成效数字不算、模型元陈述(资料缺失/节选自述)不算文章内容;
  4. 跨源绑定 + 显式排除:多文档用例每点注明所属源,写明"不是另一篇的 X";
  5. 闭环:初稿 → 稳定性实验 → 把错配样本的反例补回探针。

后来做过一次"盲写验证":只拿这五条规范、不带任何调试过程中的内部知识,基于Agent的7号输出进行验证测试,重写三个问题点,repeat 50次,错误率从 86% / 24% / 20% 降到 4% / 0% / 2%。规范是可以外化的,不是"只有踩过坑的人才会写"。

4. 第二类异常:v 与 evidence 自相矛盾

任务叠加去掉、金标准修完,异常一收口(1600/1600 零翻转)之后,回头处理开篇的异常二:v/evidence 自相矛盾。它的根因比金标准更底层,也更有意思。

当时的判官输出 schema 是:

{"o2-c1": {"v": "covered", "evidence": "……"}}

v 在前,evidence 在后。稳定的翻车模式是:v 标 covered,evidence 里写的全是不覆盖的理由——判官"嘴上说覆盖,手上写证据时发现覆盖不了"。

归因只有一个:Transformer 是单向生成的,无法回改已生成的 token。 v 先生成时,它只是读完输出后的"即时印象快照";evidence 后生成时才是真正的逐点核对。核对完发现对不上,但 v 已经吐出去了,改不了——于是出现结构性的自相矛盾。这不是判官"不诚实",是输出结构不符合模型的生成机制。

修法简单到离谱:把字段顺序对调,evidence 在前、v 在后,提示词加一句"先核对写出依据,再依据给出判定":

{"o2-c1": {"evidence": "先核对并引用输出原句(≤100字)", "v": "covered|not_covered|equivocal"}}

数据:同一份输出、同样的问题点,改前 ×10 是 5 covered / 5 not_covered(5 次 covered 的 evidence 全部准确指出未覆盖,核对没错,错的是v);改后 ×10 是 1 covered / 9 not_covered(剩余 1 次是真实的核对错误,不是结构矛盾)。落库后整轮 repeat 20:该点 20/20 not_covered,一个建立在此前假阳性上的"假 pass"就此消失。

这里有个重要的限定,是把 evidence-first 经验搬到引用真实性判官上、翻车之后才想明白的。把 evidence-first 经验搬到引用核对侧(判"这句话是否被所引证据块支撑"),错误率反而暴涨(20/20 pass → 6 pass / 4 pending / 10 fail),判官开始"编证据"——把 A 块的内容安到 B 块上圆。辨析下来,其实是两种 reason-first:

  • 覆盖侧有效:证据 = 摘抄模型输出原文。摘抄是提取动作,不会编造;
  • 引用侧翻车:依据 = 在几十个高度相似的证据块里定位到某一个。大批量重复文本里定位不可靠,判官被迫给每个成分找支撑时,找不到就会编。

所以那条设计原则要精确表述为:结论依赖依据的输出,一律"依据先行";但依据的形态必须是提取/摘抄,不能是定位/检索。基于定位/检索的判断的瓶颈往往不在推理深度,在证据隔离。 (引用侧后来走了完全不同的路:确定性提取 + 小批量分组隔离,那是另一篇文章的素材。)

5. 一个反直觉的配套发现

排查过程中还有一个发现,跟主流直觉相反,值得单列。

判官的输入不是越少越好。 一个自然的想法:覆盖判官只需要"模型输出 + 点表",干嘛把检索证据块也喂给它?少给点输入,注意力不就集中了?做了对照实验(同一份输出 ×10):带证据块 10/10 判对;去掉证据块后 6/10 把模型的元陈述("我掌握的两文均只有若干片段""Claude Cowork 未展开细节")误判为文章自身的"已知局限"内容——错误率 0% → 60%。

机制:覆盖判官判断"输出是否覆盖点 X"时,隐含要区分"输出里这句话是文章内容"还是模型自己的取材说明。证据块是它唯一的原文参照系,砍掉参照系它就只能凭字面联想。职责分离 ≠ 输入最小化——覆盖判官不负责核对引用,但它仍然需要原文作语义参照。(此前担心的"证据块稀释注意力导致跨源错配"实测是 <10% 的低频事件,砍块引来的元陈述误判才是高频错误。)

6. 什么时候停手:防过拟合纪律

最后一个问题:修到什么程度算修完?

前述 3 个修复(任务拆解、金标准修复、字段顺序对调)全部落地后,重新运行Agent repeat产生20条输出,使用新的判官配置及金标准进行Judge(如下图),并对存在不一致的点进行50 repeat的判官重放,不再有可疑的机制性问题,这时候我停手了,不再继续收紧反例。

Pasted image 20260906232830.png

图注:每列是一条独立生成的 Agent 输出(repeat 20 次生成),每格为修复后的判官在该输出、该覆盖点上的单次判定。本图展示的是新输出上的判定分布,不是稳定性测试——同一行内颜色不同,反映的是不同输出覆盖情况的真实差异;判官本身的稳定性证据见第 3 节(16 点 × 100 次送判零翻转)。

理由要展开讲,因为这是整个排查里最容易被忽略的判断:

每补一个反例,本质上是把"本次跑批发现的失败形态"填进金标准。 补到最后,金标准就变成历史输出样本的补丁集——对见过的输出形态过拟合,对新的输出形态欠拟合。这是个循环:用未来的知识填平过去的坑,再用"能判对过去"来验证金标准是对的。

所以判别准则是:先确认错判根源是"机制性不可消除的判官噪声",而不是"系统性缺陷"(探针没拦住的判官主流误读),才停止强压。 系统性缺陷的标志是错误率两位数以上、evidence 呈同一模式;机制性噪声的标志是个位数百分比、无固定模式。前者的正确修法是修任务定义(第 3 节),后者的正确修法是交还给 repeat 统计吸收——多次跑、看分布,而不是继续在金标准里打地鼠。

LLM 判官单次判定存在机制性的不稳定,温度 0 也一样。这不是 bug,是这类测量仪器的物理性质。你能做的是让系统性误差归零、把随机噪声交给统计,而不是追求单次判定 100% 稳定——后者在原理上就做不到。

7. 那"能确定性的就确定性"呢?

排查中段,AI 结对助手(我全程和 Codex 结对开发)在提示词约束失效后推荐了一个看起来更工程化的方案:给每个点配程序可读的锚词表,程序先查证据是否命中锚词,缺锚直接强制 not_covered,LLM 只保留"证据在时判等价表达"的权力。

我没有立即采纳,多年的工程直觉让我犹豫,这显然会带来后续维护工作量,而且会越来越大,最后选择先做第 2 节的变量分离实验把问题根因定位清楚。结果实验证明任务拆解 + 金标准修复后 LLM 完全够用(1600/1600),锚点闸门最终没有上线。

回头看,这是整个排查里最值钱的一次"犹豫"。如果当时上了闸门:7% 的假阳性确实会被压掉,但会引入整套词表维护基础设施(每个新点都要维护锚词、等价表达词表),而且掩盖了真正的根因是金标准粒度——闸门每触发一次,你就少一次发现点表问题的机会。

由此把一条老原则做了修订。"能确定性的就确定性"听起来天经地义,但确定性手段有隐性熵增成本:词表要维护、例外要枚举、系统整体朝规则膨胀的方向演化。修订为两条件启用原则——只在以下其一成立时才考虑确定性手段:

  1. 实验证明 LLM 在任务充分拆解后仍然不可靠,且分析确认不可靠来自 LLM 能力本身、不能靠继续拆任务解决;
  2. LLM 方案的显性成本显著高于确定性方案。

注意条件 1 前半句的限定语: "任务充分拆解后" 。绝大多数"LLM 判官不行"的结论,是在任务没拆解、金标准没修好的状态下得出的——你测的是一台被错误操作绑住的仪器,然后得出"仪器不行"的结论。

实践中的分工是分层的:LLM 负责"带原文参照系的语义核对"(这是它真正擅长的),程序负责切分、配对、聚合、上限兜底(这是它真正可靠的) 。比如引用声明的提取后来整个改成纯规则实现——发现 [n] 编号、按句切分、正则配对——因为"从输出中提取带编号的声明"本来就是确定性任务,交给 LLM 是冤枉它,它还会不稳定地发挥。

8. 收拢:一份排查流程

全部经历压缩成一张排查流程,遇到判官不稳定时从上往下走:

  1. 固定输入重试:同一份输出让判官判 N 次,先分清是"裁决漂移"还是"点级标注漂移";
  2. 变量分离:把判定任务拆开测——单点 → 小批 → 全量纯标注 → 全量+附带任务,定位不稳定的引入层;
  3. 聚类假阳性的 evidence:如果错误引用集中在同一句话/同一模式,是系统性串位,不是噪声;
  4. 回上游核对金标准:点是否正交?概括与细节有没有拆成两个点?探针有没有实例化?有没有反例?
  5. 修金标准,不是压判官——判官在多数点的稳定表现说明能力够用,问题在任务定义;
  6. 大样本钉翻转点:×10 不够就 ×50、×100,把系统性错误和噪声区分开;
  7. 残余个位数噪声交 repeat 统计吸收,停手——继续强压就是给金标准喂过拟合。

以及三个设计原则,贯穿始终:

  • 一次调用只做一类认知任务(任务纯净 ≠ 输入最小化,该给的参照系要给);
  • 结论依赖依据的输出,依据先行(且依据必须是摘抄/提取,不能是定位/检索);
  • 确定性是可选兜底,不是默认起点(两条件启用原则)。

9. 边界声明与残余问题

诚实标注本文结论的验证范围:

  • 单一判官模型、温度 0;单一项目(RAG 学习助手)、12 条基准用例、16 个覆盖点、一个对比型任务族。换模型、换领域,结论需要重验——但我怀疑主要结论(任务构造 > 判官能力)是跨模型成立的,因为它根植于生成机制而非特定模型;
  • 1600/1600 是"纯覆盖标注"任务的收口数字,不是整个评测系统的通过率;评测系统还有引用真实性、事实边界等其他判官,各有各的工程问题;
  • 残余未解:个位数的机制性噪声(交统计吸收,未追求归零);覆盖判官只能查"该有的都有",查不了"不该有的没有"(排除性检查,结构性更难,还在想)。

业界锚点,供延伸阅读:逐点核对是主流框架的共识做法(Ragas、TruLens RAG Triad 都把回答拆成声明逐条核),但"金标准点表怎么写才可判定"这件事,几乎没有公开资料——本文第 3 节算是一份补充。引用粒度方面,ALCE(EMNLP 2023)是引用自动评测的起点;约翰霍普金斯大学 2026 年的一篇论文《Are Finer Citations Always Better?》实证了引用核对粒度必须对齐声明的语义范围——跟我们的实践结论一致。

完整现场在这里:ai-study-helper——判官提示词和聚合规则在 backend/eval/runner.py,金标准点表在 backend/eval/cases/rag/,全部实验记录在 memory/ 目录的编号笔记里。每一条本文引用的数字,都能在对应的 run 记录里找到原始输出。

判官不稳定的时候,先别急着换判官。你面对的很可能不是一台坏仪器,而是一道出错的题。


本文是"RAG 评测工程"系列的一篇。上一阶段的记录(评测体系设计、从零搭 RAG 的完整过程)也在同一个仓库里。欢迎在评论区讨论你们遇到的判官翻车案例——尤其是反例:如果你换了判官/上了确定性规则之后真正解决了问题,我想知道那个场景长什么样。