引言
一家电商公司的客服机器人上线了。用户开始聊天:
第 1 轮:用户问退货政策 → AI 准确回答 ✅
第 10 轮:用户换了个话题问物流 → AI 仍然准确 ✅
第 30 轮:用户回到退货话题 → AI 还能关联上下文 ✅
第 51 轮:用户说「刚才说的那个订单号多少来着?」→ AI:「抱歉,我没有相关记录。」❌客服机器人「失忆」了。原因是:50 轮对话积累了约 15000 个 token,接近了模型的上下文窗口。系统自动截断了早期消息,所以第 3 轮提到的订单号被丢弃了。
多轮对话的上下文管理,是 AI 应用中最容易被忽视、也最容易出问题的环节。
问题:对话只会越来越长
上下文增长
每轮对话的 token 增长:
第 1 轮:system(100) + user(50) + assistant(100) = 250 tokens
第 2 轮:250 + user(50) + assistant(100) = 400 tokens
第 3 轮:400 + user(50) + assistant(100) = 550 tokens
...
第 N 轮:250 + (N-1) × 150 tokens
第 50 轮 ≈ 7,650 tokens(还好)
第 100 轮 ≈ 15,150 tokens(开始紧张)
第 500 轮 ≈ 75,150 tokens(可能超出 128K 窗口?不太会)
但如果每轮对话很长(比如 RAG 检索了 3000 token 的文档):
第 50 轮 ≈ 250 + 49 × 3200 = 157,050 tokens → 超了!超出上下文时的表现
上下文溢出的症状:
1. API 报错:context_length_exceeded
→ 直接崩溃
2. 模型「失忆」
→ 忘记了之前的重要信息
3. 模型产生幻觉
→ 因为缺少上下文,开始编造
4. 回答质量下降
→ 因为信息太多,模型难以聚焦四种上下文管理策略
1. 截断策略(Truncation)
最简单的策略:只保留最近 N 轮对话
完整对话:
[轮次1] [轮次2] ... [轮次45] [轮次46] [轮次47] [轮次48] [轮次49] [轮次50]
截断后(保留最近 10 轮):
[截断] [截断] ... [截断] [截断] [轮次41] ... [轮次50]
优点:实现简单,token 消耗可控
缺点:丢失早期重要信息
适用:闲聊、简单问答python
def truncate_messages(messages: list, max_rounds: int = 10) -> list:
"""只保留最近 N 轮(system 消息始终保留)"""
system_msg = [m for m in messages if m["role"] == "system"]
conversation = [m for m in messages if m["role"] != "system"]
# 每轮包含 user + assistant(可能还有 tool)
# 简化处理:保留最后 2*max_rounds 条消息
if len(conversation) > max_rounds * 2:
conversation = conversation[-(max_rounds * 2):]
return system_msg + conversation2. 滑动窗口(Sliding Window)
更精细的截断:基于 token 数而非轮数
设定 max_context_tokens = 10000
从最新消息往前加,直到 token 数超过限制
[早期消息] ... [消息N-5] [消息N-4] [消息N-3] [消息N-2] [消息N-1] [消息N]
↑ 从这里开始保留
总 token 数刚好 ≤ max_context_tokenspython
def sliding_window(messages: list, max_tokens: int = 10000) -> list:
"""基于 token 数的滑动窗口"""
import tiktoken
enc = tiktoken.encoding_for_model("gpt-4o")
system_msgs = [m for m in messages if m["role"] == "system"]
other_msgs = [m for m in messages if m["role"] != "system"]
total_tokens = sum(len(enc.encode(m["content"])) for m in system_msgs)
selected = []
for msg in reversed(other_msgs):
msg_tokens = len(enc.encode(msg["content"]))
if total_tokens + msg_tokens > max_tokens:
break
selected.insert(0, msg)
total_tokens += msg_tokens
return system_msgs + selected3. 摘要压缩(Summarization)
用 LLM 总结旧对话,用摘要替代原文
原始对话(10000 tokens):
[轮次1] [轮次2] ... [轮次50]
处理后(3000 tokens):
[摘要:用户是张三,订单号 #12345,之前问过退货政策...] [轮次41] ... [轮次50]
优点:保留了关键信息
缺点:摘要可能不准确,额外 API 调用成本python
def summarize_old_messages(messages: list, keep_recent: int = 6) -> list:
"""总结旧消息,保留最近几轮"""
system_msgs = [m for m in messages if m["role"] == "system"]
other_msgs = [m for m in messages if m["role"] != "system"]
if len(other_msgs) <= keep_recent * 2:
return messages
# 旧消息
old_msgs = other_msgs[:-keep_recent * 2]
recent_msgs = other_msgs[-keep_recent * 2:]
# 生成摘要
old_text = "\n".join(
f"{m['role']}: {m['content']}" for m in old_msgs
)
summary_response = client.chat.completions.create(
model="gpt-4o-mini",
messages=[{
"role": "user",
"content": f"总结以下对话中的关键信息(名字、订单号、重要决策等):\n{old_text}"
}]
)
summary = summary_response.choices[0].message.content
# 构造新消息列表
summary_msg = {"role": "system", "content": f"之前的对话摘要:{summary}"}
return system_msgs + [summary_msg] + recent_msgs4. 混合策略(推荐)
生产级方案:System Prompt + 摘要 + 滑动窗口 + 重要信息提取
┌─────────────────────────────────┐
│ System Prompt(始终保留) │
├─────────────────────────────────┤
│ 重要信息摘要(用户信息、关键事实) │
├─────────────────────────────────┤
│ 最近 N 轮完整对话 │
└─────────────────────────────────┘
重要信息持续提取和更新:
每轮对话后,提取新出现的关键信息追加到摘要中四种策略对比
| 策略 | 信息保留 | 实现复杂度 | Token 成本 | 适用场景 |
|---|---|---|---|---|
| 截断 | 差 | 低 | 低 | 闲聊 |
| 滑动窗口 | 一般 | 低 | 中 | 通用 |
| 摘要压缩 | 好 | 中 | 中(额外调用) | 长对话 |
| 混合策略 | 最好 | 高 | 中 | 生产环境 |
本节小结
| 概念 | 要点 |
|---|---|
| 核心问题 | 对话只会越来越长,最终超出上下文窗口 |
| 截断 | 最简单,丢弃早期消息——适合闲聊 |
| 滑动窗口 | 基于 token 数控制,比截断更精确 |
| 摘要压缩 | 用 LLM 总结旧对话——保留关键信息 |
| 混合策略 | System Prompt + 摘要 + 最近对话——生产级方案 |
思考题
- 摘要压缩引入了额外的 API 调用成本。在什么情况下,这个额外成本是值得的?
- 如果用户在对话中给出了一个重要的密码或 API key,摘要时不小心把它总结进去了,会有什么安全问题?
- 你能设计一个自动判断「哪些信息值得保留」的机制吗?需要考虑什么因素?