摘要

如果你刚接触AI开发,“上下文窗口”这个词可能会让你一脸懵逼。这篇文章就是为零基础小白准备的,手把手带你搞懂这个LLM最核心的概念之一。我会用大量比喻和实操例子,保证你看完就能动手干活。

先来聊聊:什么是上下文窗口?

打个比方

想象你是一个老师,现在要回答学生的问题。但你有个限制:一次性只能看到10页纸的内容

学生问:“第二次世界大战是怎么开始的?”

你翻开历史书第1-10页,发现讲的是工业革命; 再翻到第50-60页,讲的是二战后重建; 但关键的第40-50页(二战爆发原因)你没看到…

这种情况在LLM里就叫做**“上下文窗口不足”**——模型能一次性处理的内容是有限的,超出的部分它根本”看不到”。

**上下文窗口(Context Window)**就是LLM在一次推理过程中能处理的最大token数量。你可以理解成模型的”工作台面积”——面积越大,能同时放的东西越多。

再说人话一点

看个实际例子:

假设上下文窗口 = 1000 tokens

你给ChatGPT发了一段小说:"从前有座山,山里有座庙..."
(这段文字 = 800 tokens)

现在你问它:"庙里住着谁?"
模型需要:
1. 理解你的问题(消耗一些tokens)
2. 回忆小说内容(使用那800 tokens)
3. 生成回答(还要预留输出空间)

如果小说太长(1500 tokens),模型根本装不下,只能截断!

为什么这很重要?

根据2025-2026年的统计数据:

问题类型根本原因
AI回答不准确关键信息被截断在窗口外
长对话突然失忆历史信息超出窗口被丢弃
无法分析长文档文档太长塞不进去
成本突然飙升反复传递大上下文

上下文窗口管理,本质上就是:如何在有限的”工作台面积”上,放下最关键的信息。


上下文窗口的硬核原理

Transformer架构的先天限制

LLM的核心是Transformer架构,而这玩意儿有个著名的O(n²)复杂度问题

# 这是注意力计算的简化原理
def attention(Q, K, V):
    # Q, K, V都是 [batch, seq_len, hidden_dim]
    
    # 计算注意力分数:seq_len × seq_len 的矩阵
    scores = Q @ K.T  # 假设seq_len=1000,这就是1000×1000=100万个数字
    
    # 这个计算量和序列长度的平方成正比!
    # seq_len翻倍,计算量增加4倍!

这就解释了为什么上下文窗口有上限——太长了,显卡显存扛不住,计算也慢得要死。

主流模型的上下文窗口对比

来看看现在主流模型的”工作台面积”:

模型上下文窗口相当于多少文字发布时间
GPT-4o128K tokens约10万中文2024年5月
Claude 3.5 Sonnet200K tokens约15万中文2024年6月
Claude 4 Opus1M tokens约75万中文2025年5月
Gemini 1.5 Pro1M tokens约75万中文2024年5月
Gemini 2.0 Flash1M tokens约75万中文2024年12月
DeepSeek V364K tokens约5万中文2024年12月
Qwen 2.5128K tokens约10万中文2024年9月

K = 1024,所以128K = 128 × 1024 ≈ 13万个tokens

Token是什么?怎么估算?

Token是LLM处理的基本单位,大致规则:

英文:4个字符 ≈ 1个token
     "hello world" = 2 tokens

中文:1-2个汉字 ≈ 1个token
     "你好世界" = 4 tokens

代码:每个token平均处理更快
     复杂的代码1行可能=3-5 tokens

实用估算代码:

def estimate_tokens(text: str) -> int:
    """估算文本的token数量"""
    # 中文字符
    chinese_chars = sum(1 for c in text if '\u4e00' <= c <= '\u9fff')
    # 英文单词(粗略)
    english_words = len(text.split()) - chinese_chars
    
    # 粗略公式:中文0.5 token/字,英文0.25 token/词
    estimated = int(chinese_chars * 0.5 + english_words * 0.25)
    
    return estimated
 
# 测试一下
text = "你好,这是一个测试文本,Hello World!"
print(f"估算token数: {estimate_tokens(text)}")
 
# 实际使用专业库
# pip install tiktoken
import tiktoken
enc = tiktoken.get_encoding("cl100k_base")  # GPT-4/Claude用的
tokens = enc.encode(text)
print(f"精确token数: {len(tokens)}")

Lost in Middle:中间信息消失的谜题

这是个什么怪问题?

2024年,斯坦福等机构的研究者发现了一个诡异的现象:

当关键信息位于上下文”中间”位置时,模型检索准确率会大幅下降!

做个实验:

实验设计:
1. 给模型一段1000行的文本
2. 在开头插入问题:在第X行提到的城市是?
3. 在中间插入问题:在第X行提到的城市是?
4. 在结尾插入问题:在第X行提到的城市是?

结果:
┌────────────────────────────────────────────┐
│  位置     │  准确率                        │
├────────────────────────────────────────────┤
│  开头(1-10%)   │  95%  ████████████████████▌  │
│  结尾(90-100%) │  93%  ████████████████████   │
│  中间(40-60%)  │  52%  ██████████░░░░░░░░░░░░  │
└────────────────────────────────────────────┘

开头和结尾的信息模型能很好地利用,但中间的信息就像被黑洞吸走了一样!

为什么会有这个问题?

原因1:注意力机制的天然倾向

想象你读一篇长文章:

你的注意力分布大概是:

开头:████████████████████████████████████(超高)
前部:████████████████████████████░░░░░░░░(高)
中间:████████████░░░░░░░░░░░░░░░░░░░░░(低)
后部:████████████████████████████░░░░░░░░(高)
结尾:████████████████████████████████████(超高)

LLM的自注意力机制也有类似的问题——开头和结尾的信息会被反复”强化”,而中间的信息容易被稀释。

原因2:位置编码的影响

位置编码告诉模型”这个词在句子的哪个位置”。不同的编码方式对中间信息的影响不同:

编码方式中间丢失程度说明
ALiBi较严重远程位置天然权重低
RoPE较轻通过旋转矩阵缓解
标准位置编码严重完全依赖绝对位置

实战:如何解决Lost in Middle?

策略1:重要信息放首尾

def reorder_for_llm(contexts: list, query: str) -> str:
    """
    重排上下文:最相关的内容放首尾
    
    核心思路:既然中间容易丢信息,
    那就把最重要的放两头!
    """
    # 1. 计算每个上下文与查询的相关性
    scored = []
    for ctx in contexts:
        score = calculate_relevance(ctx, query)
        scored.append((score, ctx))
    
    # 2. 按相关性排序
    scored.sort(key=lambda x: x[0], reverse=True)
    
    # 3. 重排:最高相关放开头,次高放结尾
    if len(scored) >= 3:
        reordered = [
            scored[0][1],           # 最高相关 - 开头
            *scored[1:-1],          # 中间的(可能丢失)
            scored[-1][1]            # 次高相关 - 结尾
        ]
    else:
        reordered = [ctx for _, ctx in scored]
    
    return "\n\n---\n\n".join(reordered)

策略2:使用结构化标记

## 关键结论(必须使用!)
这是最重要的结论,必须记住...
 
## 背景信息
这里是一些背景知识...
 
## 详细论证(参考)
这里可能不太重要...

在LLM的系统提示里加一句:“优先关注标签内的内容”

策略3:渐进式信息披露

不要一次性给太多信息,而是分层加载:

# 分层加载示例
class ProgressiveDisclosure:
    def __init__(self, llm_client):
        self.llm = llm_client
    
    def get_context(self, query: str, depth: int = 1):
        """
        根据深度返回不同级别的上下文
        depth=1: 只返回摘要
        depth=2: 返回摘要+关键细节
        depth=3: 返回完整信息
        """
        if depth == 1:
            return "文档摘要:这是关于XX技术的主要结论..."
        elif depth == 2:
            return """文档摘要:...
关键发现:
1. XX技术可以提升30%效率
2. 适用于XX场景...
"""
        else:
            return self.get_full_document()

位置编码:LLM的空间感

什么是位置编码?

想象你在看这句话:

"狗咬人" 和 "人咬狗" 
用完全相同的字,但意思完全不同!

LLM处理的是一串token,它需要知道:

  • token A在token B前面还是后面?
  • 它们之间隔了多少距离?

位置编码就是给每个token贴上一个”位置标签”

RoPE:旋转位置编码

当前最流行的位置编码是RoPE(Rotary Position Embedding),它的核心思想很巧妙:

不是直接告诉模型”你在第5个位置”,而是用旋转来表达位置关系

import math
 
class RotaryPositionEmbedding:
    """RoPE的简化实现"""
    
    def __init__(self, dim, base=10000):
        self.dim = dim
        self.base = base
        # 预计算频率
        self.inv_freq = 1.0 / (base ** (2 * torch.arange(0, dim, 2) / dim))
    
    def rotate(self, x, position_ids):
        """
        对Query和Key应用旋转
        这样它们之间的"旋转角度差"就编码了相对位置
        """
        # 旋转公式:cos(mθ), sin(mθ)
        # 其中m是位置,θ是频率
        
        freqs = position_ids.unsqueeze(1) * self.inv_freq.unsqueeze(0)
        
        # 旋转后的值
        x_rotate = x * torch.cos(freqs) + self.rotate_half(x) * torch.sin(freqs)
        
        return x_rotate
    
    def rotate_half(self, x):
        """半旋转"""
        x1, x2 = x[..., : x.shape[-1] // 2], x[..., x.shape[-1] // 2 :]
        return torch.cat([-x2, x1], dim=-1)

RoPE为什么好?

  1. 自然编码相对位置:位置5和位置8的关系,天然体现在它们的旋转差里
  2. 支持长度外推:训练时见过2048位置,推理时可以外推到更长的序列
  3. 效率高:不需要额外的位置嵌入表

上下文窗口使用实战

窗口规划原则

在设计LLM应用时,要合理分配有限的窗口空间:

class ContextWindowPlanner:
    def __init__(self, model="claude-3-5-sonnet-20241022"):
        self.model = model
        # 不同模型的窗口大小
        self.limits = {
            "claude-3-5-sonnet-20241022": 200000,
            "gpt-4o": 128000,
            "gemini-1.5-pro": 1000000,
        }
    
    def plan_context(
        self,
        system_prompt: str,
        user_query: str,
        retrieved_contexts: list,
        few_shot_examples: list = None,
        reserve_output: int = 4000
    ):
        """规划上下文分配"""
        
        # 计算各部分token数
        system_tokens = estimate_tokens(system_prompt)
        query_tokens = estimate_tokens(user_query)
        context_tokens = sum(estimate_tokens(c) for c in retrieved_contexts)
        example_tokens = sum(estimate_tokens(e) for e in (few_shot_examples or []))
        
        # 目标窗口大小
        max_tokens = self.limits.get(self.model, 100000)
        
        # 可用于检索内容的空间
        available = max_tokens - system_tokens - query_tokens - example_tokens - reserve_output
        
        print(f"""上下文规划报告
══════════════════════════════
系统提示:   {system_tokens:>8} tokens ({system_tokens/max_tokens:.1%})
用户查询:   {query_tokens:>8} tokens ({query_tokens/max_tokens:.1%})
示例:       {example_tokens:>8} tokens ({example_tokens/max_tokens:.1%})
输出预留:   {reserve_output:>8} tokens ({reserve_output/max_tokens:.1%})
────────────────────────────────
可用空间:   {available:>8} tokens ({available/max_tokens:.1%})
检索内容:   {context_tokens:>8} tokens
══════════════════════════════
""")
        
        # 判断是否需要压缩
        if context_tokens > available:
            print(f"⚠️ 需要压缩!超出 {(context_tokens - available) / available:.1%}")
            return self._compress_needed(contexts, available)
        
        return retrieved_contexts
    
    def _compress_needed(self, contexts, available):
        """需要压缩时的处理"""
        print("执行压缩策略...")
        # 1. 优先保留开头和结尾
        # 2. 丢弃中间冗余
        # 3. 或者使用LLM压缩
        return contexts  # 返回压缩后的结果

窗口使用配置示例

# 一个实际的Claude API调用示例
import anthropic
 
client = anthropic.Anthropic()
 
def ask_with_long_context(
    system_prompt: str,
    user_query: str,
    document_chunks: list
):
    """
    使用长文档上下文进行问答
    """
    
    # 构建上下文
    context_text = "\n\n".join(document_chunks)
    
    # 检查是否需要截断(优先保留首尾)
    estimated = estimate_tokens(context_text)
    if estimated > 60000:  # 留足余量
        context_text = prioritize_head_tail(context_text, max_tokens=60000)
    
    message = client.messages.create(
        model="claude-3-5-sonnet-20241022",
        max_tokens=4096,
        system=system_prompt,
        messages=[
            {
                "role": "user",
                "content": f"""## 参考文档
{context_text}
 
## 用户问题
{user_query}
 
## 回答要求
1. 首先说明你使用了文档的哪些部分
2. 如果文档中有明确答案,直接引用
3. 如果文档中没有相关信息,明确说明"根据提供的文档,我无法找到答案"
"""
            }
        ]
    )
    
    return message.content
 
def prioritize_head_tail(text: str, max_tokens: int) -> str:
    """
    保留文本的开头和结尾,截断中间
    这是解决Lost in Middle问题的简单有效方法
    """
    tokens = text.split()
    total = len(tokens)
    
    # 开头保留40%,结尾保留40%
    head_count = int(total * 0.4)
    tail_count = int(total * 0.4)
    
    if total <= max_tokens:
        return text
    
    # 截断中间部分
    head = ' '.join(tokens[:head_count])
    tail = ' '.join(tokens[-tail_count:])
    
    return f"""{head}
 
[... 中间内容已截断 ...]
 
{tail}"""

常见问题与解决方案

问题1:模型忽略中间的信息

原因解决方案
关键信息被截断重排:重要内容放首尾
无关内容太多压缩:删除冗余
格式混乱结构化:用标记分隔

问题2:上下文被截断

# 解决方案:智能截断
def smart_truncate(contexts: list, max_tokens: int, query: str):
    """
    智能截断策略
    1. 按相关性排序
    2. 优先保留高相关
    3. 截断低相关
    """
    # 给每个上下文打分
    scored = [(calculate_relevance(ctx, query), ctx) for ctx in contexts]
    scored.sort(key=lambda x: x[0], reverse=True)
    
    result = []
    current_tokens = 0
    
    for score, ctx in scored:
        ctx_tokens = estimate_tokens(ctx)
        if current_tokens + ctx_tokens <= max_tokens:
            result.append(ctx)
            current_tokens += ctx_tokens
    
    return result

问题3:长对话突然失忆

这是因为对话历史超出了窗口,被截断了。

解决方案:

  1. 定期摘要:对话超过一定轮次后,用LLM生成摘要
  2. 分层存储:最近对话用完整内容,历史用摘要
  3. 关键信息提取:把重要的用户偏好、结论等单独存储

问题4:成本过高

窗口越大,消耗的token越多,成本越高。

优化建议:

  • 不要盲目追求大窗口,适合就行
  • 启用上下文缓存(后续章节详述)
  • 压缩重复使用的系统提示

一图总结

┌─────────────────────────────────────────────────────────────┐
│                    上下文窗口使用指南                           │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  1️⃣ 了解你的模型窗口有多大                                   │
│      Claude 3.5: 200K | GPT-4o: 128K | Gemini: 1M        │
│                                                             │
│  2️⃣ 合理分配窗口空间                                         │
│      ┌────────────┬────────────┬────────────┐                │
│      │ 系统提示   │ 上下文    │ 用户查询  │                │
│      │ 5-10%     │ 50-70%    │ 5-10%     │                │
│      └────────────┴────────────┴────────────┘                │
│      别忘了预留输出空间!                                       │
│                                                             │
│  3️⃣ 解决Lost in Middle问题                                  │
│      核心原则:重要信息放首尾!                                │
│                                                             │
│  4️⃣ 选择合适的压缩策略                                       │
│      轻压缩:规则删除  |  重压缩:LLM摘要                     │
│                                                             │
└─────────────────────────────────────────────────────────────┘

相关主题


参考文献

  1. Liu, N. F., et al. (2024). Lost in the Middle: How Language Models Use Long Contexts. arXiv.
  2. Vaswani, A., et al. (2017). Attention Is All You Need. NeurIPS.
  3. Anthropic (2024). Claude Model Card. Official Documentation.
  4. Google DeepMind (2024). Gemini 1.5 Technical Report.
  5. Su, J., et al. (2024). RoFormer: Enhanced Transformer with Rotary Position Embedding. arXiv.