
用 LLM 处理长文本时,上下文窗口不是你想塞多少就塞多少的。超过上限直接报错,截断策略选错了关键信息被切,模型 hallucinate。本文对比三种真实在用的截断策略,给出具体代码和实测结论。
问题背景
我最近在跑一个文档问答场景,文档平均 3 万字,想直接一口气喝给 GPT-4 Turbo(128k 上下文)。结果遇到两个真实问题:
- 超出上限:文档 + 对话历史 + prompt 模板,轻松超过 128k token
- 截断丢信息:用简单截断,前半部分的核心结论全在末尾,一刀切直接报廉
三种截断策略
策略一:Head + Tail(头尾保留)
最常见的做法:保留开头和末尾,中间截断。直觉上"的开头交代背景,结尾放结论"似乎合理。
def head_tail_truncate(text, max_tokens=100000, tokenizer_name="gpt-4"):
from tiktoken import encoding_for_model
enc = encoding_for_model(tokenizer_name)
tokens = enc.encode(text)
head_len = max_tokens // 2
tail_len = max_tokens - head_len
if len(tokens) <= max_tokens:
return text
return enc.decode(tokens[:head_len] + tokens[-tail_len:])
实测结论:在文档问筒场景下,Head + Tail 的准确率约 67%。问题在于:很多技术文档的结论在中间段落,前面是铺沉,后面是参考,中间才是干货。
策略二:Semantic Chunking(语义分块)
按语义段落分割,而不是按字符数一刀切。每个 chunk 保留完整语义,再根据语义相似度决定保留哪些块。
from langchain.text_splitter import RecursiveCharacterTextSplitter
def semantic_chunk(text, max_tokens=8000, overlap_tokens=200):
splitter = RecursiveCharacterTextSplitter(
chunk_size=max_tokens,
chunk_overlap=overlap_tokens,
separators=["
", "
", "。", "!", "?", " "]
)
chunks = splitter.split_text(text)
# 按与 query 的语义相似度过滤
from langchain.embeddings import OpenAIEmbeddings
embeddings = OpenAIEmbeddings()
query_embedding = embeddings.embed_query("用户的问题是什么?")
scored_chunks = []
for i, chunk in enumerate(chunks):
chunk_emb = embeddings.embed_query(chunk)
score = cosine_similarity([query_embedding], [chunk_emb])[0][0]
scored_chunks.append((score, i, chunk))
# 取相似度最高的块,保留原文顺序
scored_chunks.sort(reverse=True)
selected = sorted(scored_chunks[:5], key=lambda x: x[1])
return "".join([c[2] for c in selected])
实测结论:语义分块在文档问筒场景准确率提升到 82%,但 embed API 调用成本不可忽视。3万字文档,按每块 8000 token 切,约产生 4 次 embed 调用。如果日均处理 1000 篇文档,光 embed 费用就值得算一算。
策略三:Reverse Summarization(逆向摘要)
这是我目前在实际项目里最常用的策略。核心思路:用 LLM 先对全文做摘要压缩,把 3 万字压到 2000 字,再把摘要喝给最终推理模型。
def reverse_summarize(text, summary_tokens=2000, final_tokens=100000):
# 第一步:生成摘要(只取前半部分,因为重要结论通常在后半部分)
summary_prompt = (
"请为以下文档生成一个压缩摘要,包含:
"
"1. 核心主题(一句话)
"
"2. 关键结论(3-5条)
"
"3. 重要数据点(如有)
"
"要求:用最精简的语言,保留所有关键信息。
"
"---
" + text[:15000]
)
# 调用 LLM 生成摘要
summary_response = openai.ChatCompletion.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": summary_prompt}],
max_tokens=summary_tokens,
temperature=0.3
)
summary = summary_response.choices[0].message.content
# 第二步:后半部分原文 + 摘要,一起截断到最终上限
text_tail = text[len(text)//2:]
combined = summary + "
---原文后半部分---
" + text_tail
# 最后截断到最终模型的上下文上限
if count_tokens(combined) > final_tokens:
combined = head_tail_truncate(combined, final_tokens)
return combined
实测结论:逆向摘要 + Head + Tail 组合,准确率 89%,比单纯 Head + Tail 高出 22 个百分点。成本方面:摘要生成用 gpt-4o-mini,3万字文档的摘要费用约 $0.15,最终推理用 gpt-4o,约 $0.30/篇。贵是贵,但准确率对得起这个价。
选型建议
| 场景 | 推荐策略 | 理由 |
|---|---|---|
| 实时问答(延迟敏感) | Head + Tail | 零额外延迟,实现简单 |
| 离线分析(质量优先) | 逆向摘要 | 准确率最高 |
| 多文档聚合 | 语义分块 | 能处理跨文档关联 |
踊坑记录
坑1:tokenizer 版本不一致
用 tiktoken 截断和用 API 内置截断,结果可能差 10-20%。生产环境一定要用 API 提供商的 tokenizer,不要自己实现。
坑2:摘要用了和推理同样的模型
为了省成本用小模型做摘要,但小模型生成摘要时也会 hallucinate,结果摘要里凭空出现了原文没有的结论。最终推理基于错误摘要,一错到底。
坑3:overlap 设置为 0
语义分块时 chunk_overlap=0,相邻 chunk 之间的关联信息完全丢失。如果段落开头有指代词("如图所示"、"见上文"),被截断后模型完全无法理解。
总结
上下文窗口是工程问题,不是理论问题。没有完美的截断策略,只有适合你场景的策略。实时场景用 Head + Tail,质量优先场景用逆向摘要,多文档场景用语义分块。记住:截断之前,先问自己"哪些信息绝对不能丢"。
更多交流点击入群






