你的评估套件设置了确定性种子,但供应商却悄悄忽略了它
你设置了 seed=42。你设置了 temperature=0。你记录了运行情况,发布了仪表板,并在模型更换上签了字。第二天早上,重新运行相同的提示词却返回了不同的数字,你给出的解释——“肯定是采样噪声”——错得离谱:根本没有采样,而且噪声是结构性的。Seed 离开了你的客户端,网关将其丢弃,内核将你的请求与 17 个不相关的请求打包在一起,浮点归约(floating-point reduction)顺序在你眼皮底下发生了变化。你所谓的“可复现”基准测试,距离变成另一个基准测试其实只差一个批次(batch)。
这种失败模式是悄无声息的,因为技术栈中的每一层在技术上都是正确的。SDK 接受了 seed。供应商记录了 seed。模型返回了 system_fingerprint。评估工具记录了这三者。没有 5xx 错误,没有警告,没有抗议。仪表板上的数字只是发生了偏移,而团队将这种偏移合理化为一直存在的某种抖动——因为他们没有工具能告诉他们,他们看到的究竟是随机解码(stochastic decoding),还是导致三周的对比实 验全部失效的后端轮换(backend rotation)。
Seed 可能消失的三个地方,而且没有一处会报警
当你调用 seed=42 的聊天补全(chat completion)时,这个整数会经过你的客户端 SDK,经过你和模型宿主之间的任何网关或代理,经过推理服务器,最后进入采样器。每一跳都有理由把它弄丢。
SDK 通常没问题。现代 OpenAI、Anthropic 和 Google 的 SDK 都会原样转发 seed。但 SDK 并不总是最后一个接触你请求的代码。如果你通过一个与供应商无关的网关路由——例如 LiteLLM、Portkey 或你平台团队编写的内部抽象层——seed 的存亡取决于该网关针对你所选供应商的适配器(adapter)是否知晓它。适配器的覆盖范围参差不齐。OpenAI 适配器会转发 seed;Bedrock 适配器可能不会将其透传;六个月前针对旧模式编写的自定义代理可能有一个硬编码的白名单,将其排除在外。无论哪种情况,请求都会返回 200。你收不到任何负面信号。
供应商自己的网关是第二个沉默的“丢弃者”。截至 2026 年中期,Anthropic 的 Opus 4.7 迁移指南更改了 temperature 和 top_p 的语义——非默认值现在会返回 400 错误——但各供应商对 seed 的处理情况依然复杂:既有“支持但尽力而为”(OpenAI 的某些模型),也有“不支持但默许接受”(历史上 Anthropic 的某些端点),还有“支持但未记录”(各种托管的开源权重模型)。最令人沮丧的是那些“默许接受”的情况:网关在进入时剥离了该字段,模型没有收到 seed,响应正常返回,而你的评估工具记录了一个毫无影响的 seed 值。
第三个地方是推理服务器本身,seed 确实到达了那里,但它只能控制极小部分的方差。vLLM 用户在过去两年的大部分时间里都在提交问题,内容大致是“我设置了 temperature=0, top_p=1, seed=42,但输出仍然不同”——而标准的回答是,seed 仅控制采样器的伪随机抽取。当采用贪婪采样(greedy sampling)时,seed 基本上不起任何作用。你在 temperature 为零时看到的差异完全来自其他地方。
批次不变性是你无法在日志中看到的方差
Thinking Machines 团队在 2025 年底发布了一项结果,重塑了严肃的从业者讨论 LLM 确定性的方式。在托管的 vLLM 上以 temperature 0 运行 Qwen3-235B,他们发送了 1,000 个相同的查询,并得到了 80 个唯一的补全结果。不是 80 个带有细微变化的补全,而是 80 个截然不同的字符串。Seed 已设置。Temperature 为零。贪婪解码已开启。模型仍然生成了 80 个答案。
起因是批次大小依赖(batch-size dependency)。现代推理服务器将并发请求打包进同一个前向传播中,以摊销 GPU 成本。在该前向传播中运行的矩阵乘法、RMSNorm 和注意力归约(attention reductions)并不是批次不变的——当批次形状(batch shape)改变时,它们的数值结果也会随之改变。浮点加法不满足结合律,因此归约过程中累加部分和(partial sums)的顺序会影响结果的最后几 位,而这个顺序是由内核如何对批次进行分块(tiles)决定的。在他们的测量中,同一输入的单次矩阵乘法在 batch size 为 1 和 2048 之间相差了 1669.25。这种差异经过数十层的传播,足以翻转至少一个 token 的 argmax,而这种分歧随后会产生级联效应。
这意味着真正控制你输出的“seed”并不是你发送的那个整数。它是你的请求到达服务器那一刻隐式的批次组成——还有谁在调用 API、调度器如何将你分组、内核是否针对你的 token 数量选择了 split-K 策略。所有这些都不在你的控制之下。所有这些都不会出现在你的日志中。
同一篇论文表明,通过使用批次不变内核(batch-invariant kernels)——将矩阵乘法、RMSNorm 和注意力路径替换为无论批次形状如何,其分块和归约策略都固定的版本——所有 1,000 个补全结果都变得完全相同。代价是实打实的:运行耗时从 26 秒增加到了 42 秒,慢了约 60%。这就是实现真正可复现性的代价,而没有一家托管供应商收取这项费用,这一事实足以说明你实际购买的是什么样的保证。
system_fingerprint 是你拥有的唯一信号,而你可能根本没有在关注它
OpenAI 针对更大的结构性问题提供了一个部分解决方案。每个聊天补全(chat completion)响应都包含一个 system_fingerprint 字段,这是一个针对产生输出的权重、基础设施和配置 组合的不透明标识符。如果你的指纹在多次运行中保持稳定,你就有机会获得近似的确定性。如果它发生了轮换,你就是在拿苹果比橘子 —— 即使使用相同的种子(seed)、相同的提示词(prompt)、相同的采样温度(temperature),后端也已经发生了足够的改变,以至于 OpenAI 自己都不再承诺可复现性。
对于每个模型家族,后端轮换每年会发生几次,但它们并不会被正式宣布。它们会以新的指纹哈希值的形式出现在你的一部分流量中。如果你的评估框架记录了指纹但没有基于它进行过滤,你可能会在周二早上看到看起来像模型退化(regression)的漂移,而实际上那只是部署更替。如果你的评估框架根本不记录指纹,你看到的漂移将毫无头绪。
大多数团队都会跳过的防御性模式:按 system_fingerprint 对每次评估运行进行分桶,报告每个桶的分数,并拒绝进行跨桶比较。如果两次运行是由两个不同的指纹提供服务的,那么出于基准测试的目的,它们就是两个不同的模型,即使 URL 上的模型名称完全相同。将指纹视为模型身份的一部分。供应商不会为你做这件事,因为供应商的产品故事是“你两次调用的都是 Opus 4.7”。你的评估故事必须更加具体。
Azure 的文档对于相同的参数表现得更诚实:它告诉你要预见漂移,监控指纹,并将任何与之前值不匹配的情况视为一次全新的评估。非 Azure 的文档说的也大致相同,但将其掩盖在“尽力而为(best-effort)”之下。无论哪种方式,操作行为都是一样的 —— 你的基准测试是一个数字加上一个指纹再加上一个日期,丢掉这三者中的任何一个,数字就会变成孤证。
