我们每天都在往 ChatGPT、Claude 这类模型里打字、看它一个字一个字地吐出来。但中间这条链路其实很具体:你的 prompt 不是”被模型读懂”,而是被切成数字、变成向量、在几十层网络里反复加权汇总,最后变成一张”下一个词的概率表”,再被采样、拼回去、循环。
这篇就把这条链路从头到尾拆开。前置的注意力机制(Q/K/V)可以先看这篇打底:用一个玩具例子看懂 Transformer 的 Q/K/V。
一张全景图
1 | 你的 prompt(字符串) |
下面逐步展开。
① 分词:模型不认识字,只认识编号
模型不能直接吃”我喜欢猫”这几个字。第一步是 Tokenizer 把字符串切成一个个 token,再映射成整数 id。
现代大模型大多用 BPE / BBPE(Byte-level Byte Pair Encoding) 这类子词算法。它的关键点:
- 切分粒度介于”单字”和”整词”之间。常见词是一个 token,生僻词会被拆成多个子词片段,任何字符都能被表示(不会出现”不认识的字”)。
- 一个英文单词常常 ≈ 1 个 token,一个汉字往往 ≈ 1~2 个 token。这也是为什么 API 按 token 计费、而不是按字符。
- 切完得到的是一串 token id,比如
[101, 2769, 1599, 4344, 102]。id 本身没有语义,只是词表里的下标。
直觉:分词就像把你的话翻译成模型内部的”行话编号”。同样一句话,不同模型的 tokenizer 切出来的 token 数可能不一样。
那图像和音频也讲「token」,它们又被切成了什么?这其实是同一套路的延伸,单独写在这篇:多模态 Token:文本、图像、音频分别被切成了什么。
② Embedding 与位置编码:把编号变成有语义的向量
光有编号还不够——1599 和 1600 在数值上挨着,但语义可能毫不相干。所以下一步是 Embedding:
- 词嵌入(Token Embedding):用 token id 去查一张可学习的大表(
[vocab_size, d_model]),每个 id 取出一个d_model维向量(比如 4096 维)。语义相近的词,训练后向量也彼此靠近。 - 位置编码(Positional Encoding):自注意力本身”看不见顺序”——打乱词序它算出来一样。所以要额外注入位置信息。早期用正弦位置编码,现在主流是 RoPE(旋转位置编码),把位置信息通过旋转直接编进 Q/K 里,对长序列和外推更友好。
经过这一步,你的 prompt 变成一个矩阵 X ∈ [seq_len, d_model]:序列里每个位置一行向量,既带语义又带位置。
③ Transformer 堆叠:信息在这里反复”对齐与提炼”
这是模型的主体,由 N 个结构相同的 Transformer Block 堆叠而成(GPT、Claude 这类是 decoder-only 架构)。每一层做两件事:
3.1 多头自注意力(Multi-Head Self-Attention)
每个 token 把自己的向量投影成三张卡:Q(我在找什么)、K(我能被什么匹配)、V(我携带的内容)。然后用 Q 和序列里每个 K 算相关度,softmax 成权重,再按权重把对应的 V 加权汇总。这一步让每个位置都能”参考上下文”。细节见 Q/K/V 那篇。
多头:把这件事并行做好几组(比如 32 个头),每个头关注不同的关系模式(语法、指代、远距离依赖……),最后拼接。
因果掩码(Causal Mask) 是生成式模型的关键约束:第 i 个位置只允许看 0..i 的 token,看不到未来。这保证了”预测下一个词”时不会偷看答案,也使得训练时能并行、推理时能复用缓存。
3.2 前馈网络(FFN / MLP)
注意力负责”在 token 之间搬运信息”,FFN 负责”对每个 token 单独做非线性变换和加工”。通常是一个先升维再降维的两层网络(现代常用 SwiGLU 等门控结构)。很多研究认为模型的”知识”主要沉淀在这些 FFN 权重里。
3.3 残差连接 + 归一化
每个子层都包了 残差连接(x + 子层(x),让梯度好传、信息不丢)和 LayerNorm / RMSNorm(稳住数值分布)。现代模型多用 Pre-Norm(先归一化再进子层),训练更稳定。
一层处理完,输出又喂给下一层。几十层下来,向量被反复对齐、提炼,最后得到隐藏状态 H ∈ [seq_len, d_model]。
④ LM Head:把向量翻译回”词表上的分数”
生成下一个词时,我们只关心最后一个位置的输出向量(它已经”看过”整段上文)。
把这个 d_model 维向量乘以输出投影矩阵 LM Head([d_model, vocab_size],常和词嵌入表共享权重),得到一个长度等于词表大小的向量 logits:词表里每个候选 token 都拿到一个原始分数。
1 | 最后位置的隐藏向量 (d_model) |
⑤ Softmax 与采样:从分数到”选哪个词”
logits 只是原始分数,先用 softmax 归一化成一个概率分布(所有候选 token 概率加起来为 1)。然后按采样策略选出一个 token:
- Greedy(贪心):直接选概率最高的。确定但容易呆板、重复。
- Temperature(温度):在 softmax 前把 logits 除以
T。T<1让分布更尖锐(更保守),T>1更平缓(更随机有创意),T→0退化成贪心。 - Top-k:只在概率最高的 k 个里采样,截掉长尾。
- Top-p(核采样):按概率从高到低累加,凑够
p(如 0.9)就截断,在这个动态集合里采样。
这一步的随机性,正是同一个 prompt 多次运行结果不同的原因。把 temperature 设 0 / 用 greedy,输出就接近确定。
⑥ 自回归循环:一个字一个字地长出来
选出的这个 token 会被拼回到输入序列末尾,然后整条链路从第 ③ 步再走一遍,预测再下一个 token。如此循环——这就是 自回归(Autoregressive)生成:
1 | [我, 喜欢] → 预测 → "猫" |
停止条件通常是:模型生成了特殊的结束符 <EOS>,或达到了 max_tokens 上限,或命中调用方设置的停止序列。
最后把所有生成的 token id 经过 tokenizer 解码(decode) 回字符串,就是你看到的回答。流式输出(打字机效果)正是因为生成本身就是一个 token 一个 token 产生的,每产出一个就推给前端。
一个绕不开的工程细节:KV Cache
注意到了吗——每生成一个新 token,都要把”越来越长的序列”重新跑一遍注意力。如果每次都从头算所有位置的 K 和 V,会有大量重复计算。
KV Cache 解决了这个问题:把每一层、每个已处理 token 算出的 K、V 缓存起来。生成下一个 token 时,只需要为新来的那一个 token 算 Q/K/V,再和缓存里的历史 K/V 做注意力即可。
由此推理被分成两个阶段:
- Prefill(预填充):把整个 prompt 一次性并行跑完,填满 KV Cache。这一步算力密集,决定了”首字延迟”。
- Decode(解码):之后每步只处理一个新 token,访存密集,决定了”吐字速度”。
这也是为什么:长 prompt 会让首字变慢(prefill 变重)、长对话会吃显存(KV Cache 随上下文线性增长),以及上下文窗口为什么是硬约束。
串起来:一句话版本
你的 prompt 被切成 token、查表成向量、注入位置信息,在几十层”自注意力 + 前馈网络”里反复对齐提炼;模型用最后一个位置的向量投影出一张覆盖整个词表的概率表,按采样策略选一个词,拼回去再来一遍——一个字一个字地,直到说完。
理解了这条链路,很多现象就不再玄学:为什么按 token 计费、为什么 temperature 影响发挥、为什么长上下文又慢又贵、为什么模型”一本正经地编”(它只是在选概率高的下一个词,并不会”知道”自己对不对)。