Book 第二十一章:上下文快满时,系统怎么继续活下去
第五部分:会话续航与治理

第二十一章:上下文快满时,系统怎么继续活下去

从 snip 到 autoCompact,把 Claude Code 的上下文压力治理链路完整拆开。

1. 为什么这一层必须单独拆

前面我们已经把回合状态机、工具运行时和各类工具都拆得差不多了。

但如果你后面真要做 C# 版,最容易抄歪的地方,其实不是工具接口,而是“上下文快满了以后系统怎么继续活下去”。

Claude Code 在这里做的不是一个简单的“总结一下历史消息”,而是一整套分层治理:

  1. 先清理高噪音的大块内容
  2. 再尝试保留更多原始上下文
  3. 实在不行再做整段摘要
  4. 摘要完还要把运行时状态重新补回去

所以这一层更准确的名字不是 compact,而是:

conversation survival runtime。

2. 先说结论

我对这套设计的判断是:

Claude Code 的上下文压缩不是一个函数,而是四层串联的上下文治理流水线。

可以先把它理解成这样:

flowchart LR
    A["原始消息历史"] --> B["tool result budget<br/>按单条结果限流"]
    B --> C["snip<br/>删中段历史,保留首尾语义"]
    C --> D["microcompact<br/>清旧工具结果,不改主叙事"]
    D --> E["context collapse<br/>更细粒度归档,快照里只看到接线"]
    E --> F["autoCompactIfNeeded"]
    F --> G["session memory compact<br/>优先保留最近原文"]
    F --> H["full / partial compact<br/>真正摘要重写"]
    G --> I["post-compact cleanup + 状态回灌"]
    H --> I
    I --> J["继续下一轮 query"]

这张图里最关键的点有三个:

  • compact 不是第一道防线,而是最后一道重写防线。
  • 真正被 Claude Code 优先清掉的,不是“用户意图”,而是旧工具结果、旧大块输出、可重建上下文。
  • 压缩之后不会只留下一个 summary,还会把 plan、skill、异步 agent、动态工具、最近读过的文件再补回去。

3. 在主回合里,它排在什么位置

源码锚点:

  • src/query.ts:372
  • src/query.ts:396
  • src/query.ts:411
  • src/query.ts:439
  • src/query.ts:466
  • src/query.ts:638

query.ts 里的顺序非常重要。

真正发模型请求之前,Claude Code 会按这个顺序处理上下文:

  1. applyToolResultBudget
  2. snipCompactIfNeeded
  3. microcompactMessages
  4. contextCollapse.applyCollapsesIfNeeded
  5. autoCompactIfNeeded
  6. 然后才真正 callModel

这说明作者的思路非常明确:

能不碰主叙事,就先别碰主叙事。

也就是说:

  • 单条工具结果太大,先截工具结果
  • 中段历史能删,先删中段
  • 旧工具结果能清空,先清空
  • 还能靠 collapse 保留颗粒度,就别摘要
  • 最后才进入真正的 compaction

对 C# 迁移来说,这个顺序比任何单个函数都更重要。

4. microcompact 不是摘要,而是“旧工具结果清理层”

源码锚点:

  • src/services/compact/microCompact.ts
  • src/services/compact/timeBasedMCConfig.ts
  • src/services/compact/compactWarningState.ts

4.1 它清的不是所有内容

microCompact.ts 里只把一部分工具当成可清理对象:

  • Read
  • shell 工具
  • Grep
  • Glob
  • WebSearch
  • WebFetch
  • Edit
  • Write

这里的设计思路很清楚:

优先清“高 token、低结构价值、可通过文件或环境重建”的结果。

它不是去压缩用户意图,也不是去压缩 assistant 结论,而是优先清掉这类“上下文垃圾热区”。

4.2 它有两条路径

路径 A:cached microcompact

这是更像“缓存编辑”的那条路。

特点是:

  • 不直接改本地消息内容
  • 只记录哪些旧 tool_result 应该从服务端 cache 视角删掉
  • 通过 pendingCacheEdits 交给 API 层处理
  • 只在主线程跑,避免 fork agent 污染全局状态

也就是说,这条路追求的是:

尽量瘦身,但不要打碎 prompt cache。

路径 B:time-based microcompact

这条路更激进,但判断条件也更明确:

  • 如果距离上一次 assistant 消息已经超过阈值
  • 说明服务端 1 小时 cache 大概率已经冷掉
  • 那就直接把旧工具结果内容改成固定占位文案

默认清空后的内容是:

[Old tool result content cleared]

这条路背后的思路是:

既然反正要 miss cache,那就别把旧垃圾再整段重发一遍。

4.3 它为什么先于 autocompact

因为 microcompact 能省下来的,往往是最贵但最不值得摘要的 token。

如果它先跑成功,很多轮其实根本不需要再进入 full compact。

这是一种非常典型的“先 cheap win,后 expensive rewrite”的系统设计。

5. compactConversation 才是真正的“整段摘要重写”

源码锚点:

  • src/services/compact/compact.ts
  • src/services/compact/prompt.ts
  • src/services/compact/grouping.ts

5.1 它不是直接拿历史消息喂模型

compactConversation() 里真正做的事非常多:

  1. PreCompact hooks
  2. 合并用户自定义 compact 指令
  3. 构造严格的 no-tools summary prompt
  4. 尝试复用主会话 prompt cache
  5. 处理 prompt_too_long 的重试
  6. 拿到 summary 后清理旧 read cache
  7. 回灌文件、plan、plan mode、skill、异步 agent、动态工具等附件
  8. SessionStart hooks
  9. 写 compact boundary
  10. PostCompact hooks

所以它不是:

messages -> summary

而更像:

messages -> 新会话基线

5.2 summary prompt 本身也很“工程化”

prompt.ts 里的 compact prompt 有几个明显特点:

  • 明确禁止任何工具调用
  • 强制输出 <analysis> + <summary> 双段结构
  • 明确要求按用户诉求、关键技术点、文件、错误、未完成任务来写
  • summary 落地前还会剥掉 <analysis>

这背后体现的不是 prompt engineering 小技巧,而是产品上的硬要求:

compact 产物不是给人看的总结,而是给下一轮 agent 接着干活的续命上下文。

5.3 它还专门处理“compact 自己也太长”的问题

这是这套实现里很值得抄的一点。

如果 compact 请求自己也撞上 prompt_too_long,Claude Code 不会直接让用户卡死,而是:

  • groupMessagesByApiRound() 按 API 回合分组
  • 从最老的 round 开始丢
  • 加一个 synthetic marker
  • 然后重试 compact

这说明作者接受一个现实:

极限情况下,compact 也必须支持有损降级。

不然用户就真的出不来了。

6. full compact 后,它会把哪些运行时状态补回来

这是 Claude Code 和很多“聊天摘要系统”最大的不同。

它不是只保留:

  • 一条 boundary
  • 一条 summary

它还会主动回灌这些东西:

6.1 最近读过的文件

createPostCompactFileAttachments() 会按最近访问时间选文件,并重新生成 file attachment。

但它还加了两层约束:

  • 文件数量上限
  • 总 token budget 上限

并且会跳过:

  • preserved tail 里已经还看得见的 Read 结果
  • plan 文件
  • memory 文件

这说明它不是“无脑重放文件上下文”,而是在做预算化恢复。

6.2 plan / plan mode

如果当前还在 plan mode,compact 之后会显式回灌:

  • plan file
  • plan mode attachment

否则模型会在 compact 后丢失“现在还处于计划态”这件事。

6.3 invoked skills

Claude Code 不会简单重放完整 skill_listing,而是只把真正用过的 skill 作为 invoked_skills attachment 补回来。

而且它还会:

  • 按最近使用排序
  • 做每个 skill 的 token 截断
  • 再做总预算截断

这说明它在有意识地区分:

  • 工具发现信息
  • 真正已经进入执行语义的 skill 指令

6.4 异步 agent / task 状态

如果后台还有 agent 在跑,或者结果还没被取回,compact 后也会再注入 task_status 附件,避免主线程忘记它们还存在。

这点非常关键,因为摘要模型本身并不知道 runtime 里还有哪些异步执行主体。

6.5 deferred tools / agent listing / MCP instructions

compact 会吞掉很多 delta attachment,所以 compact 之后 Claude Code 会重新 diff 当前状态,再把:

  • deferred tools
  • agent listing
  • MCP instructions

补回第一轮 post-compact 上下文里。

这说明 compact 的目标不是“会话归零”,而是“把系统当前运行边界重建出来”。

7. partialCompactConversation 说明它支持“局部重写”

源码锚点:

  • src/services/compact/compact.ts:767

Claude Code 还支持 partial compact,而且有两个方向:

  • from:总结某个点之后的消息,保留前面的
  • up_to:总结某个点之前的消息,保留后面的

这两种方向的缓存语义不一样:

  • from 更像保前缀
  • up_to 会让 summary 跑到 kept messages 前面,旧 boundary 必须清掉

这说明 compaction 在 Claude Code 里不是单一“截断头部”动作,而是:

一个可按消息区间操作的上下文重写器。

如果你后面做 C# 版,这层最好独立成 ConversationRewriter,不要把它和 autocompact 写死绑在一起。

8. sessionMemoryCompact 是另一套更保守的压缩路径

源码锚点:

  • src/services/compact/sessionMemoryCompact.ts

这条路径很值得注意,因为它代表 Claude Code 在尝试:

先保留更多原文,再用会话记忆补摘要。

它的大概思路是:

  • 如果 session memory feature 开着
  • 先看能不能用 session memory 作为 summary
  • 然后尽量保留最近一段原始 messages
  • 并且严格保证不打断 tool_use / tool_result 配对,也不打断同一 message.id 的 thinking 合并

这里最值钱的不是“有 session memory”,而是它对 API 不变量的保护。

adjustIndexToPreserveAPIInvariants() 明确在防两类 bug:

  1. tool_use 在保留区外,但 tool_result 在保留区内
  2. 同一 assistant message.id 的 thinking / tool_use 被切开

这说明作者非常清楚:

上下文裁剪最容易做坏的,不是摘要质量,而是消息协议合法性。

8.1 这条路为什么重要

因为 full compact 一旦发生,原始上下文会变成“一段 summary + 若干补丁附件”。

而 session memory compact 的目标是:

  • 尽量保住最近的真实对话
  • 只把更老的部分折叠进 session memory

从 agent 连续工作体验上说,这条路比 full compact 更像“轻断点续跑”。

9. autoCompact 管的不是摘要内容,而是“何时触发、何时停手”

源码锚点:

  • src/services/compact/autoCompact.ts

9.1 它有明确的 token 预算模型

autoCompact.ts 里先算:

  • 模型 context window
  • 要预留给 compact summary 输出的 token
  • autocompact buffer
  • warning / error / blocking 阈值

所以它不是看到“消息很多”就 compact,而是一个明确的预算调度器。

9.2 它还专门给失败做了熔断

MAX_CONSECUTIVE_AUTOCOMPACT_FAILURES = 3

也就是说,如果某个 session 已经进入“怎么 compact 都救不回来”的状态,Claude Code 不会每一轮都继续徒劳地 compact。

这个设计非常像生产系统,而不是 demo:

失败也要限流。

9.3 它优先尝试 session memory compact

autoCompactIfNeeded() 里先调:

  • trySessionMemoryCompaction()

失败了才落回:

  • compactConversation()

这再一次证明:

full summary rewrite 是后手,不是首选。

10. compact 完成后,还会做一轮全局清理

源码锚点:

  • src/services/compact/postCompactCleanup.ts

runPostCompactCleanup() 会清:

  • microcompact 状态
  • classifier approvals
  • speculative checks
  • system prompt sections
  • memory file cache
  • session message cache
  • beta tracing state

而且它还区分主线程 compact 和 subagent compact,避免 subagent 把主线程共享状态一起清坏。

这说明 compact 在 Claude Code 里不是局部替换数组,而是一次:

runtime state invalidation event。

11. snip 在这份快照里能确认到什么

这里要单独说明边界。

11.1 能确认的事实

虽然这份快照里缺了 snipCompact.jssnipProjection.js 的主体实现,但外围接线是明确存在的:

  • query.ts 会在 microcompact 前调用 snipCompactIfNeeded()
  • snipTokensFreed 会传给 autocompact,修正 token 估算
  • normalizeMessagesForAPI 前会给 user message 注入 [id:...] 标签,便于 snip 引用消息
  • getMessagesAfterCompactBoundary() 默认会走 projectSnippedView()
  • sessionStorage.ts 在 resume 时会按 removedUuids 真删消息并重链 parentUuid
  • attachments.ts 里还有 context_efficiency nudge,会在长时间没 snip 时提醒模型

这些外围证据已经足够说明:

snip 不是摘要工具,而是一个“删除中段历史、保持链路可恢复”的结构化修剪器。

11.2 不能当成事实写的部分

因为主体文件不在快照里,所以这些点现在只能推断,不能下结论:

  • SnipTool 的完整 schema
  • 模型如何选取 snip 区间
  • snip boundary 的完整写盘格式
  • snip 节流策略的全部实现

所以在 C# 迁移阶段,更稳的做法是:

  • 先把 snip 当作“可选高级优化”
  • 先把 full compact / microcompact / session memory compact 做扎实

12. apiMicrocompact.ts 说明团队也在试“把压缩下沉到 API”

这个文件很有意思。

它暴露的是一套 API-native context management config,比如:

  • clear_tool_uses_20250919
  • clear_thinking_20251015

也就是说,Claude Code 团队并不满足于客户端自己改消息数组,他们也在试:

让 API 层直接按策略清 tool use / thinking。

这对你做 C# 版很有参考价值:

  • 本地 compaction 是稳定主线
  • API-native context edit 是可插拔增强

两者不要耦死。

13. 我建议你在 C# 里怎么拆

如果目标是“把 Claude Code 的设计思路转成 C#”,这一层我建议至少拆成这几块:

13.1 上下文治理总编排

public interface IContextPressurePipeline
{
    Task<ContextPreparationResult> PrepareAsync(
        ConversationState state,
        QueryContext context,
        CancellationToken ct);
}

它负责串联:

  • tool result budget
  • snip
  • microcompact
  • collapse
  • autocompact

13.2 摘要与重写器

public interface IConversationCompactor
{
    Task<CompactionResult> CompactAsync(...);
    Task<CompactionResult> PartialCompactAsync(...);
}

13.3 轻量清理器

public interface IMicroCompactor
{
    Task<MicrocompactResult> RunAsync(...);
}

13.4 触发策略器

public interface IAutoCompactPolicy
{
    AutoCompactDecision Evaluate(...);
}

13.5 post-compact 状态恢复器

public interface IPostCompactRestorer
{
    Task<IReadOnlyList<ConversationAttachment>> RestoreAsync(...);
}

13.6 一个非常重要的原则

不要把 compact 结果只设计成一段字符串。

Claude Code 的 compact 结果至少包含:

  • boundary marker
  • summary messages
  • preserved messages
  • attachment messages
  • hook results
  • token telemetry

如果你的 C# 版只返回 string summary,后面几乎一定会重构第二次。

14. 这一层最值得抄的设计习惯

最后收一下,我觉得最值钱的设计习惯有五个:

  1. 先删可重建垃圾,再碰主叙事。
  2. 上下文裁剪首先要保证消息协议合法,而不是先追求摘要优雅。
  3. compact 后必须做运行时状态回灌,不然 agent 会“失忆”。
  4. 自动 compact 必须有熔断,不然会把系统拖进失败风暴。
  5. 高级压缩策略要和基础 full compact 解耦,才能逐步演进。

15. 一句话总结

如果说 query.ts 是 Claude Code 的心脏,那这套 compact / microcompact / session memory / snip 机制,就是它的循环系统。

它的目标不是“总结聊天记录”,而是:

在 context window 这个硬物理限制下,让 agent 尽量不断片地继续工作。

补充阅读: