先搞清楚:上下文窗口到底是什么?
很多刚接触大模型的同学会把它理解为"模型的记忆容量"——这个理解其实不太准确。
上下文窗口说白了就是模型一次推理能处理的最大Token数量,它包含两部分:你塞进去的所有内容(系统提示、对话历史、检索文档等)+ 模型吐出来的回复。
⚠️ 这里有个关键认知要纠正:上下文窗口 ≠ 模型记忆力。模型每生成一个Token,都要重新算一遍整个上下文的注意力关系。窗口越大,计算开销就越高——这也是为什么长文本推理又贵又慢。
目前主流模型的窗口大小差异还挺大的:
| 模型 | 上下文窗口 | 典型场景 |
|---|---|---|
| Qwen2.5-7B-Instruct | 128K | 长文档分析、代码审查 |
| Qwen3-4B-Instruct | 256K ~ 1M | 超长文本、整本书处理 |
| Qwen3.7-Max | 超大窗口 | 复杂Agent编排 |
| Claude 3.5 Sonnet | 200K | 法律文档、技术手册 |
| GPT-4-turbo | 128K | 多语言长文本 |
Token:大模型的"计费单位"
Token是大模型处理文本的最小单位,它既不是字符也不是单词。这个概念搞不清楚,后面预算管理全得乱套。
几个经验值你直接记住就行:
- 英文:1个Token ≈ 0.75个单词(约4个字符)
- 中文:1个Token ≈ 1.5~2.5个汉字(不同分词器差异不小)
- 代码/JSON:比自然语言更"费Token",这点在做代码分析时要特别注意
实际项目中,我建议你用目标模型的分词器做预计算,别靠估算:
# pip install tiktoken
import tiktoken
enc = tiktoken.get_encoding("cl100k_base")
tokens = enc.encode("hello word!")
print(f"近似Token数: {len(tokens)}")
# 输出:近似Token数: 3
💡 踩坑Tip:不同平台的分词器不一样,OpenAI的tiktoken和Qwen的tokenizer对同一段中文的计数可能差10%-20%。所以一定要用目标模型的分词器来算,并且预留10%-15%的安全余量。
为什么上下文不能无限长?
Transformer的核心是自注意力机制,计算复杂度是 O(n²)。什么概念呢?1M Token的注意力计算量是32K的大约1000倍。再加上KV缓存随序列长度线性增长,显存分分钟被吃光。
实际开发中你会遇到三个典型问题:
- 计算爆炸:长文本推理慢到怀疑人生
- 显存瓶颈:KV缓存可能占几十GB显存
- 注意力衰减:模型对开头内容"记不住"了,这就是所谓的"Lost in the Middle"现象
长文本处理:四招实战策略
第一招:分块(Chunking)
当输入超过窗口限制时,最直接的办法就是把内容切成小块分别处理。但怎么切很有讲究:
| 切分策略 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 固定长度 | 简单高效 | 可能切断语义 | 日志分析 |
| 按段落 | 保留基本语义 | 段落长度不均 | 文章、报告 |
| 按句子 | 语义完整 | 粒度太细 | 精确检索 |
| 语义切分 | 最优语义保留 | 计算成本高 | RAG、知识库问答 |
⚠️ 实战经验:chunk_size和chunk_overlap的设置很关键。overlap太小会导致上下文断裂,太大会浪费Token预算。我一般设overlap为chunk_size的15%-20%,效果比较稳。
try:
from langchain_text_splitters import RecursiveCharacterTextSplitter
except ImportError:
try:
from langchain.text_splitter import RecursiveCharacterTextSplitter
except ImportError:
print("请先安装 langchain-text-splitters: pip install langchain-text-splitters")
raise
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=50,
chunk_overlap=10,
separators=["\n\n", "\n", "。", " ", ""]
)
# 示例长文档
long_document = """大语言模型(LLM)是一种基于Transformer架构的人工智能模型,通过在大规模文本数据上进行预训练,学习语言的统计规律和语义表示。
上下文工程是LLM应用开发中的关键环节,它涉及如何有效地组织和管理输入给模型的上下文信息,以获得最佳的输出效果。
Token是LLM处理文本的基本单位,理解Token的计数规则对于控制成本和确保模型正常运行至关重要。"""
chunks = text_splitter.split_text(long_document)
print(f"分块数量: {len(chunks)}")
for i, chunk in enumerate(chunks, 1):
print(f"\n--- 第 {i} 块 ---\n{chunk}")
# 输出:
# 分块数量: 9
#
# --- 第 1 块 ---
# 大语言模型(LLM)是一种基于Transformer架构的人工智能模型,通过在大规模文本数据上进行预
#
# --- 第 2 块 ---
# 规模文本数据上进行预训练,学习语言的统计规律和语义表示
#
# --- 第 3 块 ---
# 。
#
# --- 第 4 块 ---
# 上下文工程是LLM应用开发中的关键环节,它涉及如何有效地组织和管理输入给模型的上下文信息,以获得最
#
# --- 第 5 块 ---
# 上下文信息,以获得最佳的输出效果
#
# --- 第 6 块 ---
# 。
#
# --- 第 7 块 ---
# Token是LLM处理文本的基本单位,理解Token的计数规则对于控制成本和确保模型正常运行至关重
#
# --- 第 8 块 ---
# 保模型正常运行至关重要
#
# --- 第 9 块 ---
# 。
第二招:压缩与摘要
核心思路就一句话——保留关键信息,丢掉冗余内容。
三种常见做法:
- 提取式压缩:从原文摘关键句子
- 生成式摘要:让模型生成精简摘要替代原文
- 指令式压缩:在prompt里明确告诉模型忽略哪些内容
第三种最简单也最实用,直接在prompt里加一句"只提取与问题直接相关的原文片段"就能省下大量Token。
第三招:滑动窗口
流式输入和持续对话场景下,滑动窗口是标配。核心就是每次处理一个窗口范围的内容,窗口之间留一定重叠:
class SlidingWindowManager:
def __init__(self, max_tokens=120000, overlap_tokens=10000):
self.max_tokens = max_tokens
self.overlap_tokens = overlap_tokens
self.windows = []
def add_content(self, text, tokenizer=None):
# 如果没有tokenizer,简单按字符估算
if tokenizer is None:
# 中文粗略估算:1个token ≈ 2个汉字
tokens = list(range(len(text) // 2))
else:
tokens = tokenizer.encode(text)
if len(tokens) <= self.max_tokens:
self.windows.append(text)
return
start = 0
while start < len(tokens):
end = min(start + self.max_tokens, len(tokens))
if tokenizer is None:
self.windows.append(text[start*2 : end*2])
else:
self.windows.append(tokenizer.decode(tokens[start:end]))
if end >= len(tokens): # 到达末尾,防止死循环
break
start = end - self.overlap_tokens # 重叠保留上下文
# 测试滑动窗口
manager = SlidingWindowManager(max_tokens=50, overlap_tokens=10)
long_text = "这是一段很长的文本," * 50
manager.add_content(long_text)
print(f"生成窗口数: {len(manager.windows)}")
# 输出:
# 生成窗口数: 6
第四招:对话历史管理
多轮对话场景下,历史消息累积是Token超限的重灾区。我在线上项目里踩过这个坑——用户聊了20多轮之后直接报错,排查才发现是历史对话撑爆了上下文。
| 策略 | 做法 | 效果 |
|---|---|---|
| 截断旧消息 | 只保留最近N轮 | 简单粗暴,但丢上下文 |
| 摘要压缩 | 用模型压缩历史为摘要 | 保留关键信息,省Token |
| 分层记忆 | 短期完整 + 长期摘要 | 效果最好,实现最复杂 |
💡 落地建议:中小项目用"摘要压缩"就够了,历史超过一定轮数就让模型生成一段摘要替代原始对话。复杂Agent场景再考虑分层记忆。
Token预算管理:别让输出被截断
这是很多人忽略的一个点。总Token = 系统提示 + 上下文 + 用户输入 + 输出预留。如果你不提前给输出留够空间,模型生成到一半就会被硬生生截断。
推荐分配比例(以128K窗口为例):
总窗口: 128K tokens
├── 系统提示: ~1K(固定开销)
├── 历史对话/上下文: ~100K(动态)
├── 用户输入: ~5K(动态)
└── 输出预留: ~22K(必须留!)
⚠️ 血泪教训:务必监控API返回的 finish_reason。如果返回的是 "length" 而不是 "stop",说明输出被截断了,生成的摘要永远缺最后一段。
Prompt优化:省Token就是省钱
这里分享几个我在项目里反复验证过的技巧:
1. 精简指令——能省15-20% Token
❌ 冗余写法:
请你作为一个专业的技术文档撰写助手,帮我详细分析一下下面的代码,
并给出非常详尽的改进建议,包括但不限于代码风格、性能优化、安全性等方面。
✅ 精简写法:
分析以下代码,从代码风格、性能、安全性三方面给出改进建议。
2. 结构化输出模板
直接告诉模型你要什么格式,减少它"自由发挥"浪费的Token:
以JSON格式输出,字段:style_issues / performance_issues / security_issues / refactored_code
不要输出任何解释文字,只返回JSON。
3. 条件思维链
简单问题别让模型强行推理,复杂问题再展开思考过程:
分析问题复杂度:
- 简单问题(单步计算/直接回答)→ 直接给答案
- 复杂问题(多步推理/逻辑判断)→ 先展示推理过程再给答案
这个技巧在混合任务场景下特别好用,Token开销能降30%以上。
上下文缓存:重复查询的省钱利器
如果你用的是阿里云百炼/DashScope平台,上下文缓存一定要用起来。原理很简单——缓存请求的公共前缀(比如系统提示、长文档),后续请求只算差异部分。
两种模式:
| 模式 | 特点 | 适用场景 |
|---|---|---|
| 显式缓存 | 主动创建,5分钟有效,命中率100% | 固定prompt+长文档的重复查询 |
| 隐式缓存 | 自动识别公共前缀 | 多轮对话等通用场景 |
import dashscope
messages = [
{
"role": "system",
"content": [
{"type": "text", "text": long_system_prompt},
# 关键:cache_control标记让这段内容被缓存
{"type": "text", "text": long_document, "cache_control": {"type": "ephemeral"}}
]
},
{"role": "user", "content": "总结这份文档的核心观点"}
]
response = dashscope.Generation.call(
model="qwen3.7-max",
messages=messages
)
✅ 缓存最佳实践:重复内容放开头,差异内容放末尾;显式缓存要求单块≥1024 Token;单次请求最多4个缓存标记。
模型选择:别拿牛刀杀鸡
不同任务用不同模型,这个道理大家都懂,但实际项目中我发现很多人还是习惯性地用最大的模型。其实简单分类、意图识别这种任务,Flash级别的模型又快又便宜,效果完全够用。
| 任务类型 | 推荐模型 | 理由 |
|---|---|---|
| 简单分类/意图识别 | Qwen-Flash / GPT-4.1 mini | 快、便宜 |
| 常规对话/摘要 | Qwen-Plus / GPT-4.1 | 性价比最优 |
| 复杂推理/代码生成 | Qwen-Max / GPT-5 | 能力强,按需用 |
| 超长文档处理 | Qwen-Turbo | 1M上下文窗口 |
CoT推理优化:什么时候该让模型"想一想"
Chain-of-Thought(思维链)是提升复杂推理准确率的有效手段,但不是所有场景都需要。
| 策略 | Token开销 | 准确率提升 | 适用场景 |
|---|---|---|---|
| 标准CoT | +50-100% | +20-40% | 数学、逻辑推理 |
| 条件CoT | +0-30% | +10-20% | 混合复杂度任务 |
| 零样本CoT | +20-50% | +10-30% | 快速部署 |
| 无CoT | 基准 | 基准 | 简单问答、分类 |
💡 实战Tip:如果对准确率要求高,可以用"自一致性"策略——同一个问题采样多次推理路径,然后投票取最一致的答案。虽然Token开销翻倍,但准确率提升明显。
高级架构:当上下文真的不够用
RAG:长文本场景的标准解法
当上下文远超模型窗口时,RAG(检索增强生成)基本是必选项:
用户问题 → 向量化 → 向量数据库检索 → 取Top-K相关片段 → 拼接上下文 → 模型生成
RAG优化几个关键点:
- Embedding模型选择:要和生成模型的语义空间对齐
- 重排序(Rerank):先粗召回再精排,效果比纯Top-K好很多
- 查询扩展:把用户问题扩展为多个相关查询,提高召回率
- 上下文去重:相似片段合并,别让重复信息浪费Token
稀疏注意力与位置编码
这块偏底层,简单提一下。Qwen系列用的RoPE位置编码通过动态NTK-aware缩放支持超训练长度的序列。vLLM部署时可以配置:
python -m vllm.entrypoints.api_server \
--model Qwen/Qwen2.5-7B-Instruct \
--max-model-len 131072 \
--rope-scaling '{"type": "dynamic", "factor": 4.0}'
稀疏注意力方面,Qwen-Turbo用了Dual Chunk Attention(DCA),分块处理+块间有限交互,在超长文本场景下效果不错。
常见踩坑与排查清单
最后总结几个我在线上环境反复遇到的问题:
1. 输出被截断(finish_reason = "length") → 加大max_tokens、压缩输入、拆分任务、启用流式输出
2. 长文本"遗忘"——开头内容回答不准 → 关键信息放末尾(模型对末尾注意力更强)、用引用标记、分块处理
3. Token计数不一致 → 用目标模型分词器预计算、预留10-15%余量、检查API返回的usage字段
# 每次调用后都检查一下
response = dashscope.Generation.call(...)
print(f"输入Token: {response.usage.input_tokens}")
print(f"输出Token: {response.usage.output_tokens}")
print(f"结束原因: {response.output.choices[0].finish_reason}")
写在最后
上下文工程这个话题,说到底就是在有限的Token预算内,把最有效的信息喂给模型。核心就三件事:
- 算好账——Token预算分配要合理,输出预留不能忘
- 挤水分——精简prompt、压缩上下文、启用缓存,能省则省
- 选对刀——简单任务用小模型,长文本上RAG,推理任务按需开CoT
这些经验都是我在实际项目里一点点踩出来的,希望对你有用。有问题欢迎评论区交流 😊
#大模型 #上下文工程 #Prompt优化 #RAG #Qwen