摘要
如果你刚接触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-4o | 128K tokens | 约10万中文 | 2024年5月 |
| Claude 3.5 Sonnet | 200K tokens | 约15万中文 | 2024年6月 |
| Claude 4 Opus | 1M tokens | 约75万中文 | 2025年5月 |
| Gemini 1.5 Pro | 1M tokens | 约75万中文 | 2024年5月 |
| Gemini 2.0 Flash | 1M tokens | 约75万中文 | 2024年12月 |
| DeepSeek V3 | 64K tokens | 约5万中文 | 2024年12月 |
| Qwen 2.5 | 128K 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为什么好?
- 自然编码相对位置:位置5和位置8的关系,天然体现在它们的旋转差里
- 支持长度外推:训练时见过2048位置,推理时可以外推到更长的序列
- 效率高:不需要额外的位置嵌入表
上下文窗口使用实战
窗口规划原则
在设计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:长对话突然失忆
这是因为对话历史超出了窗口,被截断了。
解决方案:
- 定期摘要:对话超过一定轮次后,用LLM生成摘要
- 分层存储:最近对话用完整内容,历史用摘要
- 关键信息提取:把重要的用户偏好、结论等单独存储
问题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摘要 │
│ │
└─────────────────────────────────────────────────────────────┘
相关主题
- 滑动窗口技术 - 如何把长文本切分成块
- 上下文压缩技术 - 如何在有限空间里塞更多信息
- Long Context Model详解 - 长上下文模型的底层技术
- Context Caching详解 - 如何节省重复上下文的成本
- RAG上下文优化指南 - 检索增强场景下的上下文处理
参考文献
- Liu, N. F., et al. (2024). Lost in the Middle: How Language Models Use Long Contexts. arXiv.
- Vaswani, A., et al. (2017). Attention Is All You Need. NeurIPS.
- Anthropic (2024). Claude Model Card. Official Documentation.
- Google DeepMind (2024). Gemini 1.5 Technical Report.
- Su, J., et al. (2024). RoFormer: Enhanced Transformer with Rotary Position Embedding. arXiv.