拆.
主循环——AI 怎么'思考'一轮对话 · 第 04

Compaction

当 AI 的'内存'满了

4400读完约 23 分钟craft:B+发布于 2026-06-21

一、原理:上下文超限的几种应对

一个 session 跑久了——比如 100 个 turn 之后——你会遇到一个不可避免的问题:context window 满了

LLM 的 context window 是硬上限。Claude 3.5 是 200K tokens,GPT-4 是 128K,Gemini 1.5 是 1M。不管多大,跑得够久都会满。

满了之后会发生什么?

如果产品没做任何处理——LLM 直接返回错误:"context length exceeded"。session 终止。用户的工作半途而废。

显然不能这样。AI 产品必须有应对 context 超限的策略。理论上有几种方案:

方案 A:截断(truncation)。简单粗暴——从历史最开头删,删到能塞下。优点是简单。缺点是丢的是最早的内容——但最早的内容往往是用户的"原始需求"——丢了 LLM 就忘了最初要做什么。

方案 B:滑动窗口(sliding window)。只保留最近 N 个 turn 的完整内容。比截断好——保留了"近期上下文"。缺点跟截断类似——长期任务的初始上下文会丢。

方案 C:压缩(compaction)。用 LLM 把历史总结成一段简洁的摘要,用摘要代替历史。优点是不丢核心信息——摘要保留了"做过什么、决定了什么、还剩什么"。缺点是摘要本身要消耗 token(要 LLM 读完历史写摘要),而且摘要可能丢细节。

方案 D:回放(replay)。重新跑用户的所有原始 message——但不带历史响应——让 LLM 重新从头思考。优点是上下文重置干净。缺点是重新执行的工作不可控——之前的进展全废。

方案 E:分层组合。compaction 是主策略,但保留一些"不该压"的内容(比如最近的工具结果、关键的代码文件)。最后兜底是回放。这是大多数严肃 AI 产品的选择。

opencode 选了方案 E。我们看它怎么做。

二、案例:opencode 的 PRUNE 三态机制

opencode 的 compaction 在 packages/opencode/src/session/compaction.ts。它的核心是三个魔法数字:

PRUNE_MINIMUM = 20_000      
PRUNE_PROTECT = 40_000      
DEFAULT_TAIL_TURNS = 2      

这三个数字驱动整个 compaction 决策。我们一个个解释。

PRUNE_MINIMUM = 20K——这是触发门槛。如果 context 满了但能"压缩出"的 token 少于 20K,opencode 不做 compaction。

为什么?因为 compaction 本身有成本:

  • 启动一个 LLM 调用让它读历史写摘要(几千 tokens 的成本)
  • 摘要后的内容塞回 session,下次 LLM 调用还要重新理解(认知成本)
  • 摘要必然丢细节,可能影响后续 LLM 的判断(质量成本)

如果只能压缩出 10K,付这些成本不值得——直接让 session 自然消亡,让用户开新 session 继续。

但如果能压缩出 50K,付这些成本就值——大概率换 session 寿命延长 50-100 个 turn。

PRUNE_PROTECT = 40K——这是保护范围。最近 40K tokens 内的工具输出不参与压缩——它们完整保留。

为什么是工具输出?因为工具输出是 LLM 的"工作记忆"——它刚读的文件、刚跑的命令、刚抓的网页。如果这些被压缩掉,LLM 会立刻迷失——"我刚才看的文件是什么来着"。

但 40K 之外的工具输出可以压缩——那是"过去的工作记忆",已经不重要了。LLM 当前的注意力在最近的工具结果上,更早的可以用摘要代替。

DEFAULT_TAIL_TURNS = 2——这是完整尾部。最近 2 个完整 turn(user message + assistant 响应 + 所有 tool call/result)不压缩。

为什么是 turn 级别而不是 token 级别?因为 turn 是用户感知的"对话单元"。如果你压缩了上一个 turn 的内容,用户的下一句"刚才你说的那个方案……" LLM 就看不到"刚才"了——上下文断裂。

把 2 个完整 turn 留下,保证 LLM 能继续"对话流"。

这三个数字叠加,opencode 的 compaction 决策大致是:

  1. 检查 context 是不是 overflow(超过模型限额减去 buffer)
  2. 如果是,看能压缩出多少(除掉 PRUNE_PROTECT 和 TAIL_TURNS 之后剩下的)
  3. 如果能压缩出 > PRUNE_MINIMUM,启动 compaction agent
  4. compaction agent 读历史写摘要,用摘要替换原内容
  5. 如果压缩后还是 overflow——更激进的措施(剥离图片、回放原 message)

这是一套保守的、分级的、有 fallback 的压缩策略。每一步都有明确的判断标准——不是"凭感觉压"。

我们还要看 compaction agent 本身的 prompt。在 packages/opencode/src/agent/prompt/compaction.txt

You are an anchored context summarization assistant for coding sessions.

Summarize only the conversation history you are given. The newest turns may be kept verbatim outside your summary, so focus on the older context that still matters for continuing the work.

If the prompt includes a <previous-summary> block, treat it as the current anchored summary. Update it with the new history by preserving still-true details, removing stale details, and merging in new facts.

Always follow the exact output structure requested by the user prompt. Keep every section, preserve exact file paths and identifiers when known, and prefer terse bullets over paragraphs.

Do not answer the conversation itself. Do not mention that you are summarizing, compacting, or merging context. Respond in the same language as the conversation.

注意几个细节:

细节 1:anchored summarization——"锚定式摘要"。不是从头总结,是基于"上一次摘要"更新——保留还成立的细节、删去过时的、合并新事实。这是增量压缩——session 跑得越久,摘要越精炼。

细节 2:用 <previous-summary> XML 标签——给 LLM 一个明确信号"这是上次摘要"。让它知道应该"更新"而不是"重新写"。

细节 3:明确禁令 "Do not mention that you are summarizing, compacting, or merging context"——别告诉 LLM 它在做摘要。这是 4.7 章会讲的"隐性管理"原则的体现——让 LLM 保持任务专注,不要意识到"系统在管理它"。

细节 4:preserve exact file paths and identifiers——文件路径、变量名这种"硬标识符"必须原样保留。摘要可以丢叙事但不能丢这些精确信息——下次 LLM 用这些路径继续工作。

这些细节加起来,让 opencode 的 compaction 不只是"省 token",是有质量保证的省 token——压缩后的摘要还能驱动后续工作。

三、设计启示:怎么设计自己的 compaction 策略

这一章的核心论点:Compaction 不是技术问题,是产品判断——什么该留什么该丢取决于你的产品场景

如果你做 AI 产品需要 compaction,下面几条原则有用。

1. 区分"工作记忆"和"长期记忆"

工作记忆是 LLM 当前正在用的——最近读的文件、最近跑的命令、最近 user 的指令。这部分不能压缩——压了 LLM 立刻迷失。

长期记忆是 session 早期的——用户最初的需求、做过的决定、定下的方向。这部分可以摘要——保留核心、丢细节。

把这两层分清楚是 compaction 设计的基础。opencode 的 PRUNE_PROTECT + TAIL_TURNS 就是工作记忆的保护范围。

2. 摘要要保留"硬标识符"

文件路径、变量名、用户名、URL、数字——这些是 LLM 后续继续工作的"接口"。摘要时它们必须原样保留,不能"按意思总结"。

"用户改了 auth.ts 第 145 行的逻辑" → 不能压缩成 "用户改了 auth 相关代码"。前者后续可用,后者后续 LLM 不知道改哪里。

3. 摘要要可增量更新

session 跑得越久,需要的摘要越长——一次摘要可能不够。要支持"基于上次摘要再总结"——anchored summary。每次 compaction 不是从头开始,是把"新增的历史"merge 到"现有摘要"。

这样摘要质量稳定——不会因为多次重压缩而逐渐失真。

4. compaction 要可触发也可主动

被动触发:context 即将 overflow 时自动启动。这是兜底,但用户感知不到。

主动触发:用户可以 /compact 这种 slash command 手动启动——比如他知道接下来要做的工作不需要前面所有上下文,主动清一清。

两种都要有。

5. compaction 失败的兜底

compaction 本身可能失败——LLM 写的摘要不够好、网络中断、超时。失败后要有降级——比如直接截断、或者剥离图片这种重型内容。

如果所有降级都失败——告诉用户"session 太大了,建议开新的"。让用户知道发生了什么,而不是莫名其妙的"context length exceeded"。

6. 给用户看到 compaction 在跑

UI 上显示"正在压缩对话历史…"。这是几秒到十几秒的操作,不能假装"什么都没发生"——用户会以为产品卡了。

显式告诉用户在做什么,建立信任。compaction 是个透明的过程——不要藏着。

7. compaction 后不要重启 LLM

opencode 的设计是 compaction 完成后继续当前 turn——不会让用户重发请求。LLM 看到的 messages 历史变了(摘要代替原内容),但当前任务上下文没断。

如果你的 compaction 要求"重启 session"——那就不是真正的 compaction,是 session 截断。这种体验对用户不友好。

最后一个观察。Compaction 是 AI Agent 工程里最被低估的功能之一。大多数 AI 产品的 demo 都不展示 compaction——demo 里 session 不会跑到那么久。但真实生产环境,80% 的长期用户都遇到过 context 超限——他们怎么体验这一刻决定了他们继续用还是流失。

把 compaction 做好的产品和不做的产品体验差天差地别。做好的产品用户感觉"AI 真的能跟我做长期项目"。不做的产品用户感觉"AI 用一阵就不灵了,得开新 session 从头来"。

这是个沉默的差异化维度——marketing 不讲、benchmark 不测、但用户能感受到。

下一章 2.5 我们看 compaction agent 是 opencode 三个 hidden agent 之一——它们都是 user 看不见但产品在用的"系统级 AI"。