LLM上下文工程实战指南|Token优化、长文本处理与踩坑全记录

0 阅读12分钟

先搞清楚:上下文窗口到底是什么?

很多刚接触大模型的同学会把它理解为"模型的记忆容量"——这个理解其实不太准确。

上下文窗口说白了就是模型一次推理能处理的最大Token数量,它包含两部分:你塞进去的所有内容(系统提示、对话历史、检索文档等)+ 模型吐出来的回复。

⚠️ 这里有个关键认知要纠正:上下文窗口 ≠ 模型记忆力。模型每生成一个Token,都要重新算一遍整个上下文的注意力关系。窗口越大,计算开销就越高——这也是为什么长文本推理又贵又慢。

目前主流模型的窗口大小差异还挺大的:

模型上下文窗口典型场景
Qwen2.5-7B-Instruct128K长文档分析、代码审查
Qwen3-4B-Instruct256K ~ 1M超长文本、整本书处理
Qwen3.7-Max超大窗口复杂Agent编排
Claude 3.5 Sonnet200K法律文档、技术手册
GPT-4-turbo128K多语言长文本

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缓存随序列长度线性增长,显存分分钟被吃光。

实际开发中你会遇到三个典型问题:

  1. 计算爆炸:长文本推理慢到怀疑人生
  2. 显存瓶颈:KV缓存可能占几十GB显存
  3. 注意力衰减:模型对开头内容"记不住"了,这就是所谓的"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-Turbo1M上下文窗口

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优化几个关键点:

  1. Embedding模型选择:要和生成模型的语义空间对齐
  2. 重排序(Rerank):先粗召回再精排,效果比纯Top-K好很多
  3. 查询扩展:把用户问题扩展为多个相关查询,提高召回率
  4. 上下文去重:相似片段合并,别让重复信息浪费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预算内,把最有效的信息喂给模型。核心就三件事:

  1. 算好账——Token预算分配要合理,输出预留不能忘
  2. 挤水分——精简prompt、压缩上下文、启用缓存,能省则省
  3. 选对刀——简单任务用小模型,长文本上RAG,推理任务按需开CoT

这些经验都是我在实际项目里一点点踩出来的,希望对你有用。有问题欢迎评论区交流 😊


#大模型 #上下文工程 #Prompt优化 #RAG #Qwen