现学现卖,是一个电路
大模型最像「智能」的本事之一,是在上下文里现学:你在提示词里给它看一个它从没见过的格式、一个新造的词、一张临时的对照表,它马上就能照着用。这看起来需要理解。这一章做全书最干净的一个实验:给 GPT-2 一串毫无意义的乱码,看它怎么在第二遍「学会」它——然后找到干这件事的那几个注意力头,把它们关掉。
从 GPT-2 的词表里随机抽 50 个 token,拼成一串乱码(比如 ␣press ␣Constitution ␣coffee gency ␣Book who rop ␣Care…),然后原样再接一遍,一共 100 个。这样的乱码做 30 串,交给 GPT-2,逐个 token 量它的意外。
第一遍它当然猜不中——这些组合它从来没见过。问:第二遍,平均每个 token 的意外是多少?
答案在本章末尾。
一串乱码,读两遍
30 串乱码,每串 50 个随机 token 重复两遍,开头加一个「文档开始」符。GPT-2 的成绩:
| 平均意外 | 第一名猜中率 | |
|---|---|---|
| 闭眼瞎猜(50257 选 1) | 15.62 比特 | — |
| 第一遍 | 17.36 比特 | — |
| 第二遍 | 0.63 比特 | 97.1% |
两件事都值得停下来。第一遍比瞎猜还差:它不是一台「不知道就平均分配」的机器,它满心期待着正常的英文,把概率押在了 ␣the、. 这些词上,于是每个乱码 token 拿到的概率比 1/50257 还低。第二遍几乎全中:97.1% 的位置,它排第一的候选就是乱码里的下一个。看逐个位置的平均意外:
第二遍开头那一个 token(第 51 个)仍然是 15.13 比特——它没法预知乱码要重复。但只要读到重复开始后的第一个 token,下一个就降到 3.68 比特,再下一个 1.83,然后一路贴着 0 走。看一个具体位置的候选表:
同一个 token ␣Care,第一遍出现时花了 14.05 比特;第二遍,GPT-2 给它 70.42%,只花 0.51 比特。它没有「理解」这串乱码,它只是看了一眼,就会照抄了。
谁在抄:归纳头
照抄的规则用一句话就能写下来:「我现在读到的是 A;上一次 A 出现的时候,后面跟的是 B;所以下一个大概是 B。」写成序列:
… [A] [B] … … … [A] → ⟪B⟫ ?
如果有哪个注意力头在执行这条规则,它的注意力应该有一个非常具体的形状:在第二遍的每个位置上,它看向的是「上一次同一个 token 出现的位置的下一格」——也就是 B 所在的地方。按这个形状给 144 个头打分(在第二遍的位置上,平均有多少注意力落在那一格):
| 头(层.编号) | 注意力落在「上次那个词的下一格」 |
|---|---|
| 5.5 | 93.4% |
| 6.9 | 91.9% |
| 7.10 | 91.2% |
| 5.1 | 90.5% |
| 7.2 | 84.2% |
| 10.1 | 51.2% |
| 9.9 | 48.0% |
有四个头干净利落地做着这件事:九成以上的注意力,准确地落在那一格上。这种头有一个名字,叫归纳头(induction head)——「看过 A 后面跟 B,就归纳出 A 后面跟 B」。
一个头做不到,要两个头接力
这里有个难题。归纳头站在第二个 A 的位置上,它要找的是「B 所在的那一格」。可是 B 那一格的向量里,原本只写着「我是 B」——它不知道自己前面是 A。按内容查找的注意力,凭什么认出那一格?
答案是:要有人提前在 B 那一格上写一句「我前面那个是 A」。这正是第 9 章那个只看上一个 token 的 4.11 号头干的活。于是整个电路是两步接力:
- 第 4 层,上一个 token 头:在每一个位置上,把「上一个 token 是谁」写进这个位置的总线。B 那一格于是带上了「我前面是 A」。
- 第 5 到 7 层,归纳头:在第二个 A 的位置上,它的查询说「我要找前面是 A 的那一格」,B 那一格的键恰好匹配;它把 B 的内容(「我是 B」)取回来,写进总线,最后被读成「下一个是 B」。
两个头分在不同的层里,后一个的查询要用到前一个写下的东西——这是 Anthropic 在 2022 年的论文《In-context Learning and Induction Heads》里拆出来的一个电路:几个部件按固定方式接起来,完成一个算法。那篇论文还发现,归纳头是在训练中某个时刻突然形成的,而模型「在上下文里学习」的能力,恰好在同一时刻跳了一大截。
把它关掉
说一个部件在干某件事,最有说服力的证据是:把它拿掉,那件事就做不成了;而别的事照常。这里用均值消融来「关掉」一个头:把它的输出,换成它在一段正常文字(第 4 章那篇文章)上的平均输出。这个头照常发声,只是说的永远是同一句平均的废话,不再携带当下的任何信息。
| 关掉哪些头 | 第一遍 | 第二遍 | 第二遍猜中率 |
|---|---|---|---|
| 都不关 | 17.36 | 0.63 | 97.1% |
| 5.5 | 17.30 | 0.76 | 96.3% |
| 5.5、6.9 | 17.27 | 1.16 | 94.2% |
| 5.5、6.9、7.10 | 17.26 | 1.31 | 92.9% |
| 5.5、6.9、7.10、5.1 | 17.22 | 10.78 | 9.1% |
| 再加 7.2 | 17.21 | 12.32 | 3.4% |
| 前 10 名全关 | 17.21 | 14.30 | 0.3% |
| 对照:随便关 4 个别的头(5 次平均) | — | 0.68 | 96.7% |
144 个头里关掉 4 个,第二遍的意外从 0.63 比特回到 10.78 比特,猜中率从 97.1% 掉到 9.1%。而第一遍纹丝不动(17.36 → 17.22):这几个头不管别的事,只管照抄。随便关掉 4 个别的头当对照,第二遍几乎不受影响(0.68 比特)。
还有一个细节:关掉 3 个时,猜中率还有 92.9%;关掉第 4 个才断崖式崩塌。这四个头互为备份,剩下任何一个都能把照抄的活大致干完。同样,第一步那个「上一个 token 头」也有备份:单独关掉 4.11,第二遍只涨到 1.04 比特;把最像它的 5 个头一起关掉(4.11、3.7、6.8、2.2、5.6),第二遍涨到 4.03 比特,猜中率降到 71.1%——电路的前半截断了,后半截就拿不到「前面是谁」的信息。
为什么这件事比乱码重要
拿乱码做实验,是为了排除一切「理解」的可能:这串东西没有意思,GPT-2 也从没见过它,它能做的只有照抄。可同一个电路在正常文字里时时刻刻都在工作:
- 文章里再次出现的人名、专有名词、变量名——第 4 章那篇文章里,「␣ChatGPT」(三个 token)在句中出现了 113 次:第一次花了 11.16 比特,之后平均只要 5.14 比特,便宜了一半多。
- 提示词里的例子——第 16 章会看到,给 GPT-2 看一个「问:…答:…」的例子,它答对的概率从 1.77% 跳到 63.71%。照抄格式,是这类「少样本学习」的一个核心零件。
- 复读——第 14 章那些原地打转的输出,也有这个电路的功劳:它太擅长「前面出现过什么,后面就再来一遍」了。
更大的模型里,有「模糊」版本的归纳头:不是照抄完全相同的 token,而是照抄相似的模式(A′ 后面跟 B′)。Anthropic 那篇论文认为,这类机制可能是大模型上下文学习能力的主体。这是一个有分量的猜想,还没有被完全证实——但至少在这一章的实验里,「现学现卖」被一个可以指着、可以关掉的电路完整地解释了。
同样的 30 串乱码(用同一个随机种子生成),同样的四个头,用 PyTorch 的钩子在 c_proj 的输入上做均值消融:
import math, numpy as np, torch
from transformers import AutoTokenizer, AutoModelForCausalLM
tok = AutoTokenizer.from_pretrained("gpt2")
model = AutoModelForCausalLM.from_pretrained("gpt2").eval()
rng = np.random.default_rng(42)
seq = torch.tensor(np.stack([rng.integers(1000, 10000, 50) for _ in range(30)]))
x = torch.cat([torch.full((30, 1), 50256), seq, seq], 1) # 开头符 + 50 个随机 token + 原样再来一遍
def two_passes():
with torch.no_grad():
logp = torch.log_softmax(model(x).logits[:, :-1], -1)
b = -logp.gather(-1, x[:, 1:, None])[..., 0] / math.log(2) # 每个位置的意外(比特)
return b[:, 1:50].mean().item(), b[:, 50:100].mean().item()
print("第一遍 %.2f 比特/token,第二遍 %.2f 比特/token" % two_passes())
# 均值消融:把四个头的输出,换成它们在一段正常文字上的平均值
heads = [(5, 5), (6, 9), (7, 10), (5, 1)]
text = torch.tensor([tok.encode(open("chatgpt-2026-10-21.txt").read())[:1024]])
mean = {}
grab = lambda L: (lambda mod, args: mean.__setitem__(L, args[0][0].mean(0)))
hooks = [model.transformer.h[L].attn.c_proj.register_forward_pre_hook(grab(L)) for L in {5, 6, 7}]
with torch.no_grad():
model(text) # c_proj 的输入 = 12 个头的输出拼在一起
for hk in hooks:
hk.remove()
def patch(L):
def hook(mod, args):
z = args[0].clone()
for l, h in heads:
if l == L:
z[..., h * 64:(h + 1) * 64] = mean[L][h * 64:(h + 1) * 64]
return (z,)
return hook
for L in {5, 6, 7}:
model.transformer.h[L].attn.c_proj.register_forward_pre_hook(patch(L))
print("关掉 4 个头:第一遍 %.2f,第二遍 %.2f" % two_passes())
本机输出(和正文用 numpy 版跑出来的数逐位一致):
第一遍 17.36 比特/token,第二遍 0.63 比特/token 关掉 4 个头:第一遍 17.22,第二遍 10.78
试试只关 3 个、或者换成第 9 章的上一个 token 头 (4, 11),看电路在哪一步断。
pip install torch transformers numpy && python 12.py
Colab 里几秒钟跑完。想深入:Anthropic 的 transformer-circuits.pub 上有归纳头的原始论文和交互图;Neel Nanda 的 TransformerLens 教程从这个例子开始。
- 代码补全特别擅长沿用你文件里的变量名、函数名,哪怕那些名字是你刚刚才起的、世上独一份——这就是归纳头的日常工作。
- 「照下面的格式输出:……」这种提示特别管用,因为照抄格式是模型里最便宜、最可靠的一个机制。与其用文字描述格式,不如直接给它看一个例子。
- 提示注入之所以难防,部分原因也在这里:模型天生倾向于延续上下文里出现过的模式,不管那段上下文来自你,还是来自它读到的一个网页。
「模型能从提示词里现学一个新规律,说明它真正理解了这个规律。」
这一章的乱码实验里根本没有规律可理解,GPT-2 照样在第二遍做到了 97.1%。「现学现卖」至少有一大块,是一个两步的机械电路:一个头记下「我前面是谁」,后面的头据此找到「上次这个词后面跟了什么」,然后照抄。关掉 144 个头里的 4 个,这个本事就没了,别的能力一点不受影响。
这不是在贬低它——照抄恰好是学习里最有用的一个零件。但下次看到模型「一看就会」时,值得先问一句:它是理解了,还是找到了可以照抄的东西?
正确答案是 D:第二遍平均 0.63 比特,97.1% 的位置第一名就猜中。
第一遍是 17.36 比特——比 50257 选 1 闭眼瞎猜的 15.62 比特还差。
《意外》第 9 章的 LZ 压缩器做的是同一件事的另一种实现:「这一串前面出现过,记下往回跳多少、抄多长」。归纳头就是长在 Transformer 里的 LZ77——难怪 GPT-2 压缩一篇文章时,第二次出现的名字那么便宜。《拨动》的干预思路在这一章用到了极致:要证明一个部件是原因,就把它拿掉,并且用一个随机拿掉的对照组排除「拿掉什么都会坏」。
这一章的一句话
「在上下文里现学」至少有一大块是一个两步电路:一个头记下「我前面是谁」,后面几层的归纳头据此找到「上次这个词后面跟了什么」并照抄。它在毫无意义的乱码上也能做到 97.1%;关掉 144 个头里的 4 个,这个本事就没了。
拆完这些零件,还剩一个让可解释性研究头疼的问题:为什么大多数神经元都说不清自己代表什么?下一章用一个只有 2 维的玩具模型给出答案:当 5 个特征都很少同时出现时,模型会把 5 个全塞进 2 维里,排成一个相邻夹角约 72 度的五角星——概念比维度多,每一维都身兼数职。