一文看懂Agent对话历史压缩:解决上下文窗口爆炸难题
Agent从诞生之初不断进化:
- 工具
- 记忆
- 规划
- Rules
- SubAgent
- Teams
- …
这样下去Agent的对话历史会被无限延长,直到撑爆LLM的上下文窗口。如果用Agent干些复杂的活,这个问题将会很快出现。
当然有很多种办法来延缓这个问题,其中一个是上下文压缩。
为什么messages会被撑爆
每一次循环对话,messages会新增至少两条消息:
- 第一轮对话: messages + LLM回答
- 第二轮对话:messages += LLM回答
- 第三轮对话:messages += LLM回答
- …
假设有一个任务:找到nginx日志,提取error,并统计近一天内每半个小时error总数量以及每个接口的的数量,按数量大小降序排序,并写入statistic.txt文件。将这样的任务完成 messages会累积到几十条,其中每个工具的输出包含几十上百行的命令输出。
任何LLM的上下文窗口都是有限的,不管是 几W tokens还是几十W tokens,只要Agent读取几个大文件、执行几次grep返回大量结果,再进行几轮工具调用(sort、 cat…)这个窗口就会被迅速填满。对于本地的小模型,一般这个context window可能只有几千tokens,几轮循环就会装满。
有人问:现在的LLM context window越来越大了,还需要压缩吗? 窗口变大只是推迟了问题而没有解决问题。大量的上下文背后就是高额的token费用、更慢的响应速度以及LLM在超长上下文文本中可能会迷失重点,导致分析过程漫长而结果可能出现偏差。
在解决这个问题之前,先梳理下思路
- 用更大的 context window模型:只能缓解
- 限制最大循环次数:不可取,复杂任务就是需要多轮会话才能完成
- 截断工具返回结果:不可取,可能丢失关键信息
- 压缩旧对话历史:把先前的详细对话压缩成一段摘要,只保留重点信息。Agent靠摘要回忆之前做了什么来完成复杂任务。
压缩原理、几种压缩策略
| 压缩策略 | 原理 | 优点 | 缺点 |
|---|---|---|---|
| 摘要法 | 利用大模型自身的能力,定期(例如每 10 轮对话)将之前的对话历史总结成一段简短的“摘要” | 极度节省空间,保留逻辑连贯性 | 可能丢失具体的细节(如具体的数字或时间) |
| 关键信息提取 | 不生成自然语言,而是将对话中的实体(Entity)、**状态(State)和意图(Intent)**提取为结构化数据(JSON 或 Key-Value) | 机器读取极快,适合结构化任务(如订票、查数据) | 丢失了对话的“语气”和“上下文情感” |
| 语义向量检索 | 将历史消息向量化(Embedding),存入向量数据库。当新任务到来时,只检索与当前任务语义最相关的那几条历史消息,而不是全部塞入 | 最灵活,能保留最相关的细节,适合长跨度记忆 | 需要额外的向量数据库存储和检索成本 |
压缩的核心思想:忘掉细节,保留现场
┌──────────────────────────────────────────────────────────────────────────────────────┐
│ 1. 原始历史对话 (Raw History) - 占用巨大,包含大量噪音 │
│ ┌──────────────────────────────────────────────────────────────────────────────┐ │
│ │ [User]: 你好,我想查一下明天的天气。 (Hello, check weather) │ │
│ │ [Agent]: 你好!请问您在哪座城市? (Hi! Which city?) │ │
│ │ [User]: 北京。 (Beijing) │ │
│ │ [Agent]: 好的,正在查询北京明天的天气... (Querying Beijing...) │ │
│ │ [User]: 顺便帮我查一下北京明天的限行吗? (Also check traffic restriction) │ │
│ │ [Agent]: 正在查询限行... (Checking traffic...) │ │
│ │ ... (中间穿插了 100 条类似的寒暄、确认、重试) ... (大量冗余对话) │ │
│ │ [User]: 现在帮我规划一个去故宫的路线。 (Plan route to Forbidden City) │ │
│ │ [Agent]: 好的,正在规划... (Planning...) │ │
│ └──────────────────────────────────────────────────────────────────────────────┘ │
│ ❌ 问题:Token 浪费,关键信息淹没,模型注意力分散,导致回答变慢或遗忘前文。 │
└──────────────────────────────────────────────────────────────────────────────────────┘
⬇️ ⬇️ ⬇️
⬇️ (压缩算法介入) ⬇️ (压缩算法介入) ⬇️ (压缩算法介入)
┌───────────────────────────┐ ┌───────────────────────────┐ ┌───────────────────────────┐
│ 策略 A: 摘要 (Summarization)│ │ 策略 B: 关键信息提取 (Extraction)│ │ 策略 C: 向量检索 (Vector Retention)│
│ 核心:用一句话概括过去 │ │ 核心:只保留实体和状态 │ │ 核心:只保留高相似度片段 │
└───────────────────────────┘ └───────────────────────────┘ └───────────────────────────┘
⬇️ ⬇️ ⬇️
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ 生成摘要文本 │ │ 结构化数据块 │ │ 语义向量索引 │
│ "用户已确认位置 │ │ {city: "北京", │ │ [User]: "天气" │
│ 为北京,需查询 │ │ goal: "天气", │ │ [User]: "限行" │
│ 天气、限行及宫 │ │ restriction: │ │ [User]: "故宫" │
│ 廷路线。" │ │ true, │ │ [User]: "路线" │
│ (从 100 行 变为 1 行) │ │ route: "待规划" │ │ (非文本,仅索引) │
└─────────────────┘ └─────────────────┘ └─────────────────┘
⬇️ ⬇️ ⬇️
┌─────────────────────────────────────────────────────────────────────────────────┐
│ 压缩后的上下文 (Compressed Context) │
│ ┌─────────────────────────────────────────────────────────────────────────────┐│
│ │ [Summary]: 用户在北京,任务列表:[查天气,查限行,规划故宫路线]。 ││
│ │ [State]: 天气=待查,限行=待查,故宫路线=待规划。 │ │
│ │ [Key_Facts]: 城市=北京,目的地=故宫。 │ │
│ │ [Recent_Q]: 用户刚问"现在帮我规划路线"。 │ │
│ └─────────────────────────────────────────────────────────────────────────────┘ │
│ ✅ 效果:Token 消耗减少 70%+,关键信息保留,模型注意力更集中,响应更快。 │
└─────────────────────────────────────────────────────────────────────────────────┘
摘要法代码实现
COMPACT_THRESHOLD = 20 # 超过 20 条就压缩
KEEP_RECENT = 6 # 保留最近 6 条不压缩
def compact_messages(messages):
if len(messages) <= COMPACT_THRESHOLD:
return messages # 没超阈值,不压缩
system_msg = messages[0] # system prompt 永远保留
old_messages = messages[1:-KEEP_RECENT] # 旧消息 → 要被压缩
recent_messages = messages[-KEEP_RECENT:] # 最近的消息 → 保留原样
# 把旧消息拼成文本
old_text = ""
for msg in old_messages:
role = msg.get("role", "unknown") if isinstance(msg, dict) else getattr(msg, "role", "unknown")
content = msg.get("content", "") if isinstance(msg, dict) else getattr(msg, "content", "")
if content:
old_text += f"[{role}]: {content}\n"
# 调用 LLM 生成摘要,用 LLM 来压缩 LLM 的对话历史。这不是浪费吗?不是。因为这次 LLM 调用的唯一任务就是"总结",不带工具,输出简短,token 开销远小于把完整历史塞进每次请求
summary_response = client.chat.completions.create(
model=MODEL,
messages=[
{"role": "system", "content": "Summarize the following conversation history. Keep all important facts, file paths, command results, and decisions. Be concise but don't lose critical details."},
{"role": "user", "content": old_text}
]
)
summary = summary_response.choices[0].message.content
# 重新组装
return [
system_msg,
{"role": "user", "content": f"[Previous conversation summary]: {summary}"},
{"role": "assistant", "content": "Understood. I have the context from our previous conversation. Let me continue."},
*recent_messages
]
切分位置
- system_message = messages[0] # 需保留,包含了 Agent核心指令,压缩进摘要会丢失基础设定
- compact_messages = messages[1:-KEEP_RECENT] # 要压缩消息
- recent_messages = messages[-KEEP_RECENT:] # 最近消息,需保留,这些信息一旦被压缩成摘要,LLM 就无法精确引用了
循环中调用压缩
Agent 核心循环里加入这行代码:每轮循环开始前检查一次。没超阈值就原样返回(零开销),超了就压缩,messages 数量像锯齿波一样:涨到阈值 → 压缩回去 → 继续涨 → 再压缩。永远不会超过阈值太多,Agent 可以无限工作下去
def run_agent(user_message, max_iterations=30):
messages = [...]
for i in range(max_iterations):
messages = compact_messages(messages) # ← 就这一行
response = client.chat.completions.create(...)
...
压缩会丢失信息吗
LLM保留了关键事实信息,而这些信息足够做接下来的决策,但是如果某个细节信息真的需要呢,Agent可以再次调用工具获取细节,就比如小时候看《鲁滨逊漂流记》记得主人公救下了一个土著人“星期五”(摘要),是怎么救下的?具体我忘记了,让我再翻下书本回忆下。(再次获取细节)
更多推荐



所有评论(0)