LLM生成机制
"模型在思考"是一种错觉
这是当前 AI 讨论中最容易被误解的一点。
结论先行
模型内部不存在"思考过程"和"最终结果"的区分。
在模型眼里,只有一件事:
根据已有 token,预测下一个 token
所谓:
- 思考过程
- 推理步骤
- Chain-of-Thought
只是训练数据中高概率出现的语言模式。
为什么它"看起来像在想"
因为人类在解决复杂问题时,习惯把中间过程写出来。
模型学到的是:
在这种上下文下,"先写一段推导,再给答案",概率更高。
不是因为它先想好了,而是因为它还没"结束输出"。
一个工程类比
日志系统不会区分:
- INFO(过程)
- RESULT(结果)
它们都是字符串。区分它们的,是你这个人。
为什么 LLM 只能用 Decoder,而不是 Encoder-Decoder?
先说一句很多人不敢说的实话:
Encoder-Decoder 并不是更高级,只是为"输入-输出映射"设计的,比如:
- 翻译
- 摘要
- 问答(有明确输入、明确输出)
这些任务是:Input → Encode → Decode → Output
而 GPT 要做的是:无限续写一个序列
这在工程上意味着:
- 序列在不断增长
- 上下文长度不固定
- 输入和输出是同一个东西
于是 Encoder 的角色就彻底消失了。
Decoder-only 的真实含义是:
每一步都对"当前完整上下文"做一次建模,然后预测下一个 token。
Mask 的真正作用(工程解释)
你一定见过 "causal mask / 下三角矩阵" 的图。
绝大多数科普会说:"为了防止模型看到未来。"这是表面现象,不是本质。
本质是:
把一个"并行可计算的模型",约束成"看起来像顺序生成"的模型。
一次前向计算里,模型其实"已经算完所有未来"
这是一个反直觉但极其重要的事实:
在训练时,Transformer 是一次性并行计算整个序列的。
假设训练数据是:"I love coding"
模型在一次 forward 中:
- 同时计算:"I" → 预测 "love"
- 同时计算:"I love" → 预测 "coding"
- 同时计算:"I love coding" → 预测
<EOS>
它不是一步一步算的,是一次性算完的。
Mask 的真正作用
Attention 本来是:任意 token 可以看任意 token
但生成模型有一个硬约束:位置 i 的 token,不能依赖位置 > i 的 token
否则就会发生:
- 训练时偷看答案
- 推理时行为不一致
所以 mask 做的事只有一件:在 attention 的路由表里,把"未来节点"的权重强行设为 0。
这不是防作弊,是定义计算图的因果结构
你可以把它理解成:在一张全连接图上,人为删掉了"指向未来的边"。
为什么生成时一定是"一个 token 一个 token"?
训练时能并行,推理时却不能,这是很多人迷惑的点。
原因非常工程化:
- 训练阶段:全序列已知,可以一次性建图,GPU 并行跑
- 推理阶段:下一个 token 不存在,必须先生成它,才能成为下一步的上下文
这不是模型限制,是信息论限制:你不能用一个还不存在的信息。
所以生成必然是:
上下文 → forward → 采样 → 拼到上下文 → 再 forward
GPT 每一步"真的在干什么"?
现在我们用你已经理解的 Transformer 心智模型,把生成过程说清楚。
假设当前上下文是:"I love"
模型做的事情是:
- 把 "I love" 当成一个完整序列
- 对这个序列跑完整的一次 Transformer 前向
- 得到每个位置的输出向量
- 只取最后一个位置的输出
- 映射成词表概率
- 采样一个 token,比如 "coding"
- 拼接成:"I love coding",然后重复
注意重点: 模型并没有"记住上一步的中间状态",每一步都是对"当前完整上下文"的一次全量重算。
这也是为什么:
- 上下文越长,推理越慢
- KV cache 才如此重要
KV Cache 的真实意义(不是黑魔法)
KV cache 本质上是一个工程优化:
把"前面已经算过的 K/V 结果缓存下来",避免每一步都从头算。
没有 cache,复杂度是:O(n²) 每步 × n 步
有 cache,变成:O(n²) 一次 + O(n) 每步
注意:
- cache 不改变模型逻辑
- 只是减少重复计算
- attention 的路由机制完全不变
为什么 GPT 看起来"像在思考"?
这是最容易被神化的一点。
本质原因只有一个:
每一步生成,模型都在对"越来越复杂的上下文"做全局建模。
随着上下文增长:
- 注意力图越来越复杂
- 信息流路径越来越多
- 表达空间越来越丰富
于是你看到的现象是:
- 自我修正
- 回指前文
- 多步推理
- 风格一致性
但底层没有任何:
- 规划模块
- 推理引擎
- 逻辑树
- 思考步骤
只有:反复执行的 attention + FFN。
为什么要"隐藏思考过程"
你可能注意到:很多产品刻意不展示模型的推理文本。
原因不是"怕你看不懂",而是:
- 推理文本不稳定
- 容易误导用户对正确性的判断
- 会泄露训练分布和系统策略
隐藏 CoT 是产品与安全决策,不是模型能力变化。
采样策略:Top-K、Top-P、Temperature
生成过程中,每一步都要从一个概率分布里选一个 token。选法不同,结果的"确定性"和"多样性"也不同。
采样流程全貌
输入文本
↓
模型前向计算(全词表概率分布)
↓
【Temperature】调整概率陡峭度
↓
【Top-K】限制候选上限(最多 K 个)
↓
【Top-P】累计概率筛选(质量阈值)
↓
归一化概率
↓
【采样策略】选择 1 个 token
↓
输出拼入序列,重复
Temperature:调整概率分布的"陡峭程度"
Temperature 在 Top-K/Top-P 之前应用:
原始概率: [0.5, 0.3, 0.15, 0.05]
Temp = 0.5(更确定)→ [0.64, 0.24, 0.09, 0.03] ← 几乎总选最高
Temp = 1.0(保持原样)→ [0.5, 0.3, 0.15, 0.05]
Temp = 2.0(更随机)→ [0.33, 0.27, 0.22, 0.18] ← 机会更均等
数学上,Temperature 把 logits 除以一个温度值,再做 softmax——温度越高,分布越平;温度越低,分布越"尖峰"。
Top-K 和 Top-P:筛选候选池
即使 Temperature 调整了分布,全词表(5 万个 token)里大部分概率接近于零。Top-K 和 Top-P 的作用是把真正值得考虑的候选缩小到一个可控范围。
Top-K:硬性上限,只保留概率最高的 K 个 token。比如 Top-K=50,就只从 5 万个里挑前 50 个。
Top-P:自适应筛选,从概率最高的 token 开始累加,累加到超过阈值 P 为止。比如 Top-P=0.9,实际候选数可能是 5~20 个(取决于分布形状)。
原始词表 50,000 个 token
↓
Top-K = 50(只保留前 50 个)
↓
Top-P = 0.9(从 50 个中筛选到 3~10 个)
↓
从这 3~10 个中按概率加权随机采样 1 个
常见配置:
| 任务 | Temperature | Top-K | Top-P |
|---|---|---|---|
| 代码生成 | 0.2 | 50 | 0.9 |
| 事实问答 | 0.7 | 40 | 0.85 |
| 日常对话 | 0.8 | 50 | 0.9 |
| 创意写作 | 1.0 | 0 | 0.95 |
三种采样策略
1. 加权随机采样(最常用,默认)
经过 Top-K/Top-P 筛选后,按概率加权随机采样。概率高的 token 更容易被选中,但概率低的也有机会被抽到。
候选词: ["很", "真", "挺", "非常"]
概率: [0.5, 0.3, 0.15, 0.05]
生成随机数 r ∈ [0, 1):
- r ∈ [0, 0.5) → 选 "很" (50%)
- r ∈ [0.5, 0.8) → 选 "真" (30%)
- r ∈ [0.8, 0.95) → 选 "挺" (15%)
- r ∈ [0.95, 1.0) → 选 "非常" (5%)
特点:高概率更易选中,低概率也有机会。每次生成可能不同,适合创意任务。
2. 贪心采样(Greedy Decoding)
永远选概率最高的 token,不做任何随机。
selected = candidates[argmax(probs)]
特点:完全确定性(相同输入 → 相同输出),速度快,但容易重复、缺乏创意。适合数学计算等精确任务。
3. Beam Search(束搜索)
这是真正的"多分支"方法。每一步保留 beam_size 条最优路径,最终选总分最高的 1 条。
beam_size = 3
步骤 1:保留 top-3 候选
A: "很" B: "真" C: "挺"
步骤 2:每个分支扩展,再保留总分最高的 3 条路径
A → ["很好", "很不错", "很棒"]
B → ["真好", "真不错", "真棒"]
C → ["挺好", "挺不错", "挺棒"]
最终:选分数最高的 1 条路径
特点:全局更优解,适合翻译、摘要等有标准答案的任务。但 Token 消耗是普通采样的 beam_size 倍,且缺乏多样性。
三种策略对比
| 维度 | 加权随机采样 | 贪心采样 | Beam Search |
|---|---|---|---|
| 分支数 | 1 条 | 1 条 | beam_size 条 |
| 确定性 | 随机 | 完全确定 | 较确定 |
| 多样性 | 高 | 无 | 低 |
| Token 消耗 | N 个 | N 个 | N × beam_size |
| 适用场景 | 创意、对话 | 数学、精确任务 | 翻译、摘要 |
采样本质:一句话总结
LLM 串行生成,每次用 Temperature 调整概率分布陡峭度,再用 Top-K/Top-P 筛选候选池,最后按概率加权随机选 1 个 token(除非用贪心或 Beam Search)。
工程实现
import torch
import torch.nn.functional as F
def sample_next_token(logits, temperature=1.0, top_k=50, top_p=0.9):
# 步骤 1:应用 Temperature
logits = logits / temperature
# 步骤 2:计算概率分布
probs = F.softmax(logits, dim=-1)
# 步骤 3:Top-K 过滤
if top_k > 0:
top_k_probs, top_k_indices = torch.topk(probs, min(top_k, len(probs)))
else:
top_k_probs, top_k_indices = probs, torch.arange(len(probs))
# 步骤 4:Top-P 过滤
cumsum = torch.cumsum(top_k_probs, dim=-1)
cutoff_index = (cumsum >= top_p).nonzero()
if len(cutoff_index) > 0:
cutoff_index = cutoff_index[0].item() + 1
else:
cutoff_index = len(top_k_probs)
final_probs = top_k_probs[:cutoff_index]
final_indices = top_k_indices[:cutoff_index]
# 步骤 5:归一化
final_probs = final_probs / final_probs.sum()
# 步骤 6:加权随机采样
sampled_index = torch.multinomial(final_probs, num_samples=1)
return final_indices[sampled_index]
延伸阅读
- 数学基础:Softmax — 采样之前,模型输出的原始分布由 Softmax 生成
- 数学基础:反向传播 — 采样策略影响的是推理阶段的输出,训练时的 loss 计算依赖反向传播