2.5 上下文窗口与长文本处理
在上一节中,我们深入探讨了 LLM 的推理过程与采样策略——从贪心搜索到核采样,从温度调节到 Top-P 控制,这些技术决定了模型"怎么说"的问题。然而,在讨论模型如何生成回答之前,有一个更基础的问题需要回答:模型一次能读进去多少文本? 你给模型的提示词、对话历史、参考文档,加起来不能超过一个上限,这个上限就是"上下文窗口"。它直接决定了模型能处理多长的文档、能记住多少轮对话、能同时参考多少信息。本节将围绕这一核心约束展开,从底层原理到工程实践,帮助你全面理解大模型的长文本能力与局限。
2.5.1 什么是上下文窗口
上下文窗口(Context Window) 是指大语言模型在单次推理中能够处理的最大 token 序列长度。这个长度涵盖了输入提示词(system prompt + user message + 历史对话)和模型生成的输出 token——两者共享同一个窗口。
书桌类比:想象你在图书馆里做研究。书桌上能同时摊开的书的数量是有限的——桌子就这么大,你只能把当前正在参考的几本书放在桌上,其余的只能放回书架。上下文窗口就是模型的"书桌"。不管图书馆(你的知识库)里有多少书,模型一次只能"看到"桌上这些内容。桌子越大,能同时参考的信息越多;但桌子再大,也放不下整个图书馆。
理解这一点至关重要:上下文窗口不是"模型的知识量",而是"模型一次推理时能关注的信息量"。模型的参数中存储了训练时学到的知识,但这些知识是隐式分布在权重中的;而上下文窗口中的内容,是模型在当前推理中能显式"读到"的信息。
举个具体的例子:假设你有一个 8K 上下文窗口的模型。当你发送一条消息时,系统提示词占 500 token,用户消息占 2000 token,历史对话已有 4000 token,那么剩余可用空间只剩 1500 token 给模型回复。如果历史对话继续增长,最终模型要么截断早期对话,要么直接拒绝请求。
2.5.2 为什么上下文窗口有限制
上下文窗口的限制并非人为设定,而是源于 Transformer 架构的内在特性。具体来说,有两个核心瓶颈:计算复杂度和显存压力。
(1)自注意力的 O(n²) 计算复杂度
Transformer 的核心是自注意力机制。对于长度为 n 的输入序列,每个 token 都需要与序列中所有其他 token 计算注意力分数,因此注意力矩阵的大小为 n × n,计算复杂度为 O(n²)。
序列长度 n = 1,000 → 注意力矩阵 = 1,000 × 1,000 = 1M 个元素
序列长度 n = 100,000 → 注意力矩阵 = 100K × 100K = 10B 个元素
序列长度 n = 1,000,000 → 注意力矩阵 = 1M × 1M = 1T 个元素可以看到,序列长度增长 10 倍,计算量增长 100 倍。从 4K 到 128K 是 32 倍长度增长,对应计算量增长超过 1000 倍。这就是为什么早期 Transformer 模型的上下文窗口通常只有 512 或 2048——再长就算不动了。
(2)KV Cache 的显存压力
在推理过程中,为了避免重复计算已经处理过的 token 的 Key 和 Value 向量,推理引擎会将它们缓存下来,这就是 KV Cache(Key-Value Cache)。
记笔记类比:想象你在听一堂很长的课。老师每说一个字,你都要结合前面所有内容来理解当前这句话。如果你不记笔记,每次听到新内容时,都要从头回忆老师说过的一切——这太慢了。于是你边听边记笔记:把每个要点记在笔记本上,下次只需要翻看笔记就行。KV Cache 就是模型的"笔记本"——把已经计算过的 Key 和 Value 存下来,生成新 token 时直接查阅,避免重复计算。
KV Cache 虽然能加速推理,但它占用显存,而且占用量与序列长度成正比。以 LLaMA-7B 为例:
LLaMA-7B 单层 KV Cache 大小计算:
seq_len = 4096, n_heads = 32, d_head = 128, dtype = fp16(2 bytes)
单层 KV Cache = 2(K和V) × 4096 × 32 × 128 × 2 bytes = 67 MB
总层数 = 32
总 KV Cache = 32 × 67 MB ≈ 2.1 GB
当 seq_len 扩展到 128K 时:
总 KV Cache = 2 × 128K × 32 × 128 × 2 bytes × 32 layers ≈ 67 GB也就是说,仅 KV Cache 一项,128K 上下文就需要 67 GB 显存——这还不包括模型本身的权重。对于 7B 参数的模型,权重本身大约 14 GB(fp16),而 KV Cache 在长上下文下的开销几乎是权重的 5 倍。这就是为什么长上下文推理的硬件门槛如此之高。
这两个瓶颈意味着:盲目增大上下文窗口并不现实,需要在算法层面做创新。
2.5.3 位置编码的演进
Transformer 中的自注意力机制本身是"顺序无关"的——打乱输入 token 的顺序,注意力计算的结果不会自动反映这种变化。为了让模型感知 token 的顺序,需要引入位置编码(Position Encoding)。位置编码的演进直接影响了模型处理长文本的能力。
(1)绝对位置编码(Sinusoidal)
Transformer 原论文(Vaswani et al., 2017)使用正弦/余弦位置编码,为每个位置生成一个固定的向量:
PE(pos, 2i) = sin(pos / 10000^(2i/d_model))
PE(pos, 2i+1) = cos(pos / 10000^(2i/d/d_model))其中 pos 是 token 在序列中的位置,i 是维度索引,d_model 是模型维度。
问题:这种编码方式只能覆盖训练时见过的位置范围。如果训练时最大序列长度是 4096,推理时输入 8192 个 token,模型从未见过位置 4097 以后的位置编码,表现会急剧下降——这被称为无法外推(extrapolation)。
(2)相对位置编码
绝对位置编码告诉模型"这个 token 在第几个位置",而相对位置编码告诉模型"这两个 token 之间隔了多远"。不编码绝对位置,而是编码两个 token 之间的相对距离。
这种方法的优势在于:相对距离具有一定的泛化性——训练时见过"距离为 100"的 token 对,推理时遇到"距离为 200"的 token 对,模型仍能给出合理的注意力分数。但传统相对位置编码的实现较为复杂,且通常仍受限于训练时的距离范围。
(3)RoPE(旋转位置编码)
RoPE(Rotary Position Embedding,旋转位置编码) 是目前最主流的位置编码方案,被 LLaMA、Qwen、DeepSeek、Mistral 等主流开源模型广泛采用。
RoPE 的核心思想是:将 Query 和 Key 向量按维度两两分组,每组根据 token 的位置旋转一个角度。旋转角度与位置成正比,与维度频率成反比:
RoPE 的数学表达:
对于位置 m 的 Query 向量和位置 n 的 Key 向量:
将 Q_m 和 K_n 按维度对分组,第 i 组旋转角度为:
θ_i = m / 10000^(2i/d) (对 Q)
φ_i = n / 10000^(2i/d) (对 K)
旋转后做点积,结果只依赖于相对位置 (m - n):
Q_m^T · K_n = f(m - n)
这意味着:两个 token 的注意力分数只取决于它们之间的距离,
而不是各自的绝对位置。RoPE 的优雅之处在于:
- 相对位置性质:注意力分数只依赖于 token 间的相对距离,天然具有平移不变性。
- 可通过修改频率实现外推:调整 RoPE 的基频(base frequency),可以让模型在更长序列上工作。这是后续长文本扩展技术(如 NTK-aware、YaRN)的基础。
- 计算高效:RoPE 只是在 Q 和 K 上做旋转操作,不引入额外参数,计算开销极小。
(4)ALiBi(Attention with Linear Biases)
ALiBi 走了一条完全不同的路:它不添加任何位置编码向量,而是直接在注意力分数上加上一个与距离成正比的线性偏置:
Attention = softmax(QK^T / √d_k + m × B)
其中:
B 是一个预先计算的偏置矩阵,B[i][j] = -(i - j) (当 i > j 时)
m 是每个注意力头相关的斜率(slope),不同的头使用不同的 m 值直观来说,距离越远的 token 对,注意力分数被减得越多,模型自然倾向于关注更近的 token。
| 特性 | RoPE | ALiBi |
|---|---|---|
| 实现方式 | 旋转 Q、K 向量 | 注意力分数加偏置 |
| 额外参数 | 0 | 0 |
| 长度外推能力 | 需配合扩展技术 | 天然支持外推 |
| 表达能力 | 较强 | 略弱于 RoPE |
| 主流采用 | LLaMA、Qwen、DeepSeek 等 | BLOOM、MPT 等 |
优点:ALiBi 天然支持长度外推——训练时只见过 1024 长度,推理时直接外推到 2048 甚至更长,性能下降很小。而且不引入任何额外参数。
缺点:表达能力不如 RoPE 灵活。实践表明,在大规模预训练场景下,RoPE 的最终性能通常优于 ALiBi。这也是为什么主流开源模型大多选择 RoPE。
2.5.4 长文本扩展技术
既然位置编码决定了模型的外推能力,那么通过修改位置编码的方式,就可以扩展模型的上下文窗口。以下是目前主流的几种长文本扩展技术。
| 方法 | 原理 | 代表模型 |
|---|---|---|
| 位置插值(PI) | 将较长位置等比映射到训练范围内 | LLaMA context extension |
| NTK-aware 缩放 | 调整 RoPE 的基频,使不同频率维度有不同的缩放 | CodeLlama、Qwen |
| YaRN | 结合 PI 和 NTK,对不同频段分组处理 | 被广泛采用 |
| 窗口注意力 | 每个 token 只关注局部窗口,复杂度降为 O(n) | Mistral、Longformer |
| Ring Attention | 将长序列分块分布到多个 GPU 上协同计算 | 大规模分布式推理 |
(1)位置插值(Position Interpolation, PI)
位置插值的核心思路非常直观:如果模型在训练时只见过 0~4096 的位置,那推理时遇到 0~32768 的位置,就把后者"压缩"到前者范围内。
原始位置:0, 1, 2, ..., 32768
缩放因子 s = 32768 / 4096 = 8
插值后位置:0, 0.125, 0.25, ..., 4096
效果:所有位置都被压缩到模型见过的范围内这就像把一张大地图缩小后放进一个小相框——所有内容都进来了,但细节可能有些模糊。PI 的优点是实现简单,只需在推理时对位置做除法;缺点是对高频维度的分辨率有所损失。
(2)NTK-aware 缩放
NTK-aware 方法不做简单的线性缩放,而是调整 RoPE 的基频(base frequency),让不同频率的维度有不同的表现:
原始 RoPE base = 10000
训练长度 = 4096
目标长度 = 32768(8 倍扩展)
缩放因子 s = 32768 / 4096 = 8
新 RoPE base = 10000 × s^(d/(d-2)) ≈ 10000 × 8 ≈ 80000
效果:
低频维度(编码大范围位置信息)→ 保持稳定,外推能力增强
高频维度(编码精细位置信息)→ 感受野扩展NTK-aware 的关键洞察是:RoPE 中不同维度编码了不同尺度的位置信息——低频维度编码"大致在第几百个位置",高频维度编码"精确在第几个位置"。NTK-aware 让低频维度负责外推,高频维度保持原有精度,从而在扩展长度的同时尽量保留位置信息的精度。
(3)YaRN(Yet another RoPE extensioN)
YaRN 可以看作是 PI 和 NTK-aware 的"加强版"——它不再对所有维度做统一的缩放,而是按频段分组,对不同频率范围的维度采用不同的插值策略:
- 高频维度:保持原始频率不变(保留精细位置信息)
- 中频维度:使用 NTK-aware 式的平滑过渡
- 低频维度:使用位置插值式的外推
这种分频段处理的方式使得 YaRN 在各种长度外推场景下都取得了较好的效果,是目前社区最广泛采用的长文本扩展方案之一。
(4)窗口注意力(Window Attention)
上述方法都是在位置编码层面做文章,而窗口注意力则从注意力机制本身入手:不让每个 token 关注整个序列,而是只关注周围一个固定大小的窗口(比如前后各 2048 个 token)。
传统注意力:每个 token 关注所有 token → O(n²) 复杂度
窗口注意力:每个 token 只关注 W 个邻居 → O(n × W) 复杂度
当 W = 4096 时:
序列长度 32K:传统需要 32K × 32K = 1B 次计算
窗口需要 32K × 4K = 128M 次计算(减少 87.5%)Mistral 引入的"滑动窗口注意力"(Sliding Window Attention)就是这种方法的典型代表。部分模型还使用"全局 + 局部"混合策略:大多数层用窗口注意力保持高效,少数层用全局注意力确保长距离信息传递。
(5)Ring Attention
当序列长度达到百万级(如 Gemini 1.5 Pro 的 1M 上下文),单张 GPU 的显存完全不够用。Ring Attention 将长序列分块分布到多个 GPU 上,每个 GPU 只负责计算一部分注意力,然后通过环形通信(ring communication)传递中间结果,最终拼出完整的注意力矩阵。
这种方案使得百万级 token 的注意力计算成为可能,但需要多 GPU 协同,硬件成本很高。
2.5.5 主流模型上下文窗口对比
以下是截至 2025 年 7 月,主流大语言模型的上下文窗口对比:
| 模型 | 上下文窗口 | 位置编码 | 扩展技术 | 备注 |
|---|---|---|---|---|
| GPT-4o | 128K | 未公开 | 未公开 | OpenAI 旗舰 |
| Claude 3.5 Sonnet | 200K | 未公开 | 未公开 | Anthropic |
| Claude 4 Opus | 200K(标准)/ 1M(Beta) | 未公开 | 未公开 | 2025年5月发布 |
| Claude 4 Sonnet | 200K | 未公开 | 未公开 | 2025年5月发布 |
| Gemini 2.5 Pro | 1M | 未公开 | Ring Attention + MoE | 2025年3月发布 |
| Gemini 2.5 Flash | 1M | 未公开 | Ring Attention + MoE | 轻量版 |
| DeepSeek-V3 | 128K | RoPE | MLA(多头潜在注意力) | MoE 架构,671B 总参数 |
| Qwen 2.5 | 128K | RoPE | YaRN + NTK | 开源 |
| Qwen3-235B | 256K | RoPE | YaRN + NTK | 2025年4月发布 |
| LLaMA 3 | 8K(原始) | RoPE | - | 社区扩展可达 128K+ |
| LLaMA 3.1 | 128K | RoPE | YaRN | 原生支持长上下文 |
从表中可以看出几个趋势:
- 128K 已成为标配:主流商业模型和开源模型的上下文窗口基本都达到了 128K token 级别。
- 百万级窗口出现:Gemini 2.5 Pro 和 Claude 4 Opus 已经将上下文窗口推向了 1M token 级别。
- 开源与闭源的差距在缩小:Qwen3 达到 256K,DeepSeek-V3 达到 128K,开源模型的长文本能力正在快速追赶闭源模型。
换算参考:128K token 大约相当于 96,000~160,000 个汉字(中文 1 token ≈ 0.75~1.25 汉字),约等于一本 300 页的书籍。1M token 则相当于约 75 万字——差不多是《指环王》三部曲的总字量。
2.5.6 长文本处理的工程策略
即便模型支持 128K 甚至 1M 的上下文窗口,在实际工程中,直接将超长文本一次性塞入模型也不总是最优解——延迟高、成本高、且效果未必最好。以下介绍几种常见的长文本工程处理策略。
(1)分块处理(Chunking)
将长文本分成多个较小的块(chunk),分别送入模型处理,最后合并结果。
具体例子:假设你有一份 50 页的合同需要总结。直接把 50 页塞入模型可能超出窗口,或者即使不超,模型也可能"注意力分散",漏掉中间部分的关键条款。分块策略的做法是:将合同按章节分成 10 个块,每个块约 5 页,分别让模型生成摘要,再将 10 份摘要合并成最终总结。
分块处理适合:翻译、摘要、分类等"局部即可完成"的任务。
分块处理不适合:需要全局理解的推理任务——比如"这篇文档中三个角色之间的因果关系是什么",因为分块后模型无法看到全局。
(2)滑动窗口(Sliding Window)
滑动窗口是分块处理的一种变体:相邻块之间保留一定的重叠区域,确保跨块边界的信息不会丢失。
具体例子:文本长度 10,000 token,窗口大小 3,000 token,重叠 500 token。那么分块为:
- 块 1:token 0~3,000
- 块 2:token 2,500~5,500(与前一块重叠 500 token)
- 块 3:token 5,000~8,000
- 块 4:token 7,500~10,000
重叠区域确保了:即使一个关键信息恰好出现在块 1 和块 2 的边界处,也不会被截断。
(3)Map-Reduce 策略
Map-Reduce 是一种更结构化的分块处理范式,分为两个阶段:
具体例子:你需要对一本 500 页的书做内容摘要。
Map 阶段:将书分成 50 个块,每块 10 页。对每个块分别调用模型,生成一段摘要("这个块讲了什么")。这一步可以并行执行,50 个块同时处理。
Reduce 阶段:将 50 段摘要合并成一份(如果摘要总长度仍然超出窗口,可以递归地再做一次 Map-Reduce),最终生成整本书的摘要。
这种策略的优势在于:每一步都在模型的上下文窗口内完成,且 Map 阶段可以并行加速。
(4)检索增强生成(RAG)
RAG(Retrieval-Augmented Generation)是处理超长文本最常用的方案,核心思路是"按需检索"而非"全量输入":
具体例子:你有 10,000 篇技术文档,用户问"Kubernetes 的 Pod 调度策略是什么"。
- 存储:将 10,000 篇文档分块后存入向量数据库,每块对应一个向量表示
- 检索:将用户问题"Kubernetes 的 Pod 调度策略"转为向量,在数据库中检索最相似的 Top-5 文档块
- 生成:只将这 5 个相关块(约 5,000 token)放入模型上下文,让模型基于这些片段回答
这样,无论知识库多大,模型每次只需要处理几千个 token,完全突破了上下文窗口的限制。代价是:如果检索质量不好,模型可能拿不到正确的信息。
RAG 是目前企业级应用中最主流的长文本处理方案,我们将在第 5 章中详细讲解。
(5)Prompt 压缩
当输入文本确实很长但又必须在一次推理中处理时,可以使用工具对 Prompt 进行压缩:
- LLMLingua 等工具通过计算每个 token 对最终输出的贡献度,移除贡献度低的 token
- 保留关键信息,压缩比可达 2~10 倍
- 适合处理冗余文本(如格式化文档、重复性内容),但对精炼文本效果有限
注意:Prompt 压缩可能会丢失信息,使用前需要评估压缩对任务准确率的影响。
2.5.7 代码实践
以下代码示例展示了如何测试模型的上下文窗口以及实现长文本的分块处理。
示例 1:测试不同模型的上下文窗口
from openai import OpenAI
# 初始化 OpenAI 客户端(需要设置 OPENAI_API_KEY 环境变量)
client = OpenAI()
def test_context_window(model: str, text_length: int) -> str:
"""
测试模型是否能处理指定长度的文本。
参数:
model: 模型名称,如 "gpt-4o-mini"
text_length: 目标文本长度(token 数的近似值)
返回:
模型的回复内容或错误信息
"""
# 生成指定长度的测试文本(每个"测试 "约 2-3 个 token)
# 通过重复填充来达到目标长度
test_text = "测试 " * (text_length // 3)
try:
# 构造 API 请求
response = client.chat.completions.create(
model=model, # 指定模型
messages=[
# 系统提示词:告诉模型任务
{"role": "system", "content": "请统计以下文本中出现了多少次'测试'"},
# 用户消息:包含超长测试文本
{"role": "user", "content": test_text}
],
max_tokens=100, # 限制输出长度,降低成本
)
# 返回模型回复内容
return response.choices[0].message.content
except Exception as e:
# 如果超出上下文窗口,API 会返回错误
return f"Error: {e}"
# 测试不同模型对长文本的处理能力
for model in ["gpt-4o-mini", "gpt-4o"]:
result = test_context_window(model, 1000) # 先用 1000 token 测试
print(f"{model}: {result}")这段代码的思路是:构造一个已知内容的超长文本(大量重复的"测试"),然后让模型统计其中关键词出现的次数。如果模型能在长文本中准确计数,说明它有效利用了完整的上下文窗口;如果计数出错,则可能存在"中间遗忘"的问题。
示例 2:使用 LangChain 实现文本分块
from langchain.text_splitter import RecursiveCharacterTextSplitter
# 模拟一段长文本(实际使用时替换为真实文档)
long_text = "这是一段很长的文本,需要被分成多个块。" * 10000
# 创建递归字符文本分割器
splitter = RecursiveCharacterTextSplitter(
chunk_size=1000, # 每个 chunk 的最大字符数
chunk_overlap=200, # 相邻 chunk 之间的重叠字符数(滑动窗口)
separators=[ # 分隔符优先级(从上到下依次尝试)
"\n\n", # 优先在段落边界分割
"\n", # 其次在换行处分割
"。", # 中文句号
"!", # 中文感叹号
"?", # 中文问号
",", # 中文逗号
" ", # 空格
"" # 最后退化为逐字符分割
]
)
# 执行分块
chunks = splitter.split_text(long_text)
# 输出分块结果统计
print(f"原始文本长度: {len(long_text)} 字")
print(f"分成了 {len(chunks)} 个 chunk")
print(f"平均 chunk 长度: {sum(len(c) for c in chunks) / len(chunks):.0f} 字")
print(f"第一个 chunk 预览: {chunks[0][:100]}...")RecursiveCharacterTextSplitter 的工作原理是:按照 separators 列表中的顺序,依次尝试用每个分隔符来切分文本。先用段落分隔符(\n\n),如果切出来的块还是太大,再用换行符(\n),以此类推。这样能尽量在语义边界处切分,避免把一个完整的句子切成两半。
示例 3:Map-Reduce 文本摘要
from openai import OpenAI
client = OpenAI()
def summarize_chunk(text: str, model: str = "gpt-4o-mini") -> str:
"""
对单个文本块生成摘要(Map 阶段)。
"""
response = client.chat.completions.create(
model=model,
messages=[
{"role": "system", "content": "请用 2-3 句话总结以下文本的核心内容。"},
{"role": "user", "content": text}
],
max_tokens=200,
)
return response.choices[0].message.content
def map_reduce_summarize(long_text: str, chunk_size: int = 3000) -> str:
"""
使用 Map-Reduce 策略对长文本生成摘要。
参数:
long_text: 需要总结的长文本
chunk_size: 每个块的字符数
返回:
最终的摘要文本
"""
# ---- Map 阶段:分块并分别生成摘要 ----
chunks = []
for i in range(0, len(long_text), chunk_size):
chunk = long_text[i:i + chunk_size] # 按固定大小切片
chunks.append(chunk)
print(f"文本已分成 {len(chunks)} 个块,开始逐块生成摘要...")
chunk_summaries = []
for idx, chunk in enumerate(chunks):
summary = summarize_chunk(chunk) # 对每块单独生成摘要
chunk_summaries.append(summary)
print(f" 块 {idx + 1}/{len(chunks)} 完成")
# ---- Reduce 阶段:合并所有摘要 ----
combined = "\n\n".join(chunk_summaries) # 将所有块摘要拼接
# 如果合并后仍然太长,可以递归调用本函数
if len(combined) > chunk_size:
print("合并后仍然较长,执行第二轮 Map-Reduce...")
return map_reduce_summarize(combined, chunk_size)
# 最终生成整体摘要
final_response = client.chat.completions.create(
model="gpt-4o-mini",
messages=[
{"role": "system", "content": "请基于以下各段摘要,生成一份完整的总结。"},
{"role": "user", "content": combined}
],
max_tokens=500,
)
return final_response.choices[0].message.content
# 使用示例
long_document = "这是一段很长的文档内容..." * 5000 # 替换为真实长文档
summary = map_reduce_summarize(long_document)
print(f"最终摘要:{summary}")这段代码实现了完整的 Map-Reduce 文本摘要流程。Map 阶段将长文档分块并逐块生成摘要,Reduce 阶段将所有摘要合并后再生成最终总结。如果合并后的摘要仍然超出窗口,函数会递归地再做一轮 Map-Reduce。
2.5.8 常见误区
在理解上下文窗口时,有几个常见的认知误区需要澄清。
误区 1:"上下文越长越好"
很多人认为上下文窗口越大,模型能力越强。这是一个危险的误解。
首先,研究表明,随着输入长度增加,模型的性能并非单调上升——存在所谓的**"上下文腐烂"(Context Rot)** 现象。即使模型声称支持 1M token,在输入超过一定长度后,回答质量也会下降。原因在于:序列越长,注意力越分散,模型更容易"遗忘"序列中间的信息(这一现象也被称为"Lost in the Middle")。
其次,更长的上下文意味着更高的计算成本和延迟。128K token 的推理延迟可能比 4K token 高数十倍,API 调用成本也线性增长。在实际应用中,精简上下文往往比"塞满窗口"效果更好。
实践建议:优先使用 RAG 检索相关片段放入上下文,而不是把整个文档直接塞进去。把模型当成一个"专注力有限"的研究者——给它最相关的几页材料,比给它一整本书效果好得多。
误区 2:"128K 真的能用满 128K"
模型标注的上下文窗口是理论最大值,但实际有效利用范围往往打折扣。
首先,上下文窗口是输入 + 输出共享的。如果窗口是 128K,你输入了 120K token 的内容,那模型只剩下 8K token 的输出空间——对于需要生成长回复的任务,这可能不够。
其次,"能装下"不等于"能用好"。在长上下文的"针在草堆中"(Needle in a Haystack)测试中,大部分模型在简单信息检索任务上表现良好(能找到特定位置的文本),但在需要跨长距离推理的任务上(如"第 3 页提到的概念如何影响第 80 页的结论"),性能会显著下降。
最后,不同模型对长上下文的处理能力差异很大。有的模型在 32K 以内表现优秀,到 64K 就开始明显衰退;有的模型则在 128K 仍能保持稳定。选择模型时,不能只看标注的窗口大小,还要看实际的 long-context benchmark 表现。
误区 3:"有了长上下文就不需要 RAG 了"
随着 Gemini 2.5 Pro 等模型支持百万级 token,一种声音认为 RAG 即将被淘汰——直接把所有文档塞进上下文不就行了?
实际上,两者并非替代关系,而是互补关系:
- 长上下文适合需要全局理解的任务(如分析整篇论文的论证结构)
- RAG 适合从海量文档中精确检索少量相关信息(如从 10 万篇文档中找 5 篇相关的)
- 最佳实践往往是两者结合:用 RAG 检索出相关文档,再利用长上下文窗口做深度理解
此外,RAG 在成本控制、信息更新(知识库可以随时更新而无需重新训练)、数据隐私(敏感数据不出本地)等方面仍有不可替代的优势。
误区 4:"KV Cache 只影响速度,不影响功能"
KV Cache 确实是推理加速技术,但当上下文超过 KV Cache 容量时,推理引擎会自动丢弃最早的 KV Cache 条目(通常采用 LRU 策略)。这意味着模型实际上"忘记了"最早的对话内容——这不是模型能力的问题,而是工程实现的取舍。在使用长上下文功能时,需要了解所使用的推理引擎的 KV Cache 管理策略。
2.5.9 本节小结
| 要点 | 说明 |
|---|---|
| 上下文窗口 | 模型一次推理能处理的最大 token 数,输入和输出共享 |
| 限制原因 | 自注意力 O(n²) 计算复杂度 + KV Cache O(n) 显存占用 |
| KV Cache | 缓存已计算的 Key/Value 向量,避免重复计算,但占用显存 |
| RoPE | 主流位置编码方案,通过旋转编码相对位置,支持长度外推 |
| ALiBi | 在注意力分数上加线性偏置,天然支持外推,零额外参数 |
| 扩展技术 | 位置插值(PI)、NTK-aware、YaRN、窗口注意力、Ring Attention |
| 工程策略 | 分块处理、滑动窗口、Map-Reduce、RAG、Prompt 压缩 |
| 常见误区 | 上下文越长不代表效果越好;标注窗口不等于有效窗口;长上下文不替代 RAG |
理解上下文窗口的关键在于:它不是一个简单的"越大越好"的数字,而是一个涉及计算复杂度、显存压力、位置编码、工程策略等多维度的系统问题。在实际开发中,需要根据任务需求、模型能力、成本预算来选择合适的长文本处理方案。
在前面的几节中,我们从 Tokenizer 到嵌入,从注意力到推理采样,再到上下文窗口,完整地走过了大语言模型处理文本信息的全链路。但现实世界的信息远不止文本——图像、音频、视频同样是重要的数据形态。下一节,我们将进入多模态模型基础,看看大模型如何突破纯文本的限制,理解和生成图像、声音等多模态内容。
参考资料
RoFormer: Enhanced Transformer with Rotary Position Embedding (Su et al., 2021)
https://arxiv.org/abs/2104.09864Train Short, Test Long: Attention with Linear Biases (Press et al., 2022) - ALiBi
https://arxiv.org/abs/2108.12409YaRN: Efficient Context Window Extension (Peng et al., 2023)
https://arxiv.org/abs/2309.00071Ring Attention with Blockwise Transformers (Liu et al., 2023)
https://arxiv.org/abs/2310.01889LLMLingua: Compressing Prompts (Jiang et al., 2023)
https://arxiv.org/abs/2310.05736Lost in the Middle: How Language Models Use Long Contexts (Liu et al., 2023)
https://arxiv.org/abs/2307.03172