13 · 自动压缩:上下文快满了,把历史折叠成摘要
缓存省钱但不缩小历史;当历史真的装不下,就得把老对话摘要掉。讲清压缩边界(保留近期、摘要远古)、摘要 prompt 为何是成败关键、proactive 与 reactive 两种触发,以及压缩与缓存那对绕不开的矛盾。
13 · 自动压缩:把历史折叠成摘要
缓存解决了“重复的别重算”,但它救不了另一半问题:当历史本身膨胀到装不下窗口时,缓存无能为力——你得真的把历史变小。这一篇,我们正面处理这头猛兽。
一、背景:缓存不缩小任何东西
分清两件事:
这时唯一的出路是:把一部分历史,从“逐字保留”变成“摘要保留”——用一小段摘要,替换掉一大坨老消息。这就是压缩(compaction)。
二、解决方案:保留近期,摘要远古
粗暴地把整个历史塞给模型“缩短一下”是不行的——你会丢掉太多细节。聪明的压缩有个清晰的结构:一条压缩边界,把历史切成两半。
压缩前:[msg1][msg2][msg3]...[msg40][msg41][msg42] ← 太长,装不下
压缩边界 ┘
压缩后:[—— 摘要:msg1..msg40 的精华 ——][msg41][msg42] ← 瘦身完成- 边界之前(远古历史):喂给模型,让它写一段结构化摘要,然后用这段摘要替换掉这几十条消息。
- 边界之后(近期历史):逐字保留——模型干活最依赖最近的细节(刚读的文件、刚跑的报错),这部分不能动。
压缩本身是一次额外的 LLM 调用:把要压缩的历史发给模型,配一个专门的“压缩 prompt”,拿回摘要:
async function compact(oldMessages: Message[]): Promise<string> {
const res = await client.messages.create({
model, max_tokens: 2048,
system: COMPACT_PROMPT, // ← 摘要的质量,全系于这个 prompt
messages: [...oldMessages, { role: 'user', content: '请把以上对话压缩成结构化摘要。' }],
})
return textOf(res)
}
// 压缩后的新历史 = [摘要] + 近期逐字保留的尾部
const newMessages = [{ role: 'user', content: `<summary>${summary}</summary>` }, ...recentTail]三、决定性细节:摘要 prompt 就是成败本身
压缩的全部风险,集中在那个摘要 prompt 上。 摘要写好了,Agent 压缩完能无缝接着干,仿佛什么都没发生;摘要写砸了,Agent 当场失忆——忘了自己在修哪个 bug、忘了已经改过哪些文件、忘了用户最初的要求,然后开始胡乱重复劳动或跑偏。这是压缩最危险的地方:它是一个有损操作,而损掉的可能正是关键信息。
所以摘要 prompt 必须明确要求模型保留那些“继续干活所必需”的东西,通常包括:
- 当前任务是什么、用户最初的完整意图。
- 已经做了什么:改过哪些文件、跑过哪些命令、得到什么结果。
- 关键事实与决策:发现的根因、选定的方案、踩过的坑。
- 当前进行到哪一步、下一步打算干什么。
- 重要的文件路径、符号名、配置值——这些具体的“锚点”丢了就得重新去查。
一个好的摘要 prompt,往往会给出一个固定的结构模板(任务/已完成/关键发现/当前状态/下一步),强制模型逐项填写,而不是随意“写个大概”。把摘要的结构定死,是对抗“压缩即失忆”的主要武器。
四、两种触发:proactive vs reactive
什么时候压缩?有两条路,成熟系统两条都要:
| Proactive(主动) | Reactive(被动) | |
|---|---|---|
| 触发 | 水位到阈值(如 85%),主动压 | 请求已经 413 超长了,被迫压 |
| 时机 | 撞墙之前 | 撞墙之后恢复 |
| 心态 | 未雨绸缪 | 亡羊补牢 |
还有一个不能忘的细节:压缩本身也要消耗上下文(要把老历史发给模型)。所以水位线那篇强调的“提前动手、留出余量”在这里兑现——你不能等 100% 满了才压,那时连“执行压缩”的空间都没了。
五、绕不开的矛盾:压缩 vs 缓存
压缩和缓存天然打架,这个矛盾必须正视:
六、小结
自动压缩是上下文工程里“减负”的主力手段,也是有损、有风险、有代价的一步:
- 保留近期、摘要远古,用一条压缩边界切开。
- 摘要 prompt 决定成败——用固定结构强制保留“继续干活所必需”的信息,对抗“压缩即失忆”。
- proactive 主动 + reactive 兜底,并给压缩本身留出余量。
- 正视压缩击穿缓存的代价,想清楚这笔交易。
压缩是“大刀阔斧”——一下折叠掉几十条消息。但有时你想要更精细的控制:比如“只把那几个占了两万 token 的巨型工具结果删掉,别的都留着”。这种“外科手术式”的上下文管理,是下一篇的主题。
本篇对应代码:配套仓库加入 src/compact.ts(阈值触发的摘要压缩),接入循环的每轮开头。git checkout article-13。