The Context Crisis — Why Your Model Forgets What You Told It Three Messages Ago
第 1 章:上下文危机 — 为什么你的模型三句话前说过的事就忘了
一个价值 47,000 美元的金鱼
2024 年 11 月,某跨境电商平台的客服 Agent 团队在生产环境遭遇了一起诡异事故。客户 Mei 在第 14 轮对话中明确告诉智能助手:「我之前在 9 月 15 日下单了一个订单号 #A2-9931 的产品,那个产品有问题,请帮我处理。」Agent 回复:「好的,请问您要咨询什么?」——它把整段上下文都吃掉了,却唯独漏掉了订单号。Mei 重复了三次,Agent 重复了三次「请问您要咨询什么?」,最后 Mei 愤怒地要求转人工。
事后排查,日志显示这个对话的累计上下文已经膨胀到 187,000 tokens,系统提示词、系统检索的 FAQ 文档、历史对话、用户上传的聊天截图 OCR 文本全部塞在一起。模型本身是当时最强的版本,上下文窗口官方标注 200K。然而这个模型表现得像一条金鱼:它不仅"记不住",而且"看不清"。
这不是孤例。如果你做过任何生产级 LLM 应用,大概率遭遇过以下场景的某种变体:
- 多轮对话中,模型在第 8 轮之后开始"失忆",系统提示词里的关键规则被忽略;
- 长文档问答任务中,模型对文档中间章节的引用准确率断崖式下跌;
- RAG(Retrieval-Augmented Generation)系统中,召回的 20 个 chunk 全部塞进 prompt,模型却引用了不存在于任何 chunk 的"幻觉内容";
- Agent 工具调用链路上,超过 15 步之后,模型开始重复调用同一个工具、忘记上一步的返回结果。
这些现象共享同一个根因:把更多 token 塞进上下文窗口并不会让模型更聪明,反而经常让它变笨。 这件事违反直觉——传统软件工程中,"内存"是稀缺资源,所以工程师会小心翼翼地管理它;到了 LLM 时代,营销话术反复强调"百万 token 上下文"、"读完整个代码库"、"读完整本书",让许多工程师产生了一种"上下文是免费的"的错觉。
本章的目的就是戳破这个错觉。我们要回答三个层层递进的问题:
1. 现象:模型真的"看不见"上下文中间的内容吗?证据是什么? 2. 机理:从注意力机制的物理实现角度看,这种"看不见"是不是不可避免的? 3. 行动:作为工程师,我们应该如何诊断、量化、并最终驯服这个问题?
读完本章,你将拥有一个诊断上下文失败的 checklist,一段可以复现"上下文腐烂"(Context Rot)的 50 行 Python 脚本,以及一个看待上下文窗口的根本性思维转变:把上下文当作稀缺资源来分配,而不是当作免费垃圾桶来填满。
---
1.1 "Lost in the Middle":当 LLM 真的"看不见"自己的输入
1.1.1 一个反直觉的实验
2023 年 7 月,Princeton 的 Nelson Liu 团队发表了一篇后来被引用数千次的论文,题目叫《Lost in the Middle: How Language Models Use Long Contexts》[Liu et al., 2023]。这篇论文的设计思路异常简洁,但结论震撼了整个 NLP 社区。
实验方法是这样的:给模型一段包含大量"干扰文档"(distractor documents)的输入,然后在最关键的位置——开头、中间、或结尾——插入一个真正包含答案的文档。然后问模型一个只有这个"答案文档"才能回答的问题,看模型回答的准确率。
直觉上,如果模型真的"读完"了整个输入,文档位置不应该影响准确率。但实验结果画出了一条U 形曲线:
- 当关键文档放在**输入的最开头**时,准确率最高;
- 当关键文档放在**输入的结尾**时,准确率次之;
- 当关键文档放在**输入的正中间**时,准确率出现断崖式下跌,部分任务上甚至接近随机猜测。
这条 U 形曲线在 7B、13B、70B 三个不同规模的模型上都稳定复现,而且随着上下文长度增加(从 2K 到 8K),中间位置的衰退愈发严重。
下面这张图是原论文的核心结论(我们用 Mermaid 重绘了关键逻辑):
```mermaid flowchart LR A[Input: 大量干扰文档] --> B{关键文档位置} B -->|开头| C[高准确率] B -->|中间| D[准确率断崖式下跌] B -->|结尾| E[次高准确率] C --> F[U 型注意力曲线] D --> F E --> F F --> G[Lost in the Middle 现象] ```
1.1.2 为什么模型会"看不见"中间?
要理解这条 U 形曲线,我们必须稍微深入一下 Transformer 的注意力机制。
在自注意力(self-attention)中,每个 token 通过 Query(Q)、Key(K)、Value(V)三个向量与其他所有 token 交互。具体地,位置 $i$ 的 token 会对位置 $j$ 的 token 计算一个注意力权重:
$$\alpha_{ij} = \frac{\exp(q_i \cdot k_j / \sqrt{d_k})}{\sum_{k=1}^{n} \exp(q_i \cdot k_k / \sqrt{d_k})}$$
注意分母里的求和是对所有位置的 softmax。这意味着,无论上下文有多长,分配给单个 token 的"注意力预算"都必须与所有其他 token 竞争。
当上下文只有 100 个 token 时,每个 token 平均分到 1% 的注意力。但当上下文膨胀到 100,000 个 token 时,理论上的"平均注意力"降到了 0.001%。这种"注意力稀释"(attention dilution)效应是物理层面的——softmax 的归一化机制天然就倾向于让分布"摊平"。
更糟糕的是,相对位置信息会进一步扭曲这个分布。几乎所有现代 LLM 都使用旋转位置编码(RoPE, Rotary Position Embedding)[Su et al., 2021],它的设计让相邻 token 之间的相对位置更容易被学习,而距离越远的 token 对之间的位置信号越弱。一个训练时只见过 8K 上下文的模型,被强行拉到 200K 后,第 100,000 个位置的 token 携带的位置信息几乎等于噪声。
两个效应叠加的结果是:模型在长上下文中,天然倾向于关注开头和结尾的"锚点"(primacy bias 和 recency bias),而对中间部分视而不见。
1.1.3 Lost in the Middle 的后续工作
Liu 的论文在 2023 年引发了连锁反应。Anthropic 在他们关于 Claude 长上下文的研究中复现了类似现象,并发现一个有趣的补救方法:在 prompt 中明确告诉模型"请注意检索所有相关位置"。这条简单的指令可以让中间位置的准确率提升 10-20 个百分点——这说明 Lost in the Middle 至少部分原因是"行为模式"问题,而非纯物理限制。
OpenAI 在 GPT-4 的系统文档中也明确承认了类似现象。Google 在 Gemini 1.5 的技术报告里则提出了一种缓解方案:训练时使用"需要跨文档推理"的任务,让模型不得不注意中间位置。 2024 年,Stanford 的 Xu 等人进一步在多模态长视频理解任务上复现了 Lost in the Middle 现象,表明这不是纯文本任务特有的 bug,而是注意力机制的某种根本性限制。
总结一下,到 2024 年底,社区达成的共识是:
- **Lost in the Middle 是真实存在的、跨模型跨任务的稳定现象。**
- **它的根因是注意力稀释 + 位置编码衰减 + softmax 归一化三者共同作用。**
- **但它不是完全不可逆的:通过 prompt 设计、训练干预、上下文压缩等手段,可以部分缓解。**
---
1.2 上下文的三大物理极限
理解 Lost in the Middle 是"现象",但要真正设计出能在生产环境稳定运行的长上下文系统,我们必须搞清楚支撑这些现象的物理极限。我把它们归为三类:算力、显存、位置。
1.2.1 极限一:注意力计算的算力墙
最朴素的 Transformer 自注意力实现是 $O(n^2)$ 复杂度的——上下文长度翻倍,注意力计算量翻 4 倍。在 2020 年以前,这被视为 Transformer 的"原罪",是它难以处理长序列的根本原因。
2022 年,Tri Dao 发布的 Flash Attention 系列论文 [Dao et al., 2022; Dao, 2024] 通过两个关键技巧(tiling 和 recomputation)将注意力计算的实际复杂度降到了 $O(n)$,同时把 IO 效率拉满。这是一项了不起的工程突破。
但即便如此,长上下文推理的"有效算力"依然随长度线性增长。在 H100 上,一个 7B 模型处理 200K 上下文的注意力计算,需要消耗大约 4-5 秒的纯注意力时间(prefill 阶段),这还没有算上后续的 decode 步骤。对于实时对话场景,这意味着 5 秒的首字延迟——绝大多数用户都无法接受。
更关键的是,当上下文超过 32K 后,延迟的增长开始从线性变成超线性。这是因为虽然 Flash Attention 把核心 matmul 优化了,但 softmax、layer norm、FFN 等其他操作的开销并未等比下降。换句话说,"百万 token 上下文"在宣传上是工程奇迹,在生产部署上则是延迟灾难。
1.2.2 极限二:KV Cache 的显存墙
自回归推理中,每个生成的 token 都依赖于之前所有 token 的 Key 和 Value 向量。这些向量被缓存在 GPU 显存中,称为 KV Cache。
KV Cache 的大小与上下文长度、批大小、模型层数、隐藏维度成正比。具体公式:
$$\text{KV Cache Size} = 2 \times L \times n \times B \times d_h \times \text{precision}$$
其中 $L$ 是层数,$n$ 是序列长度,$B$ 是批大小,$d_h$ 是每头维度,precision 是数据精度(FP16 = 2 字节,INT8 = 1 字节,INT4 = 0.5 字节)。
以 Llama-3-70B 为例:80 层,128 头,隐藏维度 8192,FP16 精度。当上下文为 128K、批大小为 1 时,单个请求就需要约 40GB 的 KV Cache——比模型权重本身还大。如果同时处理 8 个并发请求,KV Cache 占用 320GB,这已经超出了绝大多数单节点的显存能力。
这就引出了一个生产环境中的常见抉择:你是要支持单个超长上下文请求,还是支持多个中等长度请求? KV Cache 是"不可共享"的——每个请求独占自己的缓存,所以长上下文本质上是在用吞吐换能力。
工程上的常见应对包括:
- **Paged Attention**(vLLM 的核心创新 [Kwon et al., 2023]):借鉴操作系统虚拟内存的分页思想,把 KV Cache 切成不连续的小块,显著提升显存利用率。
- **KV Cache 量化**:将 FP16 的 KV 压缩到 INT8 甚至 INT4,通常对质量影响很小,但显存减半或减四分之三。
- **Prefix Caching**:如果多个请求共享相同的 system prompt 前缀,可以把这部分的 KV Cache 复用,省掉重复计算。
- **StreamingLLM** [Xiao et al., 2023]:发现可以"驱逐"中间的注意力缓存,只保留开头和结尾的少量 token,模型质量几乎不下降——这反过来印证了"中间位置天然不重要"的判断。
1.2.3 极限三:位置编码的衰减墙
第三个极限是最微妙的:位置编码本身的"分辨率"。
原始 Transformer 使用绝对位置编码(sinusoidal),后来 RoPE 成为了主流。RoPE 的核心思想是:将位置信息编码为 Query 和 Key 向量的旋转角度。对于位置 $m$ 和位置 $n$ 的两个 token,它们之间的内积会包含 $\cos((m-n)\theta)$ 这种"相对位置信号"。
问题在于,RoPE 的训练位置与推理位置不匹配时,会发生"外推失效"。如果模型在 8K 上下文上训练,强行用到 200K,位置 $(m-n)$ 可能远大于训练时见过的最大差,旋转角度变得毫无意义——模型在那个位置上的 Query/Key 几乎就是随机向量。
社区提出过多种外推方案:
- **Position Interpolation**(PI,[Chen et al., 2023]):把超长位置"压缩"回训练时见过的范围,简单粗暴但有效。
- **NTK-aware scaling**:更精细的插值策略,根据维度调整缩放因子。
- **YaRN** [Peng et al., 2023]:综合考虑频率维度的不同,对高频和低频部分采用不同的缩放策略,是当前 Llama-3 等模型使用的方案。
但所有这些方案都有一个共同前提:模型必须被"再训练"或"微调"过才能用好长上下文。如果直接拿一个 8K 训练的模型跑 200K 的输入,无论你做什么 prompt 工程,质量都会显著下降。
这就解释了为什么 Claude 3.5 Sonnet(200K 训练)和 Llama-3-70B(128K 训练)在 100K+ 上下文任务上的表现差异巨大——这不是模型架构的差异,是训练时见过的位置分布的差异。
1.2.4 三个极限的叠加效应
算力、显存、位置编码,这三个极限不是相互独立的,它们会复合放大:
- 上下文变长 → 算力变贵 → 延迟变高 → 你想用更长的 batch 来摊薄成本 → KV Cache 进一步膨胀 → 显存不够 → 你被迫降精度 → 模型质量下降。
- 上下文变长 → 位置编码外推失败 → 模型对中间位置"看不见" → 你需要在 prompt 里加更多"提醒指令" → 上下文进一步变长 → 雪上加霜。
下面这张图描绘了这三个极限如何叠加:
```mermaid flowchart TD A[上下文变长] --> B1[注意力算力墙] A --> B2[KV Cache 显存墙] A --> B3[位置编码衰减墙] B1 --> C1[延迟升高] B2 --> C2[并发能力下降] B3 --> C3[中间位置注意力塌陷] C1 --> D[被迫降级处理] C2 --> D C3 --> D D --> E[模型整体质量断崖式下跌] E --> F[Context Rot] ```
这张图是本章的"诊断地图"。当你在生产环境遇到上下文相关问题时,问自己:是算力在限制我?是显存不够?还是位置信号已经失效? 这三个问题对应了三种完全不同的优化策略。
---
1.3 Context Rot:上下文长度的"腐烂曲线"
Lost in the Middle 描述的是"位置"对质量的影响。但生产环境中,工程师遇到的更多是"总长度"对质量的影响——我塞了 50K token 进去,模型怎么就崩了?
2024 年,Rhea 等人在 Chroma 的研究博客中提出了一个非常实用的概念:Context Rot(上下文腐烂)[Rhea, 2024]。它指的是:随着上下文长度的增加,模型在多种任务上的准确率并不是线性下降,而是出现"加速腐烂"的现象。
1.3.1 定义 Context Rot
Context Rot 的形式化定义是:
$$\text{Rot}(L) = \frac{\text{Accuracy}(L=0) - \text{Accuracy}(L)}{\text{Accuracy}(L=0)}$$
其中 $L$ 是上下文长度,$\text{Accuracy}(L=0)$ 是无上下文(或最短上下文)时的基线准确率。
如果模型"理想"地使用上下文,$\text{Rot}(L)$ 应该是一个常数(甚至可能略微上升——更多上下文提供更多线索)。但实际测量中,$\text{Rot}(L)$ 是一条指数式上升的曲线:
- $L=1K$:Rot ≈ 5%(几乎无影响)
- $L=10K$:Rot ≈ 15%(可察觉)
- $L=50K$:Rot ≈ 35%(明显问题)
- $L=200K$:Rot ≈ 60%(接近随机)
Rhea 团队在 18 个不同任务(从代码补全到多文档 QA)上测量了 Rot 曲线,发现几乎所有任务都符合这个模式。没有任何任务在 200K 上下文上还能保持基线准确率。
1.3.2 为什么是"腐烂"而不是"遗忘"?
我喜欢 "Rot" 这个比喻,因为它比 "Forgetting" 更准确。Forgetting 暗示有信息被主动丢弃,但 Context Rot 的本质是信号被噪声淹没。
具体来说,长上下文中至少有以下几类"噪声源":
1. 无关 token 稀释注意力(Lost in the Middle 的算力版); 2. 干扰信息(distractor)反向激活——RAG 召回的 chunk 里有些是"看似相关但实际误导"的; 3. 指令模糊化——系统提示词被淹没在海量历史对话中,模型"忘记"了它该遵守的格式; 4. 格式漂移——长上下文中混入 JSON、Markdown、代码块等多种格式,模型的输出格式容易跑偏。
这四类噪声叠加在一起,让长上下文变成了"信号-噪声比急剧下降的信道"。
1.3.3 测量你自己的 Context Rot
Context Rot 的可怕之处在于它是模型相关、任务相关、上下文内容相关的。Llama-3-70B 在 100K 时的 Rot 和 Claude 3.5 在 100K 时的 Rot 是不同的;问答任务和代码补全任务的 Rot 曲线也是不同的;塞 100K 行业文档和塞 100K 闲聊对话,效果也完全不同。
这意味着你不能依赖论文里的数字,必须在自己的应用上测量。 本章的实践部分会提供一段 Python 脚本,帮你系统化地测量自己的模型在特定任务上的 Context Rot 曲线。
---
1.4 "更多上下文"是税负,不是礼物
讲完了物理极限和腐烂曲线,我们现在可以直面一