上下文窗口越大不等于越好用
上下文窗口是什么、为什么有限制
每次发请求时 messages 列表的总 token 数(包括 system、所有历史、tools 定义、当前问题)必须在模型的"上下文窗口"内,超过的部分服务端会截断或直接报错。这个上限由模型架构决定,常见数字:
| 模型 | 上下文窗口 | 输出上限 |
|---|---|---|
| GPT-3.5 | 16K | 4K |
| GPT-4 (原始) | 8K~32K | 4K |
| GPT-4o / GPT-4.1 | 128K~1M | 16K~64K |
| Claude Opus 4 / Sonnet 4 | 200K~1M | 8K~64K |
| Gemini 2.0 Pro | 2M | 8K |
| DeepSeek-V3 / R1 | 64K~128K | 8K |
| Qwen2.5 | 32K~128K | 8K |
| Llama 3.3 | 128K | 4K |
为什么有限制?根本上是 self-attention 的代价——计算量随上下文长度的平方增长。8K 到 16K 计算量翻 4 倍,128K 到 1M 翻 60 倍。这个量级的成本让"上下文越大越好"在工程上不成立。
KV Cache 是什么、为什么关键
self-attention 处理每个新 token 时需要"看到所有历史 token"。如果每生成一个 token 都把所有历史重算一遍,输出长度 N 的总成本是 O(N²),1000 个 token 的回复就要做 50 万次注意力计算。
KV Cache 是一个把已经计算过的"键值对"(K 和 V 矩阵)缓存下来的优化:每个新 token 只需要计算它自己的 Q,然后和缓存的 K、V 做注意力。这样生成第 N 个 token 的成本变成 O(N) 而不是 O(N²)。整个生成过程从平方复杂度降到线性。
KV Cache 是现代 LLM 推理性能的命脉。它的体积通常以 GB 为单位——一个 7B 模型在 32K 上下文下的 KV Cache 大约 4 GB,128K 大约 16 GB。这就是"上下文越长越吃显存"的根本原因。
vLLM 这类高性能推理引擎的核心创新(PagedAttention)就是更高效地管理这个 KV Cache,让单卡能并发服务更多请求。
长上下文的三种实现路径
直接把上下文窗口拉到 1M+ 不是简单"加显存"就行。技术上目前有三种主流方案:
1. 原生长上下文——架构层面就能处理,训练数据里就有长文本。这是最优解但最难。Gemini 的 2M 上下文据信用了 RingAttention 这类分布式注意力。
2. 位置编码外推——用 RoPE / ALiBi 这类支持外推的位置编码,让模型在比训练长度更长的上下文里也能工作。Llama、Qwen 等开源模型大量采用,但效果会随长度衰减。
3. 检索增强——不真的让模型看完整 1M 上下文,而是先用 RAG 检索出最相关的 N 段,只送进去那一小段。这是最实用的工程方案,效果也常常比"硬塞 1M"更好。
长上下文的常见迷思
迷思 1:上下文越大越准
错。研究表明大多数模型在长上下文上有显著的"中段失忆"现象(lost in the middle)——上下文开头和结尾的信息被精确召回,中间的信息容易被遗漏。Claude 系列对中段相对鲁棒,但仍有衰减。
迷思 2:1M 上下文就不需要 RAG 了
不对。即使能塞 1M token 进去,也面临三个问题:
- 成本——每次调用都按 1M 计费,比 RAG + 5K 上下文贵 200 倍
- 延迟——首 token 延迟随上下文长度线性增长,1M 上下文可能要 30~60 秒才出首 token
- 准确性——前面提到的中段失忆问题在 100K+ 上下文显著
实务中:几千到几万 token 的上下文用全量塞,10 万以上还是得 RAG。
迷思 3:只要支持 128K 我就可以输入 128K 文档
理论支持不等于效果好。很多模型的 128K 是"能塞进去不报错",但实际有效信息容量可能只有 32K。具体能用多少要做实测,常用的测试方法是 NIAH(Needle in a Haystack)——在长文本里塞一个特定信息,让模型回忆,看不同位置的召回准确率。
Prompt Caching:长上下文的成本救星
如果你的应用确实需要把同一段长上下文反复用(比如每次问答都要带上完整产品文档),Prompt Caching 是必须的。原理:服务端检测到请求的前缀和最近某次缓存一致,复用之前已经算好的 KV Cache,只对新增部分做计算。计费上缓存命中部分通常只收 10~20% 原价。
OpenAI、Anthropic、DeepSeek 都支持,触发条件略有不同:
OpenAI——自动缓存,1024+ token 的相同前缀自动触发,缓存有效期 5~10 分钟
Anthropic——需要在 messages 里显式标记缓存点:
import anthropic
client = anthropic.Anthropic()
response = client.messages.create(
model="claude-opus-4-7",
messages=[{"role": "user", "content": question}],
system=[
{
"type": "text",
"text": "你是一个产品文档助手。",
},
{
"type": "text",
"text": LONG_PRODUCT_DOC, # 几万字
"cache_control": {"type": "ephemeral"}, # 显式缓存
},
],
)
DeepSeek——自动缓存,前 8K 内容免费缓存,可触发的最低粒度是 64 token
实测在重复 prompt 场景能省 60~80% 成本,是长上下文应用的必修知识。
把上下文留给最需要的内容
短上下文不浪费,长上下文不烂用。给一组实务原则:
1. system prompt 放最前并保持稳定
system 内容不变才能持续命中 Prompt Cache。把动态部分(用户偏好、当前任务参数)放在第一个 user 消息里,不要混进 system。
2. 消息历史超过阈值就摘要
超过比如 30 轮或 20K token 时,让一个轻量模型把"前 20 轮的关键信息"摘要成 500 字,替换掉原始历史。最近几轮保留原文。
3. 检索片段精排
RAG 检索回来的文档片段做 reranker 精排,只保留最相关的 3~5 段。多塞片段 = 多花钱 + 模型分心。
4. 工具定义按需暴露
不是把所有 50 个工具一次塞进 tools。先用一个意图分类调用决定本轮可能用到的工具子集,只把这部分塞进 tools。
5. 输出限制别忘
max_tokens 设成业务真实需要的上限。让模型生成 2K 时 max_tokens=4000 是浪费——服务端预留更多 KV Cache 显存,并发能力下降。
怎么衡量你的应用的上下文使用情况
加一段轻量统计在 chat 封装里:
import logging
from openai.types.chat import ChatCompletion
def chat_with_stats(messages, model, **kwargs) -> str:
resp: ChatCompletion = client.chat.completions.create(
model=model, messages=messages, **kwargs
)
usage = resp.usage
logging.info(
f"prompt={usage.prompt_tokens} "
f"completion={usage.completion_tokens} "
f"cached={getattr(usage, 'prompt_tokens_details', {}).get('cached_tokens', 0)}"
)
return resp.choices[0].message.content
把这些数据汇到 Langfuse 这类可观测平台,能直观看到:
- 平均 prompt 长度变化趋势
- 缓存命中率
- 每用户每天的 token 消耗
- 哪些 endpoint 上下文异常长(可能是 bug)
没有数据就没有优化。先有可观测,才谈得上控制成本。
相关阅读
- Lost in the Middle: How Language Models Use Long Contexts — 长上下文中段失忆现象的代表论文
- PagedAttention paper (vLLM)
- OpenAI Prompt Caching
- Anthropic Prompt Caching
- DeepSeek 上下文硬盘缓存
- Needle in a Haystack 测试
版权声明: 如无特别声明,本文版权归 sshipanoo 所有,转载请注明本文链接。
(采用 CC BY-NC-SA 4.0 许可协议进行授权)
本文标题:番外 4:上下文窗口进化史与 KV Cache
本文链接:https://www.sshipanoo.com/blog/ai/ai-for-python/番外04-上下文窗口/
本文最后一次更新为 天前,文章中的某些内容可能已过时!