上一篇讲了 Agent 的 7 个取舍,其中「上下文精打细算」里我埋了个坑没展开------上下文真正撑爆之后怎么办?这篇补上。
前言
Agent 运行长任务是绕不开一个事实的:Agent的上下文是有限的,而且它一定会越用越大。读个文件、跑条命令、搜个网页等,每一轮工具调用的结果都会挤进上下文。堆着堆着,要么撞上窗口上限直接报错,要么 token 越烧越贵。
压缩:把已经发生过的对话历史,浓缩成更小的体积。压缩有两种流派,各有各的代价。这篇就把它们讲清楚:什么时候用哪个,以及为什么这么选。
一、压缩的本质:用「信息」换「空间」
先明确一件事:压缩一定是有代价的。你把历史浓缩,就意味着要丢掉一部分细节。所以「怎么压缩」的本质,是你愿意用多少信息损失,去换多少空间。
在这个尺度上,有两种极端:
- 微压缩(microcompact):不调用模型,直接把旧的工具结果清空成占位符。零成本,但信息是真的没了。
- LLM 摘要(summarize):调用一次模型,把旧历史压成一份结构化摘要。有成本,但信息保留得更完整、更好一点。
二、微压缩:免费的「急救」
微压缩的思路非常简单粗暴:把旧工具的结果输出,直接替换成一个占位符 [tool result cleared]。
ts
// 只清「可清理」的工具,且保留最近 3 条
const CLEARABLE_TOOLS = new Set(['read_file', 'bash', 'grep', 'glob', ...])
const KEEP_RECENT_TOOL_RESULT = 3
它有三个很关键的细节,每一个都是取舍:
1. 只清工具结果,不动别的。 用户说的话、模型自己的思考,一个字都不动;只把工具返回的那些大块输出清掉。因为工具结果是上下文里**体积最大、又最不「金贵」**的部分------用户意图和模型的推理链,才是必须保住的。
2. 保留最近 几 条。 最近几条工具结果是模型「接下来要做什么」的直接依据,清了它模型就成了睁眼瞎。所以再激进的压缩,也要给「现在」留够缓冲。
3. 只清「可清理」的工具。 read_file、bash、grep 这些输出可能几千上万字符,值得清;而像 memory、rag 这种本来输出就小、语义又重要的工具,清了反而亏。
取舍点 :微压缩最大的优点是零成本、零延迟 ------它压根不调模型,纯字符串操作,瞬间完成。代价也很直白:被清掉的信息是永久丢失的,模型真的就「忘」了那部分内容。所以它适合当「急救」:上下文快爆了,先砍掉最肥的部分续命,别管优雅不优雅。
三、LLM 摘要:付费的「正式方案」
微压缩是丢信息,摘要则是把信息提炼出来------调用一次模型,把旧历史浓缩成一份几百字的摘要,用摘要替换掉原文。
ts
// 小于 300 token 不值得摘要;保留最近 6 条原始消息
const CONTEXT_TOKEN_THRESHOLD = 300
const KEEP_RECENT_MESSAGES = 6
这里的取舍比微压缩更多,也更有意思:
1. 阈值:太小了不摘要。 比如,历史不足 300 token 时,摘要省下的那点空间,还不够付一次摘要调用本身的成本,纯亏。所以设个下限,量不够就不折腾。
2. 保留最近 几 条。 和微压缩「保留最近 几 条」同理,但更保守------因为摘要是「大动干戈」的操作,更该给「正在进行的对话」多留点原样空间。最近的消息是任务的「当下」,压缩它等于让模型失去即时上下文。
3. 切分点要对齐到 user 消息边界。 这是最容易踩坑的细节。一轮工具调用就是「user → tool → assistant」这样一个原子单元,如果你从中间一刀切下去,会把工具调用和它的结果拆散,模型看到的就是一堆残缺的对话。所以切分前要往回退,退到一条 user 消息上:
ts
let splitIdx = messages.length - KEEP_RECENT_MESSAGES
while (splitIdx > 0 && messages[splitIdx].role !== 'user') splitIdx-- // 回退到 user 边界
4. 结构化摘要模板。 摘要不能是流水账,否则模型拿到摘要接不上上下文。所以用一个固定模板,逼模型输出「用户意图 / 已完成操作 / 关键发现 / 当前状态 / 需要保留的细节」五块,并且要求标识符(路径、UUID、版本号)原样保留。这样摘要才「可续」------下一轮模型读摘要,能无缝接着干。
5. 增量摘要。 如果已经有过一次摘要,再压第二次时,不是把旧摘要当历史扔掉,而是把「旧摘要 + 新对话」一起压。否则每次压缩都会丢掉上一次的成果,历史越压越稀。
取舍点 :摘要的信息保真度远高于微压缩,但它是有成本的------每压一次,就要多付一次模型调用。所以它适合「正式」场景:长任务跑到中段,历史已经积累到值得花一次调用去提炼的时候。
四、怎么选:不是二选一,是分工
把它们放一起看,其实是一个很清晰的权衡:
| 微压缩 | LLM 摘要 | |
|---|---|---|
| 成本 | 零(不调模型) | 一次模型调用 |
| 保真度 | 信息永久丢失 | 提炼保留要点 |
| 适用 | 急救、临时腾空间 | 长任务中段的正式压缩 |
取舍点 :真实工程里,这两者是配合 的,而不是二选一。我的策略是------先用微压缩兜底 (便宜、无脑、随时能上),当任务确实很长、历史确实很「重」的时候,再上摘要做一次正经的提炼。一句话总结:微压缩是「砍」,摘要是「炼」;砍了还能续命,炼了才不亏本。
结语
回到上一篇的那个原点:LLM 是一个上下文有限的函数,Agent 的工程就是围绕这个限制做设计。 截断、TTL 是在「入口」做减法,压缩是在「存量的历史」上做提炼------一个管「新来的」,一个管「已经攒下的」,合起来才是完整的上下文管理。
下一篇我想聊聊 Agent 的另一个老大难:防死循环------模型为什么会陷入重复,以及怎么用「指纹 + 熔断」给它加一道负反馈。