LLM 评测为何不能只看答案完全一致
一个论文审查服务发现,精确匹配适合部分判断题,却会在引文和结构化提取任务中把格式差异误判为错误。文章讨论生产系统中评测方式的调整,以及如何持续检验需求、修复和提示词变更。
AI 深度解读
给 LLM 做评测,答案和标准结果完全一致就算对,看起来既客观又省事。但一个已经上线的论文审查服务发现,这套办法只能应付少数检查:模型找对了证据,仅仅因为标点不同、引用多了一句话,也会被判错。
作者最终没有引入更复杂的裁判模型,而是按字段类型组合 exact match、F1 和模糊匹配。这篇最值得程序员借鉴的,是如何区分「模型答错了」和「评分规则写错了」,以及怎样用一套简单、可解释的评测保护线上行为。
为什么一个正确答案会被评测判错
作者的服务读取研究论文,回答编辑会据此采取行动的问题:有没有伦理审批声明,有没有临床试验注册信息,图表引用是否一致,自引比例是否过高。误报缺失会浪费编辑时间,错误放行则会让真实问题进入后续流程。
这些检查并非同一种任务。有的是判断声明是否存在,有的是检查位置,有的是提取作者、机构和资助信息,还有一些需要核对引用,或者综合论文多个部分推断答案。
大部分检查返回结构化 JSON,因此最初的评测很直接:每篇测试论文对应一个标准 JSON,模型输出与标准值相同得 1 分,不同得 0 分。对于「有没有知情同意声明」这样的布尔问题,这种精确匹配(exact match)确实好用。
问题出在证据文本。
模型可能找对了伦理审批段落,却使用了不同的引号或连字符;PDF 转换也可能改变希腊字母、上标和特殊字符。还有一种常见情况:标注者只选了一句话,模型返回这句话及其相邻句子。
这些差异未必改变答案,却都会触发精确匹配失败。作者举出的实测例子是:一次元数据提取检查,exact match 只有 0.50,F1 却达到 0.97。团队查看失败样本后,发现大量扣分来自表面差异。
这里不能直接推出「真实准确率就是 97%」:原文没有交代 F1 的具体计算口径。但它足以说明,单看一个分数可能误判问题所在。
评测持续制造误报,最终会让真正的回归也失去辨识度。
字段不同,正确答案的边界也应该不同
作者没有寻找一个通吃所有任务的指标,而是按字段制定规则:
- 布尔值和枚举值保持严格,阈值为 1.0,明确答案不能靠相似度过关。
- 元数据字段使用约 0.95 的阈值,容忍姓名、机构等内容的细微表达差异。
- 引用文本使用约 0.85 的阈值,重点检查有没有找到对应证据。
文本比较前先做归一化,包括转小写、合并空白,以及去掉无害的标点差异和括号。随后用 rapidfuzz 比较,取 token sort、token set、partial ratio 三种结果中的最高分。
下面这段代码处理「证据内容基本一致,但文本形式或长度不同」的情况:
def fuzzy_match_score(expected: str, actual: str, threshold: float = 0.85):
if expected == actual:
return {"exact_match": True, "fuzzy_score": 100.0, "passes_threshold": True}
a, b = normalize_text(expected), normalize_text(actual)
shorter, longer = sorted([a, b], key=len)
if len(shorter) > 20 and shorter in longer:
ratio = len(shorter) / len(longer)
if ratio > 0.3:
score = 85 + ratio * 15
return {
"exact_match": False,
"fuzzy_score": score,
"passes_threshold": score >= threshold * 100,
}
score = max(
fuzz.token_sort_ratio(a, b),
fuzz.token_set_ratio(a, b),
fuzz.partial_ratio(a, b),
)
return {
"exact_match": False,
"fuzzy_score": score,
"passes_threshold": score >= threshold * 100,
}
重点看包含关系分支:短文本长度大于 20、完整出现在长文本中,并且长度占比超过 0.3 时,分数按 85 + ratio * 15 计算。覆盖越完整,得分越高。原文没有提供 normalize_text 的实现,这段代码也依赖已导入的 fuzz,不能直接单独运行。
对于图表引用这类列表,作者按条目做贪心匹配,不按位置比较。同样的五条引用,只是顺序变了,不应该算五次错误。
报告则并排展示 exact match、F1 和 fuzzy match。精确匹配低、模糊匹配高,通常提示格式噪声;三个指标都低,再优先排查提取、推理或流程问题。这是一种诊断线索,仍然需要查看实际差异。
为什么他们没有让另一个 LLM 当裁判
作者考虑过 LLM as a judge,即让另一个模型判断答案是否正确。它能灵活处理不同表达,但也引入第二个不确定因素:分数变化时,需要分清是被测系统变了,还是裁判的判断变了。
Embedding 相似度也有类似局限。两句话可能语义很接近,却提到了不同的审批委员会。对于这种任务,「大致相似」仍可能是关键事实错误。
promptfoo、DeepEval 和 Ragas 并没有被作者否定。只是他们已经有领域专用 JSON 和逐字段规则,即使用框架,大部分评分逻辑仍需自己写。逐次人工审核则被作者视为准确但难以扩展的办法,原文没有给出不同方案的对照实验。
他们最后保留的是一套普通 Python 流程:脚本运行论文检查,将输出写入 .jsonl;每项检查的 evaluator 对照标准 JSON 评分;报告脚本汇总结果,比较不同运行。
标准答案由 QE 团队准备,测试材料混合真实生产论文和专门挑选或构造的边界案例,例如结构异常、声明藏得很深、缺少章节,以及看起来像目标声明却不满足要求的文本。
每项检查大约只有八篇论文。
这个规模用于捕捉明显回归,不能用来宣称精确的总体准确率,作者对此说得很清楚。固定样本的价值,是让每次需求修改、缺陷修复和提示词更新都有可重复的比较基线。
我的判断
我认同这套做法的出发点:先定义字段怎样才算正确,再决定评分方式。对于结构化抽取、固定分类和证据定位,可解释的确定性规则很实用,失败后能打开实际 JSON 和标准 JSON 直接检查原因。
但 0.95 和 0.85 是这个团队的经验阈值,不是可以直接复制的质量标准。 原文没有提供阈值选择过程,也没有展示它们对误报、漏报的影响。
代码还有一个需要打问号的地方:取三种模糊分数的最大值,可能让某一种宽松规则决定最终结果。包含关系分支在默认 0.85 阈值下,只要满足条件就会通过;而 partial ratio 也可能给局部重合的内容较高分。这些规则能减少格式误报,却不能证明关键事实正确。
例如,委员会名称、否定词或注册编号只错一点,字符串仍可能高度相似。原文批评 Embedding 会放过语义相近的错误,但字符串模糊匹配也需要防范同类问题。
对这类关键字段,我更倾向于单独提取、严格比较,再对外围证据文本放宽要求。至于开放式回答,人工复核或模型裁判仍有价值,不能据此认定简单字符串评分普遍更好。
能拿来做什么
- 给日常 AI 编程中的结构化输出逐字段定规则。例如评测结果包含状态和证据文本时,状态严格比较,证据再做归一化与模糊匹配,不要直接比较整段 JSON 字符串。
- 改提示词后先读失败差异,再决定是否继续改提示词。把实际输出、标准输出和各项分数一起保留,避免为了消除格式扣分,反而改变原本正确的行为。
- 为文档抽取或检查流程保留少量真实样本,同时加入缺失字段、干扰文本和异常结构案例。若流程包含确定性后处理,也要一起评测,因为用户看到的是完整流程的结果。
- 将提示词版本与评测结果关联保存,并明确小样本评测的用途。它适合提醒你「这次改动可能弄坏了什么」,不足以独立证明系统在真实流量中的准确率。