「洞察好不好」能不能评?能,但前提是先把维度拆开——方向相关性、证据充分性、可操作性、表达清晰度。四维分开之后,每一维都能写出可判定的评分标准。随后的问题更棘手:怎么让机器打分可信?答案是必须先认清机器打分的四类偏差。
编辑部注 · 本章的 Rubric 设计与偏差控制是评测工程的深水区。第 1 题和第 3 题是面试中的高频追问。
办事型任务的评测有客观结果可依,洞察型任务的输出是一段分析、一份建议、一个报告。它没有唯一的正确答案,所以「好不好」看起来只能靠人判断。但「看起来只能靠人判断」不等于「无法系统化」——关键在于把模糊的总评拆成若干个可分别判断的维度。
一、把「有洞察」拆成四个维度
「这篇分析很有洞察」是一个整体印象。要让它可评,必须拆到每一维都能独立回答「是/否」的程度。
| 维度 | 回答什么问题 | 5 分(优秀) | 3 分(合格) | 1 分(不合格) |
|---|---|---|---|---|
| 方向相关性 | 是否回答了用户真正关心的问题? | 直击核心问题,且发现了用户没明说但更重要的那个问题 | 回答了明确提出的问题 | 答非所问,或只复述了数据没有回应问题 |
| 证据充分性 | 每条结论是否有可核验的依据? | 每个结论都有具体数据/来源支撑,且给出了量级或对比基线 | 主要结论有依据,个别结论为推测但已标注 | 结论无依据,或用了「显著」「大幅」等无量化词 |
| 可操作性 | 用户看完能做什么? | 给出了具体动作、执行主体、判断标准,用户可直接排期 | 给出方向性建议,但未细化到可执行 | 只有现象描述,没有下一步 |
| 表达清晰度 | 信息是否结构化、可快速定位? | 结构清晰、有摘要、长短得当、关键数字突出 | 结构基本清楚,篇幅略长或重点不突出 | 大段文字堆砌,找不到重点 |
二、LLM-as-Judge:能用,但要认清它的偏差
让模型来打分是必要手段(人工成本太高),但它的四类偏差必须被系统性地处理。
| 偏差 | 表现 | 成因 | 缓解手段 |
|---|---|---|---|
| 位置偏差 | 在 A/B 对比中偏向先出现的那一个 | 注意力对前文更敏感 | 交换顺序跑两次,两次结论一致才采纳;不一致则标记为「平局」 |
| 长度偏差 | 更长、更啰嗦的回答得分更高 | 长度与「详尽」在训练语料里高度相关 | Rubric 里明确写「简洁且信息密度高得高分」,并单独统计长度与得分的相关性 |
| 自我偏好 | 评审模型偏袒与自己风格相似的输出 | 同源模型的表达习惯一致 | 用不同家族/不同规模的模型做评审;关键样本用人工复核 |
| 格式偏好 | 结构化、带小标题、带 emoji 的输出得分更高 | 排版特征被当成质量信号 | 评分前统一格式(去掉 Markdown 装饰),或把「格式」单列一维 |
- 一致性检查:同一份输出打两次,分数差异超过 1 分的样本占比应低于 10%。超过说明 Rubric 描述不清。
- 与人工对齐:抽 30-50 条人工打分,计算与机器打分的相关性。同向率低于 75% 就不能直接用机器分数做决策。
- 反例注入:故意放入几条明显很差(如空输出、答非所问)的输出,看机器是否能给低分。给不出低分说明评分尺度整体偏松。
三、动手:可校准的评分器
import json, re
from dataclasses import dataclass
RUBRIC = """
你是评审员。请对下面这份分析输出按四个维度打分(1-5 的整数),
并给出理由。评分标准:
【方向相关性】5=直击核心问题且发现了用户未明说但更重要的问题;
3=回答了明确提出的问题;1=答非所问或只复述数据。
【证据充分性】5=每个结论都有具体数据/来源,并给出量级或对比基线;
3=主要结论有依据,个别为推测但已标注;1=结论无依据或使用无量化词。
【可操作性】5=给出具体动作、执行主体、判断标准,可直接排期;
3=给出方向性建议但未细化;1=只有现象描述没有下一步。
【表达清晰度】5=结构清晰、有摘要、关键数字突出;3=结构基本清楚;
1=大段文字堆砌找不到重点。
注意:
· 不要因为篇幅长而给高分,信息密度高才给高分。
· 不要因为排版漂亮而给高分,格式不计入表达清晰度之外的维度。
· 如果某维度确实无法判断,给 0 并在理由里说明。
只输出 JSON:
{"direction":n,"evidence":n,"actionable":n,"clarity":n,
"reason":"一句话理由","blocking":"最严重的短板维度名或 null"}
"""
@dataclass
class Verdict:
direction: int
evidence: int
actionable: int
clarity: int
reason: str
blocking: str | None = None
@property
def weighted(self) -> float:
"""加权:方向相关性权重最高——方向错了,其余努力全部作废"""
w = {"direction": 0.35, "evidence": 0.22, "actionable": 0.25, "clarity": 0.18}
return (self.direction * w["direction"] + self.evidence * w["evidence"]
+ self.actionable * w["actionable"] + self.clarity * w["clarity"])
def passed(self) -> bool:
"""一票否决:可操作性过低 → 直接判定不合格,不用平均分"""
if self.actionable <= 2:
return False
return self.weighted >= 3.5
def explain(self) -> str:
return (f"方向 {self.direction} 证据 {self.evidence} "
f"动作 {self.actionable} 表达 {self.clarity} "
f"加权 {self.weighted:.2f} "
f"{'通过' if self.passed() else '不通过'}"
f"{f'(卡在 {self.blocking})' if self.blocking else ''}")
def judge(output: str, call_model) -> Verdict:
raw = call_model(RUBRIC, output, temperature=0)
data = parse_lenient(raw)
return Verdict(**{k: data[k] for k in
("direction", "evidence", "actionable", "clarity", "reason")},
blocking=data.get("blocking"))
def parse_lenient(raw: str) -> dict:
"""宽容解析:模型常把 JSON 包在代码块里,或带尾随逗号"""
text = raw.strip()
text = re.sub(r"^```(?:json)?|```$", "", text, flags=re.M).strip()
text = re.sub(r",(\s*[}\]])", r"\1", text) # 去掉尾随逗号
if "{" in text:
text = text[text.index("{"): text.rindex("}") + 1]
return json.loads(text)
def judge_stable(output: str, call_model, repeats: int = 2) -> tuple[Verdict, dict]:
"""
稳定性检查:同一份输出多次打分。
方差大的样本不能进入自动决策 —— 它们必须转人工。
"""
verdicts = [judge(output, call_model) for _ in range(repeats)]
scores = [v.weighted for v in verdicts]
spread = max(scores) - min(scores)
meta = {
"spread": spread,
"reliable": spread <= 0.8, # 分差超过 0.8 视为不可靠
"verdicts": [v.explain() for v in verdicts],
}
# 取中位数(比均值更抗单次异常)
scores_sorted = sorted(scores)
median = scores_sorted[len(scores_sorted) // 2]
best = min(verdicts, key=lambda v: abs(v.weighted - median))
return best, meta
def pairwise_stable(a: str, b: str, call_model) -> dict:
"""
两两对比必须交换顺序跑两次。
两次结论不一致 → 判平局,而不是选一个。
"""
first = call_model(RUBRIC + "\n请判断 A 与 B 哪个更好。", f"【A】{a}\n【B】{b}", temperature=0)
second = call_model(RUBRIC + "\n请判断 A 与 B 哪个更好。", f"【A】{b}\n【B】{a}", temperature=0)
# 解析出「选了谁」,交换后应指向同一个原始样本
pick1 = "A" if "A" in first[:20] else "B"
pick2 = "B" if "A" in second[:20] else "A" # 第二次的位置已交换
return {
"consistent": pick1 == pick2,
"winner": pick1 if pick1 == pick2 else "tie",
"note": "两次顺序一致的结论才可采信;否则记为平局",
}如果用 A 模型生成洞察、又用 A 模型做评审,你会得到一组很漂亮且很一致的分数——但它是自证。这类系统在换用户之后会突然「失灵」,因为真实用户的偏好与 A 模型不一致。
要求:评审模型与生成模型不同源;并且每轮评测都要抽样人工复核,用人工结论定期校准机器结论。机器打分是用来降低人工成本的,不是用来替代人的判断的。
四、自测
五、小结
| 议题 | 结论 |
|---|---|
| 维度拆分 | 方向相关性 / 证据充分性 / 可操作性 / 表达清晰度,四维正交 |
| 打分标准 | 每维给出 5 / 3 / 1 分的具体描述,标准要能回答「是/否」 |
| 合成规则 | 权重差异(方向相关性最高)+ 一票否决(可操作性过低直接不合格) |
| 四类偏差 | 位置 / 长度 / 自我偏好 / 格式偏好,各有对应的缓解手段 |
| 可信度校准 | 一致性检查、与人工对齐、反例注入三步 |
| 一票否决示例 | 可操作性 < 2 分 → 不合格,不管其他维度多高 |
| 必须避免 | 生成模型与评审模型同源(循环论证) |
机器打分是用来降低人工成本的,不是用来替代人的判断的。这两件事在长期看差别巨大。本刊编辑部
下一章把前面所有测评工作接成闭环:怎么让评测真正影响线上质量,而不是变成一份没人看的报告。
◇ 面试官会怎么问
共 3 条 · 其中 2 条高频 · 先自己答一遍,再展开对照
洞察型 Agent 的"内容质量"怎么评? 高频
① 方向相关性——产出的分析是否落在问题上,有没有跑偏到无关角度;
② 证据充分性——结论是否有可溯源的数据支撑,还是只是合理推测;
③ 可操作性——是否给出了能落地的判断或建议,而不是正确的废话;
④ 表达清晰度——结构是否清楚、重点是否突出、有没有冗余。
每个维度用 3–5 级量表并写清每级的判定标准(rubric 的关键是让不同评分者得出接近的结果)。
最重要的设计是:维度分不是用来算总分的,而是用来做归因的——低分出现在"证据充分性",说明是检索没召回;出现在"表达清晰度",说明是提示或组织问题。只有能定位到环节,评分才有迭代价值。
追问链
- 维度之间高度相关怎么办?
- 怎么知道评分标准本身是合理的?
LLM-as-Judge 有哪些偏差?怎么控制? 高频
① 位置偏差——倾向给排在前面的答案更高分。对策:交换顺序各评一次,取一致结论;不一致就判为平局或人工介入。
② 长度偏差——倾向给更长、更"全面"的回答高分。对策:rubric 里显式区分"信息量"与"篇幅",加一条"冗余扣分";或对长度做归一化后再比较。
③ 自我偏好——偏爱自己(同族模型)生成的文本。对策:用不同模型评判,或至少做交叉验证;关键评测用人工校准。
④ 格式/表述偏差——被排版、markdown 结构、自信的语气影响。对策:rubric 明确只看内容不看形式,或统一后处理格式再评。
此外必须有人工抽检校准:定期抽一批让真人评,与机器评分比对,一致率下降就说明评判标准漂了,要回去修 rubric。
为什么不能只看平均分?
三种典型情况:① 平均分涨了但关键场景崩了(比如回答变得很长,平均分因信息量提升而上涨,但简短咨询类场景全部变糟);② 平均分不动但方差变大(一端显著变好、另一端显著变差,稳定性实际下降);
③ 高分样本挤占(某些原本满分的场景现在稳定扣一点分,被其他场景的涨幅掩盖)。
所以指标要成组看:均值 + 分位数(P5/P95)+ 关键场景单独看 + 最差类别。我在实践中会额外维护一份"绝不能退化的场景清单",无论平均分怎么涨,这些场景退化就是不能上线。
结论先行(一句话给出取舍)→ 机制(为什么是这样,涉及哪条链路)→ 代价或边界(这么做放弃了什么)→ 你的实践或数字(真实场景里怎么落地)。 只讲机制不讲代价,是背题;只讲取舍没有数字,是空谈。