Skip to content

上下文窗口与 KV Cache

上下文窗口(Context Window)是 LLM 一次能”看到”的最大 token 数量,包括输入和输出的总和。

类比:上下文窗口就像一张书桌的大小。桌子越大,你能同时摊开的资料越多,理解得越全面。但桌子总有边界——超出的资料就放不下了。

上下文窗口(如 128K tokens)
System Prompt
(固定消耗)
对话历史
(累积增长)
当前输入
(用户提问)
模型输出
(逐步生成)
← 所有部分的 token 总和不能超过窗口大小 →
模型上下文窗口等价约
GPT-4o128K tokens~300 页文档
Claude Sonnet/Opus 4200K tokens~500 页文档
Gemini 1.5 Pro1M-2M tokens~数千页文档
DeepSeek-V3128K tokens~300 页文档
Llama 3.1 405B128K tokens~300 页文档

更大的窗口不等于更好——模型在窗口中间区域的关注度往往低于开头和结尾(“Lost in the Middle” 问题)。

在自回归生成中,每生成一个新 token,需要重新计算所有 token 的 Attention。如果序列长度为 nn,每步的计算量是 O(n2)O(n^2)

已经生成的 token 的 Key 和 Value 不会改变,只需计算一次并缓存。

没有 KV Cache:
Step 1: 计算 [t1] 的 K,V
Step 2: 计算 [t1,t2] 的 K,V ← t1 重复计算了!
Step 3: 计算 [t1,t2,t3] 的 K,V ← t1,t2 重复计算了!
有 KV Cache:
Step 1: 计算 t1 的 K,V → 缓存 {K1,V1}
Step 2: 只计算 t2 的 K,V → 缓存 {K1,V1,K2,V2}
Step 3: 只计算 t3 的 K,V → 缓存 {K1,V1,K2,V2,K3,V3}

KV Cache 将每步计算从 O(n2)O(n^2) 降低到 O(n)O(n),但代价是显存消耗。对于 128K 上下文的大模型,KV Cache 可能占用数十 GB 显存。

KV Cache 大小=2×层数×头数×头维度×序列长度×精度字节数\text{KV Cache 大小} = 2 \times \text{层数} \times \text{头数} \times \text{头维度} \times \text{序列长度} \times \text{精度字节数}

例如 LLaMA 70B(80 层,64 头,128 维,FP16):

=2×80×64×128×序列长度×2  bytes=序列长度×2.6  MB= 2 \times 80 \times 64 \times 128 \times \text{序列长度} \times 2\;\text{bytes} = \text{序列长度} \times 2.6\;\text{MB}

128K 序列 → ~330 GB KV Cache(单请求!)

这就是为什么长上下文模型需要 GQA(Grouped Query Attention)MQA(Multi-Query Attention) 等压缩技术。

当内容超出上下文窗口时,有几种应对方法:

长文档 → LLM 生成摘要 → 用摘要替代原文放入上下文
优点: 简单直接
缺点: 可能丢失细节
文档: [段落1] [段落2] [段落3] [段落4] [段落5]
窗口1: [段落1] [段落2] [段落3] → 处理
窗口2: [段落2] [段落3] [段落4] → 处理(有重叠)
窗口3: [段落3] [段落4] [段落5] → 处理
最后合并各窗口的结果
全量文档 → 切片 → Embedding → 存入向量数据库
用户提问 → Embedding → 检索最相关的片段
只把相关片段放入上下文 → LLM 生成回答

RAG 是目前最实用的长文本方案,将在后续章节详细讲解。

Prompt Caching 是 API 层面的优化,可以大幅降低重复请求的成本和延迟。

第 1 次请求:
[System Prompt (2000 tokens)] + [用户问题 A] → 全价计费
第 2 次请求:
[System Prompt (2000 tokens)] + [用户问题 B]
↑ 命中缓存,不重新计算 ↑ 只计算新增部分
→ 缓存 token 按折扣计费(Anthropic: 90% 折扣)
  • 不变的内容(System Prompt、文档上下文、Few-shot 示例)放在消息开头
  • 变化的内容(用户问题)放在末尾
  • 最小缓存长度:Anthropic 要求 ≥1024 tokens,OpenAI 自动缓存
# Anthropic Prompt Caching 示例
response = client.messages.create(
model="claude-sonnet-4-20250514",
system=[{
"type": "text",
"text": "你是一个代码审查专家...(很长的 system prompt)",
"cache_control": {"type": "ephemeral"} # 标记为可缓存
}],
messages=[{"role": "user", "content": "请审查这段代码..."}]
)
# 第二次调用相同 system prompt 时,会命中缓存
自测题 1:KV Cache 为什么能加速推理?它的代价是什么?
在自回归生成中,每生成一个新 token 都需要与所有历史 token 做 Attention 计算。KV Cache 将已计算的 Key 和 Value 矩阵缓存在显存中,新 token 只需计算自己的 K/V 并追加到缓存,避免了对所有历史 token 的重复计算,将每步复杂度从 $O(n^2)$ 降到 $O(n)$。

代价是显存消耗:以 LLaMA 70B 为例,128K 序列的 KV Cache 需要约 330GB 显存(单个请求)。这也是为什么需要 GQA/MQA 等技术来压缩 KV Cache 的大小,以及为什么长上下文推理通常需要多张 GPU。
自测题 2:RAG 相比直接加大上下文窗口有什么优势?
RAG 有四个核心优势:1) 成本低:只检索最相关的几个片段放入上下文,而非把全部文档塞进去,大幅减少 token 消耗;2) 知识可更新:向量数据库可以随时增删文档,无需重新训练模型;3) 准确性:避免"Lost in the Middle"问题——研究表明模型对上下文中间部分的关注度显著低于首尾;4) 可扩展:理论上可处理任意规模的知识库。

打个比方:加大上下文窗口就像把整个图书馆的书都搬到桌子上;RAG 则是先在图书馆检索目录,只把需要的几本拿过来翻阅,更高效也更精准。
自测题 3:Prompt Caching 的最佳实践是什么?
核心原则是把不变的长内容放在消息序列的开头,变化的用户输入放在末尾。这样不变的部分(System Prompt、参考文档、Few-shot 示例)在多次请求间可以命中缓存,只对新增的用户输入全价计算。Anthropic 的 Prompt Caching 可节省 90% 的费用并显著降低首 token 延迟。

需要注意:缓存有最小长度要求(Anthropic 要求至少 1024 tokens),且缓存前缀必须完全一致——哪怕改了一个字符,缓存就会失效。因此应避免在 System Prompt 中嵌入时间戳等动态内容。