ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

大模型生成原理:从Token概率到温度采样与注意力机制

大模型生成原理:从Token概率到温度采样与注意力机制

简介:这份107页的PDF围绕ChatGPT的工作机制展开,面向希望系统理解大语言模型原理的学生、开发者与技术爱好者,帮助读者从Transformer架构、预训练与文本生成等基础环节,逐步认识对话模型如何理解上下文并生成回复。压缩包内仅含1个PDF文件,大小约4.98MB,篇幅完整、便于通读与按章节检索。已有190人学习,适合作为课程补充、技术入门或面试复习材料。内容覆盖预训练数据与损失优化、上下文连贯性、温度参数对输出多样性的影响,以及面向特定任务的微调、对话状态管理与反馈循环等主题,并讨论模型可能产生错误或误导信息的使用边界。读者可借助它建立从模型架构到对话策略的整体认知,理解生成质量与参数设置之间的关系,并形成对ChatGPT能力与局限的客观判断。

1. 预测下一个词:一句听腻了的话,藏着整个机制

“ChatGPT 就是在不停预测下一个词”——这句话被重复到几乎失去信息量。真把它当结论接受的人,往往答不出下一层问题:如果只是预测下一个词,为什么它会同时出现“答非所问”和“一本正经胡说”两种失败模式?为什么同一个问题问两次答案不一样?为什么温度参数从 0.8 调到 0.1,语气立刻从发散变成照本宣科?这些现象都不是“预测下一个词”这五个字能解释的,它们来自 LLM 内部的分层结构:token 化、概率分布、采样策略、Transformer 的注意力权重、预训练固化的参数、以及每次对话都要重新计算的上下文。把这几层拆开,“预测下一个词”才从一个玄学口号,变成一条能推理、能验证的因果链。

2. Token 与概率分布:模型的输出从来不是“文字”

在讨论 ChatGPT 为什么会聊天之前,有一件事需要先明确:模型没有输出过“句子”“段落”或者“回答”这类东西。它唯一会做的动作是——给定一段已经存在的 token 序列,输出下一个 token 在词表上的概率分布,然后从中选一个;把选中的 token 接到序列末尾,再重复一次。所谓的“回答”,是几千次这个动作连起来之后,人类读出来的错觉。

2.1 一次只吐一个 token:从半句话开始的续写

最常见的教学切面是一个没写完的句子:“The best thing about AI is its ability to”。把它喂给模型,模型不会先在心里构思整段话再打出来,而是先算出一个分布,从里面抽一个 token,比如 “learn”;然后把 “learn” 接上去,重新算一遍分布,再抽下一个。中途每一轮,它看到的都是“原始输入 + 自己已经写出来的部分”。这个“自己写的东西又成为自己下一步输入”的结构,是后面所有现象的根源:一次采样的偏差会被后续步骤继承、放大,最终导向完全不同的句子。

这里有个容易被忽略的细节:token 不等于词。常见词通常对应一个 token,罕见词会被拆成若干片段,比如 “dogs” 可能被切成 “dog” + “s”,让 “dog” 和 “dogs” 共享同一段语义表示;“q” 后面大概率跟着 “u”,这类规律在分词层面就已经被统计学习到了,不需要模型“懂拼写”。中文的情况更极端,一个汉字可能占一个 token,也可能占一个以上,这直接导致了后面要说的“上下文长度估算翻车”。

理解这一点之后,很多争论会自动消解。比如“模型是不是在搜索训练数据里的原句”——它没有在搜索,它只是在每一步做一个四万到十几万分类的分类任务(词表大小),而这个分类器的参数被几十亿上百亿次文本喂过。

2.2 n-gram 与四万词表:组合爆炸为什么救不了长上下文

在神经网络接管之前,语言模型的主流做法是数数:统计语料里 “cat” 后面跟 “dog” 出现了多少次,把频率归一化成概率。这就是 n-gram。它的机制简单到可以用几行代码验证,也恰恰因为简单,暴露了它无法跨越的障碍。

# 组合爆炸的量级验证:词表大小 V,n-gram 的理论空间是 V ** n V = 40000 # 一个常见词表规模 for n in (1, 2, 3, 5, 10, 20): print(f"{n:>2}-gram 理论上界: {V ** n:.3e}")

跑一下就能看到量级的跳跃:1-gram 是 4 万,2-gram 是 16 亿,3-gram 已经是 6.4 万亿,到 20 个词的窗口时,数字大得没有任何物理意义。这不只是“算力不够”的问题——就算你把全人类产生过的文本都存下来,绝大多数 20-gram 组合在真实语料里出现次数是0。样本稀疏意味着概率估计直接失真:没见过的组合,n-gram 只能回退到更短的上下文去猜,而一旦回退,长距离依赖就断了。

这就是为什么“数数”这条路在长上下文上彻底失效。一段 2000 字的文章里,第 1 段提到的专有名词和第 20 段的一个代词指代关系,任何固定窗口的 n-gram 都覆盖不到。理解了这个失效点,才能理解下一章自注意力到底替代了什么。

顺带一提,n-gram 并不是被彻底淘汰的历史遗物。分词器里的字节对编码、某些拼写检查、输入法的联想候选,都还在用它的变体。它的价值在于“局部统计可靠”,缺陷在于“全局关联丢失”。

2.3 logits → softmax:概率是被算出来的,不是被“想”出来的

神经网络版本的语言模型,最后一层输出的是一个与词表等长的实数向量,每个位置对应一个 token 的“打分”,这个打分叫 logit。logit 可以是负数、可以大于 1、可以是任意实数,它本身不是概率。把它变成概率要做一步指数归一化,也就是 softmax。

import math def softmax(logits): # 减去最大值:防止 exp 在大 logit 时溢出,这是工程上的常规做法 m = max(logits) exps = [math.exp(z - m) for z in logits] total = sum(exps) return [e / total for e in exps] logits = [2.0, 1.0, 0.1, -1.0] probs = softmax(logits) print([round(p, 4) for p in probs]) # 归一化后概率 print(round(sum(probs), 6)) # 恒等于 1.0

这段代码里每一行的含义都值得说清。logits是模型前向计算的原始输出,长度等于词表大小;减去最大值是数值稳定性处理,数学上不改变结果,因为分子分母同乘一个常数;exp把所有分数映射成正数并拉开差距——logit 差 1,概率就差 e 倍;最后除以总和保证概率和为 1。参数上唯一能调的是 logits 本身,它由模型权重和输入决定,用户改不了;能改的是下一步的采样方式,也就是第三章的内容。

这里有一个必须点破的事实:softmax 一定输出一个和为 1 的分布,即使模型对答案一无所知。哪怕所有 logits 都接近 0、分布接近均匀,它也会从里面抽一个 token 出来,然后接着往下写。模型没有“我不知道”这个输出通道——除非训练时专门教过它说这句话。幻觉的数学根源不在别处,就在这个“必须给出一个分布”的结构里。

3. 温度 0.8:一个除法如何改写模型的“性格”

如果你只用过默认配置,很容易把温度当成某种“聪明程度”旋钮——调高变聪明、调低变严谨。它不是。温度做的事情非常机械:在 softmax 之前,把整个 logits 向量除以一个数。就这么一个除法,能同时改变输出的多样性、稳定性、重复率,甚至影响你对模型“水平”的判断。

3.1 温度就是 logits 除以 T:从锐化到摊平

把上面的 softmax 改成带温度参数的版本,机制就一目了然了。

import math def softmax_with_temp(logits, T=1.0): # 温度 T 只做一件事:在指数归一化之前把 logits 整体缩放 # T < 1:差距被放大,分布更尖锐;T > 1:差距被压缩,分布更平坦 scaled = [z / T for z in logits] m = max(scaled) exps = [math.exp(z - m) for z in scaled] total = sum(exps) return [e / total for e in exps] logits = [3.0, 1.5, 0.5, -1.0] for T in (0.2, 0.8, 1.0, 2.0): p = softmax_with_temp(logits, T) print(f"T={T:<4} -> {[round(x, 4) for x in p]}")

参数变化对应如下行为:T 趋近 0 时,指数缩放把微小差距放大成数量级差距,分布几乎变成一个独热向量,等价于每次都选概率最高的 token,也就是贪心解码;T 等于 1 时不做任何改变;T 大于 1 时,原本排第五、概率不到 1% 的 token 会被抬到有实际机会被抽中的位置,长尾开始出现。

温度 T分布形态典型表现常见用途
0.1 ~ 0.3极尖锐几乎每次选同一个词,容易整段复读信息抽取、分类、结构化输出
0.7 ~ 0.9略平缓有变化但主体稳定,语义不跑偏对话、问答的默认区间
1.2 ~ 2.0明显摊平冷门 token 频繁出现,容易跑题创意发散、起名、头脑风暴

需要注意的一点是:温度不改变 token 的相对排序,只改变它们被抽中的相对机会。排第一的 token 在任何温度下都是排第一,低温只是让“抽中第二名”的概率趋近于零。这意味着——温度不能把错的答案变成对的答案,它只能改变你在正确答案和错误答案之间的抽签倾向。

3.2 为什么是 0.8:复读机与胡言乱语的中间地带

0.8 这个数字不是从公式里推出来的,它是经验值。往低温走,模型会开始重复:因为每一步都选最高概率的 token,一旦进入某个高频句式,后续每一步的最高概率 token 都指向同一个延续,整个回答就卡死在循环里,读起来像复读机。往高温走,模型会开始“发散”:冷门 token 被抽中的机会变大,一个不合适的词进入序列后,后续每一步都要基于这个错误前缀继续生成,误差被继承放大,最后整段话逻辑断裂。

0.8 落在这样一个区间里:绝大多数步骤仍然选到主流 token,保证语义和语法稳定;少数步骤允许同义替换、句式变化,让回答不至于每次都一模一样。这个值在对话场景里经过了大量实际使用检验,但它是场景相关的——写代码时 0.2 往往比 0.8 好用,写小说时 0.8 又显得太保守。把这个数字当“默认”而不是“最优”,是理解它的第一步。

还有一层需要区分:temperature 影响的是“采样随机性”,不是“模型的知识量”。同一个问题,T=0.1 和 T=0.8 拿到的都是同一个概率分布,只是抽取策略不同。所以那种“把温度调低就能减少幻觉”的说法,只对了一半:它减少的是“随机选到错答案”的概率,但对“分布里排第一的就是错答案”这种情况,一点办法也没有。

3.3 top-k 与 top-p:给长尾加一道闸

温度解决了“分布有多平”,但没解决“多长的尾巴应该被允许出现”。于是有了截断策略。top-k 的做法是:只保留概率最高的 k 个 token,其余置零,重新归一化后再采样。top-p(也叫核采样)的做法是:按概率从高到低累加,累加到超过 p 就截断,只保留这一小撮。

def top_p_filter(probs, p=0.9): # 按概率降序排列,累积到超过 p 为止,剩下的长尾全部丢弃 idx = sorted(range(len(probs)), key=lambda i: probs[i], reverse=True) kept, cum = [], 0.0 for i in idx: kept.append(i) cum += probs[i] if cum >= p: break # 保留项重新归一化,保证仍然是一个合法分布 s = sum(probs[i] for i in kept) return {i: probs[i] / s for i in kept}

两者的差别在分布形状上体现得很明显。top-k 保留固定数量,不管这批 token 的概率是集中在头部还是摊得很开,k 个就是 k 个;top-p 保留的是固定“概率质量”,分布尖锐时可能只留两三个 token,分布平坦时会放宽到几十个。后者适应性更强,所以在对话类模型里更常见。

这里有一个工程顺序问题容易被忽略:温度、top-k、top-p 的执行顺序会影响结果。常见实现是先用温度缩放 logits,再做 top-k/top-p 截断,最后在截断后的集合上重新归一化采样。如果顺序反过来——先截断再调温度——那么被截掉的候选就再也没有机会,温度的作用域被限制在了一个已经很窄的集合里,两个参数互相干扰。踩过这个坑的人都懂:调参调不出来效果,问题往往不在参数值,在参数被应用的顺序。

4. 从 n-gram 到 Transformer:注意力替换掉了什么

到这里为止,讨论的还是“怎么从一个分布里选 token”。真正决定这个分布长什么样的是模型结构。n-gram 用频率计数构造分布,Transformer 用多层矩阵乘法构造分布,两者的差别不在于“谁更聪明”,而在于“能不能把远处的信息带过来”。

4.1 n-gram 的死穴:上下文一长,样本立刻稀疏

回到那个失效点。假设你想让模型知道“这句话里的‘它’指的是前面第三段提到的那个系统”,n-gram 需要在训练语料里见过足够多“同样的长距离指代结构”,才可能给出像样的概率估计。但语言的长距离依赖几乎是无穷组合,样本必然稀疏。这也是为什么早期统计机器翻译在句子一长就崩——它真正缺的是表示长距离关系的能力,算力并不是主要瓶颈。

Transformer 的处理方式是把“距离”这个概念从结构里拿掉。序列里任意两个位置之间的关系,由一个可学习的打分函数直接计算,不管它们相隔 3 个 token 还是 3000 个 token,计算路径长度都是 1。这就是“注意力”这个名字的来源:每个位置在生成自己的表示时,可以主动去“看”其他所有位置,并按相关性加权。

4.2 自注意力:把“看最近几个词”换成“看全部并加权”

自注意力的核心只有三个矩阵:查询(Q)、键(K)、值(V)。同一个输入序列,分别乘上三个可学习权重矩阵,得到三组向量。然后用 Q 和 K 做点积算相关性,归一化成权重,再对 V 加权求和。写成代码不到二十行。

import numpy as np def attention(Q, K, V, causal_mask=True): # Q/K/V: (seq_len, d_k),由同一输入乘不同权重矩阵得到 d_k = Q.shape[-1] scores = Q @ K.T / np.sqrt(d_k) # 缩放:防止点积随维度增大而爆掉 if causal_mask: seq_len = scores.shape[0] # 上三角置为 -inf:位置 i 只能看到 j <= i,不能偷看未来 mask = np.triu(np.ones((seq_len, seq_len)) * -1e9, k=1) scores = scores + mask scores = scores - scores.max(axis=-1, keepdims=True) # 数值稳定 weights = np.exp(scores) weights /= weights.sum(axis=-1, keepdims=True) # 每行权重和为 1 return weights @ V, weights

几个参数的含义值得逐个说清。Q @ K.T得到的是一个 seq_len × seq_len 的矩阵,第 i 行第 j 列表示“位置 i 对位置 j 的关注程度”;/ np.sqrt(d_k)是缩放因子,如果不除,维度一大点积的方差就大,softmax 会饱和成近似独热,梯度几乎消失;因果掩码把上三角设成极大负数,让每个位置只能看到自己和左边,这是生成式模型与双向编码器的关键分界;最后对 V 加权求和,得到的新表示里混入了整个可见上下文的信息。

多头注意力则是在这个基础上并行做多组,每组学到不同的关注模式——有的头盯相邻词,有的头盯句法结构,有的头负责长距离共指。多个头的结果拼接后再投影一次,输出维度回到原尺寸。这套机制不依赖“距离窗口”,所以它在结构上就具备处理长上下文的能力;代价是计算量与序列长度的平方成正比,O(n²) 的复杂度直接决定了上下文窗口为什么这么贵。

4.3 权重是拟合出来的:a+bx+cx² 那个类比到底在说什么

理解 Transformer 最容易卡住的地方,是把注意力权重想象成人工设计的规则——比如“主语和动词要连起来”。实际上没有任何一条这样的规则被写进去。所有权重矩阵,包括 Q/K/V 的投影矩阵、注意力输出的投影矩阵、前馈层的两层矩阵、嵌入表,全部是随机初始化之后,通过梯度下降在“预测下一个 token”这个任务上一点点调出来的。

一个常用的类比是函数拟合:数据里存在某种关系 y = a + bx + cx²,人不去解析地解出 a、b、c,而是先随便给一组初值,算预测值和真实值的误差,沿误差下降的方向挪一小步,重复几百万次,参数自然收敛到一个能拟合数据的值。语言模型的参数规模把这个类比推到了极端——那一代模型有 1750 亿个参数,它们不是 1750 亿条规则,而是 1750 亿个可调的数值,共同编码了“什么样的 token 序列在人类文本里更可能出现”这件事。

这个类比也解释了一个常见困惑:为什么模型能做题却不能可靠算数。因为它在拟合的是“文本分布的规律”,数学运算的正确性只是这个分布里的一部分,且是稀疏的一部分。纯靠模式匹配学到的东西,在训练分布密集的地方很强,在稀疏的地方很脆。

4.4 1750 亿参数花在哪:嵌入表、注意力矩阵、前馈层

参数量不是一个笼统的数字,它分布在几个差异极大的结构里,各有各的作用。

结构大致作用规模特点
词嵌入表把 token id 映射成向量词表 × 隐藏维度,占比不小但固定
注意力投影矩阵计算 Q/K/V 及输出投影每层四组,随层数线性增长
前馈层逐位置的非线性变换,容量大户通常隐藏维度的 4 倍宽,参数最多
输出投影把隐藏状态映射回词表打分隐藏维度 × 词表,与嵌入表同量级

前馈层往往是参数量的最大来源,它做的事情比名字听起来朴素得多:对每个位置独立地做“升维 → 非线性 → 降维”。正因为独立于其他位置,它承载了大量“事实性”的模式记忆;注意力层负责把信息搬来搬去,前馈层负责对搬来的信息做变换。这个分工在可解释性研究里被反复观察到,也是理解“为什么改上下文能改变输出、但改不动模型的固有知识”的一个切入口。

4.5 预训练与微调:一次固化,一次塑形

预训练阶段做的事情,本质上就是在海量文本上反复执行“给前文、预测下一个 token、算误差、反向传播”。这个过程极其昂贵,但只做一次,产出的是一组固化的权重。它学到的是语言的统计规律、世界知识的部分压缩表示、以及各种文体和格式的模仿能力。这一阶段结束时,模型会续写,但不太会“对话”——你给它一句话,它可能给你续一篇论文,因为它见到的大量文本就是这种形态。

微调和后续的对齐阶段,改变的是“行为风格”而非“知识底座”。用对话格式的数据继续训练,模型学会的是“用户说话之后,应该出现一段有帮助的回答”这个模式;用人类偏好数据做优化,模型学会的是“在这两个回答里,人选了哪个”。这也解释了一个现象:微调能让模型变得更听话、更少说教,但很难让它掌握训练数据里完全没有的事实。想注入新知识,更经济的做法往往是放进上下文,而不是重新训练。

4.6 上下文窗口:模型没有记忆,只有每次重算

这是最容易被误解的一层。多轮对话里,模型看起来“记得”你前面说过什么,实际上它在每一轮都把整段对话历史作为输入重新前向计算了一遍。它没有内部状态在轮次之间保留,对话历史就是全部的“记忆”。

工程上为了不重复计算,推理框架会缓存前面 token 的 K 和 V(即 KV cache),避免每一轮把整个前缀重算。缓存占用大致随“层数 × 序列长度 × 隐藏维度 × 精度字节 × 2”增长,序列一长,显存压力就上来了。这直接解释了为什么上下文窗口有硬上限、为什么长对话的成本是超线性的、为什么超出窗口后最早的内容会被挤掉。

知道这一层,很多“玄学”就有了解释:改一句很早之前的系统提示,输出立刻变了,因为那段文字重新参与了每一次注意力计算;把关键信息放在对话最开头,效果可能反而不如放在最后,因为注意力对不同位置的敏感度并不均匀。

5. 避坑与排查:五个被讲错的大模型原理

原理层面的误解,比 API 用错更难排查,因为它不会报错,只会让你在错误的方向上反复调参。下面五条是实际讨论里出现频率最高的,每条都按“现象 → 原因 → 解决”拆开。

5.1 把温度调低当成减少幻觉的万能药

现象:把温度从 0.8 降到 0.1,模型仍然会编造不存在的事实,只是每次编得一模一样,甚至更自信。

原因:温度只作用于采样环节,不改变分布本身。如果错误答案在分布里本来就排第一,低温只会让你更稳定地拿到这个错误答案。软化的分布至少还有机会抽到正确答案,锐化之后连这个机会都没了。

解决:把温度和事实性分开治理。事实性问题靠外部检索、工具调用、结构化校验、多次采样投票来解决;温度只用来控制表达风格的稳定性。判断标准也很简单——同一个问题跑十次,如果十次答案一致但都是错的,那问题在知识层,不在采样层。

5.2 以为模型“记住”了上一轮对话

现象:多轮对话到第十轮,模型忘了第三轮说过的约束条件;把历史消息里的一条删掉,回答立刻变化;同样的对话在别处重放,结果不一致。

原因:模型是无状态的,每一轮都把完整历史重新编码一遍。历史被删改,输入就变了;历史过长超出窗口,早期内容被截断,自然“忘记”。它记住的始终是“这次输入里有什么”,并非“上次说过什么”。

解决:把关键约束显式写进每一轮的输入,或者用系统提示固定住;超长对话做摘要压缩,把结论而不是原文留在上下文里;需要严格状态管理时,把状态放在外部存储,每次按需注入。

5.3 把 token 数当字数来估算上下文

现象:按“一个字一个 token”估算,觉得 8000 字刚好塞进 8000 token 的窗口,实际请求直接超限;中英混排时估算偏差更大。

原因:分词器的切分粒度与字数无关。常见英文词可能整体一个 token,罕见词被拆成多个片段,中文一个字可能对应一个或多个 token,标点、空格、特殊符号各有各的处理方式。不同模型的分词器还不一样。

解决:用对应模型的分词器实测长度,不要估算;在预算里预留余量给输出和系统提示;长文档先分块,按块处理再汇总,而不是赌一次塞得进去。

5.4 认为参数量大就等于知识更准

现象:模型在常识和写作上表现很好,但问一个具体到型号参数、具体到某条冷门条款的问题,就开始编。

原因:参数是训练语料统计规律的有损压缩,不是数据库。高频事实被反复强化,表示得很扎实;长尾事实出现次数少,压缩之后几乎不留痕迹。参数越多,能编码的模式越丰富,但不改变“稀疏即失真”这个本质。

解决:把模型当推理引擎,不当知识库。需要精确事实时,检索后放进上下文,让它做“基于给定材料的归纳”,而不是“从参数里回忆”。这个定位差异会直接改变你的提示词写法。

5.5 用 n-gram 统计去解释模型的语法能力

现象:拿“某两个词经常一起出现”去解释模型为什么能生成通顺句子,结论和实际行为对不上;或者反过来,因为模型偶尔犯低级语法错误,就断言它“只是高级 n-gram”。

原因:n-gram 是单层查表,Transformer 是多层非线性叠加加全局注意力,两者的表达能力不在一个量级。但“表达能力更强”不等于“永远正确”,它仍然是在做概率建模,只是建模的粒度从固定窗口变成了可学习的全局关系。

解决:把 n-gram 当成教学类比,不当机制解释。判断一个说法是否靠谱,看它能不能预测具体现象——比如改变温度会不会影响复读率、增长上下文会不会改善共指消解。能做出可验证预测的解释,才值得采信。

6. 取舍:这套机制够用,但不承诺正确

把整条链路串起来看,会发现一个贯穿始终的设计取向:用概率上的“大概率合理”换取通用性,放弃对单次输出正确性的保证。

token 化让模型摆脱了词表限制,代价是字符级信息被压缩;softmax 保证了输出永远合法,代价是模型没有“我不知道”这个出口;温度给了多样性旋钮,代价是确定性和正确性之间被迫做交换;自注意力换来了长距离建模能力,代价是 O(n²) 的开销和昂贵的长上下文;预训练固化知识,代价是知识更新困难;上下文重算保证了一致性,代价是没有真正的记忆。每一条都不是疏忽,是在“通用语言模型”这个目标下的必然取舍。

想验证这些取舍是不是真的成立,最省事的办法是做对照实验,而不是读更多解释。固定一个提示词,只改温度,跑二十次,统计重复率和错误率的变化;固定温度,只改上下文长度,看共指消解在哪一步开始崩;把同一个事实问题分别用“直接问”和“先给材料再问”两种方式测,看差距有多大。这类实验成本很低,但能把“玄学”变成可观测的量。

我自己的血泪经验是:早期调一个问答系统,输出时好时坏,我花了整整两天怀疑是温度设错了,反复在 0.3 到 1.0 之间试,结果毫无规律。后来把同一批问题的概率分布打印出来才发现,错的那些问题,正确答案在分布里根本进不了前五——问题在检索没把材料喂进去,跟采样参数一点关系都没有。从那以后我每次调 temperature 之前,都强制先跑一遍固定种子的对照测试,确认分布本身没问题,再去动采样。希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进