上下文压缩如何拯救AI长对话?一文搞懂上下文压缩的四层设计
问题
【本质:上下文空间是有限资源,工具输出会不断挤占这个空间。】
Agent 在大项目中工作时,会读很多文件、跑很多命令,每条工具输出都堆在 messages 列表里。
上下文窗口是有限的,满了之后 API直接报 prompt_too_long,Agent 就卡死了。
解决方案–4层压缩管线
4层压缩管线(便宜的先跑,贵的后跑)
| 层级 | 策略 | 成本 | 做什么 | 丢什么 |
|---|---|---|---|---|
| L3 | tool_result_budget | 0 API | 大输出写入磁盘,留预览 | 完整输出(磁盘有备份) |
| L1 | snip_compact | 0 API | 裁掉中间旧消息 | 中段对话历史 |
| L2 | micro_compact | 0 API | 旧工具结果替换为占位符 | 旧工具输出内容 |
| L4 | compact_history | 1 API | LLM 全量摘要 | 细节(保留关键信息) |
| 兜底 | reactive_compact | 1 API | API 报错后紧急裁剪 | 大部分(只留最近 5 条) |
总体流程
第一步:进入管线
messages[] – 当前完整的对话消息列表。
⬇️
第二步:L3 tool_result_budget(最便宜)
做什么:检查有没有特别大的工具输出(比如 cat 了一个大文件),有就存到磁盘,用短预览(文本形式的summary)替换。
为什么先做:不涉及 LLM 调用,零成本,直接缩小消息体积。
⬇️
第三步:L1 snip_compact(便宜)
做什么:裁剪中间的消息,只保留头和尾。比如保留最近 5 条,前面的全删。
为什么第二做:也是确定性操作,不调 LLM,但比 L3 激进,会丢信息。
⬇️
第四步:L2 micro_compact(便宜)
做什么:把旧的工具结果替换成占位符。比如 10 轮前的 read_file 结果,替换为 “[tool result omitted]”。
为什么第三做:同样不调 LLM,但会丢失具体的工具输出内容。
⬇️
第五步:判断 – token 还超阈值吗?
前三层处理完后,检查消息的 token 数:
- 没超 → 直接发给 LLM,正常工作
- 超了 → 进入 L4
⬇️
第六步:L4 compact_history(最贵)
做什么:调用 LLM 对全部历史做摘要,用摘要替换所有旧消息。
为什么最后做:需要一次额外的 LLM API 调用,有成本,而且会丢失细节。但能把 token 数大幅降下来。
⬇️
第七步:L4 之后再调 LLM
L4 摘要完成后,用精简后的消息去调 LLM 做正事。
⬇️
第八步:紧急兜底 – reactive_compact
即使经过 L4,API 仍然返回 prompt_too_long 错误(比如摘要本身还是太长),就触发紧急压缩:再做一次 LLM
摘要,只保留最后几条消息。
L3 tool_result_budget–大结果存磁盘
1. 统计最后一条 user 消息中所有 `tool_result` 的总大小 2. 总量 > 200KB? ├─ 否 → 跳过,不做任何处理 └─ 是 → 按大小从大到小排序 逐个检查: ├─ 单条 ≤ 3 万字符 → 跳过(小的不动) └─ 单条 > 3 万字符 → 存磁盘,上下文替换为预览 直到总大小降到 200KB 以下 → 停止举例
原始状态:
messages 中有一条工具结果,内容是 5 万字符的 cat 输出
→ 全部在上下文中,占用 5 万字符
L3 处理后:
完整内容写入磁盘文件:.tool-results/toolu_xxx.txt(5 万字符)
上下文中的内容被替换为:
“
Full output: .tool-results/toolu_xxx.txt
Preview:
(前 2000 字符的预览…)
”→ 上下文只占约 2000 字符
对比:
| 处理前 | 处理后 | |
|---|---|---|
| 上下文中 | 5 万字符完整输出 | 2000 字符预览 + 文件路径 |
| 磁盘上 | 无 | 完整 5 万字符 |
L1 snip_compact–裁掉无关的旧对话
1. 判断:消息数 > 50?没超过就跳过 2. 计算:头保留 3 条(初始上下文),尾保留 47 条(当前工作),中间全部裁掉 3. 替换:中间的位置放一条占位符消息,告诉 LLM "这里裁掉了一段对话"为什么保留头 3 条?
头 3 条通常是用户最初的任务描述和 agent 的初始响应,包含当前目标,丢了就不知道在干什么了。为什么保留尾 47 条?
尾部是最近的工作上下文,agent 正在做的事情,丢了就没法继续。
举例
假设 messages 有 80 条: 处理前(80 条): [msg0, msg1, msg2, msg3, msg4, msg5, ..., msg76, msg77, msg78, msg79] ←── 头 3 条 ──→ ←──── 中间 74 条 ────→ ←──── 尾 47 条 ────→ 处理后(51 条): [msg0, msg1, msg2, "[snipped 74 messages]", msg33, msg34, ..., msg79] ←─ 头 3 ─→ ←── 占位符 ──→ ←──────────── 尾 47 条 ────────────→L2 micro_compact–旧工具结果占位
1. 收集:扫描 messages 中所有 `tool_result` 块 2. 判断:`tool_result` 数量 ≤ 3?跳过 3. 替换:除了最近 3 条,更旧的且长度 > 120 字符的替换为占位符举例
假设 messages 中有 8 条 tool_result: 处理前(8 条 tool_result): [tr1(500字), tr2(800字), tr3(200字), tr4(1500字), tr5(300字), tr6(120字), tr7(900字), tr8(600字)] ←──────────────── 旧的 5 条 ────────────────→ ←──── 最近 3 条 ────→ 处理后: [tr1("[compacted]"), tr2("[compacted]"), tr3(200字), tr4("[compacted]"), tr5(300字), tr6(120字), tr7(900字), tr8(600字)] ←── 替换为占位符 ──→ ← 保留(≤120字)→ ←── 保留(最近 3 条)──→L4 compact_history–LLM全量摘要
1.保存 transcript(备份)
完整的对话保留
2.LLM生成摘要
把对话历史发给LLM,保留生成summary
- current goal 当前目标
- key findings/decisions 关键发现和决策
- files read/changed 读过/改过哪些文件
- remaining work 还剩什么没做
- user constraints 用户的约束条件
3.替换
所有旧消息被替换为一条摘要
举例
场景:Agent 在项目中工作了 30 分钟
处理前(160 条消息,节选)
msg0 [user] 帮我给这个 Flask 项目加上用户登录功能 msg1 [assistant] 好的,我先看看项目结构 ... ... msg158 [tool_call] edit_file("routes.py", old, new) msg159 [tool_result] Edited routes.py第 1 步:备份
write_transcript(messages)
→ .transcripts/transcript_1748280000.jsonl
160 条消息完整写入,数据不丢
第 2 步:LLM 摘要
summarize_history(messages)
LLM 看到 160 条消息,生成:
当前目标:给 Flask 项目添加用户登录功能 关键发现和决策: - 使用 flask-login 库实现认证 - User 模型已添加 password_hash 字段 - 登录路由使用 @login_required 装饰器 已修改的文件: - models.py:添加 User 模型和密码哈希 - routes.py:添加 /login、/logout 路由 - app.py:初始化 LoginManager - tests/test_auth.py:添加登录测试 剩余工作: - test_login_redirect 测试失败,需修复 routes.py 中的重定向逻辑 - 登录页面模板 templates/login.html 尚未创建 用户约束: - 使用 SQLite 数据库 - 密码用 werkzeug 加密第 3 步:替换
处理后(1 条消息):
[{ "role": "user", "content": "[Compacted]\n\n当前目标:给 Flask 项目添加用户登录功能\n\n关键发现和决策:\n- 使用 flask-login 库实现认证\n- User 模型已添加 password_hash 字段\n- 登录路由使用 @login_required 装饰器\n\n已修改的文件:\n- models.py:添加 User 模型和密码哈希\n- routes.py:添加 /login、/logout 路由\n- app.py:初始化 LoginManager\n- tests/test_auth.py:添加登录测试\n\n剩余工作:\n- test_login_redirect 测试失败,需修复重定向逻辑\n- 登录页面模板尚未创建\n\n用户约束:\n- 使用 SQLite 数据库\n- 密码用 werkzeug 加密" }]LLM 拿到摘要后继续工作
LLM 看到摘要 → 知道自己在做什么、做了什么、还剩什么→ 直接去修 test_login_redirect,不用重新读 160 条历史
对比:
| 处理前 | 处理后 | |
|---|---|---|
| 消息数 | 160 条 | 1 条 |
| 包含内容 | 每条工具调用的完整输出 | 5 个关键信息摘要 |
| LLM 能做什么 | 能看到所有细节 | 知道目标、进度、剩余工作 |
| token 占用 | ~50000+ | ~500 |
