跳到主要内容

那些由于模型选择了不同的 Token 而无法复现的 Bug

· 阅读需 11 分钟
Tian Pan
Software Engineer

用户提交了一个 bug。你的智能体生成的摘要掉了一段关键内容,或者 JSON 返回格式错误,或者回答一本正经地胡说八道。你打开工单,复制请求,然后重放(replay)。结果正常。你又重放了一次。依然正常。于是你把工单标记为“无法复现”并继续处理其他事情。

Bug 依然在那儿。真实用户依然在遇到它。你之所以关闭工单,是因为你的调试工具链默认了固定的输入会产生固定的输出——而你正在调试的组件实际上是从概率分布中进行采样的。

这不是罕见的边缘案例。这是 LLM 系统行为与整个调试学科构建方式之间的结构性失配。你在处理故障时使用的每一种工具——重放请求、二分法定位回归、编写回归测试——都默认这是一个确定性函数。当你的某个组件是采样器时,这些工具不会大声报错,而是会悄无声息地撒谎。它们会在 Bug 依然存在时告诉你它已经消失了。

“重放请求”是确定性世界的习惯

重放反应已经有几十年历史了,且几乎总是正确的。Web 处理器、SQL 查询、纯函数:输入相同的参数,得到相同的结果。如果它曾出错而现在正常,要么是输入变了,要么是外部状态变了。重放就是你找出原因的方式。

LLM 推理悄悄违反了这个前提。即使你认为已经固定了一切,其实并没有。设置 temperature=0 看起来应该能给你贪婪解码(greedy decoding)和单一的确定性答案。但事实并非如此。Temperature 为零只消除了 采样 步骤中的随机性——即从分布中挑选 Token 的过程。它无法解决分布本身发生偏移的问题。

在 2025 年,Thinking Machines Lab 将同一个提示词通过一个流行的开源模型在 temperature 0 的情况下运行了 1000 次,得到了 80 个不同的完整回复。罪魁祸首不是采样器,而是 批次差异(batch variance):生产环境的推理服务器将许多用户的请求打包成一个共享批次,而批次组合的变化会改变 GPU 约减算子(reduction kernels)累加浮点数的顺序。浮点数加法不满足结合律,因此 (a + b) + ca + (b + c) 在最后几位可能会有所不同。三个操作——RMSNorm、矩阵乘法和 Attention——就足以让“相同的输入”产生发散的 Logits。

为什么这最后一点微小的差异如此重要?假设前两个候选 Token 的概率仅相差 0.0000001。一个微小的数值抖动翻转了最大值索引(argmax)。于是输出了一个不同的 Token。由于每个 Token 都会反馈到下一个 Token 的上下文中,这个微小的翻转不会保持微小——生成过程会转向一条完全不同的路径。开头的一个 Token 偏差可能意味着结尾产生一个完全不同的答案。

所以,当你重放一个“无法复现”的工单时,你并不是在重新运行用户的失败过程。你是在进行一次新的采样。你的请求所处的批次不同了。共用 GPU 的其他用户不同了。混合专家(MoE)路由将 Token 分配给固定大小的专家组,可能会根据组内其他成员的情况对你的 Token 进行不同的路由。重放成功几乎说明不了什么——它只是从那个曾以未知概率产生错误答案的分布中抽取了一次新的结果。

可重运行 vs 可重抽

修复的第一步是放弃一个无法实现的目标,转而追求一个可以实现的目标。

你可能无法让托管模型 API 实现位级(bitwise)可复现。你无法控制批次、算子、路由或集群的硬件组合。严格的确定性只有在你使用固定种子、temperature 0、确定性算子和单请求批次在自己的硬件上运行开源权重模型时才现实可行——即便如此,也需要刻意的工程设计。对于大多数使用供应商 API 的团队来说,“重运行完全相同的失败”是不可能的。

可以 拥有的是 可重抽性(re-rollability)。你可能无法复现完全相同的错误输出,但你可以复现产生该输出的精确 分布。如果你捕获了该分布的完整输入,你就可以再次从中抽取样本——多次抽取——并观察它失败的频率。

这种区别就是博弈的核心。一个可重运行的 Bug 给你一个确定性的失败去修复。一个可重抽的 Bug 给你一个你可以测量、攻克和验证的 失败率。对于一个概率系统,失败率才是真正的 Bug。“这个提示词有 4% 的概率生成错误的 JSON”是一个可以处理的工程目标。“它为某位用户出错了一次”只是一个幽灵故事。

捕获分布的完整输入

要使失败变得可重抽,你需要影响分布的每一个输入——而不仅仅是用户的文本。大多数日志设置只捕获了其中一部分,而悄悄丢弃了其余部分。完整的集合包括:

  • 渲染后的精确提示词。 不是模板——而是填充了所有变量、系统消息和 few-shot 示例后的最终字符串。模板加上单独记录的变量是不够的;空格、顺序和截断都会改变分布,你需要的是真正通过网络传输的字节。
  • 完整的检索上下文。 对于 RAG 或智能体系统,是指进入上下文窗口的数据块、工具输出和之前的轮次。检索本身也是非确定性的——索引更新或 ANN 搜索可能返回不同的邻居——因此除非你记录了失败时的上下文,否则它就消失了。
  • 所有采样参数。 Temperature、top-p、top-k、频率和存在惩罚、最大 Token 数、停止序列,以及任何结构化输出或语法约束。
  • 模型和分词器(Tokenizer)版本。 “GPT-4o”或“Claude”不是版本号。供应商会发布静默更新;只有固定的快照字符串在几个月后才有意义。分词器也很重要——分词差异会在模型运行前就改变分布。
  • 种子(Seed),如果 API 提供了的话。 它本身不能带给你确定性,但结合其他因素,它可以缩小解释的范围。
加载中…
References:Let's stay in touch and Follow me for more thoughts and updates