文章是 AI 写的
这篇文章整理一次关于"为什么 temperature=0 时,相同输入仍可能得到不同输出"的原理探索。重点不是 API 使用手册,而是建立一个可操作的心智模型:模型如何逐步生成回答、上下文如何增长、早期微小分叉为什么会影响后续方向,以及"并行推理"到底在并行什么。
文中的模型执行细节以通用 Transformer/LLM 推理机制为基础。具体线上服务端的调度、kernel 选择、批处理策略通常不是公开稳定接口,因此相关部分会使用"通常""可能"来表述。
先给结论
大模型的可见回答通常不是一次性完整写好再打印出来,而是按 token 逐步生成:
text
下一个 token = f(原始输入 + 已生成的所有 token)
这里的 token 可以粗略理解成"文字小片段"。中文里可能接近一个字,也可能是一个词或词片段;英文里可能是一个词、半个词、空格加词、标点等。
因此有三个关键点:
- 生成是逐 token 进行的,不是严格逐字。
- 每一步都基于完整的当前上下文,不只是基于前一个 token。
- 上下文会随着已生成 token 递增,早期的小差异可能逐步放大成后续回答方向的差异。
逐 token 生成是什么
假设用户输入:
text
解释 temperature=0 为什么还可能输出不同结果。
模型不会先生成一整篇文章对象,再把它原样吐出来。更接近下面这个循环:
text
1. 读取原始输入
2. 计算"下一个最合适的 token 是什么"
3. 选出一个 token
4. 把这个 token 接到当前输出后面
5. 用"原始输入 + 当前输出"继续计算下一个 token
6. 重复,直到生成结束标记或达到长度限制
用流程图表示:
为了效率,推理系统通常会缓存前面 token 的中间结果,也就是常说的 KV cache。这个缓存不会改变心智模型:语义上,下一步仍然是在基于"原始输入 + 已生成输出"来决定下一个 token。
每个 token 看的是完整上下文
一个常见误解是:模型只看前一个 token 来生成下一个 token。
更准确的说法是:模型看当前上下文里的所有可见 token,包括系统指令、用户输入、历史消息、工具结果,以及当前这次回答里已经生成的 token。
可以写成:
text
T_next = f(system/developer instructions,
conversation history,
user input,
tool results,
generated_tokens_so_far)
所以"前一个 token"很重要,但它只是上下文的一部分。模型不是只沿着一个字符链条往前爬,而是在每一步重新基于完整前缀做判断。
上下文为什么会递增
假设模型已经生成:
text
主要原因是
下一步模型看到的上下文就不再只是用户问题,而是:
text
解释 temperature=0 为什么还可能输出不同结果。
主要原因是
如果下一步选了"并行计算",后面的回答自然更可能解释 GPU、浮点数和矩阵计算。
如果下一步选了"服务端不保证",后面的回答自然更可能解释 API、seed、system_fingerprint 和工程稳定性。
这两个方向都可能正确,但它们会把后续文本带向不同展开路径。
因此,偏差不一定来自"明显错误"。一个早期合理但不同的 token,就可能改变后续上下文,然后把回答带向另一条同样合理的路线。
思考方向是一开始确定的吗
不应把它理解成"模型一开始就确定完整答案,然后逐字打印"。更好的理解是:
text
不是:先完整规划全文 -> 再逐 token 输出
而是:每一步选择下一个 token -> 新 token 又影响下一步选择
不过,早期 token 的影响非常大。它像文章开头的立场和角度:一旦开头写成"从底层计算来看",后面自然会更容易继续讲并行、浮点和 GPU;如果开头写成"从 API 契约来看",后面自然会更容易继续讲参数、seed 和后端配置。
所以"方向"不是一次性完全定死的,而是在生成过程中逐步被上下文塑形。早期选择会强烈塑造后续方向,后续 token 又会继续强化或修正这个方向。
还要注意,"思考"这个词容易让人误会。这里说的是可见回答的生成过程。某些模型或产品可能还有用户不可见的推理、规划、工具选择或安全检查步骤,但这些属于具体系统实现,不能简单等同于人类脑内先想好一整篇文章。
并行推理到底在并行什么
生成下一个 token 的外层循环基本是顺序的:第 2 个 token 要依赖第 1 个 token,第 3 个 token 要依赖前两个 token。
但每一步内部有海量数学计算,这些计算通常会并行执行。
可以记住一句话:
text
外层生成是逐步的,内层计算是并行的。
1. 候选 token 打分并行
模型在决定下一个 token 时,不是只问"下一个是不是 A",再问"下一个是不是 B"。它通常会对整个词表里的大量候选 token 同时计算分数。
例如某一步可能得到:
text
"可以" 12.3456781
"能够" 12.3456779
"会" 11.9821000
"因此" 11.7312000
这些分数叫 logits。分数越高,表示模型认为这个 token 越适合作为下一个 token。
2. 矩阵计算并行
Transformer 里大量工作是矩阵乘法和加法。可以把它想成超大的表格计算。
一个人手算表格时只能一格一格算;GPU 会把表格切成很多块,让大量计算单元同时算不同区域,最后再合并结果。
3. attention head 并行
模型会通过 attention 机制从上下文中提取关系。多个 attention head 可以粗略理解成"从不同角度看上下文"。
这些 head 通常可以并行计算。比如一部分计算单元处理某些 head,另一部分计算单元处理其他 head,然后把结果合并。
4. 多 GPU 分工
大模型通常很大,一张 GPU 可能放不下完整模型,或者单卡计算太慢。推理系统可能把模型切到多张 GPU 上。
常见分工方式包括:
- 把权重矩阵按维度切开。
- 把不同 attention head 分给不同设备。
- 把不同层或不同计算阶段分给不同设备。
各 GPU 算完自己的部分后,需要通过通信把结果合并。这类合并操作可能涉及 all-reduce、gather 等分布式计算模式。具体采用哪种模式取决于模型结构和服务端推理框架。
5. 请求批处理并行
线上服务为了吞吐,通常会把多个请求放到一个 batch 里一起跑。这样 GPU 可以更充分利用。
但 batch 的形状会变化:
- 这一批里有哪些请求。
- 每个请求的上下文多长。
- 每个请求当前生成到第几个 token。
- 是否有请求已经结束。
这些变化可能影响底层 kernel 选择、计算块划分和归约顺序。对大多数请求来说,这只造成极小数值差异;但当候选 token 分数非常接近时,极小差异就可能改变第一名。
为什么 temperature=0 仍可能不同
temperature=0 可以粗略理解成"尽量选分数最高的 token"。它会显著降低随机性,但不等于"逐 bit 可复现"。
核心原因是:分数本身来自浮点计算,而浮点计算不是无限精确的。
数学上:
text
(a + b) + c = a + (b + c)
但计算机浮点数因为每一步都会舍入,可能出现:
text
(a + b) + c != a + (b + c)
并行计算时,大量求和会被拆成多个局部结果再合并。不同的拆分和合并顺序,可能带来极小误差。
平时这不重要。例如:
text
"可以" 12.80
"能够" 11.20
第一名很明显,微小误差不会改变选择。
但如果两个候选非常接近:
text
"可以" 12.3456781
"能够" 12.3456779
另一次计算中,由于极小数值差异,可能变成:
text
"可以" 12.3456778
"能够" 12.3456780
这时 temperature=0 反而会很"坚定"地选新的第一名。于是一次输出"可以",另一次输出"能够"。从这个 token 开始,上下文已经不同,后续回答就可能沿着不同路线继续展开。
OpenAI Cookbook 对 seed 和 system_fingerprint 的说明也强调,可复现性是 best effort;即使参数和 fingerprint 相同,也仍可能存在小概率差异。参考:cookbook.openai.com/examples/re...。
如何把输出差异降到最低
如果目标是工程上尽量稳定,可以做这些事:
- 固定完整输入,包括 system/developer/user 消息、历史消息、工具结果和空白字符。
- 固定模型 ID,避免使用会随时间变化的别名。
- 使用
temperature=0,并避免同时调整多个采样参数。 - 如果接口和模型支持
seed,固定seed。 - 记录
system_fingerprint,用于判断服务端后端配置是否变化。 - 关闭或固定不稳定外部输入,例如搜索结果、当前时间、文件遍历顺序、随机数和工具调用结果。
- 对机器消费场景,优先使用结构化输出或 JSON schema,而不是依赖自然语言逐字一致。
- 对测试场景,比较语义、结构或关键字段,而不是把整段自然语言当作严格快照。
最终心智模型
可以把 LLM 生成理解成这样:
其中:
- "逐步生成"解释了为什么上下文会递增。
- "完整上下文"解释了为什么不是只看前一个 token。
- "早期 token 影响后续上下文"解释了为什么小分叉会放大。
- "内部并行计算"解释了为什么极小数值差异可能改变候选 token 排名。
- "temperature=0 不是可复现契约"解释了为什么相同输入仍可能偶尔不同。
一句话总结:
大模型的回答是逐 token 生成的;每一步基于当前完整上下文选择下一个 token;生成过程外层顺序、内层并行;当候选 token 分数非常接近时,并行浮点计算和服务端调度带来的极小差异,可能让 temperature=0 的输出也发生分叉。