
问题:上下文窗口不是无限的
上线客服机器人三个月后,用户开始反馈"答非所问"。查日志发现单会话 token 数已经突破 18 万——上下文窗口爆了,最关键的用户订单信息被截断在最前面,模型根本"看不到"最近几轮对话的内容。
这不是个例。上下文窗口管理是所有 LLM 应用必经的工程坎。本文从三个真实踩坑案例出发,给出可复制的解法。
坑1:对话历史无限累积,窗口被灌爆
最常见的死法:对话历史无上限地往 context 里塞,直到触发 4096 或 128k token 限制。
# 典型错误代码:无限追加
messages = []
for turn in conversation_history:
messages.append({"role": "user", "content": turn["user"]})
messages.append({"role": "assistant", "content": turn["assistant"]})
# 没有任何截断逻辑,context 必然爆炸
实测发现:GPT-4 Turbo 128k 窗口看似很大,但 100 轮对话(含详细上下文)就能塞满 70%。超过 90% 之后模型开始"忘记"最近的信息——不是线性遗忘,是突然断层。
坑2:滑动窗口"一刀切",把关键信息切没了
有人用简单的滑动窗口:只保留最近 N 条消息或最近 4k token。看起来解决了问题,但踩到这个坑:
# 错误:只保留最近 N 条
window_size = 10
messages = conversation_history[-window_size:]
# 问题:最近10条里可能不包含用户的核心诉求
真实案例:用户问"我上周五的订单什么时候发货",滑动窗口只保留最近10条,刚好覆盖的是"确认收货地址"的流程,而历史里有"订单号 20230517J"。关键信息被切掉了。
正确做法:分离关键信息池。用户 ID、订单号、当前问题摘要永远置顶,其余才用滑动窗口。
# 正确:关键信息置顶
SYSTEM_PROMPT = """当前用户ID: {user_id}
当前订单号: {order_id}
用户当前问题: {current_issue}
"""
messages = [
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": recent_history},
]
坑3:RAG 检索到相似内容,但向量相似度欺骗了你
上了 RAG 之后,以为能精准召回相关内容。实测发现一个诡异现象:
用户问"工单处理进度",系统检索回来的内容排名第一的是"订单查询"相关,相似度 0.73。而真正相关的"工单系统"内容相似度只有 0.68。差 0.05,模型拿到的却是错误语境。
# 问题出在向量模型对中文近义词的区分能力
# "工单" vs "订单":字形接近,向量空间中可能被判定为相似
# 但语义完全不同——一个是售后流程,一个是购买流程
# 解法:用 HyDE(假设答案检索)
# 先生成一个"工单处理进度"的假设回答
hypothetical_answer = llm.generate(f"用一句话描述:用户问'{query}'时,正确的工单回复内容")
# 用这个假设答案去检索,而不是直接用用户问题
results = vector_db.search(hypothetical_answer)
三个工程解法,直接抄
方案1:滑动窗口 + 关键信息池(轻量)
import tiktoken
def build_context(conversation_history, current_issue, user_id, order_id, max_tokens=4000):
enc = tiktoken.get_encoding("cl100k_base")
# 关键信息永远置顶,不受 token 限制
system = f"[关键信息] user_id={user_id} order_id={order_id} 当前问题={current_issue}"
system_tokens = len(enc.encode(system))
# 可用 token 预算
budget = max_tokens - system_tokens - 200 # 留 200 buffer
# 从最新往最老扫描,凑满 budget
context_messages = []
total = 0
for msg in reversed(conversation_history):
msg_tokens = len(enc.encode(msg["content"]))
if total + msg_tokens > budget:
break
context_messages.insert(0, msg)
total += msg_tokens
return [{"role": "system", "content": system}] + context_messages
方案2:语义摘要双轨(适合长会话)
def summarize_and_compress(conversation_history, llm):
"""
对话历史超过阈值时,用语义摘要压缩
摘要只保留:用户目标、已确认信息、待处理事项
"""
if len(conversation_history) <= 5:
return conversation_history
raw_history = "\n".join([
f"用户: {m['content']}" if m['role'] == 'user' else f"助手: {m['content']}"
for m in conversation_history
])
summary_prompt = f"""将以下对话压缩为简短摘要,保留:
1. 用户核心诉求
2. 已确认的关键信息(订单号、地址、时间等)
3. 待解决的问题
对话:
{raw_history}
摘要格式:目标|已确认|待处理"""
summary = llm.invoke(summary_prompt)
return [
{"role": "system", "content": f"[历史摘要] {summary}"},
{"role": "user", "content": "继续上文的待处理问题"}
]
方案3:RAG + HyDE(适合知识库问答)
def hyde_search(query, vector_db, llm):
# Step 1: 生成假设答案
hypothetical = llm.invoke(
f"针对问题「{query}」,写一段100字以内的标准答案:"
)
# Step 2: 用假设答案检索(召回比直接用 query 高 15-20%)
results = vector_db.similarity_search(hypothetical, top_k=3)
# Step 3: 将真实答案与假设答案一起交给模型判断
return results
# 验证:HyDE vs 直接检索
# 直接检索"工单处理" → 召回"订单查询"(错误)
# HyDE 检索"工单处理" → 生成假设答案"关于工单号的处理进度说明" → 召回"工单系统"(正确)
方案对比
| 方案 | 适用场景 | 延迟 | 成本 |
|---|---|---|---|
| 滑动窗口+关键池 | 简单客服、单轮问答 | 低 | 低 |
| 语义摘要双轨 | 长会话、多轮对话 | 中(多一次 LLM 调用) | 中 |
| RAG+HyDE | 知识库、企业文档 | 高(2次检索) | 高 |
现在你可以做什么
第一步:检查你的代码里是否有 messages.append() 无限累积的模式。有的话立即加 token 预算检查。
第二步:如果上了 RAG,用"工单"和"订单"这类近义词测试一下你的召回效果。如果相似度差值小于 0.1,考虑上 HyDE。
第三步:上了语义摘要的同学,注意监控摘要的 token 消耗——有时候摘要本身就把 context 占满了,等于没压缩。
上下文窗口管理的本质是:在有限 token 内,让模型始终能看到最相关的信息。这不是调参数能解决的,是架构设计问题。
更多交流点击入群






