Skip to content

引言

2024 年 2 月,Google 发布了一段令人震惊的演示视频:他们把一根「针」(一句特定的话)藏在 100 万个 token 的文档「大海」里,然后问 Gemini:这段文档里提到的那句关于冰淇淋的话是什么?Gemini 准确地找到了。

这就是著名的**「大海捞针」(Needle in a Haystack)**实验。它验证了一个惊人的事实:模型确实可以在超长上下文中找到特定信息。

但这个实验也暴露了一个问题:如果你把同一根针放在大海的中间位置,模型的准确率会显著下降。这意味着模型虽然「看到」了所有内容,但并没有同等对待它们。

这个发现深刻地影响了我们对上下文工程的理解。


什么是上下文窗口?

一个直觉理解

你可以把上下文窗口理解为模型的工作记忆——就像一个人的短期记忆容量。当你在读这篇文章时,你能记住刚才读过的内容,但如果你同时要回忆三个小时前看过的东西,就会很困难。

上下文窗口 = 模型一次能「看到」的最大 token 数

  用户消息 ──┐
  System Prompt ──┤
  对话历史 ──┼──→ 全部放入上下文窗口 ──→ 模型处理
  检索到的文档 ──┤
  工具调用结果 ──┘

  总 token 数 ≤ 上下文窗口大小

为什么有上限?

上下文窗口不是任意设定的大小限制——它受到计算量的物理约束。

Transformer 的注意力机制:

  每个 token 都要和其他所有 token 计算关联度

  上下文长度 N → 计算量 ∝ N²

  N = 1,000   → 1,000,000 次计算
  N = 10,000  → 100,000,000 次计算(100 倍)
  N = 100,000 → 10,000,000,000 次计算(10000 倍)

  这就是为什么:
  ├── 上下文越长,推理越慢
  ├── 上下文越长,成本越高(输入 token 要计费)
  └── 上下文越长,显存占用越大(KV Cache)

KV Cache:上下文的物理载体

当你发送一个请求时,模型会把所有输入 token 的中间表示缓存起来,这叫 KV Cache

KV Cache 原理(简化):

  输入: "请翻译以下文本:Hello World"

  Token 1 "请"   → Key₁, Value₁   ┐
  Token 2 "翻译"  → Key₂, Value₂   │
  Token 3 "以下"  → Key₃, Value₃   ├─ KV Cache
  Token 4 "文本"  → Key₄, Value₄   │
  Token 5 "Hello" → Key₅, Value₅   │
  Token 6 "World" → Key₆, Value₆   ┘

  生成输出时,每个新 token 都要参考所有 KV Cache
  → 生成第 1 个输出 token: 看 K₁-K₆
  → 生成第 2 个输出 token: 看 K₁-K₆ + 自己

  KV Cache 大小 ∝ 上下文长度
  这就是为什么长上下文需要更多显存

上下文窗口的进化

2020GPT-3: 4K tokens(约 3000 字中文)
2023.3GPT-4: 8K / 32K tokens
2023.11Claude 2.1: 200K tokens
2024.2Gemini 1.5 Pro: 100 万 tokens(实验性)
2024.8Claude 3.5 Sonnet: 200K tokens
2024.12Gemini 2.0: 200 万 tokens
2025多数主流模型支持 128K-1M tokens

上下文长度的实际意义

上下文长度大约能放什么
4K一篇短文、一封邮件
32K一篇长文、一个代码文件
128K一本小说、一个中型代码库
200K几本书、完整的项目文档
1M大量代码库、几百万字的语料
中文估算(粗略):
  4K tokens  ≈ 2,000-3,000 中文字
  32K tokens ≈ 16,000-24,000 中文字
  128K tokens ≈ 64,000-96,000 中文字(≈ 一本小书)
  1M tokens  ≈ 500,000-750,000 中文字(≈ 几本书)

「大海捞针」实验的真相

Lost in the Middle

2023 年,斯坦福大学的研究者发表了一篇论文,揭示了一个令人不安的现象:模型对上下文中间部分的信息经常「视而不见」

「Lost in the Middle」实验结果:

  文档位置:
    ┌──────────────────────────────────────┐
    │ 开头    │    中    间    │    结尾    │
    │ ✅ 找到 │  ❌ 找不到    │  ✅ 找到   │
    └──────────────────────────────────────┘

  准确率曲线(示意):
    100% ┤ ████
         │      ╲
         │        ╲___________
     50% ┤                    ╲___________
         │                                ████
      0% ┤
         └──────┬──────────┬──────────┬──
              开头       中间       结尾

  结论:信息放在开头或结尾,模型更容易找到。
       放在中间,即使只有 20 条文档,也可能被忽略。

对上下文工程的启示

这个发现直接影响你怎么组织发给模型的信息:

上下文布局的最佳实践:

  ┌────────────────────────────────┐
  │ 1. System Prompt               │ ← 最重要:角色设定、核心指令
  │ 2. 关键背景信息                 │ ← 开头位置,模型注意力最高
  │ 3. 检索到的文档 / 中间数据       │ ← 中间位置,可能被忽略
  │ 4. 最近几轮对话                 │ ← 结尾位置,模型注意力回升
  │ 5. 当前用户问题                 │ ← 最后位置,最不可能被忽略
  └────────────────────────────────┘

  策略:最重要的信息放两端,不要放中间。

实战:计算你的上下文用量

python
import tiktoken

def check_context_fit(
    messages: list,
    model: str = "gpt-4o",
    max_output_tokens: int = 1000
):
    """检查消息是否会超出上下文窗口"""
    context_limits = {
        "gpt-4o": 128000,
        "gpt-4o-mini": 128000,
        "claude-sonnet": 200000,
    }

    limit = context_limits.get(model, 128000)
    encoding = tiktoken.encoding_for_model(model)

    total_tokens = 0
    for msg in messages:
        # 每条消息有约 4 token 的格式开销
        total_tokens += 4
        total_tokens += len(encoding.encode(msg["content"]))

    available = limit - total_tokens - max_output_tokens

    print(f"模型: {model}")
    print(f"上下文窗口: {limit:,} tokens")
    print(f"已用: {total_tokens:,} tokens")
    print(f"预留给输出: {max_output_tokens:,} tokens")
    print(f"剩余可用: {available:,} tokens")

    if available < 0:
        print(f"⚠️ 超出上下文窗口 {-available:,} tokens!")
    else:
        print(f"✅ 上下文窗口充足")

    return available > 0

# 使用示例
messages = [
    {"role": "system", "content": "你是一个有帮助的助手。"},
    {"role": "user", "content": "请总结这篇文档。"},
    {"role": "assistant", "content": "好的,请提供文档内容。"},
    {"role": "user", "content": "这是一篇很长的文档..." * 1000},
]

check_context_fit(messages)

超出上下文时的处理策略

当上下文溢出时,你有几个选择:

  1. 截断(Truncation)
     丢弃最早的消息,只保留最近的
     → 简单但丢失历史信息

  2. 摘要(Summarization)
     用模型总结旧消息,用摘要替代原文
     → 保留关键信息,但摘要可能不准确

  3. 检索(RAG)
     不把所有内容放进上下文,只检索相关的部分
     → 精准但需要额外基础设施

  4. 等待更长窗口的模型
     模型的上下文窗口在持续增长
     → 被动策略,但有时可行

本节小结

概念要点
上下文窗口模型一次能处理的最大 token 数,即「工作记忆」
为什么有限注意力机制计算量 ∝ N²,显存占用随长度增长
KV Cache上下文的物理载体,缓存所有输入 token 的中间表示
Lost in the Middle模型容易忽略上下文中间的信息,重要内容放两端
溢出策略截断、摘要、RAG、等待更长窗口

思考题

  1. 如果模型的上下文窗口可以无限大,我们还需要 RAG(检索增强生成)吗?为什么?
  2. 「Lost in the Middle」现象对 RAG 系统有什么影响?如果你检索到了 20 条相关文档,应该如何排列它们?
  3. 假设你在做一个客服机器人,用户的对话已经进行了 100 轮,超出了上下文窗口。你会选择哪种策略来处理?为什么?