Agent 的一切都跑在注意力机制之上。这一章只做三件事:把「token 怎么变成一组数、这组数怎么流过注意力、n² 究竟出在哪一步」按顺序讲清楚——因为后面每一章的成本、延迟、上下文策略,都是这个 n² 的推论。
编辑部注 · 本版重排了讲解顺序:先用一整节把名词说清楚,再看形状与公式。凡是加了虚线底的名词都可以点开——弹出来的卡片里有含义、出现场景、例子与延伸阅读,不必离开本页。
如果要给「Harness 工程师为什么要懂模型原理」找一个最实际的理由,那就是:你所有的取舍最后都会被三个物理事实定死——注意力是平方复杂度的、KV Cache 是按前缀命中的、输出是采样出来的。这一章讲第一个,而且要从头讲起:先解释清楚 token、张量、形状、投影、打分这些名词,再去看它们怎么拼成一次前向。跳过名词直接看公式,是绝大多数人读不懂 Transformer 的真正原因。
一、为什么做工程的人要关心注意力
1.1 面试官其实在问什么
面试里被问到「Transformer 原理」,很多人以为考官在考古。其实不是。考官真正想问的是:
- 你知不知道上下文变长,成本是怎么长的?
- 你知不知道「把整个代码库塞进上下文」这条路会在哪一步崩掉?
- 你知不知道长任务的瓶颈到底在算力、显存,还是在带宽?
这三个问题都从 自注意力(Self-Attention) 出发——它是 Transformer 里唯一一个「让任意两个位置互相看一眼」的机制。而「互相看」有个直接的后果:位置有 n 个,两两配对就是 n² 对,所以它的开销是 平方复杂度(Quadratic complexity)。
凡是能用 n² 解释清楚的现象,都不该用「模型能力不够」来解释。
1.2 这一章你只需要带走一件事
如果读完只记住一句话,请记住这句:
注意力里的平方项,只来自「任意两个位置都要互相比较」这件事;剩下的一切(嵌入、投影、前馈网络)都是逐位置做的,随长度线性增长。
记住它,你就能判断任何一项「长上下文优化技术」到底在优化什么:它要么减少需要比较的位置对数(滑窗、稀疏、检索),要么让比较这一步更省内存(FlashAttention),要么干脆不让模型自己回顾历史(状态外置、摘要)。三条路,没有第四条。
1.3 怎么读这一章
本章刻意分成两半:
- 上半(第二、三节):只讲「是什么」。第二节把名词逐个说清,第三节跟着数据走一遍形状变化。这一半不需要任何数学。
- 下半(第四、五节):只讲「代价」。把复杂度摊开,看 n² 出在哪,再翻译成工程推论。
凡是加了虚线底的名词——比如 自注意力——点一下就会弹出卡片,里面有它的含义、出现场景、相关学科、例子和延伸阅读。这些卡片的内容全站统一维护,所以你在任何一章点同一个词,看到的都是同一份解释。
二、先把名词说清楚:一段文字怎么变成一组数
很多人读不懂注意力,不是因为数学难,而是因为名词没对齐。所以这一节不出现任何公式,只把后面要用的七个名词逐个说清楚。已经熟的读者可以直接跳到第三节。
2.1 Token:模型眼里的最小单位
一段文字交给模型之前,先被切成一串 Token。token 不一定是「字」,也不一定是「词」,而是分词器按统计规律切出来的最小单位。同一个词在不同上下文里可能被切成不同的 token 组合。
一条够用的换算经验是:1 个中文字符约需 0.6 个 token,1 个英文字符约需 0.3 个 token,反过来粗略地说,1 个 token 约等于 0.67 个英文单词或 1.67 个汉字,因此 8000 token 大约对应 5000 多个英文单词或 1.3 万到 1.4 万个汉字,而代码行数因语言和写法差异很大,不能简单换算,实际 token 数请以模型返回的 usage 为准。
记住 token 的另一个理由更现实:它是计费单位,也是上下文窗口的计量单位。你说的「上下文 128k」指的是 128k 个 token,不是 128k 个字。
2.2 Embedding:把 token 编号变成一串数
token 本身只是个编号(比如第 12345 号)。编号是离散的,编号之间没有远近关系——12345 和 12346 不代表意思接近。
Embedding 解决的就是这件事:它是一张巨大的查找表,每个 token 编号对应一串数。这串数不是人写的,是训练出来的——训练的结果是语义相近的 token,对应的数串在空间里也彼此接近。这就是「Embedding 之后可以算相似度」的全部原理。
那串数本身叫 向量(Vector):一维排列的一组数。至于「一组数为什么能表示意思」——因为方向比大小更重要:两个向量的方向越接近,表示的意思越接近。
2.3 张量与形状:一组被排成网格的数
现在我们有 n 个 token,每个 token 用一串 d 个数字表示。把这些数摆在一起,就得到一个 张量(Tensor)。
「张量」这个词听起来吓人,其实只是「数 + 排列方式」。用一个叫阶数(rank)的东西描述它:
| 阶数 | 名字 | 例子 | 要几个下标才能定位一个数 |
|---|---|---|---|
| 0 阶 | 标量 | 3.14 | 0 个 |
| 1 阶 | 向量 | [0.2, 0.7, 0.1] | 1 个 |
| 2 阶 | 矩阵 | 一张数表 | 2 个(第几行、第几列) |
| 3 阶及以上 | 更高维网格 | 多份二维表叠起来 | 3 个及以上 |
所以「张量」和「矩阵」「向量」不是并列关系,而是同一类东西的不同阶数——就像「立方体」和「正方形」都是方的一样。
工程上我们几乎只关心两件事:阶数(有几个下标)和每一维多大。这两者合起来就叫 形状(Shape),写作方括号里的列表。
于是前面那批数据可以一句话说清:n 个 token、每个 token 一个 d 维向量,整批数据的形状是 [n, d]。看到 [n, d] 你就该读出:「这里有 n 个位置,每个位置用 d 个数描述」。
形状不是学术概念,它是最省力的正确性检查:推导形状比推导公式容易得多,也更容易发现错误。本站讲注意力时故意「先不碰公式,只跟踪形状」,就是这个原因——形状对得上,说明数据流没走错;形状对不上,公式写得再漂亮也是错的。
2.4 隐藏维度 d:4096 这个数从哪来
上一步说的「每个 token 用一串数表示」,那串数到底多长?这个长度就叫 隐藏维度(d_model),记作 d,现代模型里常见的是 4096、8192 这类数字。
它有两个来源:一是模型出厂就固定了,你改不了;二是它决定了模型内部能装多少信息。d 太小,表示能力不够;d 太大,参数和显存都爆炸。
为什么 Harness 工程师要关心一个自己改不了的数?因为成本公式里有两个变量,性质完全不同:
- n(序列长度)是你直接控制的——你决定往上下文里塞多少历史、多少工具返回、多少检索片段;
- d 是出厂固定的——你只能通过选模型来间接影响它。
所以工程上的优化空间,几乎全部落在 n 上。凡是讨论「怎么把成本降下来」而不谈 n 的,基本可以跳过。
2.5 位置编码:注意力天生不认语序
最后一个名词,也是最容易被忽略的一个。
自注意力有一个反直觉的性质:它不认语序。如果你把输入序列打乱,注意力给出的结果只会跟着一起打乱,模型没有任何机制知道「谁在前谁在后」。对「猫追狗」和「狗追猫」这两句话,纯注意力的输出完全相同。
位置编码(Positional Encoding) 就是把顺序信息注入表示的手段:在 token 的向量上,叠加一个由位置决定的向量。这样「第 3 个位置」和「第 7 个位置」在数值上就区分开了。
现在主流方案是相对位置思路(RoPE 是代表),它编码的是「两者相距多远」而不是「各自排第几」。这带来一个附带好处:模型对超出训练长度的位置有一定外推能力。但请注意一个容易搞混的推论——「把窗口调大」不等于「模型真的能用好长窗口」。窗口是接口能力,用好是能力问题,两者必须分开评测。
三、一次前向到底做了什么
名词说清楚了,现在跟着数据走一遍。这一节依然尽量不碰公式,只跟踪形状——看懂形状变化,就看懂了注意力。
设一批输入是 n 个 token,每个 token 被映射成一个 d 维向量。下面这条链路从头到尾走一遍,就叫一次 前向(Forward pass)——注意它指的是方向(数据从输入推向输出,与反向传播相反),不是某一层;真正的那一层叫「前馈」。下面的图把整条链路画成一条流水线。
[n, d] 的输入先压成 [n, n] 的权重矩阵,再用它加权聚合 V。所有关于「上下文很贵」的直觉,都来自中间那个 [n, n]:它随 n 平方增长,而输入本身只随 n 线性增长。三步走,记住这三步就够用了:
- 投影:同一份输入 X,用三组不同的权重矩阵投影出 Q(我想找什么)、K(我能被什么找到)、V(我实际携带的内容)。
- 打分:用
Q · Kᵀ算出任意两个位置之间的相关度,得到一个[n, n]的矩阵,除以√d做缩放,再 softmax 成权重。 - 聚合:用这组权重对 V 做加权求和,输出形状回到
[n, d]。
下面把这三步拆开讲,每一步都回答同一个问题:输入是什么、输出是什么、为什么非得这么做。
3.1 投影:同一份输入,三种问法
先说清楚「投影(Linear Projection)」这个词。它在数学上的意思是「换一组坐标去看同一个东西」——不改变「它是什么」,只改变「用什么语言描述它」。实现上就是乘一个矩阵。
关键在于:这个矩阵不是人写的,是训练出来的。训练时反向传播调整的就是这些矩阵里的数。所谓「模型学到了什么」,很大一部分就存在这些矩阵里。
现在看自注意力的入场动作:同一份输入 X(形状 [n, d]),乘三个不同的权重矩阵 W_q、W_k、W_v(形状都是 [d, d]),得到三个形状同为 [n, d] 的张量。
[n, d]——投影只改内容、不改革制。- 输入:
X,形状[n, d]——n 个位置,每个位置一个 d 维向量; - 输出:
Q、K、V,形状都是[n, d]——同样是 n 个位置、每个位置 d 维,但已经换了一套坐标; - 为什么要这么做:因为「找东西」和「被找到」和「实际内容」是三件不同的事,用同一套表示会互相干扰。用一个比喻:
Q是「我要找什么」,K是「我能被什么找到」,V是我实际要交出去的内容。
这三者的正式名字是 Q / K / V(Query / Key / Value)。它们来自同一份输入,所以叫「自」注意力——每个位置在向包括自己在内的所有位置提问。
顺带记住一个后面会反复用到的推论:K 和 V 只由输入决定。这正是下一章 KV Cache 成立的前提——只要前缀没变,K、V 就可以重用。
3.2 打分:让每个位置和所有位置互相看一眼
「打分」这个词指的是 打分(Scoring):给每一对位置算一个相关度分数。用什么算?用点积(点积(Dot product))——两个向量方向越接近,点积越大。
实现上,把 Q 和 K 的转置相乘:Q · Kᵀ。形状上发生了第一次也是唯一一次危险的变化:
- 输入:
Q是[n, d],Kᵀ是[d, n]; - 输出:
[n, n]; - 为什么是 n²:结果矩阵的第 i 行第 j 列 = 「第 i 个位置该给第 j 个位置打多少分」。n 个位置两两配对,就是 n × n 个格子。
这个 [n, n] 的矩阵叫 注意力矩阵(Attention matrix)。它是全章唯一一个同时被两个维度 n 撑大的东西——输入 [n, d] 只有一维是 n,而它是两维都是 n。
这就是 n² 的全部来源。请把这句话记牢:不是模型「算得慢」,是「两两比较」这件事本身就要 n² 次计算。
3.3 缩放:为什么必须除以 √d
Q · Kᵀ 出来的是原始的分数,直接送进 softmax 会有问题,所以要除以 √d。这一步叫 缩放(Scaling factor)。
为什么是 √d 而不是别的?直觉版的解释:点积是 d 个数相乘再相加。如果每个数都是均值为 0、方差为 1 的随机数,那么 d 项相加的结果方差会变成 d,标准差变成 √d。除以 √d 正好把它拉回单位量级。
- 输入:
[n, n]的原始分数; - 输出:
[n, n]的缩放后分数,量级回到 1 附近; - 不这么做会怎样:分数太大时,softmax 会被最大的那个数「吃掉」,输出接近 one-hot(极端情况下梯度会消失)。模型会变得难以训练,注意力权重也会退化——它不再是在做「综合考虑几个位置」,而是硬挑一个位置。
√d 不是可选的调参,是让它别退化的必要条件。3.4 Softmax 与加权求和:从分数回到向量
接下来两步,把 [n, n] 的分数变成 [n, d] 的输出。
第一步是 Softmax:按行归一化,让每一行的 n 个数都变成非负数且加起来等于 1。做完之后,每一行就是一组「分配比例」——第 i 行说的是「第 i 个位置把注意力按什么比例分给所有位置」。
- 输入
[n, n]原始分数 → 输出[n, n]权重,每行和为 1。
第二步是加权求和(加权求和(Weighted Sum)):用每一行的权重,把 V 里对应的向量按比例加起来,得到一个 d 维向量。n 行各做一次,就得到 n 个 d 维向量。
- 输入
[n, n]权重 ×[n, d]的 V → 输出[n, d]。
形状回到了和输入完全一样的 [n, d]。这不是巧合,而是刻意设计:注意力是一个「信息交换」层,它必须保持形状不变,这样才能一层层叠下去。你可以在图 1 里看到这个「进去 [n, d]、出来 [n, d]」的闭环。
3.5 多头:把一次注意力拆成 h 次并行
只做一套 Q/K/V 是不够的。现实中一句话里同时存在好几种关系——语法上的主谓、指代上的照应、语义上的远近。一套注意力只能表达一种「关注方式」。
多头注意力(Multi-Head Attention) 的做法是把 d 维切成 h 份,每份长度 d/h,各自独立跑一遍上面的流程,最后把 h 份结果拼回 [n, d]。
- 形状变化:
[n, d]→[h, n, d/h](多了「头」这一阶)→ 各头独立算注意力 → 拼回[n, d]; - 代价不变:总计算量和不分头基本一样,因为每个头处理的维度只有
1/h; - 换来了什么:h 套不同的 W_q/W_k/W_v,等于让模型同时用 h 种「提问方式」看同一段文本。
这就是为什么本站在讲形状时会强调阶数——多头只是多了一阶,机制一步都没变。
3.6 因果掩码:为什么 decoder 只能看左边
生成式模型有一个硬约束:预测第 i 个 token 时,不能看到第 i+1 个及以后的内容,否则等于抄答案。
因果掩码(Causal Mask) 就是实现这个约束的手段:在送进 softmax 之前,把注意力矩阵里「未来位置」那些格子填成负无穷。负无穷过 softmax 之后变成 0,权重就落不到未来位置上了。
- 输入:
[n, n]缩放后分数; - 输出:
[n, n],其中上三角(未来部分)全被压成 0。
这一步看着简单,但它是一个大前提:有了它,每个位置的计算只依赖它左边的历史。下一章要讲的 KV Cache,整个立足点就在这里——前缀算过一次就不用再算。
四、n² 究竟出在哪一步
4.1 把每一步的形状与复杂度摊开
把每一步的复杂度列出来,问题就一目了然了。这里用到大 O 记号(大 O 记号(Big-O notation))——它只关心「随 n 增长的量级」,不关心常数。
| 阶段 | 做的事 | 张量形状 | 计算量级 | 随 n 的增长 |
|---|---|---|---|---|
| 投影 | X·W_q/k/v | [n, d] → [n, d] | O(n · d²) | 线性 |
| 打分 | Q·Kᵀ | [n, d] × [d, n] → [n, n] | O(n² · d) | 平方 |
| 缩放 | 除以 √d | [n, n] | O(n²) | 平方 |
| softmax | 按行归一化 | [n, n] | O(n²) | 平方 |
| 聚合 | A·V | [n, n] × [n, d] → [n, d] | O(n² · d) | 平方 |
| 前馈网络 | 逐位置 MLP | [n, d] → [n, d] | O(n · d²) | 线性 |
结论就一句话:平方项只来自「任意两个位置都要互相看一眼」这件事,也就是打分、缩放、softmax、聚合这四步。剩下的一切都是逐位置做的:
- 投影:每个位置各自乘自己的矩阵,位置之间互不影响,线性;
- 前馈网络(前馈网络(Feed-Forward Network)):同样是逐位置的一小段 MLP,线性。
这也解释了为什么 FFN 明明占了模型大部分参数,却从来不是长上下文的主要瓶颈——它的开销随 n 线性增长,而注意力是平方。n 小的时候感觉不出来,n 一大就彻底颠倒。
4.2 一个必须记住的数量级直觉
设 d = 4096,n = 8,000(大约 3 万汉字或 6,000 行代码):
- 线性项的量级约
n·d² ≈ 1.34×10¹¹; - 平方项的量级约
n²·d ≈ 2.6×10¹¹。
此时两者还在同一数量级——这就是「中等长度上下文时,注意力还不是绝对瓶颈」的原因。但只要 n 再翻一倍,平方项就翻四倍,线性项只翻两倍。拐点大约出现在 n 与 d 可比的时候;一旦 n 远超 d,平方项就彻底主导成本。
反过来读这条结论也成立:当 n 还小于 d 时,花大力气优化注意力收益有限,先把上下文塞满反而是划算的。判断「该不该优化」比「怎么优化」更重要。
五、这对做 Agent 意味着什么
把上面的结论翻译成工程语言,会得到三条非常硬的推论。
5.1 成本不是线性的,「多塞点上下文」是高息负债
往上下文里加一倍内容,注意力部分的代价接近四倍。这解释了为什么长上下文模型的 API 定价通常按阶梯上涨,而不是线性——定价模型跟着物理规律走。
反过来说,这也给了一个判断标准:如果你发现某次调用的成本远超预期,先去看输入 token 数(n),而不是去怀疑模型供应商乱收费。
5.2 裁剪与压缩不是优化,而是必备能力
如果你的 Harness 只会「一直追加消息」,那你实际上是在让成本按平方增长。上下文压缩、状态外置、历史摘要这三件事,是 Harness 的基本功而不是加分项。
它们各自对应一条减少 n 的路径:
- 摘要:把旧的多轮内容压成一段,直接减少 n;
- 状态外置:把不必要的内容移到外部存储,需要时再取,让 n 保持小而稳定;
- 检索:只把相关内容塞进 上下文窗口(Context Window),而不是全塞。
注意这三条路线都在动 n——没有一条在动 d。这与第二节点出的结论一致:d 出厂固定,n 才是你的操作杆。
5.3 首 token 延迟与后续 token 延迟是两笔账
处理输入(Prefill(Prefill / Decode))要跑完整个 [n, n];生成每个 token(Decode)只走一步,但需要读缓存(KV Cache)。
这两个阶段的瓶颈不同:前者是算力受限,后者是显存带宽受限。所以「优化延迟」这句话是不完整的——你必须先问清是哪一段延迟。这直接决定了优化手段:加算力对 Prefill 有效,对 Decode 基本没用;而 Decode 的优化要靠减少每步要读的数据量。
这是下一章的主题。这里先记住结论的形式:当你听到一个优化方案时,先问它优化的是 Prefill 还是 Decode——分不清这两者的方案,通常两边都不讨好。
把 n² 记住,你就不需要背任何「长上下文为什么要小心」的结论了——那些结论全都是它的推论。本刊编辑部
六、动手:三十行手写一遍注意力
6.1 把图 1 写成代码
不写一遍就容易把形状记混。下面这段 NumPy 代码把图 1 完整实现了一遍——重点不是算法,而是每一行注释里标注的形状。请对照着代码读一遍,确认每一步的形状变化和第三节讲的一致。
import numpy as np
def softmax(x, axis=-1):
m = x.max(axis=axis, keepdims=True) # 数值稳定:先减去最大值
e = np.exp(x - m)
return e / e.sum(axis=axis, keepdims=True)
def attention(X, Wq, Wk, Wv):
"""X: [n, d] 三个权重: [d, d]"""
Q = X @ Wq # [n, d] 我想找什么
K = X @ Wk # [n, d] 我能被什么找到
V = X @ Wv # [n, d] 我携带什么
d = Q.shape[-1]
S = Q @ K.T # [n, n] ← 平方复杂度的来源
S = S / np.sqrt(d) # 缩放,防止 softmax 进入饱和区
A = softmax(S) # [n, n] 每行和为 1
return A @ V # [n, d] 回到原形状
n, d = 5, 8
rng = np.random.default_rng(0)
X = rng.normal(size=(n, d))
Wq = rng.normal(size=(d, d)) / np.sqrt(d)
Wk = rng.normal(size=(d, d)) / np.sqrt(d)
Wv = rng.normal(size=(d, d)) / np.sqrt(d)
out = attention(X, Wq, Wk, Wv)
print(out.shape) # (5, 8) —— 形状与输入一致,这是所有注意力变体的共同特征
# 顺手验证因果掩码(decoder 只能看左边)
mask = np.triu(np.ones((n, n)), k=1).astype(bool)
print(mask.astype(int))6.2 实操任务(建议 20 分钟)
- 把
n从 5 改到 2000,测量S = Q @ K.T与X @ Wq的耗时比例,亲手看到平方项吃掉时间; - 把
mask加进S(被 mask 的位置填-inf),再做 softmax,确认第一行只依赖第一个 token; - 回答自己一个问题:如果我想让「第 500 个 token 只关注最近 100 个 token」(滑窗注意力),代码要改哪一行?改完之后,中间那个矩阵还是
[n, n]吗?
七、常见误区与追问
这一节收的是「看起来懂了、一被追问就露馅」的几个地方。它们的共同点不是难,而是中文技术叙述里长期流传的错误类比。
7.1 把「参数量」和「序列长度」当成同一条轴上的两件事
错在哪:说「这个模型很大所以很贵」,或者反过来,用参数量去解释「为什么上下文变长会变贵」。为什么自然:日常经验里「东西大就贵」几乎总成立,而大模型的「大」字被滥用了。正确做法:这是两条正交的轴。参数量 d 决定显存下限与单 token 的算力,出厂固定、你改不了;序列长度 n 决定算力与显存的增长速度,是你唯一能直接控制的变量。判据:一个 7B 模型喂 128k 上下文的注意力开销,可以远超一个 70B 模型喂 2k 的开销 —— 因为前者是 O(n²·d),n 在平方项里,后者只是使 d 变大。
7.2 以为注意力矩阵的显存和 KV Cache 是同一个量级
错在哪:把「长上下文吃显存」笼统归给注意力矩阵。为什么自然:都是「长上下文变贵」,很容易当成一件事。正确做法:分清两个不同的东西。注意力矩阵是 [n, n],是算力侧的中间结果,FlashAttention 的思路就是「算完即弃、不显式存下来」;KV Cache 是 [n, d] 每层两份,是推理必留的状态,随 n 线性增长,省不掉。判据:n=8,000 时注意力矩阵单精度约 256 MB(可优化掉),而 80 层的 KV Cache 是 GiB 量级(必须预算)。面试里把这两笔账分开算,是分水岭。
7.3 以为「投影会改变形状」
错在哪:把 Q/K/V 三步当成「把输入变换成三种不同形状」。为什么自然:符号上 X → Q 看着就像变了个东西。正确做法:投影只换坐标,不改形状。X 是 [n, d],W_q 是 [d, d],Q 还是 [n, d] —— 三个都是 [n, d],差别只在「用什么坐标系描述」。真正改变形状、并制造出 n² 的是下一步打分:Q·Kᵀ 把 [n, d] 变成了 [n, n]。判据:如果哪天你看到一个注意力变体把形状写错了,八成是把「投影」和「打分」混在一起了。
7.4 以为「头数越多越好」
错在哪:把多头理解成「多几个模型一起投票」。为什么自然:「多个视角」听起来总是更强。正确做法:头的本质是把 d 维切成 h 份并行,d = h × head_dim。好处是可以同时维持多种关注方式(位置邻近、指代、句法);代价是每个头的表达能力被稀释,而且注意力矩阵的数量随头数成倍增加。判据:现代模型头数多在 32–128 之间,是实验调出来的,不是越大越好;同时要记住所有头共享同一层的输入,它们不是独立模型。
7.5 把注意力权重视为「模型给出的解释」
错在哪:看到某次注意力把 0.7 分给了某个词,就说「模型这样判断是因为它注意到了这个词」。为什么自然:权重看起来就是一张「谁重要」的名单,很像解释。正确做法:注意力权重是线索,不是证明。它只说明这一层、这个头、这个样本上的分配;不同层不同头的权重往往互相矛盾,而后续的 FFN 完全可以在权重之外改变结论。把它当作排查工具(找注意力涣散、找异常聚焦)是合理的,当作因果解释就不成立。
八、自测
QKᵀ 产生 [n, n]),才出现平方项。记住这个区分,就能判断任何「优化长上下文」的技术到底在优化什么。九、小结:把这一章压成四句话
- 数据是张量,形状是
[n, d]。 张量只是「数 + 排列方式」,形状是它最省力的正确性检查。n 是 token 数,d 是出厂固定的隐藏维度——你能控制的是 n。 - 注意力的三步是投影、打分、聚合。 投影把同一份输入翻译成 Q / K / V 三种问法;打分让每个位置和所有位置互相看一眼;加权求和把结果聚合回
[n, d]。 - 平方项只出在「两两比较」上。 中间那个 注意力矩阵是
[n, n],它是唯一一个两维都被 n 撑大的东西。前馈网络和投影都是逐位置的,线性。 - 两条工程推论:n 越大成本越贵,所以压缩、外置、检索是基本功;Prefill 和 Decode 瓶颈不同,所以「优化延迟」必须说清是哪一段——这是下一章 KV Cache 的起点。
下一章我们把这个 n² 换个角度再看一次:既然前缀会被反复重算,那能不能缓存?能,但缓存命中与否取决于一个你平时根本不会注意到的东西——前缀稳定性。
十、参考与延伸
本章的机制部分只讲到「够用」为止。想往下深挖,下面这几份材料按「先看图、再看代码、最后读论文」的顺序排好了。全站不做原文转载,这里只登记链接与「为什么值得读」。
先看图(建立直觉)
- The Illustrated Transformer —— 用动画式的静态图把 Q/K/V、多头、位置编码全画了一遍。读本章第三节卡住时,来这儿看图解最快。它的图比任何公式都更容易建立「形状在流动」的感觉。
- 3Blue1Brown · Attention in transformers, visually explained —— 把注意力讲成「向量之间互相传递信息」的几何过程。如果你对「点积为什么表示相关度」没有直觉,看这一集,它是目前最好的可视化解释。
再看代码(动手实现)
- Andrej Karpathy · Let's build GPT: from scratch —— 从零手写一个小 GPT,视频 + 代码逐行对照。本章第六节那三十行 NumPy 是它的极简版;想真正把形状记牢,就跟着把这一课敲完。
- PyTorch ·
scaled_dot_product_attention文档 —— 生产实现长什么样,一眼就能看到 attn_mask、is_causal 这些参数。想确认「工业实现和教科书版本差在哪」,看它的参数表最快。
最后读论文(对齐一手定义)
- Attention Is All You Need(arXiv:1706.03762) —— 一切的一手来源。建议只精读第 3.2 节(Scaled Dot-Product Attention)与 3.5 节(Positional Encoding),其余部分与本岗位关系不大。
- Lilian Weng · The Transformer Family v2.0 —— 把注意力的各种变体(稀疏、线性、FlashAttention)按「改了什么」整理成了一张谱系图。想理解「长上下文优化都有哪几条路」,读这一篇的目录结构就够了。
- RoFormer / RoPE(arXiv:2104.09864) —— 现在主流的位置编码方案。只需要看它怎么把「绝对位置」换成「相对旋转」那一段。理解这一点,才能听懂「为什么扩窗口需要额外训练」。
◇ 面试官会怎么问
共 3 条 · 其中 1 条高频 · 先自己答一遍,再展开对照
为什么注意力复杂度是 O(n²)?这对做 Agent 意味着什么? 高频
seq × seq 的注意力矩阵,所以算力随序列长度呈平方增长。对 Agent 的直接含义有三层:第一,上下文越长,成本不是线性上涨而是平方级上涨,这是"长上下文很贵"的物理原因;第二,往上下文里多塞一倍内容,代价远不止翻一倍;第三,裁剪、压缩、状态外置不是优化技巧,而是必须做的基本功。
追问链
- 那 KV Cache 的显存占用怎么估算?它和 n² 是同一个量级吗?
- 如果只关心首 token 延迟,哪个成本占主导?
多头注意力比单头好在哪?
工程意义在于:模型能同时维持多种关系,这是长链路任务里"既记住目标又盯住细节"的基础。注意头的数量与 head_dim 的乘积大致等于隐藏维度,所以加头并不等比例增加算力,但头太多也会稀释每个头的表达空间。
位置编码解决什么问题?没有会怎样?
对工程的启示:相对位置方案让模型对超出训练长度的位置有一定外推能力,但外推能力有限。"把窗口调大"不等于"模型真的能用好长窗口",这就是长上下文需要专门训练和评测的原因。
结论先行(一句话给出取舍)→ 机制(为什么是这样,涉及哪条链路)→ 代价或边界(这么做放弃了什么)→ 你的实践或数字(真实场景里怎么落地)。 只讲机制不讲代价,是背题;只讲取舍没有数字,是空谈。