一篇读懂 LLM 输出的确定性:种子、缓存与可复现性

金融与法务团队第一次把 LLM 接进流水线时都会撞上同一堵墙:昨天跑出的报告,今天用完全相同的输入重跑,输出变了。工程师的直觉是「temperature 设成 0 就确定」,设完发现还是会变;再加 seed 参数,发现官方文档只敢承诺 best-effort。这一课很重要:LLM 输出的不确定性是三层独立机制的叠加——你控制的采样层只是其中一层,另外两层藏在浮点运算和推理服务的工程栈里,任何一层都能独立打破「相同输入、相同输出」。本文把三层机制拆开讲透,然后给出评测、审计、缓存三类场景下的可复现工程方案。

一、第一层:采样随机性——你能控制的部分

采样器的随机源

temperature 大于 0 时,解码每一步从按 logits 缩放后的概率分布里抽样,抽样的随机数来自进程的随机数生成器。seed 参数的作用就是给这个生成器一个确定的初值:相同 seed、相同输入、相同软件栈,理论上能重放出完全一致的采样序列。OpenAI 在 2023 年 11 月引入 seed 参数时同时引入了 system_fingerprint 字段,并把文档措辞定为 best effort——这句话后面藏着两层保留:其一,provider 端的软硬件升级会改变 fingerprint,指纹一换,同 seed 的重放就失效;其二,seed 只锁定了「你的」随机数,锁不住服务端的批次调度与浮点行为,那两层不确定性它管不了。结论:seed 是可复现性的必要条件,远不是充分条件。

采样参数与方差的关系

采样层的方差与参数直接相关:temperature 0(贪心)在采样层面方差为零,但每一步取 argmax 时若前两名 logits 几乎并列,任何微小数值扰动都会翻转选择——这就是「温度 0 也不确定」的采样层解释。top-p 0.9 的方差集中在分布尾部 10% 的质量里,整体方差远低于纯温度采样。做确定性敏感的业务(审计流水线、报表生成)时,先在采样层把方差调到最小,再处理下面两层。

结构化输出也不是保险箱

一个常见的误解是「JSON 模式输出结构确定,所以内容也确定」:结构化输出约束的只是 token 语法(每次都生成合法 JSON),在语法约束内部的选择仍然走采样——同一个字段的内容、数值、措辞照样逐次不同。真要「结构确定加内容稳定」,组合是结构化约束加低温采样加缓存,三层缺一不可。同理,日志里常见的「每次输出都不一样,是不是有 bug」的工单,多数只是 temperature 保持默认值(多数厂商默认 1.0 或 0.7),第一件事永远是核对请求参数而非怀疑服务故障。

二、第二层:浮点非结合性——数学层面的必然

浮点加法不满足结合律

浮点运算里 (a + b) + c ≠ a + (b + c) 是常态而非异常。一个二十行的演示就能让任何人信服:

import numpy as np
rng = np.random.default_rng(42)
x = rng.standard_normal(10_000_000).astype(np.float32)

s1 = np.sum(x)                          # 顺序由 numpy 决定
s2 = x.reshape(-1, 1000).sum(axis=1).sum()  # 两阶段求和,顺序不同
s3 = np.sum(x.astype(np.float64)).astype(np.float32)

print(float(s1), float(s2))
# 输出:-115.19917 -115.19912  —— 相同数据、不同求和顺序,结果不同
print(float(s1 - s2))                   # 差异:约 5e-5

千万量级的 float32 累加,求和顺序不同结果就在小数点后四五位开始分岔。GPU 上的大矩阵乘法正是数百万次这样的累加,而求和顺序取决于硬件与调度:不同 GPU 型号、不同 kernel 实现、不同 tile 划分,累加树的结构都不同。这还没算并行归约——线程块之间的归约顺序由调度器决定,本身就是非确定的。

对 LLM 意味着什么

把非结合性接到 LLM 推理上:logits 是矩阵乘法的输出,求和顺序的微小差异让 logits 在小数点后若干位抖动;温度 0 的解码取 argmax,两个候选 logits 本来只差 1e-6,抖动一下名次就互换,输出从中间某个 token 开始走上完全不同的分岔路。注意这发生在「完全相同的模型权重、完全相同的输入」之下——没有任何随机数参与。这就是为什么 seed 锁不住一切:它只能锁定你自己代码里的随机源,锁不住 GPU 归约的顺序。

flowchart TD
    Q["相同输入请求两次"] --> A["第一层:采样随机性<br/>temperature>0 / seed 只 best-effort"]
    Q --> B["第二层:浮点非结合性<br/>加法不满足结合律,顺序由硬件与调度定"]
    Q --> C["第三层:服务工程栈<br/>连续批处理、chunked prefill、MoE 原子加"]
    A --> D["输出分岔"]
    B --> D
    C --> D
    D --> E1["对策:锁定全栈<br/>同版本同硬件同 batch、greedy + 记录响应"]
    D --> E2["对策:记账不重放<br/>存档输入输出、logprobs 复算验证"]
    D --> E3["对策:语义缓存<br/>命中即返回历史输出,绕开重算"]

三、第三层:推理服务栈——工程放大器

批处理改变了每个请求的计算方式

自托管推理(vLLM、SGLang 等)把不确定性再放大一层。连续批处理(continuous batching)让每个请求落在的批次位置、批内同伴、乃至 chunked prefill 的切块大小都随当时集群负载而变;矩阵乘法按整个 batch 一起算,batch 维度不同导致 kernel 选择与归约顺序不同,于是同一个请求在高峰期与凌晨重跑,数值路径不同,输出就可能分岔。MoE 模型再加一层:专家路由的负载统计常用原子加累加,原子操作的完成顺序不由任何 seed 控制。这就是实践中「温度 0 + seed + 相同版本」仍会变化的完整解释——三层里你只摸到了第一层的旋钮。要做严格可复现的自托管推理,需要锁定推理框架版本与配置、固定 batch 组成(牺牲吞吐)、选用确定性 kernel,几乎等于放弃服务化的全部优化,代价高到只有在离线评测集群里才值得付。

四、可复现的工程方案:按场景选策略

评测场景:控制变量加记账

模型评测要的不是「重放相同输出」,而是「分数可比」。纪律是四条:锁采样参数(temperature 0 或固定 seed,全组一致);锁推理栈(同框架版本、同硬件、同 batch 配置,或干脆统一走同一 API 端点);锁输入(few-shot 样例、提示词模板全部进版本库);记账而非假设——每次评测存下完整请求、响应、参数、模型指纹,发现异常时能回到原始记录而不是凭记忆重跑。一个容易被忽略的坑:评测脚本升级(改了解析器、换了截断逻辑)等同于换了评测,分数必须重跑全量,新旧分数不可拼接。

审计与金融场景:存档而非重放

审计链条的正确姿势不是「保证能重放」,而是「保证有原始记录」。每次生成把输入、输出、参数、seed、模型指纹、时间戳完整落库,下游决策引用记录 ID 而不是引用「可以随时重跑出来的东西」。需要更高置信时用 logprobs 做一致性抽查:记录响应时同时请求 top-k logprobs,事后重算相同上下文下这些 token 的概率,验证服务行为未漂移——这是「不重放也能审计」的关键技巧。把「可复现」的目标从「重算出相同输出」降级为「证明当时就是这么算的」,工程难度直降一个数量级,而合规价值几乎不损失。

缓存场景:把确定性变成产品特性

如果输出本身不确定,最有效的确定性手段是缓存:命中缓存的请求返回历史输出,天然 100% 一致。精确缓存用「模型指纹 + 参数 + 完整输入」做键,代价是命中率低;语义缓存用嵌入相似度做键,命中率高但引入「不同问题返回同一答案」的新风险,必须设相似度阈值与热门问题的精确白名单。缓存还有一层财务含义:对确定性敏感的请求通常也是高频重复请求,缓存命中同时买到了确定性与成本。API 侧的 prompt caching(前缀缓存)是另一回事——它缓存的是 KV 计算而不是响应,不改变输出分布,但显著降低重复前缀的延迟与费用,两者在架构上常被同时部署。

缓存与评测的相互作用

缓存和可复现评测之间有一个必须设防的冲突:如果评测流量经过带语义缓存的网关,第二次跑同一评测集时大量请求直接命中历史输出,你测到的其实是「缓存的表现」而不是「模型当前的表现」——模型换了版本、缓存没换键,评测结果完全失真还自我感觉良好。因此评测环境必须显式绕过响应级缓存(网关加旁路标记,或评测入口直连推理端点),而 prompt 缓存可以保留(它不改变输出语义)。这条纪律与「评测脚本升级等同换评测」是同一条原则的两面:你要测的是那个会变的对象本身,任何插在中间的复用层都必须在测量路径上被摘除。

五、一个实用的诊断流程

遇到「输出不可复现」的工单,按这个顺序排查:第一问采样参数——温度与 top-p 是不是非零、seed 有没有传;第二问服务指纹——API 模型版本或 system_fingerprint 变没变,自托管则查框架版本与 batch 配置;第三问数值敏感性——把温度设 0 重试若干次,若仍不稳定,基本可以定位到批处理或硬件层的浮点路径;第四问需求本质——业务要的到底是「重放一致」还是「结果一致」,前者走全栈锁定或缓存,后者走存档审计加输出校验。多数团队走完这四步会发现:他们真正需要的只是第三和第四步的方案,一开始纠结的 seed 参数反而无关紧要。

小结

LLM 输出的确定性要分层理解。第一层采样随机性由温度与 seed 控制,但 seed 在 API 服务里只有 best-effort 语义,软件栈一变就失效;第二层浮点非结合性是数学层面的必然,求和顺序由硬件与调度决定,温度 0 的 argmax 会把它放大成输出分岔;第三层推理服务栈(连续批处理、chunked prefill、MoE 原子加)让同一请求在不同时刻走上不同数值路径,这一层你几乎没有旋钮可拧。工程上的正确姿势是放弃「任何输入都能重放」的执念,按场景选策略:评测靠控制变量与全量重跑,审计靠完整存档加 logprobs 复算,产品靠缓存把一致性变成特性。理解了三层机制,「为什么 temperature 0 也确定性不了」就从一个灵异事件变成了一个可以用十分钟向团队讲清楚的系统属性——以及一套立刻可以落地的对策清单。

← 返回资讯列表

读者留言

COMMENTS 暂无
仅本站原创文章开放留言 · 请勿留下手机号、邮箱等个人信息

还没有留言,来说第一句?