上下文工程概述:理解大语言模型的记忆机制
上下文工程概述:理解大语言模型的记忆机制
为什么大语言模型需要“记忆”
当我们与 ChatGPT 进行一次长对话时,可能会遇到这样的困惑:模型似乎“忘记”了三十分钟前讨论的内容,或者在处理长文档时突然丢失了关键信息。更令人困惑的是,有时模型能够准确回忆遥远上下文中的一句无心之语,有时却对刚刚明确告诉它的内容“视而不见”。这种看似矛盾的行为并非模型的“性格缺陷”,而是由大语言模型处理信息的根本机制所决定的。
理解这一机制,是掌握上下文工程的第一步。
大语言模型的能力边界本质上由其上下文窗口决定。上下文窗口可以理解为模型在单次推理过程中能够“看到”的全部信息——包括用户输入的提示词、系统指令、对话历史以及模型生成的回复。与人类能够灵活调用不同层次的记忆不同,模型的“记忆”完全依赖这个固定大小的窗口。一旦对话内容超出这个窗口,模型就会“遗忘”超出部分的信息。
这一限制带来了一系列连锁反应。当我们需要模型处理长篇文档时,必须决定如何将文档切分、如何组织上下文结构、如何在有限空间内保留最关键的信息。当我们需要模型执行多轮复杂任务时,必须设计对话策略,确保每轮交互都能获得足够的支撑信息。当我们需要模型在长程推理中保持一致性时,必须追踪和维护那些跨越多轮对话的关键状态。
上下文工程正是为解决这些问题而诞生的技术领域。它涵盖了在有限上下文窗口内最大化模型能力的一系列技术与方法,从基础的文本压缩、语义检索,到复杂的状态管理、多模态组织,形成了一套完整的技术栈。掌握上下文工程,是将大语言模型从“玩具”升级为“生产力工具”的关键能力。
大语言模型的工作原理:自注意力与序列处理
要深入理解上下文工程的必要性,必须首先理解大语言模型处理信息的基本原理。现代大语言模型几乎全部基于 Transformer 架构,这一架构的核心是自注意力机制(Self-Attention Mechanism)。理解自注意力,是理解上下文窗口限制的钥匙。
从词向量到注意力计算
大语言模型处理文本的第一步,是将离散的文字符号转换为连续的数值表示。这一过程称为嵌入(Embedding),每个词或词片段被映射为一个高维向量。举例而言,当模型处理“人工智能”这个短语时,它实际上处理的是两个向量:一个对应“人工”,一个对应“智能”。这些向量捕捉了词语的语义信息——语义相近的词语在向量空间中距离较近。
然而,单独理解每个词语是远远不够的。“狗咬了人”与“人咬了狗”的词语完全相同,但含义截然相反。这说明词语的含义高度依赖于其上下文。这一洞察催生了 Transformer 架构中最关键的创新:注意力机制。
注意力计算的核心思想是:对于序列中的每个位置,模型需要“关注”序列中其他所有位置的信息,并根据相关性动态分配权重。假设我们有一个句子"The cat sat on the mat",处理"sat"这个词时,模型需要知道这是一个动词,主语是"cat",地点是"mat"。注意力机制使得模型能够建立这些跨距离的依赖关系。
数学上,标准的多头注意力机制可以表示为:
$$\text{Attention}(Q, K, V) = \text{softmax}\left(\frac{QK^T}{\sqrt{d_k}}\right)V$$
其中 $Q$(Query,查询)、$K$(Key,键)、$V$(Value,值)是通过输入向量线性变换得到的三个矩阵。$QK^T$ 计算了所有位置之间的相似度,$\sqrt{d_k}$ 是缩放因子防止点积过大导致梯度消失。最终的加权求和将信息从所有位置汇聚到当前处理的位置。
序列到序列的转换过程
自注意力并非独立运作,而是嵌套在完整的 Transformer 解码器结构中。大语言模型的核心任务是序列到序列的转换:给定一个输入序列(如用户的提问),生成一个输出序列(如模型的回答)。
这个生成过程是逐 token 进行的。模型首先处理完整的输入序列,计算每个位置的表示;然后开始生成输出,在每一步生成一个新的 token;新生成的 token 又被加入输入序列,影响下一步的生成。这个过程持续进行,直到生成特殊的结束 token 或达到最大长度限制。
这里出现了一个关键点:对于一个长度为 $n$ 的输入序列和长度为 $m$ 的输出序列,模型需要进行 $O(n^2)$ 级别的注意力计算。计算复杂度与序列长度的平方成正比,这意味着将输入长度翻倍,注意力计算量将变为原来的四倍。这不仅影响计算速度,更重要的是,每一步注意力计算都需要存储中间结果用于反向传播,内存消耗同样以平方级增长。
这就是上下文窗口限制的物理根源。当输入序列达到一定长度时,计算资源和内存消耗将变得不可承受。模型制造商必须在模型能力(需要更大的上下文来理解复杂任务)和部署成本(更大的上下文意味着更高的计算开销)之间做出权衡,最终形成了我们看到的上下文窗口上限。
掩码注意力与自回归生成
理解上下文窗口还需要了解另一个关键机制:掩码注意力(Masked Attention)。在生成式语言模型中,输出 token 的生成只能基于已生成的内容,不能“看到”未来尚未生成的内容。掩码机制通过在注意力计算中遮蔽掉当前位置之后的信息来实现这一点。
这意味着,当我们说模型具有“64K token 的上下文窗口”时,64K 包括两部分的和:输入部分的 token 数加上输出部分的 token 数。在长对话场景中,历史对话内容会不断占据上下文空间,而模型还需要留出空间来生成回复。因此,实际可用于新内容的上下文往往小于标称值。
自回归生成还带来了另一个后果:生成的响应越长,占用的上下文空间越多。这催生了上下文工程中一个重要的权衡:详尽的回复需要更多空间,而更多空间意味着更少的历史上下文。理解这一权衡对于设计有效的提示策略至关重要。
上下文窗口:定义、物理限制与能力边界
什么是上下文窗口
上下文窗口(Context Window)是大型语言模型在单次推理过程中能够处理的全部文本范围。这个范围通常以 token 数量计量,包括用户输入的提示、模型输出的回复以及整个对话历史。
上下文窗口的概念对于理解模型能力至关重要。当模型宣称支持"200K token 的上下文"时,这并不意味着它能完美利用这 200K token 中的全部信息。实际上,模型对上下文中不同位置信息的利用程度是不均匀的——通常距离当前位置越近的内容被利用得越充分,而早期内容的“存在感”会随着上下文增长而衰减。
这一现象有多种解释。一种观点认为,注意力机制虽然允许模型理论上访问上下文中的任何位置,但在实践中,由于参数和训练数据的分布,模型在处理中间位置时容易出现“迷失在中间”(Lost in the Middle)的问题——对开头和结尾的内容记忆清晰,对中间部分的信息提取不足。另一种观点则认为,位置编码的设计使得模型对不同位置有不同的偏好。
上下文窗口的物理限制
上下文窗口的大小并非任意设定,而是受制于多重物理和工程约束。
内存约束是最根本的限制。假设使用标准的多头注意力机制,处理长度为 $L$ 的序列需要 $O(L^2)$ 的内存来存储注意力矩阵。对于一个 100K token 的序列,仅存储注意力权重就需要约 $100000^2 \times 4\text{ bytes} \approx 40\text{ GB}$ 的内存(假设使用 float32)。这还没有计入模型参数、中间激活值和梯度的存储需求。在实际训练和推理中,GPU 显存成为硬性瓶颈。
计算约束同样重要。注意力计算的 $O(L^2)$ 复杂度意味着处理长序列需要大量算力。以 GPT-4 级别的模型为例,处理 128K token 上下文单次推理的计算量相当于处理 4K token 的约 1024 倍。这种指数增长的计算成本最终会反映到服务价格上,制约了长上下文模型的大规模应用。
位置编码是另一个技术瓶颈。早期 Transformer 使用绝对位置编码,理论上只能处理训练时见过的最大长度。后续的旋转位置编码(RoPE)和相对位置编码虽然支持更长的上下文,但超过一定范围后仍会出现“外推”问题——模型在处理超出训练长度的新位置时表现下降。
工程实现也带来限制。为了支持长上下文,需要专门的系统优化,如分块计算(Chunked Attention)、内存优化(Flash Attention)、滑动窗口注意力(Sliding Window Attention)等。这些技术虽然可以缓解内存和计算压力,但也可能引入新的问题,如边界效应和上下文断裂。
上下文窗口对模型能力的约束
上下文窗口的大小直接决定了模型能够处理任务的复杂度上限。
单文档理解方面,较短的上下文窗口限制了模型一次性能处理的信息量。要理解一本 300 页的书籍,32K token 的窗口(大约 25,000 个英文单词或 40,000 个中文字符)意味着需要分约 10-15 次处理,每次只能看到部分章节。这种分块处理带来了跨块关联的挑战——模型可能难以建立全书范围内的主题一致性,或者在块边界处丢失关键引用。
多轮对话方面,窗口限制决定了对话能够维持的深度和长度。假设每次交互平均消耗 500 token(用户输入加模型输出),一个 128K token 的窗口理论上可以支持约 250 轮对话。但在实际应用中,用户的单次输入可能很长,模型的回复也可能很详尽,有效对话轮数往往只有理论值的三分之一到一半。
复杂推理方面,上下文窗口约束了中间状态和证据的保存。对于需要引用大量背景信息进行推理的任务,窗口限制可能导致模型无法同时看到所有必要证据,被迫在有限的上下文内“压缩”信息或依赖模型的内部知识——后者在专业领域可能不够准确或不够新。
工具使用与多任务方面,当模型需要调用外部工具或同时处理多个子任务时,上下文窗口需要同时容纳指令、工具描述、调用历史和返回结果,空间竞争更加激烈。
理解这些约束,有助于在实际应用中做出明智的设计决策:是选择支持更长上下文的模型,还是设计分块策略;是预先准备摘要,还是使用检索机制定位关键信息。这些正是上下文工程要解决的核心问题。
Token 机制:语言模型的"思考单位"
什么是 Tokenization
Tokenization(分词)是将输入文本拆分成模型可处理的最小单元的过程。这些单元被称为 token。理解 token 机制对于准确计算上下文消耗、设计高效的提示策略至关重要。
与直觉不同的是,token 并不完全等同于“字”或“词”。不同的模型使用不同的分词器(Tokenizer),其分词策略各异。以字节对编码(Byte Pair Encoding, BPE)为例,它从字符级别开始,迭代地合并最常见的相邻字符对来构建词表。这种方法的优势在于能够处理任意输入文本,同时保持词表大小合理。
以英文为例,"tokenization"可能被分词为 `["token", "ization"]` 两个 token,而"indivisible"可能被分词为 `["in", "div", "isible"]` 三个 token。常见单词往往占据 1-2 个 token,不规则单词和专有名词则可能需要更多 token。标点符号、空格通常也算作独立 token。
中文的分词逻辑类似但粒度不同。由于中文字符数量有限(常用字约 3,500 个),且不像英文有天然的分词边界,中文分词器通常基于字符或基于词的混合策略。"今天天气很好"可能被分词为 `["今天", "天气", "很好"]` 或 `["今", "天", "天", "气", "很", "好"]`。通常情况下,一个中文字符对应 1-2 个 token,取决于分词器的具体实现。
Token 数量的计算方法
准确计算 token 数量需要了解所使用模型的分词器规则。一个实用的经验法则是:英文文本平均每个 token 约对应 4 个字符或 0.75 个单词。因此,1,000 个英文单词大约对应 1,333 个 token,加上特殊标记后通常记为 1,500 token 左右。
对于中文,每个字符平均约对应 1-1.5 个 token。一篇 1,000 字的中文文章通常对应约 1,200-1,500 个 token。但这个估计非常粗糙,实际消耗取决于具体内容和分词器。
实际应用中,最可靠的方法是使用模型提供商的官方工具或 API 来计算 token 数。以 OpenAI 为例,提供了 `tiktoken` 库来精确计算 GPT 模型的 token 消耗:
```python import tiktoken
使用 cl100k_base 编码器(GPT-4 和 GPT-3.5 Turbo 使用)
encoder = tiktoken.get_encoding("cl100k_base")
text = "上下文工程是处理大语言模型上下文限制的技术领域。" tokens = encoder.encode(text)
print(f"文本: {text}") print(f"Token数量: {len(tokens)}") print(f"Token列表: {tokens}") ```
这段代码会输出准确的 token 数量和对应的 token ID。不同模型使用不同的编码器,GPT-4 和 Claude 3 使用的是专门优化的高效编码器,而开源模型如 LLaMA 使用的是基于 SentencePiece 的编码器。
Token 消耗的实际构成
在实际应用中,上下文窗口的 token 消耗由多个部分构成:
系统提示(System Prompt) 消耗的 token 数相对固定,通常在几百到几千之间,主要包含角色设定、行为规则和输出格式要求。这部分在对话开始时设定,之后每轮对话都会携带。
对话历史 是主要的消耗来源。典型的用户-助手对话轮次结构为:
``` User: [用户消息] Assistant: [助手回复] ```
每条消息前面有角色标记(`<|im_start|>user` 或 `<|im_start|>assistant`),每条消息结尾有消息分隔符(`<|im_end|>`)。这些控制 token 也会占用上下文空间。
当前输入 是指用户本轮输入的内容。如果用户粘贴了一段长文档,这部分可能占据数千甚至数万个 token。
输出空间 是容易被忽视的消耗项。模型需要为回复预留空间,如果回复较长,会显著压缩可用于输入的空间。API 通常设置 `max_tokens` 参数来限制最大输出长度。
以一个具体的对话为例:
``` 系统提示: 你是专业的技术文档助手。(约20 token) 历史第1轮: 用户询问问题(200 token)+ 助手回复(300 token) 历史第2轮: 用户追问(150 token)+ 助手回复(400 token) 历史第3轮: 用户本轮输入(1,000 token) ```
对于 32K token 的上下文窗口,上述对话已消耗约 2,070 token,还剩约 30,000 token 可用于模型生成。这意味着模型可以生成相当长的回复,但历史中早期对话的上下文已经变得相当遥远。
实际 Token 限制的估算
模型标称的上下文限制(如 128K token)并不等于可用空间。在设计应用时,需要预留一定的余量,因为:
1. 模型实际处理长度可能小于标称值:某些模型在接近上下文上限时性能急剧下降,实际可用长度可能只有标称值的 70-80%。
2. 系统开销:API 的内部处理、缓存策略等可能消耗部分空间。
3. 回复生成预留:需要预留足够的 token 空间来生成预期长度的回复。
4. 边界效应:长上下文的开头和结尾信息利用度最高,中间部分可能出现信息损失。
基于这些因素,建议在设计中预留 10-20% 的安全边界。对于 128K 标称限制,设计时以 100K-115K 为实际可用上限,并据此规划内容组织和分块策略。
上下文工程的核心技术栈
上下文工程并非单一技术,而是一个包含多个子领域的综合性技术体系。这些子领域相互关联,共同支撑起在有限上下文中实现复杂任务的能力。
长上下文处理
长上下文处理是上下文工程的基础层,关注如何有效利用模型支持的最大上下文窗口。当需要处理超过普通提示长度的内容时,必须采用专门的策略。
层次化处理是处理超长文档的核心策略。其思想是将长文档分解为多个层次:首先是高层摘要或概览,然后是各部分的详细摘要,最后是原始内容的特定片段。用户查询时,系统首先在高层确定相关部分,然后逐步深入到具体的细节层。这种方法模拟了人类阅读长文档的方式——先浏览目录和摘要,再根据需要深入特定章节。
滑动窗口是另一种常用技术。在处理连续文本(如长篇文章或书籍)时,使用固定大小的窗口在文本上滑动,每次处理一个窗口的内容。相邻窗口之间保留一定的重叠区域(如 20-30%),以确保关键信息不会因为窗口边界而丢失。这种方法特别适合处理没有明确结构边界的长文本。
聚焦提取则是在长上下文中精准定位相关信息的技术。当用户提问时,系统首先理解查询意图,然后在长上下文中检索与问题最相关的片段,仅将相关内容注入提示。这大大提高了上下文利用效率,避免了让模型处理大量无关信息。实现上可以使用传统的关键词匹配,也可以使用嵌入向量相似度搜索。
压缩与摘要
压缩与摘要技术解决的是如何在有限空间内保留最多有效信息的问题。
文本压缩追求的是在不丢失语义的前提下减少 token 消耗。这可以通过删除冗余词汇、合并重复表达、使用更简洁的表述等方式实现。简单的压缩可能只去除空格和换行,高级的压缩则涉及同义词替换、句式简化等语义层面的优化。压缩需要谨慎使用,因为过度压缩可能导致关键信息丢失。
语义摘要是更智能的压缩方式。它不是简单地删减文字,而是理解文本的核心含义,用更少的 token 表达相同的语义。摘要可以是无损的(如提取关键实体和关系),也可以是有损的(如提取主要论点)。对于对话历史,可以使用摘要来压缩存储,保留每轮对话的核心要点和结论。
渐进式摘要是一种动态摘要策略。当上下文接近满载时,系统自动对早期内容进行摘要,释放空间用于新内容。摘要的程度可以根据空间压力动态调整——轻度压缩时保留更多细节,重度压缩时只保留核心要点。这种方法在长对话场景中特别有用。
检索增强记忆
检索增强记忆(Retrieval-Augmented Memory)将外部知识库与语言模型相结合,是解决上下文容量限制的经典方法。
其核心思想是:不把所有信息都塞进上下文,而是将大量信息存储在外部知识库中,