迷路的对话者——当模型失去语境
迷路的对话者——当模型失去语境
2023 年秋天,某头部电商的智能客服团队收到了一份令人困惑的工单:一位用户在第三次追问时,模型忽然"忘了"自己在干什么。用户先说自己买了冰箱,询问配送时间,客服模型礼貌地答了;接着用户报上订单号尾号 4 位,模型核对了地址;等到用户第三次询问"那台冰箱如果我不想要了能不能退"时,模型却回了一句完全无关的"请问您要咨询哪个商品"。截图在团队群里传开时,所有人的第一反应都是——这一定是 prompt 写错了。
但诡异之处在于,单看第三轮对话本身,它在语法上毫无破绽,甚至连礼貌用语都挑不出毛病。问题出在它没有携带任何上一轮的痕迹:它知道用户在说话,却不知道用户在说什么。那台冰箱、那个订单号、那笔订单的状态——这一切仿佛从未存在过。
这不是个例。在 GitHub、Stack Overflow 与各大厂的内部故障库里,类似的"模型失忆症"被反复记录:会议助手在 20 分钟后忘了议程、代码 Copilot 在长文件里忘了函数签名、法律 AI 在读完 80 页合同后答错了条款号。它们的共同特征是——模型并非"不够聪明",而是在某个临界点之后,它在对话里"迷路"了。
要理解这种迷路,我们必须回到一个根本问题:大模型拥有成百上千亿参数,按理说"记住"几千个 token 应当轻而易举,为什么它在长上下文里反而像金鱼一样只有 7 秒记忆?
答案藏在三个互相咬合的齿轮里:token 的物理边界、注意力机制的算术瓶颈、以及位置编码对长距离依赖的结构性遗忘。这三者共同构成了 LLM 上下文的"三重门",而每一道门都在静悄悄地把信息从模型的"工作记忆"中驱逐出去。
1.1 一个真实的生产事故:当大模型"忘了"它在和谁说话
让我们先把那个电商客服案例还原完整。
当时该团队使用的是某主流商用大模型 API,标注的上下文窗口是 32K token。整段三轮回合的累计 token 数,包括 system prompt、用户输入与模型回复,拢共不到 800 tokens——离 32K 的上限差了将近 40 倍。按任何朴素直觉,这样的对话对模型来说应当毫无压力。
但模型偏偏在第三轮翻了车。事后排查时,工程师把完整的多轮对话重新塞回模型,让它复述用户的核心诉求。模型的回答是:"用户似乎想咨询某件商品的配送问题。"——它把订单号、退货意图、订单状态全都丢掉了,只留下了一个被高度稀释的"语义雾气"。
进一步的诊断实验揭示了一个更让人不安的事实:即便把第三轮单独抽出、只给它第一轮的内容,模型给出的回答几乎一样。这意味着对模型而言,第三轮的输入在语义层面没有被有效编码进它的"决策"。它读了,但它没有"记住"。
这个事故是本书写作的起点,也是我接下来每一章都会反复回到的现场。它的特殊性在于——它不是由超出窗口长度引起的,而是典型的"窗口内失忆"。当窗口长度不再是限制时,模型的"上下文能力"会突然变得脆弱吗?答案是肯定的。
为了理解这一点,我们需要先承认一个反直觉的事实:上下文窗口不是一个均质的"记忆空间",而是一个充满了陷阱的剧场。即便所有 token 都进入了窗口,它们在模型注意力里的"戏份"也截然不同。某些 token 会被放在聚光灯下,某些会被推到侧幕,还有些——尤其是位于"中段"的——会被直接打入冷宫。
这个现象后来被一篇经典论文命名为 "Lost in the Middle"。但在讲它之前,我们先把物理基础搭起来——为什么"窗口"本身就是个有限的概念。
1.2 上下文窗口的物理边界:Token、显存与注意力的三角制约
要理解 LLM 的上下文为什么不能无限大,必须先承认一个工程事实:模型在处理一段文本时,几乎所有关键计算都与序列长度的平方成正比。
自回归 Transformer 的核心是注意力机制(attention),它在每一层、每一个位置上都要计算当前 token 与序列中所有之前 token 的相关性。这个相关性的计算量是 $O(n^2 \cdot d)$,其中 $n$ 是序列长度,$d$ 是隐藏层维度。也就是说,序列长度翻一倍,计算量翻四倍。这不是软件优化能轻易绕开的硬约束——它是矩阵乘法的本质。
但显存约束更为致命。在推理阶段,KV cache 必须为每一个 token、每一层、每一个注意力头保留 $2 \cdot d_{head}$ 个数值(K 与 V 各一份)。当上下文从 4K 扩到 128K,KV cache 的显存占用要扩大 32 倍。一个拥有 70B 参数、32 层、40 个注意力头的模型,仅 KV cache 就可能吃掉上百 GB 的显存——这是单机部署几乎不可能承受的代价。
让我们用一个具体数字把这件事钉死在墙上。假设模型有 70B 参数,序列长度 128K,每层 KV cache 大小为 $2 \times 128K \times 128 \times 80 \times 2 \text{ bytes} \approx 5 \text{ GB}$,如果有 80 层,那就是 400 GB——这不是单个 GPU 能装下的。所以当厂商声称支持"128K 上下文"时,我们必须追问:是真的能跑 128K,还是说"理论上能塞进 128K,但跑起来慢到无法服务"?
更隐蔽的问题是:算力与显存不是同一件事。即便显存勉强塞得下,注意力计算的延迟也会随长度二次增长。当一个请求的延迟从 200ms 涨到 4 秒,即便答案正确,业务也无法承受。这也是为什么 GPT-4、Claude、Gemini 在宣传 200K、1M 上下文时,都附带了"成本更高、速度更慢"的免责说明。
这就是上下文工程的第一道门:物理边界。
很多初学者以为"上下文窗口"是一个简单的上限,其实它是三条约束的合力:
```mermaid flowchart LR A[序列长度 n] --> B[KV Cache 显存<br/>O n] A --> C[注意力计算<br/>O n²] A --> D[位置编码精度<br/>O log n] B --> E[上下文窗口的物理边界] C --> E D --> E ```
> 图 1-1:上下文窗口的三角制约。序列长度同时被显存、算力与位置编码的精度所限制,这三条约束共同决定了"实际可用窗口"远小于理论最大值。
但物理边界只是故事的开端。更让人不安的是——即便所有 token 都"装进了"窗口,模型对它们的处理依然是不平等的。这就引出了第二道门:注意力的钟形分布。
1.3 Lost in the Middle:注意力的钟形分布与中段塌陷
2023 年,斯坦福与 Samaya AI 的研究团队发表了一篇影响深远的论文——"Lost in the Middle: How Language Models Use Long Contexts"。他们的实验设计相当精巧:在给定一段长文档后,向模型提问某个具体信息;这个信息被分别放置在文档的开头、中段、结尾。如果模型真的"读完了"整段文档,那么答案应当与位置无关。
但实验结果令人震惊。模型的表现呈现出一个典型的 U 型曲线:
- 信息在**开头**时,准确率最高。
- 信息在**结尾**时,准确率也很高(这是因为 attention sink 现象,最后几个 token 总能获得不成比例的关注)。
- 信息在**中段**时,准确率出现**断崖式下跌**。
最让人沮丧的是,这种下跌并非由"信息量过大"引起。把文档长度从 2K 扩到 8K,准确率在中段几乎线性下降。这意味着,模型在长上下文里越努力,反而越容易"漏看"中间的内容。
为什么会这样?
一个比较扎实的解释来自训练数据的分布。绝大多数预训练数据是短文本——网页片段、对话、问答——它们的平均长度不过几百到几千 token。模型在训练中从未见过"在第 5000 个 token 处精确回忆某个事实"这样的任务。它学会的是:开头和结尾最可能包含关键信息,中间往往是"过渡段"。
另一个解释与因果注意力掩码(causal mask)有关。在自回归推理中,每个位置只能看到它之前的内容。如果某个关键信息被埋在第 3000 个 token 处,那它要在第 3001 个 token 处才开始"发挥作用",但此时已经有 3000 个其他 token 在争抢注意力。这种累积竞争导致中段信息的"信号"被稀释。
还有一个更工程化的解释:RoPE(Rotary Position Embedding)等位置编码对不同位置赋予了不同的"频率"。低频分量保留全局信息但分辨率低,高频分量保留局部细节但衰减快。当一个事实位于中段时,它既没有获得"开头特权"(系统提示与早期指令通常占据开头),也没有获得"结尾特权"(最末几个 token 总是被 attention sink 锚定),只能漂在中间被其他内容挤压。
这给我们带来一个反直觉但极为重要的工程教训:
> 在 LLM 的"上下文剧场"里,开头与结尾是舞台,中间是观众席。把关键事实放在中段,相当于让主角站在观众席里演独角戏。
这个发现彻底改变了 prompt 工程的面貌。在 Lost in the Middle 之前,很多工程师把上下文当成一段线性空间,谁前谁后无所谓;在这之后,位置本身成了一阶的设计变量。我们需要把关键信息"塞进开头或结尾",或者用特殊的标记把它"顶"到舞台中央。
但 Lost in the Middle 解释的是"为什么中段被忽略",而位置编码则解释了"为什么长距离依赖本身就难以建模"。让我们把镜头推向这个更深层的问题。
1.4 位置编码的隐性暴政:长距离依赖的结构性遗忘
Transformer 架构本身是置换不变的(permutation invariant)——把同一段 token 重新打乱,模型在数学上无法区分。要让模型"感知顺序",必须注入位置信息。位置编码就是干这件事的。
最早的位置编码是绝对位置编码(Absolute Positional Embedding),把每个位置映射成一个固定向量,加到 token embedding 上。它的好处是简单,坏处是——模型在训练中见过的位置是有上限的。一旦推理时的位置超过训练时的最大位置,模型就进入了"未被训练的空间",表现可能雪崩式下跌。
更现代的方案是 RoPE(Rotary Position Embedding),它把位置信息编码进注意力计算本身:通过让 Query 与 Key 向量在复平面上旋转不同的角度,让它们的内积自然地依赖于相对位置而非绝对位置。RoPE 的精妙之处在于它能让模型"看清"相对距离,但代价是不同距离的"清晰度"是不同的。
具体来说,RoPE 把每个位置编码成多个频率的旋转。在低频维度上,相邻位置几乎不旋转,模型能感知到"这两个 token 离得很远"这种粗粒度信息;在高频维度上,相邻位置旋转角度大,模型能精细区分"这两个 token 一前一后"。但这里有个关键细节:频率的衰减与距离的对数成正比。
这意味着什么?意味着当两个 token 的距离变得很大时,它们在低频维度上的差异会被"折叠"在一起——相对位置变成了一个粗粒度的桶。RoPE 越长,桶越宽,距离信息越模糊。换句话说,RoPE 在本质上对长距离依赖是"近视"的:它能告诉你"这两个东西隔得很远",但很难精确告诉你"它们隔了多远"。
另一个常见方案是 ALiBi(Attention with Linear Biases)。它直接给注意力分数加上一个与距离成正比的负偏置:距离越远,注意力越被压制。这是一种强先验,等价于在告诉模型"距离越远越不重要"。ALiBi 的好处是长度外推能力极强(因为它没有"位置编码外推"的灾难),坏处是——它几乎是把"中段塌陷"写进了模型里。
把这些机制放在一起看,我们会得到一个让人清醒的图景:
```mermaid flowchart TD A[位置编码的三大范式] --> B[绝对位置编码<br/>固定位置向量] A --> C[RoPE<br/>复平面旋转] A --> D[ALiBi<br/>线性距离偏置] B --> E[长度外推灾难] C --> F[低频折叠 = 远距离模糊] D --> G[距离衰减 = 中段惩罚] E --> H[Lost in the Middle 的结构性根源] F --> H G --> H ```
> 图 1-2:位置编码如何塑造了 Lost in the Middle。无论采用哪种主流方案,长距离依赖都被结构性地弱化了——绝对编码会"越界",RoPE 会"模糊",ALiBi 会"惩罚"。这不是训练不够,而是数学本身的代价。
这就是为什么"把窗口扩到 128K、256K、1M"并不能根本解决问题——窗口变大了,但位置的"分辨率"未必变大。一个 256K 上下文但 RoPE 配置不当的模型,在处理第 200K 个 token 时,可能比一个 32K 上下文的模型更糟糕。
到这里我们已经看清了三道门中的两道:物理边界与位置编码。第三道门,则是模型本身的认知结构——它没有人类那种分层的记忆系统。让我们转向这个比较。
1.5 人类记忆 vs LLM 工作记忆:认知分层的启示
人类在对话中的"记忆"远比 LLM 复杂。经典的 Atkinson-Shiffrin 模型把记忆分成三层:
- **感觉记忆**(sensory memory):瞬间的感官印象,毫秒级衰减。
- **短期记忆**(short-term memory / working memory):能保持 7±2 个项目的"工作台",持续 15-30 秒。
- **长期记忆**(long-term memory):近乎无限的归档系统,通过索引与关联检索。
更精细的现代模型还加入了情景记忆(episodic memory,特定事件的记忆)、语义记忆(semantic memory,事实与概念)、程序记忆(procedural memory,技能)。这五种记忆相互协作:短期记忆是工作台,长期记忆是仓库,情景记忆让我们能"重访"过去,语义记忆让我们能"理解"现在。
对比之下,LLM 的"记忆"在架构上几乎是扁平的。它只有一个单一的上下文缓冲区(context buffer)——一段线性 token 序列。所有信息——系统提示、用户历史、模型回复、外部检索结果、工具调用记录——都被压平进这个缓冲区,不存在"分层"。
这种扁平带来了三个致命的工程后果:
第一,没有"自动归档"机制。人类的工作记忆溢出时,会自动把不重要的内容转储到长期记忆。但 LLM 的"溢出"是——直接被截断或被遗忘,没有任何缓冲。
第二,没有"检索机制"。人类的长期记忆是通过关联索引(associative indexing)检索的:你提到"冰箱",就会自动激活所有与"家电"相关的记忆。LLM 没有这种索引——它只有"在 prompt 里塞进去"这一种"检索"方式。
第三,没有"优先级机制"。人类的短期记忆有"中央执行系统"(central executive)来决定哪些信息值得关注,哪些可以忽略。LLM 的注意力机制是一种全局软注意力(global soft attention),所有 token 都会被计算一个分数,但分数的分布极易被少数高信号 token 主导。
这种对比给我们带来一个关键洞察:
> LLM 没有"记忆系统",它只有"工作记忆"。而人类之所以能在长时间对话中不迷路,正是因为我们有"工作记忆 + 长期记忆 + 检索"的完整闭环。
要把 LLM 的"单层工作记忆"改造成类似人类的多层系统,我们必须在架构之外用工程手段补齐这些缺失的层次。这就是上下文工程的真正任务。
1.6 上下文工程的五大命题:长度、压缩、摘要、检索与状态
把前面所有的分析综合起来,我们可以提炼出上下文工程必须面对的五大工程命题。这五个命题不是互斥的选项,而是一个系统的不同组件——就像人类记忆系统的不同部分。
命题一:长度(Length)——扩展窗口
最直接的思路是把窗口变大。从 4K 到 32K、128K、200K、1M,每一次扩展都被宣传为"重大突破"。但正如我们在 1.4 节看到的,单纯扩大窗口并不能解决位置编码的分辨率问题。扩展窗口的有效做法是配套优化——改进 RoPE 的频率配置、引入稀疏注意力(如 Longformer、BigBird)、或者使用状态空间模型(Mamba)等新架构。
工程建议:把窗口扩展视为必要但不充分的条件。如果不配合其他四个命题,单纯扩窗口是"用更多的钱换更慢的速度"。
命题二:压缩(Compression)——在窗口内腾出空间
如果窗口大小有限,那就让每个 token 承载更多信息。这可以通过以下技术实现:
- **Prompt 压缩**:用 LLMLingua、Selective Context 等工具把冗余 token 删除。
- **结构化表达**:用表格、JSON、Markdown 等结构替代自然语言,能大幅减少 token 数。
- **代码化指令**:用伪代码或 DSL 替代自然语言描述。
压缩的本质是用语义密度换 token 数量。一个 200 token 的精炼指令可能胜过 800 token 的自然语言描述。
命题三:摘要(Summarization)——用"记忆中的记忆"取代原始历史
当对话历史过长时,可以周期性地把早期内容压缩成摘要,把摘要塞回上下文。常见的策略包括:
- **滚动摘要**:每 N 轮生成一次摘要,旧的对话被摘要替代。
- **分层摘要**:保留最近 K 轮原文,再之前的对话用多级摘要表示。
- **主题摘要**:按话题分组,每个话题一个摘要。
摘要的风险是信息损失——一旦原文被摘要,细节就再也找不回来。所以摘要工程的核心是"哪些细节必须保留,哪些可以丢弃"。
命题四:检索(Retrieval)——把长期记忆外置
既然 LLM 没有真正的长期记忆,那就用向量数据库外部化一个。RAG(Retrieval-Augmented Generation)就是这个思路:用户的 query 触发一次相似度检索,从外部知识库里找到相关片段,再塞进 prompt。
RAG 的关键不是"塞进去"这一动作,而是检索质量。这又涉及 chunking 策略、embedd