第二十一章:上下文快满时,系统怎么继续活下去
从 snip 到 autoCompact,把 Claude Code 的上下文压力治理链路完整拆开。
1. 为什么这一层必须单独拆
前面我们已经把回合状态机、工具运行时和各类工具都拆得差不多了。
但如果你后面真要做 C# 版,最容易抄歪的地方,其实不是工具接口,而是“上下文快满了以后系统怎么继续活下去”。
Claude Code 在这里做的不是一个简单的“总结一下历史消息”,而是一整套分层治理:
- 先清理高噪音的大块内容
- 再尝试保留更多原始上下文
- 实在不行再做整段摘要
- 摘要完还要把运行时状态重新补回去
所以这一层更准确的名字不是 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:372src/query.ts:396src/query.ts:411src/query.ts:439src/query.ts:466src/query.ts:638
query.ts 里的顺序非常重要。
真正发模型请求之前,Claude Code 会按这个顺序处理上下文:
applyToolResultBudgetsnipCompactIfNeededmicrocompactMessagescontextCollapse.applyCollapsesIfNeededautoCompactIfNeeded- 然后才真正
callModel
这说明作者的思路非常明确:
能不碰主叙事,就先别碰主叙事。
也就是说:
- 单条工具结果太大,先截工具结果
- 中段历史能删,先删中段
- 旧工具结果能清空,先清空
- 还能靠 collapse 保留颗粒度,就别摘要
- 最后才进入真正的 compaction
对 C# 迁移来说,这个顺序比任何单个函数都更重要。
4. microcompact 不是摘要,而是“旧工具结果清理层”
源码锚点:
src/services/compact/microCompact.tssrc/services/compact/timeBasedMCConfig.tssrc/services/compact/compactWarningState.ts
4.1 它清的不是所有内容
microCompact.ts 里只把一部分工具当成可清理对象:
Read- shell 工具
GrepGlobWebSearchWebFetchEditWrite
这里的设计思路很清楚:
优先清“高 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.tssrc/services/compact/prompt.tssrc/services/compact/grouping.ts
5.1 它不是直接拿历史消息喂模型
compactConversation() 里真正做的事非常多:
- 跑
PreCompacthooks - 合并用户自定义 compact 指令
- 构造严格的 no-tools summary prompt
- 尝试复用主会话 prompt cache
- 处理
prompt_too_long的重试 - 拿到 summary 后清理旧 read cache
- 回灌文件、plan、plan mode、skill、异步 agent、动态工具等附件
- 跑
SessionStarthooks - 写 compact boundary
- 跑
PostCompacthooks
所以它不是:
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:
tool_use在保留区外,但tool_result在保留区内- 同一 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.js 和 snipProjection.js 的主体实现,但外围接线是明确存在的:
query.ts会在 microcompact 前调用snipCompactIfNeeded()snipTokensFreed会传给 autocompact,修正 token 估算normalizeMessagesForAPI前会给 user message 注入[id:...]标签,便于 snip 引用消息getMessagesAfterCompactBoundary()默认会走projectSnippedView()sessionStorage.ts在 resume 时会按removedUuids真删消息并重链parentUuidattachments.ts里还有context_efficiencynudge,会在长时间没 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_20250919clear_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. 这一层最值得抄的设计习惯
最后收一下,我觉得最值钱的设计习惯有五个:
- 先删可重建垃圾,再碰主叙事。
- 上下文裁剪首先要保证消息协议合法,而不是先追求摘要优雅。
- compact 后必须做运行时状态回灌,不然 agent 会“失忆”。
- 自动 compact 必须有熔断,不然会把系统拖进失败风暴。
- 高级压缩策略要和基础 full compact 解耦,才能逐步演进。
15. 一句话总结
如果说 query.ts 是 Claude Code 的心脏,那这套 compact / microcompact / session memory / snip 机制,就是它的循环系统。
它的目标不是“总结聊天记录”,而是:
在 context window 这个硬物理限制下,让 agent 尽量不断片地继续工作。
补充阅读: