跳转到主要内容

崩溃的基准测试:为什么评估框架需要自己的回归测试

一次覆盖 27 个模型的基准测试运行暴露出错误源头是评估框架本身,而非模型。评估代码就是生产软件:对它做版本管理、金丝雀发布,并用已知答案测试它。

一块电路板的微距照片,显示走线、焊点和单个错位的元件,景深较浅。

崩溃的基准测试

一篇公开报道描述了一个团队对 27 个开放权重模型进行基准测试,结果发现错误源头是评估框架本身,而非模型。人的本能反应是把这当作一件丢脸的事。其实不然。这是常态。评估代码比模型代码漂移得更快,因为它是在截止日期压力下写出来的,为了迁就难处理的输出而被反复打补丁,而且很少被当作一个独立的系统来审查。

下游代价是安静而累积的。排名在几周之间变得不可比较,却无人察觉。一个模型在三月得分 71%,四月得分 68%,未必是退步了;可能是提取器变了,可能是某个依赖项动了,也可能是少数题目泄漏进了训练语料。数字在动,叙事随之而来,而底下的测量装置从未被审计。

三类框架缺陷

大多数框架故障可归入三类。它们并不稀奇。它们是任何摄取非结构化文本并输出标量的数据管道的普通失效模式。

解析缺陷

一个默默接受畸形模型输出的提取器会虚高通过率。一个期望 JSON 对象带有 answer 字段的评分器,可能会退回到正则表达式,再退回到子串匹配,最后退回到默认值 True。每一次回退都是对模型的一点善意,也是对读者的一点谎言。这种故障之所以不可见,是因为它产生的是一个数字,而不是一个异常。

这个机制值得精确命名。一个典型的提取器链长这样:

def extract(text):
    obj = try_json(text)
    if obj is not None:
        return obj.get("answer")
    m = re.search(r"answer[:\s]+(.+)", text, re.I)
    if m:
        return m.group(1).strip()
    return text.strip()  # last resort

最后那句 return text.strip() 正是通过率葬身之地。它把一个无法解析的响应转换成一个可能碰巧匹配参考答案的字符串。修复办法不是写一个更好的正则;而是采用严格模式,在遇到无法解析的输出时抛出异常,并把该失败记录为评分器错误,与模型错误区分开来。

Aerial view of rippled golden sand dunes under a cloudy sky in Penong, South Australia

污染

泄漏进训练数据的题目无法靠提示工程修复。一个逐字出现在模型预训练语料中的基准题目,衡量的是记忆,而非能力。检测污染很难,但第一步是流程性的:留出一个私有切分,对公开题目做哈希,并针对任何你能检查的语料做近似重复检查。对于你无法检查的部分,报告风险,而不是报告分数。

环境漂移

分词器版本、工具桩和超时设置会在不同运行之间变化。一个分词器补丁版本可能改变一个提示消耗多少 token,进而改变截断行为,进而改变长上下文题目的得分。一个工具桩在一次运行中返回预设响应、在下一次运行中返回实时响应,会彻底改变任务本身。一个在一月是 30 秒、在三月是 10 秒的超时,会不成比例地惩罚推理密集型模型。这些都不是模型变化。但它们全都会移动排行榜。

框架需要一套已知答案测试套件

补救办法与任何生产服务所用的相同:一套在每次评估时都运行的测试套件,而不是偶尔运行。

纳入必须失败的故意损坏提交,以及必须通过的平凡提交。 一个返回空字符串的提交必须得零分。一个逐字返回参考答案的提交必须得一分。一个返回看似合理但错误答案的提交必须得零分。如果其中任何一项预期发生翻转,框架就是坏的,该次运行作废。这是严格意义上的金丝雀:一个已知输入对应一个已知输出,在信任真实工作负载之前先做检查。

把模型、分词器、提示模板和工具桩的版本固定在一个清单里。 一个文件,提交进仓库,被每一个结果引用。如果清单变了,这次运行就是一次新运行,其结果与上一次不可比较。这与锁文件是同一种纪律,只不过应用于评估。

发布置信区间,而不是点估计。 在 200 道题上两分的差距就是噪声Close-up of a 3D printer print head and cooling fan forming an object。

Server rack closeup dark

按类别报告结果,并附上样本数。 聚合分数会掩盖薄切片。一个总体得分不错、但在与读者用例最相关的类别里只有 12 道题的模型,并不能为该用例提供证据。把样本数显示在分数旁边,好让读者能恰当地打折。

保留原始对话记录和评分器输出。 当分数有争议时,争议应当能通过重新裁定来解决,而不是靠重跑一遍并祈祷。存储完整对话记录、提取出的答案、参考答案以及评分器的判定。这也使得在修正后的评分器下重新评分历史运行成为可能,而这是在框架修复前后做比较的唯一诚实方式。

像对待代码一样对评估集做版本管理,并把任何静默编辑视为破坏性变更。 一道被改写的题目、一个被更正的参考答案、一道因有歧义而被移除的题目:其中每一项都会改变此后计算出的每一个分数的含义。给评估集打标签,在清单里记录该标签,当编辑不可避免时,发布前后对比,而不是悄悄覆盖。

诚实的权衡

严格会牺牲吞吐量。一个在无法解析输出时抛出异常的严格提取器会让更多运行作废,而一套已知答案测试套件会给每次评估增加固定开销。时间压力下的团队会忍不住以宽松模式运行,事后再清理。反驳的理由是:宽松运行产生的数字不可信,而不可信的数字比没有数字更糟,因为它们会传播进决策。

实际的折中是默认严格运行,并允许一种在输出中明确标注的宽松模式。标签随结果一起传播。看到 lenient: true 的读者知道要给分数打折。看到 lenient: false 且已知答案测试套件通过的读者知道装置已被检查过。

这改变了什么

把框架当作生产软件会改变三件事。它把评估代码移入版本控制,并采用与线上服务代码相同的审查标准。它让已知答案测试套件成为一道关卡,而不是事后补丁。它让每一个发布的分数都携带其来源:哪个清单、哪个评估集标签、哪些样本数、哪个区间。

这一切都不会让基准测试变容易。它让基准测试变得诚实。那个对 27 个模型做了基准测试、然后修复了自己基准的团队,做了大多数已发布排行榜所跳过的工作。其结果是一个可以被辩护的数字,而这是唯一值得发布的数字。

封面照片:Jakub Pabis / Pexels。

分享XLinkedIn永久链接5 分钟阅读

相关阅读

这能回答的问题

What is a known-answer suite in an evaluation harness?

A set of submissions with predetermined correct scores, run on every evaluation. Deliberately broken submissions must fail; trivial ones must pass. If any expectation flips, the harness is broken and the run is void.

Why are benchmark rankings non-comparable across weeks?

Because the harness drifts. Tokeniser versions, prompt templates, tool stubs, timeouts, and extractor fallbacks change between runs, moving scores without any model change. Pinning these in a manifest makes drift visible.

How should benchmark results be reported?

Per-category with sample counts, with confidence intervals rather than point estimates, and with raw transcripts and grader outputs retained so disputed scores can be re-adjudicated.