大模型如何生成回答:token、上下文递增与 temperature=0 的不稳定性

文章是 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. 重复,直到生成结束标记或达到长度限制

用流程图表示:

flowchart TD A[原始输入 prompt] --> B[模型前向计算] B --> C[得到所有候选 token 的分数] C --> D[按采样参数选择下一个 token] D --> E[把 token 追加到已生成输出] E --> F{是否结束?} F -->|否| B F -->|是| G[返回完整回答]

为了效率,推理系统通常会缓存前面 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 和工程稳定性。

这两个方向都可能正确,但它们会把后续文本带向不同展开路径。

flowchart TD A[相同用户问题] --> B{早期 token 选择} B -->|并行计算| C[解释 GPU / 浮点误差 / 归约顺序] B -->|服务端不保证| D[解释 seed / system_fingerprint / API 契约] C --> E[后续上下文偏向底层计算] D --> F[后续上下文偏向工程实践]

因此,偏差不一定来自"明显错误"。一个早期合理但不同的 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 会把表格切成很多块,让大量计算单元同时算不同区域,最后再合并结果。

flowchart LR A[大矩阵计算] --> B[切成小块] B --> C[GPU 核心 1] B --> D[GPU 核心 2] B --> E[GPU 核心 3] B --> F[更多核心] C --> G[合并结果] D --> G E --> G F --> G

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 对 seedsystem_fingerprint 的说明也强调,可复现性是 best effort;即使参数和 fingerprint 相同,也仍可能存在小概率差异。参考:cookbook.openai.com/examples/re...

如何把输出差异降到最低

如果目标是工程上尽量稳定,可以做这些事:

  • 固定完整输入,包括 system/developer/user 消息、历史消息、工具结果和空白字符。
  • 固定模型 ID,避免使用会随时间变化的别名。
  • 使用 temperature=0,并避免同时调整多个采样参数。
  • 如果接口和模型支持 seed,固定 seed
  • 记录 system_fingerprint,用于判断服务端后端配置是否变化。
  • 关闭或固定不稳定外部输入,例如搜索结果、当前时间、文件遍历顺序、随机数和工具调用结果。
  • 对机器消费场景,优先使用结构化输出或 JSON schema,而不是依赖自然语言逐字一致。
  • 对测试场景,比较语义、结构或关键字段,而不是把整段自然语言当作严格快照。

最终心智模型

可以把 LLM 生成理解成这样:

flowchart TD A[完整上下文] --> B[一次模型前向计算] B --> C[并行计算大量中间值] C --> D[得到候选 token 分数] D --> E[按采样参数选择 token] E --> F[追加到上下文] F --> A

其中:

  • "逐步生成"解释了为什么上下文会递增。
  • "完整上下文"解释了为什么不是只看前一个 token。
  • "早期 token 影响后续上下文"解释了为什么小分叉会放大。
  • "内部并行计算"解释了为什么极小数值差异可能改变候选 token 排名。
  • "temperature=0 不是可复现契约"解释了为什么相同输入仍可能偶尔不同。

一句话总结:

大模型的回答是逐 token 生成的;每一步基于当前完整上下文选择下一个 token;生成过程外层顺序、内层并行;当候选 token 分数非常接近时,并行浮点计算和服务端调度带来的极小差异,可能让 temperature=0 的输出也发生分叉。

相关推荐
AI-好学者2 小时前
RAG知识点_3_高级实践
人工智能·ai·架构·langchain·ai编程
老程序猿3 小时前
一个撇号里,藏得下 3 个 bit——system prompt 隐写手法拆解
ai编程·claude
leeyi4 小时前
可观测性:Langfuse、Langsmith 集成
aigc·agent·ai编程
xiaoshuai10245 小时前
【AI 研发实战】3 个人两个月交付 512 个功能,我沉淀了这套 AI 命令体系
ai编程
ch_09185 小时前
从0构建SDK第4节:实现 ReflectionAgent 的自我反思循环
typescript·agent·ai编程
鱼疯而行5 小时前
第5章 B端与C端的真正分化机制
产品经理·ai编程
小虎AI生活17 小时前
WorkBuddy 的下一块拼图,居然是这个能力!
ai编程
米小虾19 小时前
联合国发布首份全球AI评估报告:我们正站在AI治理的十字路口
aigc·ai编程