1. 先把“秒回”这件事拆开看
你有没有遇到过这种情况:一段问题刚在 ChatGPT 对话框里发出去,对面的字就像一个接一个被吐出来一样,几乎没有迟疑,仿佛它早就把答案背好了。很多人把这个现象简称为“秒回”,但真正做模型的人知道,秒回并不是“提前写好答案”,它背后最关键的推手是 Transformer 架构,外加一串普通人看不见的推理优化。这篇文章就把“秒回”和“Transformer”放到同一张桌子上拆开聊,尽量不给论文,只给画面感。
看这篇文章的人大概分两种。一种是天天用对话式 AI、好奇它为什么能这么能聊的人;另一种是想入行做模型应用、却被“注意力机制”“Token”“KV Cache”这些术语劝退的新手。如果你是前者,你可以直接跳到第 4 部分看那些让你“原来如此”的问题答案;如果你是后者,建议从头顺着读,我会把每一步都讲成能在大脑里跑通的画面。
1.1 你看到的“秒回”,其实是两段速度合在一起
点下发送之后,模型要干的事并不是“读题→翻答案→输出”,而是分两个阶段连续工作。
第一阶段叫读题阶段,大家都叫它 Prefill。模型会把你输入的整段文本一次性并行算一遍,产出第一个字的概率分布。这个阶段在数学上做的事情最多,但因为所有 Token 可以同时计算,所以通常只需要几十到几百毫秒。你感觉“它好像马上就懂了”,主要就是这一段快。
第二阶段叫续写阶段,也叫 Decode。模型每生成一个 Token,就把这个新 Token 拼到原来的文本末尾,再重新计算一次,继续预测下一个 Token。这个阶段是严格串行的,第 5 个 Token 必须等第 4 个 Token 出来之后才能开始预测,所以它不可能瞬间给你整篇回答。真正让你觉得“秒回”的,是每一步都足够快,再加上服务端采用流式推送,一个字一个字往外蹦,体感上就变成“它一边想一边说”。
1.2 Transformer 之前,为什么做不到“秒回”
在 Transformer 出现之前,处理序列的主力是 RNN 和 LSTM。它们的思路像一条流水线:先看第一个字,把信息更新到内部状态里;再看第二个字,手里拿着上一个状态继续更新;第三个字接着往下。句子有 100 个字,就是 100 个串行步骤。每一步都依赖前一步的结果,并行度极低。
更棘手的是长距离依赖。句子开头的信息要传到最后,中间经过很多次乘法,信号会衰减。比如一句话的前半部分说“我昨天认识的那个人”,后半部分说“他居然是我小学同学”,模型需要把“那个人”和“他”对应起来。在串行结构里,这种跨越几十个位置的指代关系很容易丢。Transformer 做了一个很直接的改变:让任意两个位置之间都能直接建立连接。第 50 个 Token 和第 5 个 Token 之间只隔一次注意力计算,不再需要层层传递。这就相当于把“流水线传话”改成了“全员开会,人人都能直接对话”。
1.3 模型结构只是骨架,工程优化才是血肉
还有一个容易被忽略的事实:你感受到的秒回,并不只是 Transformer 结构的功劳。同样一个模型,用不同方式部署,体感可能差好几倍。常见工程手段包括 KV Cache、批处理、量化、流式传输、显存管理等等。模型结构决定智力的上限,工程优化决定你能不能把这套智力顺畅送到用户面前。所以后面讨论“为什么慢”“为什么卡”的时候,不能只盯着 Transformer 本身,还要看它在实际服务里被怎么使用。
2. Transformer 核心部件深度解析:五个必须搞懂的机关
Transformer 是一个组合结构,但真正决定它能力的,是几个关键机关:Token 化、注意力机制、多头注意力、位置编码、残差与归一化。这五个拆开看都不难,合在一起才是完整画面。
2.1 第 1 个机关:Token 化,模型不读“字”,读“编号”
很多人以为模型读的是中文句子,其实模型只认数字。任何文本进模型之前,都要先被切分成一个小块一个小块的 Token,再映射成编号。中文可能一个字是一个 Token,也可能一个词是一个 Token,线上 Tokenizer 会按统计规则决定怎么切。
| 输入文本 | 切分示意 | 模型看到的编号(示意) |
|---|---|---|
| 今天天气很好 | 今天 / 天气 / 很好 | 321 / 876 / 650 |
切分方式会直接影响效果。切得太碎,模型要处理的步骤多,生僻字覆盖好;切得太大,词表膨胀,训练和推理更费资源。你在使用对话式 AI 时偶尔发现它用错某个名字,往往就是 Token 化阶段把名字切成了意想不到的组合。很多高级用法会专门控制提示词,让关键名字不要被拆碎,细节从这里就开始影响结果。
2.2 第 2 个机关:注意力机制,一场全员参与的检索
注意力机制是 Transformer 的心脏。它的核心思路可以用一个检索场景来理解:句子里的每个 Token,都会生成三个向量,分别叫 Query、Key、Value。你可以把整个句子想象成一个小型图书馆,Q 是你手里的问题单,K 是每本书上的标签,V 是书里的正文。
当前 Token 会拿自己手里的 Q,去和所有 Token 的 K 算相似度。相似度越高,代表“这本书和我的问题越相关”,于是它对应的 V 在最终结果里权重就越大。用公式写就是:
Attention(Q, K, V) = softmax(Q K^T / sqrt(d_k)) V
这里有个看起来很细节、但非常关键的操作:除以sqrt(d_k)。为什么除?因为当向量维度变高以后,点积得到的分数会变得很大,一 large softmax 就会进入梯度极小的区域,模型很难训练。除以维度开根号,等于把分数的尺度拉回一个稳定范围。很多复现 Transformer 的人第一次训练失败,就是因为漏了这一步或者除法写错位置。
2.3 第 3 个机关:多头注意力,不是只问一次,而是问多组问题
如果整条句子只做一次 QKV 计算,模型很容易只看一种关系,视野太单一。多头注意力的做法,是把原来的大向量切成几段,每一段独立做一次注意力计算,最后再拼回去。
你可以把它理解成请十几个审稿人同时看同一段话。有人专门盯主谓关系,有人盯修饰词,有人盯指代,有人盯句子结构。每个“头”都在低维空间里观察一种模式,最后把所有人的意见合并起来。同一个层的不同头,可以分别学到相邻词关系、较长距离的指代关系,甚至标点与句式结构关系。这也是 Transformer 能同时处理语法、语义、逻辑的重要原因。
2.4 第 4 个机关:位置编码和因果掩码,让模型知道先后顺序
注意力机制本身对顺序不敏感。把“猫追狗”和“狗追猫”拆成同样的 Token 集合,如果不加位置信息,模型看到的本质是一样的一堆词,结果分不清谁追谁。所以模型必须额外给每个位置注入位置编码,告诉它 Token 出现的前后顺序。
对话式大模型用的是 Decoder-only 结构,训练时还必须加因果掩码。规则很简单:第 i 个 Token 只能看到它自己和前面所有 Token,不能看到后面的内容。做法是在注意力分数矩阵里,把右上角的未来位置全部设置成负无穷,这样经过 softmax 之后这些位置的权重就变成 0。否则模型训练的时候等于“开卷考试”,直接偷看正确答案,生成时却要闭卷,行为就会崩掉。
2.5 第 5 个机关:残差连接和 LayerNorm,让几十层网络能训练
Transformer 并不是一层,而是几十层叠加。每个 Block 里不只是注意力,它还会做两次归一化、两次残差连接、一次前馈网络。前馈网络是两层全连接加激活函数,负责把注意力提取出来的信息再做非线性加工。
残差连接特别好理解:把这一层的输出直接加到输入上。相当于每层旁边开一条“高速公路”,梯度可以从最后一层一路传回第一层,不容易在中途衰减。LayerNorm 则把每一层激活值拉回一个稳定区间,防止几十层叠加之后数值越来越大或越来越小。有人把这两样比作盖楼时的加固和逃生梯,少了它们,深层模型很难稳定训练,更不可能上线做秒回服务。
3. 实操过程:把一句话真正跑一遍
原理讲完,不如把一句话从头到尾过一遍。这一步很重要,因为它能帮你把“Token”“位置编码”“自回归”这些抽象词全部变成具体动作。
3.1 一个极简前向流程
下面是一段伪代码,强调结构,不是可以直接跑的完整实现。
def transformer_forward(input_ids, blocks, embed, lm_head): B, T = input_ids.shape x = embed(input_ids) # 查表:编号 -> 向量 x = x + position_encoding(T) # 加位置信号 for block in blocks: h = layer_norm(x) h = multi_head_attention(h) # 注意力 + 因果掩码 x = x + h # 残差 h = layer_norm(x) h = feed_forward(h) # 两层全连接 x = x + h # 残差 logits = lm_head(layer_norm(x)) # 输出每个候选词的分数 return logits[:, -1, :] # 只取最后一个位置流程可以概括成四步:先查表,把 Token 编号变成向量;再加位置信息;然后反复经过注意力层和前馈层;最后输出一个在所有候选词上的分数列表。分数经过 softmax 就变成概率,模型再根据概率选词。整个过程没有黑魔法,只有矩阵运算。
3.2 模型怎么从“今天天气”编出“很好”
假设模型已经训练完成。给它输入“今天天气”,它会在最后一个位置预测下一个词。概率分布可能像这样:
| 已有序列 | 下一个词概率(示意) | 实际选择 |
|---|---|---|
| 今天天气 | 很:0.45,真:0.3,不:0.12 | 很 |
| 今天天气很 | 好:0.6,快:0.15,糟:0.1 | 好 |
| 今天天气很好 | 适:0.5,,:0.2,:0.15 | ? |
模型每选出一个 Token,就拼到已有序列的末尾,再重新喂给自己,继续预测下一个。循环往复,直到遇到结束符或达到长度上限。这就是自回归生成。注意,因为每一步都要依赖前面所有内容,所以这个阶段天然无法并行,这也正是 KV Cache 必须存在的原因。
3.3 参数规模是怎么吹起来的
经常听人说“这个模型几百亿参数,那个模型几万亿参数”,这些数字到底怎么来的?以常见的 Decoder-only 结构为例,有一个粗略估算公式:
参数量 ≈ 词表大小 × 模型维度 + 12 × 层数 × 模型维度²
假设词表大小 5 万,模型维度 768,层数 12。词表部分约 5 万 × 768 ≈ 3800 万,主体部分约 12 × 12 × 768² ≈ 8500 万,合计约 1.2 亿。这正好是“一亿多参数小模型”量级的感觉。要做出百亿、千亿模型,就是把层数和模型维度继续拉大。参数变多,模型能记住的关系和知识就更丰富,但训练和推理成本也会指数级上升,工程优化就从“加分项”变成“必选项”。
3.4 KV Cache:缓存让秒回成为可能
再回到“秒回”。模型生成第 1 个 Token 的时候,已经把整段输入里所有 Token 的 K 和 V 都算完了。如果不做任何优化,生成第 2 个 Token 时,模型会重新把整段历史再算一遍注意力;生成第 3 个 Token,又重算一遍。你生成 100 个 Token,它相当于反复处理了 1+2+3+...+100 次历史,极其浪费。
KV Cache 的思路是拆开两件事:第一次读过的东西,把 K 和 V 缓存下来;之后每生成一个新 Token,只计算新 Token 自己的 K 和 V,再拼到缓存后面。推理时只需要拿新 Token 的 Q 去和缓存里的 K、V 做注意力。
# 第一个 Token:Prefill,一次算完整段输入 k_cache, v_cache = compute_kv(all_input_tokens) # 后续 Token:Decode,只算当前那一个 k_new, v_new = compute_kv(new_token) k_cache = concat(k_cache, k_new, dim=1) v_cache = concat(v_cache, v_new, dim=1) output = attention(q_new, k_cache, v_cache)没有 KV Cache,生成是平方级复杂度;有了 KV Cache,随着序列变长的每一步只需要读越来越大的缓存,复杂度从平方级降成线性级。这也是为什么生成早期的速度往往比后期更快——后期要读的缓存越来越大,显存带宽成了瓶颈。
4. 常见问题与排查技巧:为什么有时候不秒回,甚至胡说八道
在实际使用和开发中,很多人会遇到“它刚才还挺快,怎么现在这么慢”“怎么一本正经说瞎话”之类的问题。这些问题多半不是 Transformer 原理出错,而是某些细节没到位。
4.1 流式输出:秒回的“体感魔术”
你在网页上看到的逐字蹦出,是流式输出带来的体验。服务端不是等整篇回答生成完再一次性推送,而是每生成一个 Token 就推送一段数据。对于使用者来说,等待时间从“总生成时间”变成了“首 Token 时间 + 逐字刷新间隔”,体感差距非常大。
如果自己对接模型服务,尤其需要关注这一点。不开启流式输出时,用户面对的是几十秒空白页面,很容易以为服务挂了。开启流式后,哪怕首 Token 要 3 秒,用户看到第一个字开始动,耐心也会高很多。所以“秒回”不仅有结构上的原因,还有交互设计上的原因。
4.2 上下文越长,为什么生成越慢
很多人觉得“模型能读长文本,那就把所有资料都塞进去”。结果发现输入越长,生成越慢。原因有两个。
第一,Prefill 阶段注意力复杂度是 O(n²)。输入长度 1k 时,每层要计算约 100 万个位置对的分数;输入长度 16k 时,暴涨到约 2.5 亿个位置对。所以长文本一进来,第一个字可能就要等很久。
第二,KV Cache 会随长度线性增长。粗略估算一下:缓存大小约等于 2 × 层数 × 序列长度 × 模型维度。如果是 80 层、模型维度 8192、上下文 4096,用半精度存,光缓存就是 2×80×4096×8192×2 字节,接近 11GB。上下文越长,显存占得越多,每一步生成要读取的数据也越多,速度自然会降下来。
| 上下文长度 | 影响位置 | 直观感受 |
|---|---|---|
| 短提示 | Prefill 很快 | 首字秒出 |
| 中等(4k 左右) | KV Cache 开始增长 | 生成速度略降 |
| 很长(16k 以上) | Prefill 和显存都吃紧 | 首字慢,续写变卡 |
所以处理长文档时,先做摘要或分段检索,比一股脑把全文塞给模型要靠谱得多。
4.3 为什么模型会一本正经胡说八道
模型的本质是“续写”,不是“查真相”。它以“让每个位置的 Token 概率尽量接近真实文本”为训练目标,目标里根本没有“这件事是不是事实”这一项。当用户给的上下文不够明确,或者注意力被某个强相关但错误的关键词带偏,后续内容就会沿着错误方向继续编。
缓解办法有两个方向。一个是提示词层面,把问题限定清楚,给足参考材料,明确要求“只根据以下内容回答”;另一个是采样参数层面,降低随机性,比如调小 temperature。但你要记住一点:只要模型还是语言模型,胡说八道就不能被完全消除。它不是故意骗你,而是概率续写本身就包含不确定。
4.4 常见问题速查表
| 现象 | 可能原因 | 排查方向 |
|---|---|---|
| 首字迟迟不来 | 输入太长、Prefill 慢、网络/排队 | 量首 Token 耗时;压缩提示词;换更小模型 |
| 生成越来越慢 | 上下文过长、KV Cache 占用高 | 限制 max tokens;做摘要/分段检索 |
| 输出重复 | temperature 过低或贪婪解码陷入循环 | 调高 temperature / top_p;加重复惩罚 |
| 答非所问 | 提示词约束不足、长度超过训练窗口 | 加明确指令;裁剪输入;检查长文本外推能力 |
排查时最重要的一条习惯:先复现,再改参。不要一上来怀疑模型结构。先用非常短的输入测首字速度,再逐步加长;每轮只改一个变量,否则你根本分不清是采样参数、缓存开关还是提示词导致的问题。
5. 自己动手:从一个小实验开始验证所有原理
说了这么多,更建议你自己动手跑一次。我不建议一上来就从头写 Transformer 源码,那样很容易被反向传播和优化器劝退。更快的方式是先跑推理实验,亲眼看到概率、缓存、速度的变化,再回头看原理,你会觉得所有概念都落地了。
5.1 别从零复现,先从推理开始
选一个百 M 级别的小模型,加载到开源模型库里,直接做生成。为什么要选这么小的模型?因为百亿模型一次推理成本太高,内部细节也很难看清。小模型能力虽然弱一些,但注意力结构完整,非常适合做教学实验。跑通之后,你再带着问题去读源码,会顺畅很多。
5.2 五步实验法
- 加载小模型,用贪婪解码生成 20 个 Token,记录总耗时和平均 Token/s。
- 把提示词从 10 个字改成 500 个字,观察首 Token 耗时的变化,理解 Prefill。
- 关掉 KV Cache 再跑一次同样输入,对比每 Token 平均耗时,你会直观理解缓存的价值。
- 开启采样,设置 temperature=0.9、top_p=0.9,多次生成对比重复率和流畅度。
- 打印某一层某个注意力头对所有 Token 的权重,和句子里的指代关系做对照。
每一步记录到一张表里,比读十篇原理文章更能建立直觉。特别是第 3 步,很多人第一次跑完都会惊呼:原来 KV Cache 影响这么大。
5.3 调试时容易被忽略的三个细节
第一,不要只看概率最高的 Token。要同时看 Top-2 和 Top-3 的概率差距。如果前两个候选概率非常接近,采样参数会对结果产生巨大影响;如果第一名遥遥领先,那怎么调温度差别都不大。
第二,长文本变慢时,先确认 KV Cache 有没有开。很多框架默认开启,但某些接口为了省显存会把它关掉,结果就是生成速度明显下降。你看到的“变慢”,可能不是模型变笨,而是缓存没开。
第三,注意注意力权重是否健康。健康的注意力通常会集中在少数关键 Token 上。如果你发现几乎所有 Token 都对第一个 Token 特别关注,或者权重几乎均匀分布,那很可能遇到了注意力塌缩现象,常见于未充分微调的小模型或超长上下文外推场景。这时候你至少知道问题不是出在业务上,而是模型能力边界。
我最早以为“秒回”是模型提前把答案写好,等用户提问再捞出来。后来自己跑了一次小模型实验,关掉 KV Cache 之后,生成 20 个 Token 的时间肉眼可见变长,才真正意识到:所谓的智能,其实是一套套工程细节堆出来的“快速续写”。你看到的每个字,都是模型在给定上文条件下做的一次概率选择。理解了这件事,再看“为什么有时乱说”“为什么上下文变长就变慢”“为什么流式输出更流畅”,全部都能串起来。
如果你想动手,我的建议很简单:先不要背架构图,去改一个参数,再改另一个参数,用数字去看变化。等你在日志里看到 token/s 从 20 掉到 5,比任何人给你讲一百遍注意力公式都更管用。