上下文工程概述:为什么LLM需要“记忆”
第 1 章:上下文工程概述:为什么 LLM 需要“记忆”
当我们与一位新朋友交谈时,如果对方在对话中途突然问你"我们之前聊的是什么",我们会感到困惑甚至恼怒——因为在正常的社交场景中,我们都默认对方具备基本的记忆能力,能够记住对话的历史和关键信息。然而,当用户与一个基于大型语言模型(Large Language Model,简称 LLM)的应用进行交互时,这种"健忘"却成为了常态。用户在第一轮告诉客服机器人的需求,到了第五轮就被"遗忘"了;用户上传的长文档,在对话中途突然"消失"了;明明在前几轮已经确定的任务目标,到后来却变得面目全非。这些问题的根源,并非 LLM 本身不够智能,而是我们还没有学会如何为 LLM 构建一套有效的"记忆系统"。这就是上下文工程(Context Engineering)所要解决的核心问题。
本章作为全书的开篇,将首先从技术原理层面解释为什么 LLM 会"忘记"之前的信息——这涉及到 Transformer 架构的注意力机制和上下文窗口的物理限制。然后,我们将正式给出上下文工程的定义,并详细解析它的四大核心支柱:上下文长度管理、信息压缩与摘要、外部记忆系统、状态管理。接着,我们会通过对比传统 AI 应用开发与 LLM 应用开发的差异,帮助读者理解为什么上下文管理在后者的开发实践中占据如此核心的地位。最后,本章将以一个典型的失败案例作为反面教材,直观展示缺乏上下文工程设计会导致哪些具体问题,并由此引出后续章节的学习路径。
1.1 大型语言模型的“健忘症”:从日常生活到技术本质
1.1.1 一个我们都熟悉的场景
想象这样一个日常场景:你在一家餐厅用餐,服务员热情地为你点餐。你先点了前菜,然后开始主菜的讨论。你告诉服务员你不喜欢太辣的食物,希望酱汁能清淡一些,并且提到你对某种食材过敏。当你详细描述完这些饮食偏好后,服务员在记录本上潦草地写了几笔。终于轮到你点甜点了,你询问甜点有哪些选择——就在这时,服务员翻到新的一页,然后满脸困惑地问你:"请问您之前点了什么来着?"
这显然是荒谬的场景。真实世界中的服务员会带着那本记录本,记住你整桌人点的所有菜品,并在上菜时准确地将前菜、主菜和甜点按顺序端上来。人类的记忆虽然有其局限性,但我们已经进化出了一套完整的工作记忆和长期记忆系统,能够在特定的场景中保持信息的连续性。
现在,将这个场景迁移到与 LLM 应用的交互中。如果你曾经使用过 ChatGPT、Claude 或其他 LLM 产品进行过长对话,你很可能遇到过类似的情况:当你讨论到第十轮、第十五轮话题时,模型开始"忘记"你在第一轮提到的关键信息;当你上传一份长文档后,要求模型总结其中某个细节,结果模型给出了一个完全错误的答案;或者当你要求模型"继续我们刚才讨论的内容"时,它茫然地表示不知道你在说什么。
这些现象并非模型本身的缺陷,而是由 LLM 的底层架构和运行机制所决定的。要理解这一点,我们需要深入到 Transformer 架构的核心原理中。
1.1.2 Transformer 架构:上下文窗口的限制
理解 LLM 的"健忘症",首先需要理解它的"记忆"机制与人类记忆的本质区别。LLM 的"记忆"与我们日常所说的记忆有着根本性的差异。人类的记忆是一种高度复杂的信息存储和检索系统,它能够将信息编码、存储在神经网络中,并在需要时进行检索。更重要的是,人类能够主动选择记住什么、忘记什么,并且能够将不同时间获得的信息进行整合,形成连贯的认知。
然而,LLM 的工作方式与此截然不同。当代主流的大型语言模型几乎都基于 Transformer 架构。Transformer 的核心创新是自注意力机制(Self-Attention),它允许模型在处理当前输入时,关注输入序列中的任意位置。这种机制使得模型能够理解文本中的长距离依赖关系——比如一句话的开头和结尾可能存在语法上的关联——这是早期循环神经网络(RNN)难以解决的问题。
但 Transformer 有一个关键的技术约束:上下文窗口(Context Window)。上下文窗口指的是模型在单次推理(即生成一次回复)时能够"看到"的最大文本量。这个量通常以 token 为单位计量,而 token 可以粗略理解为单词或汉字的片段。如果将上下文窗口比作一块固定大小的黑板,那么当你在上面写满了内容后,想要写新的内容就必须擦掉一些旧的内容——而这个"擦除"是完全不可控的、超出窗口的历史信息会完全丢失。
让我们用具体数字来理解这个问题。截至本书撰写时(2024 年),主流 LLM 的上下文窗口大小如下:OpenAI 的 GPT-4 Turbo 支持约 128,000 个 token,Anthropic 的 Claude 3 支持高达 200,000 个 token。然而,这些是理论最大值。在实际应用中,很多 API 调用和开源模型的上下文窗口要小得多——常见的配置是 4,096、8,192、32,000 等。这意味着,如果你的对话历史、检索文档和系统提示加起来超过了窗口限制,超出部分的信息就会完全丢失。
这里有一个关键的区别需要澄清:Transformer 的注意力机制在技术上允许模型"关注"上下文窗口内的任意位置,但上下文窗口本身有物理限制。每次 API 调用时,你需要将当前输入和所有之前的对话历史拼接在一起发送给模型。当这个拼接结果超过上下文窗口大小时,模型就看不见超出部分的内容了——这就像是一本被撕掉了后半部分的书,读者无论如何都无法知道缺失部分写了什么。
1.1.3 无状态特性:每次对话都是独立的
除了上下文窗口的限制之外,理解 LLM "健忘症"的另一个关键概念是无状态(Stateless)特性。当我们在开发传统应用程序时,程序的状态(比如变量的值、数据库中的记录)可以在多次请求之间保持。你登录一个网站,服务器会记住你已经登录,并在后续请求中识别你的身份。这是通过会话管理、数据库存储等技术实现的。
然而,LLM 本身是无状态的。当你调用一次 LLM 的 API,模型本身并不保存任何关于这次对话的信息。它只是一台强大的"文本生成器":接收一段文本作为输入,输出一段文本作为响应,然后"遗忘"刚才发生的一切。下一次调用 API 时,如果不主动将对话历史作为输入传递,模型就不知道之前的任何内容。
这意味着,在 LLM 应用中,对话历史必须由开发者主动管理。每次 API 调用时,你都需要把之前所有的对话内容作为输入的一部分发送出去。LLM 应用实际上是这样工作的:
```mermaid flowchart LR subgraph 开发者侧["开发者需要管理的内容"] A["对话历史"] --> B["构建上下文"] C["用户当前输入"] --> B D["系统提示词"] --> B E["外部知识"] --> B end
B --> F["发送给 LLM 的完整输入"] F --> G["LLM API"] G --> H["LLM 回复"] ```
这张图揭示了一个传统编程中从未存在过的复杂性:作为开发者,你必须决定把什么内容放进上下文中,如何组织这些内容,以及当内容超出窗口限制时应该删除哪些部分。这不再是"写代码让计算机执行"的模式,而是"提供上下文让模型推理"的模式。上下文的质量直接决定了输出的质量。
1.1.4 上下文窗口限制的实际影响
让我们通过一个具体例子来理解上下文窗口限制在实践中的影响。假设你正在开发一个代码审查助手,用户会粘贴一段代码,助手会分析其中的问题并提供改进建议。在代码审查的过程中,用户可能会:
- 第一轮:粘贴一个 500 行的源代码文件,要求审查
- 第二轮:粘贴第二个 400 行的文件,说明两个文件的依赖关系
- 第三轮:粘贴第三个 300 行的文件,讨论三个模块之间的交互
- 第四轮:要求总结之前所有文件的共性问题
在这个场景中,如果我们不加任何处理地将所有内容放入上下文窗口,很快就会达到限制。假设每个 token 平均对应约 0.75 个汉字或 0.6 个英文单词,1000 个 token 大约等于 750 个汉字或 600 个英文单词。那么:
- 第一次输入:500 行代码 ≈ 4,000 token
- 加上系统提示词和一些额外上下文:≈ 5,000 token
- 第二次、第三次输入后,累计历史:≈ 13,000 token
- 继续对话若干轮后,很可能超过 32,000 token 的限制
当对话继续进行到一定程度时,模型开始"忘记"早期粘贴的代码内容。这会导致用户在第四轮询问"三个文件共有的问题时",模型可能只记得最近粘贴的文件,而无法准确回答关于第一个文件的问题。
更糟糕的是,这种"遗忘"是非确定性的。模型不会明确告诉你"我忘记了第一段代码",而是用一种更隐蔽的方式影响输出:它可能会捏造一个不存在的问题,或者给出与实际代码不符的建议。这种错误在代码审查这种需要高准确性的场景中,后果尤为严重。
上下文窗口限制带来的挑战是多方面的。首先是信息丢失:当历史记录超过窗口大小时,早期内容被完全删除,模型无法访问。其次是成本增加:更大的上下文窗口意味着更多的 token 消耗,而 LLM API 的计费通常是按 token 数量收费的。第三是性能下降:研究表明,当上下文变得过长时,模型在"中间位置"的内容理解能力会下降,这被称为"中间迷失"(Lost in the Middle)问题。最后是一致性破坏:当关键信息在对话中丢失时,模型的行为会变得不一致,损害用户体验。
1.2 上下文工程的定义与四大支柱
1.2.1 什么是上下文工程
面对 LLM 的这些特性,开发者需要一套系统化的方法来管理输入上下文,确保模型能够在有限的窗口内获取足够且相关的信息。这正是上下文工程所研究的核心问题。
上下文工程(Context Engineering) 是指在 LLM 应用开发中,系统化地管理、压缩、检索和同步上下文信息的理论与实践方法。它涵盖了从提示词(Prompt)设计到外部记忆系统构建的完整技术栈,旨在解决 LLM 应用开发中面临的信息管理挑战。
理解上下文工程需要将它与两个相关概念进行区分。首先是提示工程(Prompt Engineering),后者更侧重于单次交互中的提示词设计和优化,关注的焦点是如何写好一条指令。而上下文工程则涵盖了更广的范围,包括如何管理多轮对话的历史、如何在长文本中定位相关信息、如何在应用重启后恢复状态等。其次是记忆系统(Memory System),这通常是上下文工程的一个子系统,而不是全部。上下文工程不仅包括记忆,还包括压缩、检索、状态同步等更丰富的内容。
上下文工程的出现标志着 LLM 应用开发进入了一个新的成熟阶段。在这个阶段,开发者不再满足于"让模型能够回答问题",而是追求"让模型在复杂场景下可靠、准确、一致地回答问题"。这种追求促使开发者必须系统性地思考和应用上下文管理技术。
1.2.2 上下文工程的第一支柱:上下文长度管理
上下文长度管理是上下文工程的第一个支柱,也是最直接的挑战。当对话历史、系统提示、用户输入和外部知识的总量超过模型上下文窗口时,必须做出选择:保留什么,放弃什么?
滑动窗口(Sliding Window) 是最基础的长度管理策略。其核心思想是始终保持最近 N 条消息进入上下文,而丢弃更早的历史。例如,如果设置窗口大小为 10 条消息,那么当对话进行到第 15 条消息时,第 1-5 条消息会被丢弃,只有第 6-15 条消息会进入上下文。
滑动窗口的优点是实现简单、计算成本低。但它的缺点也很明显:早期的重要信息(如用户的核心需求、项目背景等)可能被过早丢弃。考虑这样一个场景:用户在前三轮对话中明确说明了项目背景和约束条件,这些信息在后续分析中被反复引用,但如果它们被滑动窗口丢弃,后续的分析就会失去必要的上下文。
为了解决滑动窗口的局限性,开发者可以采用重要性加权策略。这种方法为对话历史中的每条消息分配一个重要性分数,优先保留高分数的内容。分数的判定可以基于多种信号:
- **关键词匹配**:如果当前任务涉及某个特定领域,而某条历史消息包含该领域的关键词,它的重要性分数就应该更高。
- **时间衰减**:近期消息通常比早期消息更重要,但某些早期消息(如项目启动时的需求说明)可能需要特殊标记以维持重要性。
- **用户反馈**:根据用户对回复的满意度反馈(点赞/踩、有用/无用标记),调整相关历史消息的重要性。
分层策略(Hierarchical Strategy) 是一种更复杂但更强大的方法。它不是简单地在完整消息和丢弃之间二选一,而是将信息组织成多个抽象层级。例如,对于一段长对话,可以生成一个摘要作为高层信息保留,同时保留最近的若干条原始消息作为细节信息。当上下文空间有限时,先用摘要填充;空间充裕时,再添加更多细节。
```mermaid flowchart TD A["原始对话历史\n(共50轮对话)"] --> B["分层压缩"] B --> C1["高层:对话摘要\n(用户意图、项目背景)"] B --> C2["中层:话题片段\n(每10轮一个摘要)"] B --> C3["底层:最近消息\n(最近5轮原始消息)"]
C1 --> D["送入上下文的压缩内容"] C2 --> D C3 --> D ```
这种分层结构的关键优势在于,它能够在有限的上下文中保留尽可能多的语义信息。即使丢弃了原始消息,高质量的摘要仍然能够传达对话的核心内容。
1.2.3 上下文工程的第二支柱:信息压缩与摘要
信息压缩与摘要是上下文工程的第二个支柱,它解决的是"如何在有限空间内保留最多有效信息"的问题。当对话历史、用户上传的文档、系统提示等内容的原始形式无法全部进入上下文时,我们需要用更紧凑的形式表示同样的语义。
动态摘要(Dynamic Summarization) 是最常用的压缩技术之一。当新的对话内容加入上下文时,系统会自动生成一个摘要来替代原始内容。例如,如果用户在前三轮对话中详细描述了一个技术问题,摘要可能是:"用户报告了登录模块在并发场景下的数据竞争问题,已提供相关日志和代码片段,正在讨论解决方案 A 的可行性。"
一个好的动态摘要应该满足以下标准:
- **覆盖关键实体**:人名、项目名、订单号、产品型号等技术实体应被保留。
- **捕捉意图和状态**:用户的核心需求是什么?当前讨论进展到什么程度?有哪些悬而未决的问题?
- **指明关系**:谁和谁是什么关系?哪个问题是哪个问题的原因?
- **可验证性**:摘要中的断言应该能够在后续对话中得到验证或补充。
生成摘要的技术方案有多种选择。一种是使用 LLM 本身来生成摘要——在对话的适当时机(如每次用户输入前),调用一次 LLM 来总结之前的对话,并把这个摘要放入上下文。这种方法简单直接,但会消耗额外的 token 和 API 调用成本。另一种方案是使用更小、更快的专门模型来进行摘要,这种方案成本更低,但质量可能不如使用顶级 LLM。
结构化存储(Structured Storage) 是另一种重要的压缩思路。它不是将信息以自然语言形式存储,而是将其转化为结构化的数据格式。例如,将用户信息存储为 JSON 格式,将对话状态存储为状态机形式,将知识库存储为知识图谱。这种转换使得相同的信息可以用更少的 token 表示,同时也便于后续的检索和更新。
考虑一个客服场景的对比。如果用自然语言记录用户信息:"用户张先生,VIP 会员,注册于 2023 年 5 月 15 日,总消费金额为 12,580 元,历史订单数为 23 单,最近一次购买是 2024 年 1 月 10 日购买了一件灰色 M 码卫衣,收货地址是上海市浦东新区陆家嘴环路 1000 号。"这段描述大约需要 120 个 token。
如果用结构化 JSON 表示:`{"user_id": "U1001", "name": "张先生", "tier": "VIP", "member_since": "2023-05-15", "total_spend": 12580, "order_count": 23, "last_order": {"date": "2024-01-10", "item": "灰色M码卫衣", "address": "上海市浦东新区陆家嘴环路1000号"}}`这段 JSON 约 200 个字符,在 token 计数上与自然语言描述相近。但如果我们将字段名简化(如用 `n` 代替 `name`,用 `s` 代替 `spend`),并使用数值 ID 替代文本,结构化表示可以显著减少 token 数量。
更重要的是,结构化存储便于程序化处理。当需要查询用户等级时,直接读取 JSON 字段比让 LLM 从自然语言描述中提取要可靠得多。
1.2.4 上下文工程的第三支柱:外部记忆系统
当上下文窗口的限制变得无法逾越时,开发者需要跳出上下文窗口本身来寻找解决方案。这就是外部记忆系统的用武之地——它将 LLM 的"记忆"从有限的上下文窗口扩展到无限的外部存储中。
外部记忆系统(External Memory System) 的核心思想是:将需要长期保存的信息存储在外部(如数据库、向量数据库、键值存储等),在需要时检索相关的内容注入上下文。与模型内部的上下文不同,外部存储的容量理论上可以是无限的,并且可以在不同的对话会话之间共享。
一个典型的外部记忆系统的工作流程如下:
1. 写入阶段:当对话中产生重要信息(如用户的偏好设置、已确认的任务目标、讨论过的关键事实)时,系统将这些信息提取出来,存入外部数据库。 2. 索引阶段:为了后续能够快速检索,系统会为每条记忆生成索引。对于文本信息,通常使用向量嵌入(Embedding)技术将文本转化为高维向量,以便进行语义相似度搜索。 3. 检索阶段:当用户发起新请求时,系统分析当前上下文,生成检索query,从外部记忆中获取与当前任务最相关的记忆。 4. 注入阶段:将检索到的记忆内容格式化后加入上下文,使模型能够"想起"相关的信息。
```mermaid flowchart LR A["对话进行中"] --> B{"检测关键信息"} B -->|是新信息| C["写入外部记忆"] B -->|需要回忆| D["检索外部记忆"]
C --> E["向量数据库"] D --> F["语义检索"] F --> E
E --> G["格式化后注入上下文"] G --> H["LLM 生成回复"] ```
外部记忆系统的一个重