Rubric评测与LLM-as-Judge偏差校准#
一句话答案#
开放式任务没有标准答案,要把「好不好」拆成一条条可以单独判定的细则(rubric),让 judge 模型逐条给理由再给结论,最后按分档规则聚合。judge 本身有位置、长度、自我偏好、格式、宽严等系统性偏差,要先用人工对齐集测一致率和 Kappa,再用交换顺序、参考答案、多 judge、复判等手段校准,先把尺子校准了再用它改 Agent。
核心要点
1. 为什么开放式任务需要 rubric#
直接问 judge「这个回答打几分(1-10)」有三个问题:
- 分数没有锚点:7 分和 8 分差在哪,judge 自己也说不清,同一份输出多次采样分差很大。
- 无法归因:分低了不知道是哪方面差,也就不知道该改 prompt、工具还是检索。
- 红线被平均掉:推荐了超预算或违禁商品,这种错误不应该被「文笔好」的高分抵消。
rubric 把一个模糊判断拆成多个具体问题,每条只问一件事,比如「推荐的商品是否都在用户给的预算内」「是否向用户暴露了内部 id」。每条单独判定,可解释、可归因,也更稳定。和 LLM评测方法 里讲的整体打分相比,rubric 是把 LLM-as-Judge 做细的方法。
2. 静态 rubric vs 动态 rubric#
| 静态 rubric | 按 query 动态生成 | |
|---|---|---|
| 做法 | 一套细则打所有样本 | judge 先根据 query、约束、意图生成 6-10 条专属细则,再打分 |
| 优点 | 跨样本可比;便宜;易审 | 贴合每条 query 的红线,不漏判也不误伤 |
| 缺点 | 对不同 query 失真:没提预算的 query 也被查预算 | 细则本身会抖;跨 query 分数不能直接比 |
| 适合 | 任务类型单一(摘要、翻译、客服固定话术) | 需求差异大(导购、研究助手、Agent 任务) |
动态 rubric 要解决「尺子自己在变」的问题:
- 生成一次就缓存:缓存键包含 query 和生成 prompt 全文的 hash。生成 prompt 改一个字,所有细则重建,此前的分数不再可比。
- 生成 prompt 里写纪律:用户明确给了预算才设预算红线;软偏好不许升为红线;「泄露」只看用户可见的最终回复。
- 只比较同一条 query 在不同版本间的分数,不跨 query 比绝对值。
3. 打分设计#
分档:细则分优先级,不同档位用不同聚合方式。
| 档位 | 含义 | 判定方式 | 聚合 |
|---|---|---|---|
| P0 红线 | 超预算、推错人群、泄露、违禁 | pass / fail | 任一 fail → 总分 0 |
| P1 规范 | 格式、流程要求 | pass / fail | 每条违规扣固定分 |
| P2 质量 | 理由充分度、贴合度 | 1-5 分,每档写锚点描述 | 取均值折算到百分制 |
def aggregate(scores, p1_penalty=10):
if any(s.tier == "P0" and not s.passed for s in scores):
return 0, False # 红线一票否决
p2 = [s.score for s in scores if s.tier == "P2"]
p2_part = (sum(p2) / len(p2) / 5 * 100) if p2 else 100 # 没有质量项时不扣
p1_fail = sum(1 for s in scores if s.tier == "P1" and not s.passed)
return max(0, p2_part - p1_penalty * p1_fail), Truepython逐项判定 + 先理由后结论:
- 每条细则单独输出
{id, reasoning, passed/score},reasoning 字段放在结论前面,让模型先写依据再下判断;先给分再补理由,理由往往是在为分数找借口。 - 用结构化输出(JSON Schema)约束格式,并检查字段是否真的出现过。judge 偶尔会回一个空的
scores: [],聚合时「没有任何红线失败」,结果是评测越坏分越绿,还不报错。遇到这种情况重采样,仍为空就把这条记为错误。 - 二值判定比 1-10 分稳定;需要梯度时用 1-5 分,并给每一档写清楚描述。
评测材料要标清边界:Agent 的执行轨迹里有内部 id、工具入参,judge 容易把它当成「给用户的回答」判泄露。渲染时把轨迹标为「内部日志,用户不可见,禁止据此判泄露」,并去掉含 id 的入参。
4. Judge 的系统性偏差#
| 偏差 | 表现 | 校准方法 |
|---|---|---|
| 位置偏差 | 成对比较时偏向先出现(或后出现)的答案 | A/B 和 B/A 各判一次,结论不一致记为平局 |
| 长度 / 冗长偏差 | 回答越长分越高 | 细则里写「信息量相同时更简洁者优」;分析分数与长度的相关性 |
| 自我偏好 | 偏好自己或同家族模型的输出 | judge 用与被测模型不同家族的模型,或多 judge 投票 |
| 格式偏差 | 有标题、列表、加粗的回答显得更专业 | 细则只问内容是否满足;必要时先统一格式再判 |
| 宽松 / 严苛 | 整体分布偏高或偏低;某些 judge 几乎不给 fail | 和人工标注对比找系统性偏移,调整细则措辞或阈值 |
| 评测材料混淆 | 把轨迹、系统提示当成回答内容 | 分段渲染并标注可见性 |
| 采样抖动 | 同一份输出多次判定结论不同 | temperature 设低;关键结论(P0 fail)独立复判 |
5. 校准方法:先校尺子,再改 Agent#
① 人工对齐集:抽几十到上百条样本,人工按同一份细则标注,作为 judge 的考卷。每改一次 judge prompt 或换 judge 模型,都先在对齐集上跑一遍。
② 一致率与 Kappa:只看一致率会被类别不均衡骗(90% 都是 pass 时,全判 pass 也有 90% 一致)。Cohen’s Kappa 扣掉了随机一致的部分:
def cohen_kappa(human, judge): # 两个等长的 0/1 列表
n = len(human)
po = sum(h == j for h, j in zip(human, judge)) / n # 观察一致率
p_h, p_j = sum(human) / n, sum(judge) / n
pe = p_h * p_j + (1 - p_h) * (1 - p_j) # 随机一致率
return (po - pe) / (1 - pe) if pe < 1 else 1.0python一般把 0.6 以上视为可用、0.8 以上视为较好(经验区间,不是硬标准)。除了整体 Kappa,还要按细则类别看:往往是某几类细则拖低了一致性,改那几条的措辞就行。
上面的公式只适用于 pass/fail 这类二值判定。其他情况换指标:
- 有序分(如 P2 的 1-5 分):用加权 Kappa(差 1 档比差 3 档扣得少),或看 Spearman / Kendall 秩相关,关心的是 judge 和人工的排序是否一致;
- 多个评审者(多名标注员或多个 judge):用 Fleiss’ Kappa 或 Krippendorff’s α,后者还能处理缺失标注。
③ 交换顺序:成对比较必须双向判,只有两次结论一致才算数。
④ 参考答案:有条件时给 judge 一份参考答案或关键事实清单,把「开放判断」变成「对照检查」,宽严偏差会明显变小。
⑤ 多 judge:不同家族的 2-3 个 judge 投票或取中位数,抵消单个模型的偏好;成本随 judge 数量线性增长,一般只在关键子集上用。
⑥ 复判:首次判出 P0 fail 的样本独立再判一次,复判 pass 就翻回。这只能修假阳性,judge 漏判的真红线发现不了;对比历史基线时,两边复判开关要一致。
⑦ 固定 judge 版本:judge 模型、生成 prompt、打分 prompt 都记进报告元数据,任何一项变化后的分数不能直接和旧基线连成趋势线。
6. Bad case 回流#
flowchart LR
A[固定种子集] --> B[Agent 运行]
B --> C[rubric 打分]
C --> D{P0 fail?}
D -->|是| E[复判确认]
E --> F[按判词分类]
F -->|确定性可修| G[生成规则 → 人工确认]
F -->|需要判断| H[人工桶:改 prompt/工具]
G --> I[加入回归集]
H --> I
I --> A
- 先分类再修:按判词分泄露、违禁、判断失误等类型。只有确定性的问题(比如回复里出现内网地址)适合自动生成规则;判断类问题正则堵不住,交给人。
- 自动规则要有保护:默认 dry-run,写入前用新规则重扫已有产物,看有没有误伤。LLM 生成的贪婪正则可能把整段回复替换掉。
- bad case 进回归集:修完的 case 固定下来,之后每次改动都要跑,防止回退。
- 回流时注意区分 judge 的锅和 Agent 的锅:同一份输出重判结论就变了,先修 judge。
7. 分数回写可观测平台#
把分数挂回对应的 trace,才能在一条失败 case 上同时看到「分数 + 完整执行链路」。以 Langfuse 为例,当前 Python SDK 用 create_score 按 trace_id 写 score(旧版 SDK 方法名不同,升级时以官方文档为准):
for name, value in [("rubric_total", total), ("rubric_pass", int(passed)), ("rubric_p2_avg", p2_avg)]:
langfuse.create_score(trace_id=trace_id, name=name, value=value)pythontrace_id显式传入,不要从上下文变量里取:评测脚本常并发跑多条,上下文变量可能取到别的 case。- 分数名字固定下来,便于在平台上按分数过滤 trace、做看板。
- 与 Agent可观测性与质量保障 的线上监控配合:线上抽样同样打分,离线和线上用同一套尺子。
面试回答(2分钟版)
开放式任务没有标准答案,直接让 judge 打一个 1 到 10 分,分数没锚点、不可归因,红线还会被其他维度的高分抵掉,所以要用 rubric:把「好不好」拆成一条条只问一件事的细则。细则可以是静态一套,也可以按 query 动态生成,比如导购场景每条 query 的红线不同,可以让 judge 先根据 query 和约束生成六到十条专属细则,按 hash 缓存,保证同一条 query 前后用同一把尺子。打分分三档:P0 红线一票否决,P1 规范逐条扣分,P2 质量打 1 到 5 分;每条先写理由再下结论,用结构化输出,并且防 judge 回空表造成假绿。judge 有系统性偏差:位置、长度、自我偏好、格式、宽严,还有把内部轨迹当成回答判泄露。校准的方法论是:先做人工对齐集,看一致率和 Cohen’s Kappa,按细则类别找拖后腿的;成对比较双向判;有参考答案就给参考答案;关键子集用不同家族的多个 judge;P0 fail 独立复判一次。原则是先校尺子再改 Agent,否则分数变化分不清是谁变了。bad case 按类型回流,只有确定性可修的自动生成规则,其余进人工桶,修完加入回归集;分数按 trace_id 回写 Langfuse 这类可观测平台,方便对着链路看。结合项目时可以讲:哪些环节真正落地了、哪些还停在方法论,用什么数据证明尺子是准的。
追问与易错
追问方向:
- “动态 rubric 每次生成都不一样,分数还可比吗?” → 生成一次后按 hash 缓存,缓存键包含生成 prompt 全文,同一条 query 前后版本用同一份细则;只比同一条 query 的前后变化,不跨 query 比绝对分。
- “P0 为什么一票否决而不是扣很多分?” → 超预算、推错人群、泄露这类伤害不能被其他维度抵消;扣分制下一个文笔很好的违规回答仍可能及格。
- “为什么要求先写理由再打分?” → 自回归生成时,先输出的内容会约束后面的输出;先写依据再给结论,结论更可能基于依据,先给分后补理由容易变成事后合理化。
- “怎么证明分数波动来自 judge 而不是 Agent?” → 固定同一份 Agent 输出,让 judge 重复判几次;结论变化就是 judge 在抖。先修 judge(复判、细化细则、降温度),再去改 Agent。
- “Kappa 和一致率有什么区别?为什么不能只看一致率?” → Kappa = (观察一致率 − 随机一致率) / (1 − 随机一致率)。类别不均衡时,随机一致率本身就很高,一致率会虚高,Kappa 能反映扣除运气后的一致程度。
- “judge 用多大的模型?能用小模型省钱吗?” → 先在人工对齐集上比较:小模型 Kappa 达标就用小模型;达不到时,可以只让大模型判 P0 这类关键细则,P2 质量项用小模型。
- “成对比较怎么消位置偏差?” → 每对样本按 A/B、B/A 各判一次,两次结论一致才采纳,不一致记为平局;报告里单独统计不一致率,它本身也是 judge 质量指标。
- “空打分表为什么危险?” → 聚合逻辑是「有 P0 fail 才判不通过」,空表意味着没有任何 fail,于是全部通过。要检查判据字段是否真的出现,缺失时重采样,仍缺失就记为错误而不是满分。
- “judge prompt 改了之后历史基线怎么办?” → 不能和旧分数直接比,要用新 judge 把基线版本重跑一遍;报告里记录 judge 模型、prompt 版本、复判开关。
易错点:
- ❌ “LLM-as-Judge 给的分数就是客观质量” → judge 有系统性偏差,未和人工对齐过的分数只能看趋势,不能当绝对质量。
- ❌ “细则越多越全面越好” → 细则过多会稀释注意力、增加抖动,而且相互重叠;每条只问一件可判定的事,6-10 条通常够用。
- ❌ “复判能消除所有误判” → 只对首判 fail 的样本复判,只能修假阳性,漏判的真红线仍然漏。