上下文窗口越大不等于越好用

上下文窗口是什么、为什么有限制

每次发请求时 messages 列表的总 token 数(包括 system、所有历史、tools 定义、当前问题)必须在模型的"上下文窗口"内,超过的部分服务端会截断或直接报错。这个上限由模型架构决定,常见数字:

模型上下文窗口输出上限
GPT-3.516K4K
GPT-4 (原始)8K~32K4K
GPT-4o / GPT-4.1128K~1M16K~64K
Claude Opus 4 / Sonnet 4200K~1M8K~64K
Gemini 2.0 Pro2M8K
DeepSeek-V3 / R164K~128K8K
Qwen2.532K~128K8K
Llama 3.3128K4K

为什么有限制?根本上是 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)

没有数据就没有优化。先有可观测,才谈得上控制成本。

相关阅读

版权声明: 如无特别声明,本文版权归 sshipanoo 所有,转载请注明本文链接。

(采用 CC BY-NC-SA 4.0 许可协议进行授权)

本文标题:番外 4:上下文窗口进化史与 KV Cache

本文链接:https://www.sshipanoo.com/blog/ai/ai-for-python/番外04-上下文窗口/

本文最后一次更新为 天前,文章中的某些内容可能已过时!