一次 finish_reason=length 翻车:LLM 评测的静默失败分几层

Veral 跑 Kimi K2 多选评测翻车:reasoning 吃光 4096 token、content 为空、finish_reason=length,评分器抓不到答案。以这次为锚点,把 LLM 评测里的静默失败排成五层——生成层、评分层、统计层、数据层、链路层——每层给症状、根因、探测方法与 arXiv 出处。

今天在 Veral 上跑一批 Kimi K2 的多选评测,12 条样本挂了 1 条。点开证据页一看,不是选错,是「没答」:finish_reason=length,message.content=null,4096 个输出 token 全被 reasoning_content 吃掉,评分器连一个字母都抓不到。这不是一次普通的评测错误,它是评测里最危险的那一类——静默失败:HTTP 200、无异常、有成本、有 trace,唯独没有答案,指标悄悄变难看。

顺着这一条翻车,我把 LLM 评测里所有「不报错但分数是假的」的失败排了一遍,分五层:生成层、评分层、统计层、数据层、链路层。每一层都能在 arXiv 找到出处——这不是我一个人踩的坑,是领域里反复被写论文的坑。

名词速查

深度文默认在导语后放这张表;这些词后文会反复出现。

术语一句话解释
finish_reason模型停止生成的官方原因。stop=正常结束;length=打满 max_tokens 被截断;content_filter=被内容过滤拦下
reasoning tokens推理模型「思考」部分的 token,和可见输出共享 max_tokens 预算(OpenAI/Anthropic 系)。思考越长,留给答案的越少
reasoning_effort推理模型的思考强度档位(low/medium/high)。设 low 不代表不思考,只代表少思考
静默失败 (silent failure)请求成功返回(HTTP 200)、无异常抛出,但产物缺失或失真,只有查特定字段才能发现
IFEval用「可验证指令」测指令跟随的基准:超过 400 字、提到 AI 至少 3 次这类硬约束,约 500 条 prompt
选项位置偏差 (selection/position bias)模型偏好选某个位置的选项(如总选 C),选项重排后正确率跟着变
pass@k采样 k 次至少成功一次的概率;无偏估计公式见 Codex 论文
ECE / MCE / Brier置信度校准三件套:分箱后的期望校准误差、最大校准误差、概率预测的均方误差
基准污染 (contamination)评测数据混进训练语料,模型背题而不是做题,分数虚高
服务路由 (serving route)同一模型名经不同网关/供应商接入(OpenRouter、Fireworks、BYOK……),行为可能完全不同

一张图:静默失败五层

flowchart TD
    L0["本次翻车<br/>finish_reason=length, content=null"] --> L1

    L1["生成层<br/>输出本身不可信"]
    L1 --> P1["推理token吞掉输出预算<br/>(本次)"]
    L1 --> P2["指令跟随失败<br/>输出格式违反"]
    L1 --> P3["选项位置偏差<br/>MCQ 老毛病"]

    L2["评分层<br/>判据在撒谎"]
    L2 --> P4["字符串匹配格式敏感<br/>1,000 vs 1000"]
    L2 --> P5["判据 bug<br/>pass@k 合并污染"]
    L2 --> P6["LLM-as-judge 偏差<br/>位置/冗长/自我偏好"]

    L3["统计层<br/>公式用错"]
    L3 --> P7["pass@k 估计陷阱<br/>朴素估计有偏"]
    L3 --> P8["校准分箱脆弱<br/>ECE 可被分箱玩坏"]
    L3 --> P9["小样本假确定性<br/>12条样本下结论"]

    L4["数据层<br/>题本身有问题"]
    L4 --> P10["基准污染<br/>背题不是做题"]
    L4 --> P11["证据缺失静默吞掉<br/>target/置信度缺失"]
    L4 --> P12["黄金答案脆弱<br/>换个人名掉10分"]

    L5["链路层<br/>环境在换底牌"]
    L5 --> P13["服务链路≠模型标识<br/>同名不同行为"]
    L5 --> P14["token/成本口径不一致<br/>reasoning 也算钱"]
    L5 --> P15["并发/限流/重试丢样本<br/>completed≠total"]

第 0 层锚点:这次翻车现场

先看证据。请求侧(我做了精简,保留关键字段):

{
  "model": "veral/01a039c8.../openrouter-kimi-k2-7-code-responses/v2",
  "max_tokens": 4096,
  "temperature": 0,
  "reasoning_effort": "low",
  "system": "……每个样本只输出 A、B、C 或 D……",
  "user": "Answer the following multiple choice question. The entire content of your response should be of the following format: 'ANSWER: $LETTER' ……"
}

响应侧的关键字段:

{
  "finish_reason": "length",
  "message": {
    "content": null,
    "reasoning_content": "……Thus final: ANSWER: D.……One more thought:" 
  },
  "usage": {
    "completion_tokens": 4096,
    "completion_tokens_details": { "reasoning_tokens": 4096 }
  },
  "cost": 0.0167602
}

三个事实拼出全貌:

  1. 模型不是不知道答案reasoning_content 里它至少三次写下「Thus final: ANSWER: D.」,推理结论已经收敛。
  2. 推理停不下来,答案行没机会生成。4096 个输出 token 全被 reasoning 消耗,content 从头到尾是 null,推理文本在「One more thought:」处被硬生生掐断。
  3. 评分器只认 content。Veral 的 choice 评分器从 message.content 提取答案 → 空 → 归一化分数 0% → 判错。reasoning 里再正确的推理,在评测里一分不值。

这正好是「静默失败」的标准样例:服务端返回成功(HTTP 200)、没有重试没有报错、烧掉 0.0167 美元,唯独产物是空的。如果评测链路不检查 finish_reason,这条样本会被当成「模型答错」记进准确率分母,而不是「模型没答完」单独归类。

这不是孤例。Inspect AI 官方 evals 仓库有人报过一模一样的 bug:MMLU 在 thinking 模型上被截断成空响应——Anthropic 的 thinking 模型先生成推理 token,可见输出预算被提前耗尽,返回空响应 + max_tokens stop reason。社区里这类讨论非常多(截断响应不是错误,是 HTTP 200你的 parser 把模型最好的答案扔了Gemini thinking 模型的空输出)。

第一层:生成层——输出本身不可信

1.1 推理 token 吞掉输出预算(本次)

症状:finish_reason=length + content=null(或 content 极短),reasoning 字段巨大。

根因:推理模型的 thinking 与可见输出共享 max_tokens 预算。max_tokens=4096 看着很大,但如果模型在思考阶段就烧掉全部配额,留给答案的 token 是零。reasoning_effort=low 也不救——它只降低思考强度,不设思考上限。

探测:评测框架的 response 处理里必须检查 finish_reason。社区里大量事故的共性就是「只查 status code,不查 stop reason」。opencode 有个 issue 写得最直白:32k 输出预算全被 reasoning 烧掉,finish_reason=length 被静默当成成功空结果

修法:

  • 按用途分档:ANSWER: $LETTER 这类短输出任务,max_tokens=32 就够,模型没有空间长篇思考;或者显式给 reasoning 设预算上限(Anthropic 的 thinking.budget_tokens、Gemini 的 thinking_config.thinking_budget)。
  • finish_reason=length 单独归类成「截断失败」,和「答案错误」分开统计,不要混进准确率分母。
  • 推理模型做多选题时,评测侧可以考虑关闭显式 reasoning(如果 API 支持),只要求最终答案。

1.2 指令跟随失败:输出格式违反

症状:要求「整个响应只能输出 ANSWER: $LETTER」,模型输出一段解释、答案是 C、或者把答案包进 markdown 代码块。评分器按位置/正则提取,提取失败或提取错位。

根因:指令跟随本身是模型能力的一部分,不是零一开关。Google 的 IFEval 专门干这个:把「超过 400 字」「提到 AI 至少 3 次」这类可验证指令做成基准,25 类指令、约 500 条 prompt,分别算 strict/loose 两档准确率。strict 和 loose 的差距,本身就是「模型被无关噪音带偏」的度量——它可能完全懂任务,只是没按格式说话。后续研究更进一步:Inverse IFEval 发现模型对训练中固化下来的「标准惯例」有惯性,面对反直觉指令(故意写错、不给注释)时跟随率显著下降。

探测:评测样本里单独统计「格式违反率」——输出存在但解析失败的比例,和「内容错误率」分开。EvalSafety 综述里有个惊人的数字:GPT-3.5 在 320 种 prompt 格式 × 53 个任务下,准确率跨度高达 56 个点,且模型变大、加 few-shot、做指令微调都不消除(EvalSafety Gap, arXiv:2606.30219)。另一个独立实验在 SWE-bench/MMLU/HumanEval 上发现,仅格式微调就能造成最高 76 个准确率点的差异(acceptance-bench)。

修法:评分器宽容解析(支持 答案是XX)X.、markdown 包裹),同时报告格式违反率作为模型质量的一维;如果格式违反率持续高,那是模型本身的问题,不是评测的问题。

1.3 选项位置偏差:MCQ 的老毛病

症状:同一道选择题,把选项 A/B/C/D 的顺序打乱,正确率变化。有些模型偏好「C 位」,有些偏好「A 位」。

根因:模型在训练里习得了对选项位置的先验,而不是纯粹对内容做推理。三篇论文把这个现象钉死了:

探测:把同一批题目的选项做全排列或随机置换,多跑几轮,报准确率区间而不是单点。如果区间宽,说明位置偏差严重,单点分数不可信。

修法:基准建设期就把选项置换做成标配;评测报告期给「置换后准确率范围」;发布时注明选项顺序。

第二层:评分层——判据在撒谎

2.1 字符串匹配的格式敏感性

症状:标准答案是 1000,模型输出 1,000$10001000.00**1000**,字符串精确匹配判错;或者 answerAnswer、全角半角标点导致误判。

根因:纯文本比对不理解数值语义。这是评分器设计问题,不是模型问题。Inspect 的 answer_match 评分器提供 numeric 归一化(把数字周围的逗号、货币符、百分号剥掉后按数值比)和忽略大小写,就是为治这个。但默认配置下 1,0001000

修法:数值题必须开数值归一化;文本题按需忽略大小写、剥离 markdown;有多个合法答案时用 target 列表或语义等价匹配。同时保留「原始提取值」和「归一化分数」双轨记录——原始值用来 debug,归一化分用来聚合。

2.2 评分器判据 bug:pass@k 合并污染

症状:pass@k 计算虚高或虚低,和真实能力对不上。

根因:pass@k 的语义是「最多试 k 次,只要一次成功就算过」。我前几周在 Veral 里修过一个实打实的 bug:归并统计时把全部采样结果都算进去,而不是只看前 k 次。k=3 时采了 5 次,前 3 次全失败、第 4 次偶然成功——旧逻辑会把第 4 次的成功计入,把 pass@3 拉高。修正后只取前 k 次,后续成功不反向污染。这类 bug 完全无声:不报错、不告警,只是指标悄悄变乐观。

修法:pass@k 合并逻辑写单元测试,用构造数据验证「只看前 k 次」;更系统地讲,判据本身要接受体检——我上一篇《先测判据,再测 Agent》就是讲怎么用作弊脚本给判据找漏。

2.3 LLM-as-judge 的三块偏差

症状:让 GPT-4 当裁判打分,结果受答案先后顺序、答案长度、是否自己生成的答案影响。

根因:Judging LLM-as-a-Judge with MT-Bench and Chatbot Arena (arXiv:2306.05685) 把 LLM 裁判的偏差点名了三块:位置偏差(先展示的答案占便宜)、冗长偏差(长的答案占便宜)、自我增强偏差(自己生成的答案占便宜)。后续工作更尖锐:LLM Evaluators Recognize and Favor Their Own Generations (arXiv:2404.13076) 发现模型的自我识别能力自我偏好强度存在线性相关——它真的认得出自己的输出,然后给高分。The Vulnerability of Language Model Benchmarks (arXiv:2412.03597) 在多个基准上复现了这种自偏好。

修法:配对评测时交换答案顺序各评一次;给裁判提供参考答案;多个裁判模型投票;定期用人类标注抽检裁判一致性(MT-Bench 论文里人机一致性 >80%,但这个数字本身也需要持续验证)。

第三层:统计层——公式用错了

3.1 pass@k 的估计陷阱

症状:pass@k 数值和「直接采样 k 次看通过率」对不上,或者小样本时波动巨大。

根因:朴素估计(p = c/n,然后算 1−(1−p)^k)有偏且高估。Codex 论文 Evaluating Large Language Models Trained on Code (arXiv:2107.03374) 给出无偏估计:

pass@k = 1 − C(n−c, k) / C(n, k)

前提是 n 个独立同分布的完整采样。但实践中有人把「一次提交里的单元测试数」当 n、把「通过的测试数」当 c——这些测试不是独立样本,是同一份代码的相关子结果,算出来的 pass@k 完全失真(Beyond Pass@k: Measuring Reliability and Security of Agentic Code Generation, arXiv:2608.14711)。

修法:记录真实的 n(采样次数)和 c(通过次数),用无偏估计公式;按任务聚合而不是把全部分数倒进一个池子;报告 n 的取值。

3.2 校准指标的分箱脆弱性

症状:同一个模型,换个分箱方式,ECE 忽好忽坏;甚至可以通过选分箱把 ECE 调「好看」。

根因:ECE 对分箱参数极其敏感。经典论文 On Calibration of Modern Neural Networks (arXiv:1706.04599) 定义了现代深度网络的过自信问题,也把 ECE 带火了;但 Measuring Calibration in Deep Learning (arXiv:1904.01685) 专门数了 ECE 的毛病:分箱数量、等宽 vs 自适应分箱、用 L1 还是 L2 范数、只看最大概率还是看全部分类概率,都会改变结论;作者建议自适应分箱 + L2 范数,并强调条件化到类别上看。ECE 的另一个已知缺陷是:分箱内部的平均会掩盖非单调的校准偏差(Understanding Model Calibration, arXiv:2501.19047 的可靠性图讲解)。

修法:校准计算把分箱策略(数量、等宽/自适应)写进配置固定下来,不能每次随手选;同时报 ECE + MCE(最坏箱子)+ 可靠性图(conf vs acc 点是否贴对角线);如果只报 ECE,它可能被分箱选择悄悄美化。这也是 Veral 校准模块现在「从候选 JSON 读分箱配置」的原因——口径要可复现

3.3 小样本的假确定性

症状:12 条样本跑出 91.7% 准确率,看起来挺高;但标准误 0.083,置信区间宽得没意义。

根因:样本量小时,点估计的方差大得离谱。91.7% = 11/12,一条样本翻转就是 8.3 个点。

修法:报告置信区间(Wilson 区间或 bootstrap),我在《评测分数的统计家族:Wilson 区间》里写过口径;决策前先问「这个差异在区间内吗」。

第四层:数据层——题本身有问题

4.1 基准污染

症状:模型在公开基准上分数很高,换一批没见过的题立刻现原形。

根因:基准发布后很快被吸收进训练语料,模型背题而不是做题。Benchmark Data Contamination of Large Language Models: A Survey (arXiv:2406.04244) 系统综述了这个问题;一份针对六个前沿模型的检测发现 MMLU 检出 13.8% 污染,STEM 类 18.1%,哲学类高达 66.7%(arXiv:2603.16197)。Line Goes Up? Inherent Limitations of Benchmarks (arXiv:2502.14318) 的标题本身就是结论:基准发布即被吸收,「分数上行」不一定是能力上行。

修法:私有 held-out 集兜底;动态/私有基准;对公开基准做污染检测(如 Open-Source Data Contamination Report, arXiv:2310.17589 那套管道);关键决策不依赖单一公开基准。

4.2 标注与证据缺失静默失败

症状:某条样本没有 target、或没有置信度字段,统计时被静默跳过或算成 NaN,聚合指标悄悄失真。

根因:评测数据不完整时,多数管线的默认行为是「跳过」或「当成 0/1 的某一端」,而不是报错。我前几周在 Veral 里修的就是这个:配对组、重复采样只要证据缺失,都返回明确原因——缺失 target 返回「missing_target」,缺失置信度返回「missing_confidence」,采样次数不足返回「insufficient_samples」,而不是 NaN 静默漂移。

修法:把「证据完整性」当成评测管线的一等断言:completed == total无缺失 target无缺失置信度;任何缺失都必须有显式原因字段,可被查询、可被告警。

4.3 黄金答案脆弱

症状:题目本身经不起扰动。GSM8K 这类数学题,把题里的人名换一个,准确率掉 10 个点(苹果研究团队的发现,36氪出海报道)。

根因:黄金答案和题目措辞深度耦合,模型在做「记住答案模式」而不是「理解题」。

修法:给题配置多个等价答案;扰动诊断(改名、换数、改顺序)进评测流水线;报告的准确率要带扰动前后的区间。

第五层:链路层——环境在换底牌

5.1 服务链路 ≠ 模型标识

症状:同一个模型名,今天经 OpenRouter 跑、明天经 Fireworks 直连跑,行为、字段、token 统计都不一样。本次翻车的模型名带 openrouter- 前缀,实际 provider 是 Fireworks。

根因:评测测的从来不是「模型权重」这一个对象,而是「权重 + 路由 + 精度 + 推理设置 + 输出约束 + 评测程序」整条服务链路。IBIB: A Protocol for Measuring Enterprise AI Systems by Serving Route, Not Model Identifier (arXiv:2609.10494) 把这个说透了:按服务路由测,不要按模型名测。同名的推理模型,有的把 thinking 放 reasoning_content、有的放 provider_specific_fields,解析器写错一个字段就全军覆没。

修法:评测记录里固化「服务链路指纹」(路由、provider、推理设置、SDK 版本),换链路重测;模型对比只在同一链路上做。

5.2 token/成本口径不一致

症状:成本报表和 token 报表对不上;reasoning token 算不算钱、cache 命中算多少,各 provider 口径不同。

根因:本次失败样本烧了 0.0167 美元,全部花在推理 token 上——一条「没答」的样本比「答对」的还贵。如果成本报表只看 total_tokens,不看 reasoning_tokens 拆分,你会以为失败很便宜。

修法:成本明细按 input/output/reasoning/cache 拆分记录;评测报告里「失败样本的成本」单独一列——截断失败的成本是纯浪费,应该优先治理。

5.3 并发/限流/重试丢样本

症状:评测任务显示完成,但 completed < total;或者 API 限流触发重试,重试的样本被悄悄覆盖。

根因:并发控制(max_samples)、限流退避、重试逻辑如果不在任务层面做断言,丢样本是无声的。上次跑 12 条样本、max_samples=2,分批跑了 62 秒,好在 completed=12/12、retries=0——但如果哪次重试把样本号写重复,你根本发现不了。

修法:任务完成度断言(completed == total)、重试计数上报、样本 ID 唯一性校验;任何重试都写入 trace 而不是覆盖原记录。

共性的根:为什么它们都「静默」

把十五个失败并排看,共性是惊人的:

失败返回成功?唯一线索默认行为
生成推理截断HTTP 200finish_reason=length记成「答错」
生成格式违反HTTP 200content 与 target 不匹配记成「答错」
生成位置偏差HTTP 200置换后分数漂移报单点分数
评分匹配敏感HTTP 200归一化前后分数不同报精确匹配分
评分判据 bug无异常构造用例报乐观指标
评分judge 偏差HTTP 200换序后分数翻转报单次判决
统计pass@k 有偏无异常和无偏估计对不上报高估分数
统计分箱敏感无异常换分箱 ECE 漂移报单值 ECE
统计小样本无异常置信区间过宽报点估计
数据污染无异常换题现原形报虚高分数
数据证据缺失无异常完成度 < 总数静默跳过/NaN
数据黄金答案脆弱无异常扰动后分数掉报单点分数
链路路由差异HTTP 200换链路复现报模型名分数
链路口径不一致无异常明细对不上报总额
链路丢样本无异常completed ≠ total报完成

所有十五个失败都不抛异常。它们的共同点是:管线把「请求成功」当成了「评测成功」。而真正的防线只有一个方向——把静默变成有声:查 finish_reason、断言 completed==total、判据接受构造用例体检、分箱口径固定、选项置换、污染检测。每一道防线都在把「悄悄发生的错误」变成「报出来的错误」。

我在 Veral 里怎么防(检查清单)

落地到 Veral,我把它压成一张运行时检查清单:

  1. 响应级:每次响应先查 finish_reason;length 单独归类为「截断」,不算「答错」,不进准确率分母。
  2. 预算级:ANSWER: $LETTER 这类短格式任务,max_tokens 压到 32;推理模型显式限制 reasoning 预算或关闭显式推理。
  3. 评分级:评分器输出证据三件套——原始提取值、归一化分数、判定原因;数值题开数值归一化。
  4. 统计级:pass@k 合并只看前 k 次并记录真实 n;校准固定分箱策略,同时报 ECE + MCE + 可靠性图;准确率带置信区间。
  5. 数据级:证据完整性断言(target/置信度非缺失);配对组与重复采样证据缺失返回显式原因;公开基准标注污染检测状态。
  6. 链路级:记录服务链路指纹(provider/路由/推理设置);成本按 reasoning/cache 拆分;任务完成度断言 completed==totalretries==0
  7. 设计级:MCQ 题目选项置换多跑一轮,报区间;判据写构造用例的单元测试(作弊脚本体检)。

诚实的提醒

  • 本文的「五层十五坑」是我对评测失败的切法,是分析工具,不是行业标准分类;有些坑(位置偏差、污染)跨层,归到哪层有主观成分。
  • 本次翻车的请求/响应数据来自 Veral 证据页的真实记录;GitHub issue 与博客文章是我按主题检索到的一手来源,我未逐一复现它们的实验。
  • 数值引用(MMLU 13.8% 污染、GPT-3.5 56 点跨度、GSM8K 换名掉 10 点)来自各论文/报道的原文口径,口径各异,直接可比性有限,引用时请回到原文看细节。
  • 检查清单里的做法大多是我在 Veral 里的工程判断,没有大规模消融证据支撑「每条都有效」;它们的共同逻辑是「宁可报错,不可静默」,这条我认为站得住。

小结

三行压缩:

  • 最危险的评测失败不报错:HTTP 200 + finish_reason=length + content=null,评分器给 0 分,准确率悄悄变差——本次翻车就是这个。
  • 五层静默失败:生成(推理截断/格式违反/位置偏差)、评分(匹配敏感/判据 bug/judge 偏差)、统计(pass@k 估计/校准分箱/小样本)、数据(污染/证据缺失/黄金答案脆弱)、链路(路由/token 口径/丢样本)。
  • 防线是同一个方向:把「静默」变成「有声」——查 finish_reason、断言 completed==total、判据体检、固定分箱、选项置换、污染检测。

5 分钟第一步

一条命令看本次翻车:读 Inspect eval 日志,把 finish_reason=length 的样本全部挑出来。

# 需要:inspect-ai 已安装,替换成你的 eval 文件路径
from inspect_ai.log import read_eval_log

log = read_eval_log("你的.eval文件")
for s in log.samples:
    if s.output.finish_reason == "length":
        print(s.id, "截断!", repr(s.output.completion[:80]))

跑一遍:凡是被打出来的样本,都是「没答完」而不是「答错了」。先把它从准确率分母里分出去,你的分数会立刻变诚实——这才是修好静默失败的第一步。

参考来源

工程实践:

arXiv 论文:

站内回链: