Claude Code 如何压缩上下文:Microcompact、Prompt Cache 与 cache_edits 工程拆解
拆解 Claude Code 如何用 Microcompact 和 cache_edits在清理上下文时保住 Prompt Cache 折扣。原文链接AI 小老六导语长时间跑 Claude Code 时最先顶不住的通常不是模型能力而是 Context Window。工具调用越多messages里的tool_result越厚日志、文件内容、搜索结果、构建输出一层层堆上去Context Window 很快就被旧数据塞满。但 Claude Code 并不是等到上下文爆掉才开始处理。它在每次 API 请求发出之前都会先做一轮非常轻的清理检查历史里的旧tool_result把一部分已经没必要继续给模型看的内容从服务端缓存视图里挖掉。麻烦也正出在这里。Anthropic 的 Prompt Cache 依赖前缀 hash。理论上只要messages中任何历史内容发生变化前缀就不再相同缓存命中会失效input token 的 90% 折扣也就没了。Claude Code 一边清理历史一边又保住缓存命中这背后靠的不是普通的本地裁剪而是 Harness 和 API 的协议级配合cache_edits。这篇文章把 Claude Code 的三层compact机制拆开讲清楚重点看最容易被忽略、但调用频率最高的那一层Microcompact。图Microcompact 在请求前清理旧工具结果但不破坏本地历史Compact 不是一个开关而是一条成本阶梯Claude Code 里和压缩相关的机制大致可以分成三层Microcompact、autocompact和fullcompact。它们不是三个互斥选项而是一条 按成本排序的兜底链路。先用最便宜的办法处理处理不了再升级。这个思路贯穿整套设计。机制触发位置做什么成本Microcompact每次 API call 前按规则清理旧tool_result并在冷启动时清理历史thinking block不调用 LLM主要是本地扫描和少量请求字段autocompacttoken 和 tool call 数量接近阈值时把历史整理进本地 background notes作为后续上下文载体不额外 fork summarization agent但会重组上下文fullcompact前两层兜不住时fork 一个 sub-agent 做完整摘要生成新的压缩上下文调 LLM成本最高也最不稳定fullcompact看上去最像我们熟悉的“压缩”把长上下文交给模型让它总结成短文本。但它也是最贵、最容易出问题的一层。Finisky 对 Claude Code 的分析里提到Sonnet 4.6 在约2.79%的场景下会让 compact sub-agent 产生不该出现的工具调用Claude Code 因此设计了 circuit breaker连续失败 3 次就熔断否则线上可能出现重试循环浪费大量 API 调用。所以 Claude Code 的策略很现实别一上来就请模型总结。能靠规则删掉的就先靠规则删能靠本地文件托住的就别 fork agent真到无路可走再动fullcompact。图Microcompact、autocompact 与 fullcompact 的分层兜底链路这张图里最关键的一点是Microcompact不等压力触发。它挂在热路径上每一轮都跑。Microcompact贴在 API 请求前的轻量清扫器很多人把 compact 理解成“上下文快满了才触发的事件”。这对autocompact和fullcompact基本成立但不适用于Microcompact。Microcompact更像一个 request pre-hook。每次 Claude Code 准备向 Anthropic 发起请求之前它先扫一遍历史消息找出那些符合条件、已经过了保留窗口的tool_result然后把清理意图附带到这次请求里。它敢这么高频地运行原因有两个。第一它 不调用 LLM。候选识别靠本地规则完成看工具名、看出现顺序、看是否超过阈值、看最近几条是否需要保留。没有 prompt没有模型推理也没有额外 token 账单。第二它只碰 工具返回不碰 对话骨架。用户说过什么、assistant 的正文回答是什么、当前任务的推理主线如何推进这些都不动。被清掉的是旧工具输出某次Read返回的大段文件内容、某次Grep的匹配列表、某次Bash的日志。这些内容通常已经被模型消费过或者需要时可以重新取。白名单只有八类工具能被安全遗忘Microcompact不是见到tool_result就删。Claude Code 内部有一份很保守的白名单只有这些工具的结果有资格进入 Microcompact 候选池工具为什么可以进入候选池Bash大量输出是一次性消费的日志或命令结果后续价值递减Read文件内容可以重新读取Grep查询条件稳定时结果可以重新生成Glob文件匹配结果通常可重放WebFetch旧网页内容多数是阶段性证据必要时可重新抓取WebSearch搜索结果通常只支撑当轮判断FileEdit返回值多是写入确认不需要长期保留FileWrite返回值多是写入确认真正状态在文件系统里这份白名单的设计很有 Harness 味它关心的不是“内容是否有用”而是“丢掉后能不能恢复或者丢掉是否不会伤害主线”。自定义 MCP 工具默认不在白名单里。原因也简单Harness 不知道它们有没有副作用不知道结果是否幂等也不知道输出里是否包含无法重放的状态。一个业务系统查询工具、一段审批流工具、一个写数据库的内部工具返回内容一旦被擦掉可能就再也拿不回同一份状态了。对工具设计者来说这里其实有个暗示如果希望自己的工具适合长程 Agent 任务输出要么小要么可重放要么把关键状态落到外部系统里而不是全塞进tool_result。图只有可重放或低价值的工具结果适合进入遗忘候选池cache_edits不改本地历史只改服务端缓存视图真正绕不开的问题是 Prompt Cache。如果 Claude Code 真的直接修改本地messages前缀 hash 必然变化缓存命中就会被打断。Anthropic Prompt Cache 的命中收益很大命中时 input token 只按原价的 10% 计费相当于 90% 折扣但每个 cache entry 只有 5 分钟 TTL。在半小时以上的长程任务里只要前缀稳定input 成本可以省下 80% 以上。Microcompact要每轮都跑又不能每轮都破 cache。于是有了cache_edits。cache_edits不是普通的 messages rewrite。它是请求里的一个独立字段用来告诉服务端在已经建立的 cache 条目中把指定的tool_use/tool_result槽位从可见上下文里挖空但不要把这次操作当成前缀变化。可以把它理解成服务端缓存层的一块“遮罩”。本地messages仍保留完整历史Harness 侧 dump 出来的对话没有少一个字但服务端在后续构造模型输入时会按cache_edits的指令忽略掉某些旧工具结果。前缀 hash 仍按原来的缓存结构匹配因此 cache 还能命中。这件事有四个工程细节值得单独拎出来。细节含义本地messages不变客户端历史仍完整便于调试、回放和后续判断服务端 cache 视图变化模型实际可见的旧工具结果会被挖空前缀 hash 不重新计算清理动作不会像普通改历史那样打断 Prompt Cachecache break detector 会被告知cache_read token 下降是预期结果不能误报成 cache miss最后一点很容易被忽视。cache_edits生效后下一轮的cache_read_input_tokens可能会下降因为服务端确实少读了一部分旧内容。如果 Claude Code 的缓存断裂检测器只看数值下降就会误判为 cache miss。所以Microcompact在排队cache_edits时还会通知 detector这次下降是计划内的不要报警。状态机候选注册、年龄阈值和最近窗口把细节剥掉Microcompact的核心状态机并不复杂。# 收集当前 messages 中可压缩的 tool_call_id compactable_ids collect_compactable_tool_ids(messages) for tool_id in compactable_ids: state.register(tool_id) # 从已注册集合里挑出足够旧的工具结果 tools_to_delete state.get_oldest_beyond_threshold( trigger config.trigger_threshold, keep_recent config.keep_recent, ) # 本地 messages 不动只为下一次请求生成 cache_edits if tools_to_delete: pending_cache_edits create_cache_edits_block(tools_to_delete) notify_cache_break_detector(expected_drop len(tools_to_delete))这里有两个设计点。state.register说明 Claude Code 维护了一份跨轮持久的候选注册表。工具结果不是每轮临时扫完就忘而是会被记录进一个按时间推进的集合里避免重复处理也方便按年龄淘汰。keep_recent则是安全阀。再便宜的压缩也不能擦掉模型马上要用的东西。最近几条tool_result往往还在当前推理链路上比如刚Read完文件下一轮就要基于文件内容做修改刚跑完测试下一轮就要解释失败原因。保留最近窗口是Microcompact不伤语义的关键。Harness 和 API 必须一起完成这件事cache_edits有意思的地方在于它不是 Harness 单方面能做出来的也不是 API 单方面能决定的。如果只在 Harness 层做客户端一改messagesPrompt Cache 的前缀 hash 就变了。服务端缓存机制不认识“我只是想逻辑上隐藏一下旧结果”这种意图它只看到请求内容变了。如果只在 API 层做服务端又不知道哪些内容可以删。Read的旧文件片段能不能丢、某个Bash输出是不是一次性日志、一个 MCP 工具返回是不是不可重放状态这些判断都属于 Harness 的业务语义。合理分工只能是角色负责什么Harness判断哪些tool_result可以安全遗忘何时遗忘保留最近多少条API在缓存层执行可见性编辑并保持前缀缓存结构不被打碎这类机制说明 Harness 已经不只是 API 的调用者。它开始把自己的运行时语义反馈给模型服务层让 API 提供更贴近 Agent 场景的低层能力。两条执行路径热路径和冷启动Microcompact内部不是只有一条路。它有两种执行方式一条服务于高频请求一条服务于长时间 idle 之后的恢复。图Microcompact 在热路径和冷启动场景下的不同执行方式两条路径共用同一套候选识别逻辑白名单工具、年龄阈值、最近窗口。区别只在执行后端。热路径使用cache_edits因为服务器端 cache 还活着最重要的是别破坏 Prompt Cache。冷启动路径直接改写本地messages因为 cache 已经没有可保的价值了。Layer 1每次请求前的热路径热路径是Microcompact的主战场。每次 request 发出前Claude Code 会扫描当前历史找出符合白名单且足够旧的工具结果把它们放进pending_cache_edits。请求发到 Anthropic 后服务端按照cache_edits在 cache 视图里挖空这些槽位。本地对话不改前缀 hash 不变Prompt Cache 继续命中。这条路径的收益不是某一次请求突然省很多而是持续省。长程任务往往会产生大量工具调用如果每轮都把已经过期的工具输出清掉Context Window 就不会被历史日志和文件片段慢慢淹没。为什么 sub-agent 不参与热路径清理cache_edits只在 main thread 发起。fork 出去的 sub-agent 即使也产生了工具结果也不会自己参与这套清理调度。这不是性能问题而是一致性问题。sub-agent fork 出来时会共享主线当时的缓存前缀。但 fork 之后它自己的请求、工具调用和缓存视图会独立推进。如果 main thread 在 sub-agent 运行期间改了共享前缀里的某个tool_resultsub-agent 下一轮请求可能面对一个它本地历史并不知道的服务端缓存状态。轻则 cache miss重则 cache break detector 看到不一致的 token 读数。Claude Code 的选择是保守的sub-agent 自己跑结果 fan-in 回主线之后再由 main thread 统一管理上下文清理。少压一点没关系不能让多条执行线互相踩缓存状态。Layer 2超过 60 分钟后的冷启动路径第二条路径只在 idle gap 超过 60 分钟后出现。为什么是 60 分钟因为 Anthropic Prompt Cache 的 TTL 是 5 分钟。用户离开 60 分钟等于服务器端 cache 已经过了十几个 TTL 周期。此时再发cache_edits已经没意义因为下一次请求无论如何都要重建缓存前缀。既然 cache 已经保不住Claude Code 就不再坚持“本地 messages 一个字节不动”。冷启动路径会直接把旧的可压缩tool_result替换成轻量占位符例如[Old tool result content cleared]。下一次请求带着瘦身后的messages出发新 cache 也建立在瘦身后的状态上。冷启动还会顺手处理thinking block。在 extended thinking 模式下assistant message 里除了普通text还可能有独立的thinking块。它们体量不小常常是几千 token而且通常不享受 Prompt Cache 的输入折扣。更重要的是历史 thinking 的价值大多已经沉淀到同一轮的 assistant 正文里后续继续携带并不划算。所以冷启动会通过clear_thinking_20251015这类 context edit把最近一轮之前的历史 thinking 清掉。保留最近一轮是为了不破坏当前推理连续性清掉更早的是为了不让旧思考过程反复占上下文和账单。Microcompact和 Context Offloading 的关系如果把 Claude Code 的上下文治理放大看Microcompact其实是 Context Offloading 的另一面。Context Offloading处理的是“大块信息放哪里”文件内容、长日志、外部资料不一定都要直接塞进messages可以搬到磁盘或外部载体里只在上下文里保留摘要和指针。Microcompact处理的是“已经塞进来的旧痕迹怎么退场”工具结果已经被模型看过短期内不再需要就通过cache_edits或冷启动 rewrite 把它从模型可见上下文里拿掉。一个偏上游一个偏下游。前者少搬进来后者及时扫出去。两者合在一起才是长程 Agent 任务真正需要的上下文卫生机制。对 Agent 工程的启发Microcompact看起来像一个小优化但它透露出几个很硬的工程判断。判断含义压缩要分级不要一遇到上下文压力就调用 LLM 摘要工具输出要可遗忘Harness 需要知道哪些结果能重放哪些不能碰缓存语义要协议化仅靠客户端改数组无法兼顾清理和 Prompt Cache多执行线要保守main thread 和 sub-agent 的缓存视图不能随意交叉编辑thinking 也要治理历史思考过程不是免费资产长期保留会变成成本这里最值得关注的是第三点。cache_edits这种字段说明未来 Agent Harness 和模型 API 的关系会越来越深。API 不再只是接收messages并返回文本它需要理解更多运行时意图哪些内容可以遗忘哪些缓存段可以局部失效哪些工具结果可以按 TTL 分层甚至哪些信息由模型自己标记为“几轮之后可丢”。现在的cache_edits还很克制只处理tool_result和thinking block这类相对安全的位置。但方向已经很清楚Agent 上下文管理不会只停留在 prompt 拼接层它会逐步变成 Harness、工具系统和模型服务之间的协议设计问题。结语Claude Code 的三层compact机制本质上是在回答一个朴素问题长程 Agent 任务里哪些历史必须留下哪些只是在占地方fullcompact用模型摘要兜底能力强但贵且不稳定。autocompact用本地 notes 承接中等压力避免过早进入高成本路径。Microcompact最不起眼却跑得最频繁每次请求前都扫一遍旧工具结果能用cache_edits就走服务端缓存编辑cache 已经过期就直接本地瘦身。这套设计最聪明的地方不是“压缩得多”而是把不同成本的手段放在了正确的位置。热路径上只允许便宜、稳定、可解释的规则昂贵的 LLM 摘要被推到最后Prompt Cache 的 90% 折扣则通过协议级编辑尽量保住。如果说早期 Harness 只是把 ReAct loop、tool calling 和 messages 管起来那么Microcompact展示的是下一阶段的能力Harness 开始参与模型运行时协议本身。它不只是问模型“下一步做什么”也在告诉 API“这些历史已经可以忘了但别把我的缓存一起忘掉。”