03 · Context Window#
每次调用 LLM,你能放进去的内容是有上限的——这个上限就是 Context Window(上下文窗口)。
理解它对实际开发至关重要:
为什么模型会「忘记」对话开头说的内容?
为什么处理长文档需要特殊策略?
为什么更大的窗口意味着更高的成本?
本章从原理讲起,然后动手实验,最后给出实用的长文本处理方案。
1. Context Window 是什么#
Transformer 模型在生成每个 token 时,会对所有已有 token 做 Attention 运算:
Attention 计算量 ∝ n² (n = token 数量)
这意味着:
窗口翻倍 → 计算量翻 4 倍、显存翻 4 倍
存在物理上限,超出则无法计算
Context Window 包含什么:
┌─────────────────────────────────────────┐
│ System Prompt │
│ ───────────────────────────────────── │
│ 对话历史 (User + Assistant turns) │
│ ───────────────────────────────────── │
│ 当前 User 输入 │
│ ───────────────────────────────────── │
│ [待生成的 Assistant 输出] │
└─────────────────────────────────────────┘
全部加起来不能超过窗口上限
Input tokens + Output tokens ≤ Context Window
2. 各模型的窗口大小#
# 主流模型的 context window(2025 年数据,请以官网为准)
models = [
# (模型名, 上下文窗口, 最大输出, 发布时间)
("gpt-3.5-turbo", 4_096, 4_096, "2022"),
("gpt-4", 8_192, 4_096, "2023"),
("gpt-4o", 128_000, 16_384, "2024"),
("gpt-4o-mini", 128_000, 16_384, "2024"),
("claude-3-haiku", 200_000, 4_096, "2024"),
("claude-sonnet-4-6", 200_000, 64_000, "2025"),
("gemini-1.5-pro", 1_000_000, 8_192, "2024"),
("gemini-2.0-flash", 1_000_000, 8_192, "2025"),
]
# 换算成「能装多少内容」
CHARS_PER_TOKEN_EN = 4 # 英文
CHARS_PER_TOKEN_ZH = 1.5 # 中文
WORDS_PER_PAGE = 500 # 英文,每页约 500 词
print(f"{'模型':<22} {'窗口(K)':>8} {'最大输出(K)':>12} {'约等于(英文页数)':>16}")
print("-" * 65)
for name, ctx, out, year in models:
pages = (ctx * CHARS_PER_TOKEN_EN) / (WORDS_PER_PAGE * 5) # ~5 chars/word
print(f"{name:<22} {ctx//1000:>7}K {out//1000:>11}K {pages:>14.0f} 页")
模型 窗口(K) 最大输出(K) 约等于(英文页数)
-----------------------------------------------------------------
gpt-3.5-turbo 4K 4K 7 页
gpt-4 8K 4K 13 页
gpt-4o 128K 16K 205 页
gpt-4o-mini 128K 16K 205 页
claude-3-haiku 200K 4K 320 页
claude-sonnet-4-6 200K 64K 320 页
gemini-1.5-pro 1000K 8K 1600 页
gemini-2.0-flash 1000K 8K 1600 页
import tiktoken
enc = tiktoken.get_encoding("cl100k_base")
# 实际可用的 input token = 窗口上限 - 预留给输出的 token
def available_input_tokens(
context_limit: int,
reserved_for_output: int = 1000,
system_prompt: str = "",
) -> int:
system_tokens = len(enc.encode(system_prompt))
return context_limit - reserved_for_output - system_tokens
system = "You are a helpful assistant that answers questions concisely."
print("实际可用 Input Token(扣除 system prompt + 预留 1K 输出):")
print(f"{'模型':<22} {'窗口':>10} {'可用 Input':>12}")
print("-" * 48)
for name, ctx, out, year in models:
avail = available_input_tokens(ctx, reserved_for_output=1000, system_prompt=system)
print(f"{name:<22} {ctx:>10,} {avail:>12,}")
实际可用 Input Token(扣除 system prompt + 预留 1K 输出):
模型 窗口 可用 Input
------------------------------------------------
gpt-3.5-turbo 4,096 3,084
gpt-4 8,192 7,180
gpt-4o 128,000 126,988
gpt-4o-mini 128,000 126,988
claude-3-haiku 200,000 198,988
claude-sonnet-4-6 200,000 198,988
gemini-1.5-pro 1,000,000 998,988
gemini-2.0-flash 1,000,000 998,988
3. 超出窗口时会发生什么?#
超出限制时,API 会直接报错(不会静默截断)。 我们来构造一个接近边界的场景,用真实 API 感受一下。
import os
from dotenv import load_dotenv
load_dotenv()
# 当前配置的模型
current_model = os.getenv("LLM_MODEL", "未设置")
print(f"当前模型: {current_model}")
# 模拟一个很长的对话历史
def make_long_text(target_tokens: int) -> str:
"""生成大约 target_tokens 个 token 的文本。"""
unit = "The quick brown fox jumps over the lazy dog. " # ~10 tokens
unit_tokens = len(enc.encode(unit))
repeats = target_tokens // unit_tokens
text = unit * repeats
actual = len(enc.encode(text))
return text, actual
# 生成不同长度的文本
for target in [100, 1_000, 10_000, 50_000]:
text, actual = make_long_text(target)
print(f"目标 {target:>6} tokens → 实际 {actual:>6} tokens ({len(text):,} 字符)")
当前模型: openai/gpt-5-mini
目标 100 tokens → 实际 91 tokens (405 字符)
目标 1000 tokens → 实际 901 tokens (4,050 字符)
目标 10000 tokens → 实际 9091 tokens (40,905 字符)
目标 50000 tokens → 实际 45451 tokens (204,525 字符)
from utils.llm_client import chat
# 发送一个正常大小的请求,观察 token 使用情况
long_text, token_count = make_long_text(500)
prompt = f"""{long_text}
请用一句话总结上面这段文字的主要内容。"""
print(f"Prompt token 数: {len(enc.encode(prompt)):,}")
response = chat(prompt)
print(f"Response: {response}")
Prompt token 数: 469
Response: 这段文字多次重复了英文示例句 “The quick brown fox jumps over the lazy dog。”
# 验证「遗忘效应」:把关键信息放在很长文本的开头,看模型还记不记得
secret = "SECRET_CODE_42XZ"
filler, _ = make_long_text(3000) # 3K tokens 的填充内容
prompt = f"""记住这个暗号:{secret}
{filler}
请告诉我之前给你的暗号是什么?"""
print(f"Prompt token 数: {len(enc.encode(prompt)):,}")
response = chat(prompt)
print(f"\n模型回答: {response}")
print(f"\n暗号是否在回答中: {secret in response}")
Prompt token 数: 2,755
模型回答: 暗号是:SECRET_CODE_42XZ
暗号是否在回答中: True
4. 长文本处理的三种策略#
当文本超出窗口,有三种基本策略:
策略 |
适用场景 |
缺点 |
|---|---|---|
截断 |
只关心开头或结尾 |
丢失信息 |
滑动窗口 |
需要连续处理整个文本 |
边界处信息割裂 |
分块摘要 |
需要理解全文 |
多次 API 调用,摘要有损 |
# ── 策略 1:截断 ──────────────────────────────────────────
def truncate(
text: str,
max_tokens: int,
from_end: bool = False,
) -> str:
"""
截断文本到 max_tokens。
from_end=True 保留末尾(适合对话历史:保留最近的内容)。
"""
ids = enc.encode(text)
if len(ids) <= max_tokens:
return text
if from_end:
ids = ids[-max_tokens:]
else:
ids = ids[:max_tokens]
return enc.decode(ids)
sample = "这是一段很长的文本。" * 200
print(f"原始: {len(enc.encode(sample))} tokens")
t1 = truncate(sample, 100, from_end=False)
print(f"保留开头 100 tokens: '{t1[:50]}...'")
t2 = truncate(sample, 100, from_end=True)
print(f"保留末尾 100 tokens: '...{t2[-50:]}'")
原始: 2200 tokens
保留开头 100 tokens: '这是一段很长的文本。这是一段很长的文本。这是一段很长的文本。这是一段很长的文本。这是一段很长的文本。...'
保留末尾 100 tokens: '...这是一段很长的文本。这是一段很长的文本。这是一段很长的文本。这是一段很长的文本。这是一段很长的文本。'
# ── 策略 2:滑动窗口分块 ──────────────────────────────────
def sliding_window_chunks(
text: str,
chunk_size: int = 500,
overlap: int = 50,
) -> list[str]:
"""
把文本切成有重叠的块,避免边界信息丢失。
overlap: 相邻块之间共享的 token 数。
"""
ids = enc.encode(text)
chunks = []
start = 0
while start < len(ids):
end = min(start + chunk_size, len(ids))
chunks.append(enc.decode(ids[start:end]))
if end == len(ids):
break
start += chunk_size - overlap
return chunks
long_doc = "人工智能的发展历史悠久。" * 100
chunks = sliding_window_chunks(long_doc, chunk_size=100, overlap=20)
print(f"原始文本: {len(enc.encode(long_doc))} tokens")
print(f"分块结果: {len(chunks)} 块")
for i, c in enumerate(chunks):
print(f" Chunk {i+1}: {len(enc.encode(c))} tokens")
原始文本: 1700 tokens
分块结果: 21 块
Chunk 1: 100 tokens
Chunk 2: 99 tokens
Chunk 3: 100 tokens
Chunk 4: 100 tokens
Chunk 5: 100 tokens
Chunk 6: 100 tokens
Chunk 7: 100 tokens
Chunk 8: 100 tokens
Chunk 9: 100 tokens
Chunk 10: 100 tokens
Chunk 11: 100 tokens
Chunk 12: 100 tokens
Chunk 13: 100 tokens
Chunk 14: 100 tokens
Chunk 15: 99 tokens
Chunk 16: 100 tokens
Chunk 17: 100 tokens
Chunk 18: 100 tokens
Chunk 19: 99 tokens
Chunk 20: 100 tokens
Chunk 21: 100 tokens
# ── 策略 3:分块摘要(Map-Reduce 模式)─────────────────────
def map_reduce_summarize(
text: str,
chunk_size: int = 800,
overlap: int = 50,
) -> str:
"""
Map: 每个 chunk 单独摘要
Reduce: 把所有摘要合并,再做一次最终摘要
"""
chunks = sliding_window_chunks(text, chunk_size=chunk_size, overlap=overlap)
print(f"文档分成 {len(chunks)} 块,开始 Map 阶段...")
# Map:每块单独摘要
summaries = []
for i, chunk in enumerate(chunks):
summary = chat(
f"请用 2-3 句话概括以下内容的要点:\n\n{chunk}",
max_tokens=500,
)
summaries.append(summary)
print(f" Chunk {i+1}/{len(chunks)} 摘要完成")
# Reduce:合并所有摘要,生成最终摘要
print("\nReduce 阶段...")
combined = "\n\n".join(f"[部分 {i+1}]\n{s}" for i, s in enumerate(summaries))
final = chat(
f"以下是一篇文档各部分的摘要,请整合成一个连贯的总体摘要(200字以内):\n\n{combined}",
max_tokens=1000,
)
return final
# 用一篇「假长文档」测试(实际场景替换成真实内容)
fake_long_doc = """
人工智能(AI)的发展可以追溯到 20 世纪 50 年代。1956 年,达特茅斯会议标志着 AI 作为一个学科正式诞生。
早期 AI 研究主要集中在符号推理和专家系统。这一阶段的代表成果包括 LISP 语言和第一批专家系统。
然而,受限于计算能力和数据量,AI 在 80 年代末进入了第一次「寒冬」。
进入 90 年代,机器学习的兴起带来了新的希望。统计方法取代了符号推理,SVM、随机森林等算法相继出现。
1997 年,IBM 的深蓝击败国际象棋世界冠军卡斯帕罗夫,成为 AI 史上的里程碑事件。
2012 年,深度学习迎来爆发。AlexNet 在 ImageNet 竞赛中的惊人表现,宣告了深度学习时代的到来。
此后,卷积神经网络(CNN)在图像识别领域全面超越人类水平。
2017 年,Google 发表了划时代的论文《Attention is All You Need》,提出了 Transformer 架构。
这一架构彻底改变了自然语言处理(NLP)领域,为后来的大型语言模型奠定了基础。
2022 年,ChatGPT 的发布让 AI 真正走入大众视野。其流畅的对话能力和广泛的知识令全球用户震惊。
随后,各大科技公司纷纷推出自己的大语言模型:Google 的 Gemini、Anthropic 的 Claude、Meta 的 LLaMA 等。
如今,AI 已经渗透到医疗、教育、金融、创作等各个领域。
如何确保 AI 的安全性、可解释性和公平性,成为当下最重要的研究课题。
""" * 3 # 重复 3 次模拟更长的文档
print(f"文档长度: {len(enc.encode(fake_long_doc))} tokens")
print("="*50)
final_summary = map_reduce_summarize(fake_long_doc, chunk_size=300)
print("\n最终摘要:")
print(final_summary)
文档长度: 1774 tokens
==================================================
文档分成 7 块,开始 Map 阶段...
Chunk 1/7 摘要完成
Chunk 2/7 摘要完成
Chunk 3/7 摘要完成
Chunk 4/7 摘要完成
Chunk 5/7 摘要完成
Chunk 6/7 摘要完成
Chunk 7/7 摘要完成
Reduce 阶段...
最终摘要:
本文梳理人工智能发展里程碑:从符号推理向统计方法转变,涌现SVM、随机森林等算法;1997年深蓝胜卡斯帕罗夫,2012年AlexNet及CNN在图像识别上实现超越人类;2017年Transformer奠定现代NLP与大型语言模型基础,2022年ChatGPT使AI走入大众并催生Gemini、Claude、LLaMA等竞品。如今AI已渗透医疗、教育、金融与创作领域,研究重心转向安全性、可解释性与公平性。
5. 实用工具:发送前检查#
在真实项目中,每次调用 API 前都应该检查 token 用量,避免超限报错。
CONTEXT_LIMITS = {
"openai/gpt-4o": 128_000,
"openai/gpt-4o-mini": 128_000,
"anthropic/claude-sonnet-4-6": 200_000,
"anthropic/claude-haiku-4-5": 200_000,
"gemini/gemini-2.0-flash": 1_000_000,
}
def token_budget(
messages: list[dict],
model: str = None,
reserved_output: int = 1000,
) -> dict:
"""
计算当前 messages 的 token 用量和剩余预算。
messages 格式:[{"role": "user", "content": "..."}, ...]
"""
if model is None:
model = os.getenv("LLM_MODEL", "openai/gpt-4o")
# 统计所有 message 的 token
total = sum(len(enc.encode(m["content"])) for m in messages)
# 每条消息有约 4 token 的格式开销
total += len(messages) * 4
limit = CONTEXT_LIMITS.get(model, 128_000)
remaining = limit - total - reserved_output
return {
"used": total,
"limit": limit,
"reserved_output": reserved_output,
"remaining": remaining,
"usage_pct": round(total / limit * 100, 1),
"ok": remaining > 0,
}
# 示例:模拟一个多轮对话
conversation = [
{"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": "什么是机器学习?"},
{"role": "assistant", "content": "机器学习是人工智能的一个分支..."},
{"role": "user", "content": "能详细解释一下监督学习吗?"},
]
budget = token_budget(conversation)
print(f"已用: {budget['used']:,} tokens")
print(f"上限: {budget['limit']:,} tokens")
print(f"剩余: {budget['remaining']:,} tokens")
print(f"用量: {budget['usage_pct']}%")
print(f"安全: {budget['ok']}")
已用: 65 tokens
上限: 128,000 tokens
剩余: 126,935 tokens
用量: 0.1%
安全: True
小结#
概念 |
要点 |
|---|---|
Context Window |
Attention 的 O(n²) 代价决定了物理上限 |
超出限制 |
API 直接报错,不会静默截断 |
遗忘效应 |
长文本中「中间」的内容容易被忽略(Lost in the Middle) |
截断 |
最简单,适合只关心首尾的场景 |
滑动窗口 |
适合需要连续处理的场景,overlap 避免边界割裂 |
分块摘要 |
Map-Reduce 模式,适合需要理解全文的场景 |
下一章 → Sampling Parameters:temperature、top-p、top-k 如何控制模型的「创造力」