使用社交账号登录
本文分析的是 Claude Code 的上下文压缩机制——对话历史过长时,如何缩减已存在于上下文中的消息。
需要区分两个完全不同的阶段:
| 阶段 | 名称 | 作用 |
|---|---|---|
| 工具结果处理 | Tool Result Budget | 工具执行后、结果进入上下文前,控制单次输出大小 |
| 上下文压缩管线 | Compaction Pipeline | 上下文已存在后,在每轮迭代中释放 token 空间 |
长时间对话中,messages 数组会被逐渐填满:用户和模型的多轮对话、工具执行结果、附件注入(技能、记忆、文件状态)。当接近 LLM 的上下文上限(如 200K tokens)时,必须有机制在保留关键信息的前提下释放空间。
注意:第一级 Snip 是 Anthropic 内部特性,外部不可用。Context Collapse 受 feature flag 控制,目前对外部用户禁用。 外部用户实际运行的是两级管线:Microcompact → Autocompact。
执行顺序(每次向 API 发请求之前,按固定顺序执行):
工具结果预算 → Snip → Microcompact → Context Collapse → Autocompact → 发 API 请求
核心纠偏:Snip 不是系统按阈值自动截断头部、保留尾部 N 条的机制。 它是让模型自己用工具决定删哪些消息——系统只负责给消息打 ID 标签、在合适时机提醒模型、以及执行模型下达的删除指令。
可用性:这是 Anthropic 内部特性,外部公开版本的 Claude Code 不包含。
Snip 要解决一个问题:对话历史里有些消息彻底没用了(已完成的中间步骤、读过的文件内容),但系统不知道哪些是"没用的",只有模型自己知道。
解决方案:让模型主动"扔掉"它认为没用的消息。具体分三步:
每次给 API 发消息之前,系统会在特定用户消息的末尾追加一个短 ID 标签:
\n[id:k8f3z1]
打标签的前提条件:消息顶层 content 里存在 type: 'text' 的 block。满足这个条件的才打,不满足的跳过。
这个条件决定了实际上只有用户的真实输入(用户打的字)才会被打上 ID。原因是:
哪些消息有 ID:
| 消息类型 | 有 ID 标签 | 原因 |
|---|---|---|
| 用户真实输入 | ✓ | 顶层有 text block |
| tool_result 消息 | ✗ | 合并后顶层无 text block |
| 系统注入的元消息(nudge、附件) | ✗ | isMeta:true,直接跳过 |
| 助手消息 | ✗ | 不在遍历范围 |
追加位置:
发给 API 的消息示例:
ID 的生成:由消息内部 UUID 确定性派生(UUID 前 10 位十六进制 → 转 base36 → 取前 6 位)。同一条消息每次生成的 ID 始终相同,不单独存储,系统收到短码后反向计算找对应消息。
ID 打在用户真实输入上,不是偶然,而是设计意图:ID 代表的是一个"用户输入轮次"——从这条用户输入开始、到下一条用户输入之前的整段对话(包括这期间所有的 assistant 回复、tooluse、toolresult)。模型传入一个 ID,系统删除的是这整个段落,不是单条消息。这样 tooluse/toolresult 始终成对,不会产生孤儿。
系统不会主动替模型做删除决策,但会在适当时机提醒模型"可以删了"。
触发条件:距上一次 snip 执行、compact 执行、或上一次提醒,上下文增长了约 10K tokens 时触发一次提醒。
机制:以 <system-reminder> 包裹注入到当前轮次尾部,不破坏历史消息的前缀缓存。
NUDGE_TEXT 大意(闭源):
模型决定删消息时,以工具调用的形式发出请求:
系统收到后执行删除,并向磁盘的会话日志里追加一条内部标记记录(记录本次删了哪些消息的 UUID),用于会话恢复时重放删除操作。这条记录不会出现在发给 API 的 messages 数组里。
删除前,发给 API 的 messages(注意:tool_result 消息没有 ID):
snip 的删除粒度是整个用户输入轮次:模型传入 [id:aa1bb2],系统删除从这条用户输入开始、到下一条用户输入之前的所有消息——包括其间的 assistant 回复、tooluse、toolresult。
模型调用 snip(["aa1bb2"]) 后:
[assistant] "实现如下:..."
[user] "继续写测试" ← [id:gg7hh8]
关键特点:
| 情况 | 能否删 | 说明 |
|---|---|---|
| 用户真实输入 | 能 | 顶层有 text block,会打 ID 标签 |
| tool_result 消息 | 不能直接引用 | 合并后顶层无 text block,不打 ID |
| 系统注入的元消息(nudge、附件) | 不能 | isMeta:true,跳过打标签 |
| 助手消息(包括 tool_use) | 不能 | 不在打标签范围 |
| 当前轮次最近的助手消息 | 不能 | 系统保护(protected tail) |
消息历史以 append-only 的方式存在磁盘上,被 snip 掉的消息记录仍然在磁盘里,不会被真正删除。
当用户重新打开会话时,系统读取磁盘上的 boundary 记录,重放删除操作:将记录的 removed_uuids 对应的消息从内存链表里摘除,并修复链表指针。
为什么需要修复指针:消息之间用 parentUuid 形成链表。被删除消息的后继消息的 parent 指针指向了已删除的节点,如果不修复,链在 gap 处断开。
修复方式:对每个 parent 指向已删除节点的消息,沿着已删除节点的 parent 链往前回溯,直到找到第一个未被删除的祖先,重新指向它。
| Snip | Autocompact | |
|---|---|---|
| 谁决定删什么 | 模型,调工具指定 ID | 系统,按时间顺序截断头部 |
| 删除位置 | 任意位置,可以删中间段 | 只截头部,保留最近的尾部 |
| 删后是否有替代 | 没有,直接消失 | 有,LLM 生成摘要替换 |
| 触发方式 | 系统提醒 + 模型主动 | token 超阈值,系统强制 |
| 适用场景 | 精准清理已完成的中间步骤 | 上下文即将溢出时大规模压缩 |
Microcompact 的核心目标是清除已经没有价值的工具调用结果,释放 token 空间。不同于 Autocompact 用 LLM 生成摘要,Microcompact 不调用 LLM——它直接把旧的工具结果内容替换成占位符,或者通过 API 在服务端缓存里删除。
有三种独立的机制,各自独立触发、互不排斥,同一轮迭代中可能有多种同时生效:
场景:用户暂时离开,过一段时间回来继续工作。
触发条件:距上一条助手消息超过 60 分钟(可由后台配置调整,功能默认关闭)。
60 分钟这个值对应 Anthropic 提示词缓存的 TTL——普通缓存 5 分钟,开启 extended cache 后是 1 小时。超过缓存有效期后,旧的工具结果内容已经无法命中缓存,继续带着它们只是白白占用 token,不如直接清掉。所以触发阈值与 extended cache TTL 保持一致。
做什么:把历史工具结果的内容替换成占位符,只保留最近 5 个工具结果的真实内容。
被清理的工具只限于这几类:Read 文件、Bash/Shell、Grep、Glob、WebSearch、WebFetch、FileEdit、FileWrite。
变形前后(发给 API 的 messages):
对缓存的影响:修改消息内容会破坏服务端的提示词缓存前缀——这是预期行为,因为用户已经离开很久,缓存本身已过期,释放 token 空间更重要。
场景:活跃会话中,工具调用越积越多,但用户还在持续工作,不希望破坏缓存。
触发条件:工具调用数量达到某个阈值(后台动态配置,feature flag CACHED_MICROCOMPACT,ant-only)。
做什么:利用 Anthropic API 的 cache_edits 特性,在服务端缓存里原地删除旧的工具结果——本地 messages 数组内容不变,但服务端在处理请求时不再看到那些被删掉的工具结果内容。
关键设计:本地不动,服务端删。这样前缀缓存前面的部分完全不变,缓存继续命中,只是旧工具结果在服务端被"剪掉"了。
API 层面的体现:每次 API 请求的最后一条 user message 里会附带一个 cache_edits block:
服务端处理后,响应里会返回 cache_deleted_input_tokens,告知本次删了多少 token。这个 cache_edits block 在后续每轮请求里都要重新发送,以保持服务端缓存状态一致。
这是 Anthropic API 提供的原生上下文管理能力——在 API 请求参数里直接声明清理策略,服务端按策略自动执行,客户端不需要修改 messages 数组。
与前两种机制的关系:这不是 Claude Code 自己实现的逻辑,而是 Anthropic API 的服务端能力。Claude Code 只是在请求参数里声明策略,由 API 服务端执行。三种机制各自独立,可以同时生效——机制一改本地 messages,机制二操作服务端缓存,机制三由 API 服务端自行执行。
两类策略:
① 清理思维链(thinking blocks):适用于开启了 extended thinking 的会话,把历史轮次的 thinking block 从服务端上下文里清除。外部用户也可用。
正常情况下保留所有 thinking:
空闲超过 1 小时(缓存已过期)时,改为只保留最后 1 个 thinking turn:
② 清理工具结果(ant-only):仅 Anthropic 内部用户可用。
触发条件:input_tokens >= 180K,目标保留 40K token,即每次清掉约 140K。
整个 context_management 配置作为请求体的顶层参数发给 API,服务端看到后按策略执行,响应里体现为 token 数减少,本地 messages 不变。
| 基于时间 | 缓存编辑 | API Context Management | |
|---|---|---|---|
| 触发条件 | 离开 60 分钟 | 工具数量达阈值 | input tokens ≥ 180K(工具清理 ant-only) |
| 本地 messages 改动 | 是(内容替换) | 否 | 否 |
| 替换文字 | [Old tool result content cleared] | — | — |
| 执行位置 | 客户端 | 服务端缓存层 | 服务端 API 层 |
| 缓存影响 | 破坏前缀缓存(预期) | 保持前缀缓存 | API 层管理 |
| 外部用户可用 | 是(功能默认关闭) | ant-only | thinking 清理可用;工具清理 ant-only |
可用性:受 feature flag
CONTEXT_COLLAPSE(环境变量CLAUDE_CONTEXT_COLLAPSE)控制,目前对外部用户禁用。启用后会完全禁用 Autocompact(防止两者竞争)。
Context Collapse 这个名字在 Claude Code 里有两层含义,经常被混淆:
第一层:UI 显示折叠——把连续的 Read/Grep/Glob 工具调用在界面上折叠成一行,纯视觉效果,messages 数组不变。
❯ Searched 8 patterns, read 12 files, listed 3 directories
第二层:上下文投影压缩——本章讨论的核心机制。把已完成的工具调用组生成简洁摘要,在发 API 之前通过投影减少实际发出去的 token。
与 Autocompact 的"写时替换"不同,Context Collapse 采用读时投影:
系统把连续的"搜索/读取类"工具调用识别为一个可折叠序列。
可折叠的工具:
序列断开条件——遇到以下任一情况,当前序列结束:
序列一旦被识别,就进入暂存队列,等待后续提交。暂存本身不改变任何东西——消息照常发给 API。
Context Collapse 有两个阈值(基于有效窗口 180K):
| 上下文利用率 | 对应 token 数(200K 窗口) | 动作 | 是否调 LLM |
|---|---|---|---|
| 90% | 162K | 提交所有已暂存序列,规则生成摘要 | 否 |
| 95% | 171K | 派生子 agent,LLM 生成更高质量摘要 | 是(阻塞) |
90% vs 95% 的区别:
"Searched 2 patterns, read 1 file" 这样的计数摘要。"用户在调试认证模块,搜索了 handleAuth 和 middleware 相关代码,读取了 auth.go 全文")。压缩比更高,但需要 LLM 调用。两者的压缩范围相同——都只替换可折叠的工具调用序列,不动对话文本本身。这是和 Autocompact(全量压缩整个对话历史)的本质区别。
167K(autocompact 阈值)在 Collapse 开启时被跳过:因为 Collapse 在 90%(162K)就开始管理上下文,如果 autocompact 在 93%(167K)又介入,两者会打架——collapse 刚提交完降下来,autocompact 又全量压缩掉,collapse 保留的原始消息全白费。所以启用 collapse 时直接禁用 autocompact 的主动触发。
完整生命周期示例:
90% commit(规则摘要)→ 95% ctx-agent(LLM 摘要)→ 413 drain → reactive compact(兜底)
每一级比上一级更重、更贵,但也能处理更极端的情况。前两级只折叠工具序列不动对话文本;reactive compact 作为最终兜底,等同于全量 autocompact,会把整个历史替换为结构化摘要。
变形前(原始 messages,6 条):
投影后(发给 API 的 messages,1 条摘要替换了中间 6 条):
原始消息保留在磁盘上不动,投影是每次发请求时实时计算的。
归档信息以 append-only 的方式持久化到磁盘:
Session resume 时,系统重放 commit 记录,重建投影——因为投影是确定性的,同样的 commit 记录总能产生同样的视图。
与 compact boundary 的交互:如果 reactive compact 最终触发并写入 boundary marker,之前所有的 collapse commit 和 snapshot 记录被逻辑废弃(因为它们引用的消息已经在 boundary 之前)。
禁用的是"主动 autocompact",不是"被动 reactive compact"。
具体来说:
原因:Context Collapse 在 90% 就开始工作,主动 autocompact 在 92.8%(167K/180K)触发,两者阈值太近会竞争——collapse 刚提交完降下来,autocompact 又检测到超阈值想全量压缩,造成混乱。所以主动路径二选一。但 413 兜底必须保留,否则 collapse 无法处理的场景(全是不可折叠的对话文本)会导致会话卡死。
Context Collapse 的优劣势:
当 messages 数组的 token 数接近模型上限时,系统自动把前面的历史压缩成一条结构化摘要,释放空间让对话继续。
Autocompact 的检查点在每轮迭代开始、向 API 发请求之前,不是实时监控 token 数。
具体场景举例(200K 窗口,触发点 167K):
每次向 API 发请求之前都会检查,包括模型在任务进行中发出的每一次工具调用请求。所以压缩可以在任务还在跑时触发,不需要等用户发新消息:
各阈值的含义(200K 窗口示例):
| 阈值 | 数值 | 含义 |
|---|---|---|
| 有效窗口 | 180K | 原始 200K 减去 20K 摘要输出预留 |
| autocompact 触发 | 167K | 有效窗口再减 13K 提前量 |
| blocking limit | 177K | 有效窗口减 3K,超过此处请求被拒 |
20K 摘要输出预留:压缩时要调用子模型生成摘要,子模型需要输出 token 空间。实测摘要 p99.99 输出是 17,387 token,预留 20K。如果不留这个空间,压缩时子模型会因为输出空间不足而失败。
13K 提前量:messages 的 token 估算不是精确值,可能比实际偏低;提前 13K 触发确保压缩后有足够空间继续工作,不会压完立刻又超。
3K blocking 缓冲:autocompact 关闭或失败时,还有 3K 空间让用户手动执行 /compact。
有两条触发路径:
路径一:主动检查(每轮迭代开始,如上所述)
路径二:被动触发(API 返回 413 之后) 主动检查没触发,但发出去的请求被 API 拒绝(prompt too long)。这时走 Reactive Compact:
触发后,当前对话立即暂停,系统 fork 出一个子模型来生成摘要,主对话等待子模型完成后才继续。整个过程是同步的。
子模型收到什么:当前完整的 messages 数组(图片被剥离),加上一条 user message 形式的摘要 prompt(见 5.3)。
子模型只跑 1 轮,不允许调用任何工具,产出摘要文本后退出。
压缩后 messages 变成(全量替换,没有任何原始消息保留):
然后用这个新的 messages 数组继续当前轮次,向 API 发请求。用户还没发新消息,模型继续完成手头的任务。
注意:autocompact 是全量压缩,触发时的 tool_result 内容被压进摘要里,不会单独保留在外面。
子模型收到的最后一条 user message 内容(不是 system prompt):
子模型产出摘要后,系统剥离 <analysis> 部分(只是生成时的草稿),把 <summary> 里的内容格式化成上面 5.2 里的那条 user message。
压缩把所有历史消息替换成了摘要,模型因此"忘记"了之前读过的文件内容。系统需要把关键状态重新注入。
恢复策略是按预算重新注入:
文件选择优先级:按最近读取时间排序,超出预算的直接丢弃(不截断)。
层级一:PTL Retry(compact 内部自救)
compact 把当前所有消息发给子模型生成摘要,但如果消息本身太大,连这个请求也超限(prompt too long),compact 会截掉最旧的消息重试。
消息分组方式:按 API round 分组——以 assistant 消息的 response ID 为边界,每次 API 调用对应一个 group。tooluse 和对应的 toolresult 在同一个 group 里,配对完整。
裁切逻辑:
[earlier conversation truncated for compaction retry]截断是有损的——被丢掉的 group 里的历史不会出现在摘要里,但至少能解除卡死。3 次都失败则抛出错误,走外层熔断器。
层级二:熔断器(circuit breaker)
compact 抛出错误时,外层记录失败次数,成功则重置为 0。连续失败 3 次后,后续轮次直接跳过 compact,不再尝试。
失败后当前轮次不中断,用原来未压缩的 messages 继续发请求。
这个机制因真实事故而加入:曾有 1279 个会话连续失败 50 次以上(最高 3272 次),每天全球浪费约 25 万次 API 调用。
层级三:blocking limit 兜底
熔断后 compact 不再触发,但 token 继续累积。到 177K(有效窗口 - 3K)时,系统拒绝发 API 请求,对话停止,需要用户手动 /compact 或放弃会话。
这 3K 缓冲就是留给用户手动操作的空间。
每次向 API 发请求之前,系统按固定顺序依次执行各压缩机制。以下用一个具体场景走一遍完整流程。
场景:用户让模型分析一个项目,模型连续执行了多轮工具调用,累计消耗了 170K token。
本轮迭代开始(准备向 API 发下一次请求):
Step 1:工具结果预算控制
检查刚产生的工具结果大小。单条超 50K 字符的,把内容存到磁盘,消息里换成"已保存到 /path/to/file";所有工具结果合计超 200K 字符的,选最大的几条替换,直到总量达标。
这步不是压缩,是防止单次工具调用输出把窗口撑爆。
Step 2:Snip(ant-only,外部用户跳过)
如果 Snip 功能启用,模型可能在上一轮调用了 snip 工具指定删除某些历史 user input 轮次。这里执行那些删除,更新 messages 数组。
Step 3:Microcompact
检查是否满足微压缩条件:
[Old tool result content cleared],只保留最近 5 个cache_edits 在服务端缓存里删除旧工具结果,本地 messages 不变Step 4:Context Collapse(feature flag 控制,外部用户跳过)
如果 Context Collapse 启用:
Step 5:Autocompact 检查(Context Collapse 启用时跳过此步)
检查当前 messages token 数是否超过触发阈值(167K)。
本场景是 170K > 167K,触发 Autocompact:
<analysis> 草稿块如果 Autocompact 不触发(token 未超阈值):直接向 API 发请求。
整体顺序总结:
工具结果预算 → Snip → Microcompact → Context Collapse → Autocompact → 发 API 请求
注意:Context Collapse 和主动 Autocompact 是互斥的(启用 collapse 则跳过 autocompact 的主动检查),但 API 返回 413 时的 reactive compact 仍然可用作最终兜底。
前几步越来越重,目的是尽量在轻量级机制就把 token 降下来,只有前几步都不够用时才走全量 Autocompact。Autocompact 成功后,后续轮次从约 60K 重新开始积累,直到再次触发。
| 阶段 | 触发条件 | 成本 | 信息保留 |
|---|---|---|---|
| 工具结果预算 | 工具执行完成 | 极低(即时) | 预览 + 磁盘路径 |
| Snip | 每 ~10K token nudge + 模型自主 | 极低(无LLM) | 中间段直接消失,无占位 |
| Microcompact | 时间/数量/cache | 低(即时/API) | 占位文本标记 |
| Context Collapse | 上下文利用率 90%(162K)/ 95%(171K) | 中(读时投影;95% 时调 LLM) | 摘要 + 原始留存 |
| Autocompact | 达到 167K 阈值 | 高(LLM 调用) | 结构化摘要 |
关键原则:
"Once a tool result is seen by the model (whether full content or replacement preview), this decision must remain consistent across all subsequent API calls."
实现为三态分区:
连续失败 3 次后跳过 compact,不再尝试。
原则:宁可让用户手动执行 /compact,也不要用注定失败的重试浪费 API 预算。
压缩后的恢复不是"越多越好",而是严格的预算分配:
| 恢复项 | 预算 |
|---|---|
| 文件恢复 | 50K(最多 5 个文件,单文件 5K) |
| 技能恢复 | 25K(单个 skill 截断到 5K) |
| 其他附件 | 无单独预算(计划、Delta 等直接注入) |
总计约 75K,为后续对话留出 100K+ 空间。
注意:这不是上下文压缩的一部分,而是工具执行后的结果处理阶段。放在这里是为了与压缩管线做明确区分。
| 单工具结果限制 | 单消息聚合限制 | |
|---|---|---|
| 阈值 | 50K 字符 | 200K 字符 |
| 说明 | 单个工具输出超过则持久化到磁盘文件 | 一轮并行工具调用的结果总和限制 |
| 持久化后模型看到 | "Output too large (82.3 KB). Full output saved to: /path/to/..." + 2KB 预览 | 按大小降序选择最大的结果进行持久化,直到剩余总量低于预算 |
200K 字符限制是按消息组计算的。分组方式:同一轮次的所有并行 tool_result 被视为一个组。
分组边界由 assistant 消息的 ID 决定——同一轮 API 调用(同一个 assistant message ID)产生的所有 tooluse 对应的 toolresult,属于同一个组。
从轻到重,从便宜到昂贵 →
┌────────────┬────────────┬───────────────┬──────────────┐
│ 第一级 │ 第二级 │ 第三级 │ 第四级 │
│ Snip │ Micro- │ Context │ Autocompact │
│ (模型点删) │ compact │ Collapse │ (全量摘要) │
│ │ (微压缩) │ (折叠) │ │
├────────────┼────────────┼───────────────┼──────────────┤
│ 模型调工具 │ 清除/删除 │ 读时投影 │ LLM生成摘要 │
│ 精准删中间 │ 工具结果 │ 归档旧消息 │ 替换整个历史 │
│ 任意消息 │ │ │ │
├────────────┼────────────┼───────────────┼──────────────┤
│ ant-only │ 无LLM调用 │ 无LLM调用 │ 需要LLM调用 │
│ │ 即时执行 │ 即时执行 │ 成本最高 │
└────────────┴────────────┴───────────────┴──────────────┘
{
"role": "user",
"content": "帮我实现登录功能\n[id:aa1bb2]"
}
Context is growing. You can use the snip tool to remove messages
you no longer need. Call snip(["<id1>", "<id2>"]) with the
[id:xxx] tags you see on user messages.
{
"type": "tool_use",
"name": "snip",
"input": {
"message_ids": ["k8f3z1", "m2x9p4", "r7qw12"]
}
}
[user] "帮我实现登录功能" ← 有 [id:aa1bb2],用户真实输入
[assistant] "好的,先读一下配置文件"
tool_use: read(config.json)
[user] tool_result: "{...}" ← 无 ID
[assistant] "再读一下 auth 模块"
tool_use: read(auth.go)
[user] tool_result: "func Login..." ← 无 ID
[assistant] "实现如下:..."
[user] "继续写测试" ← 有 [id:gg7hh8],用户真实输入
删前链:A → B → C → D → E → F
删除: {B, C}
D 的 parent 指向 C(已删除)
→ 回溯:C.parent = B(已删除)→ B.parent = A(存活)
→ 修复 D.parent = A
结果链:A → D → E → F ✓
变形前:
[assistant] tool_use: read(config.json)
[user] tool_result: "const config = { host: 'localhost', port: 3000 }" ← 早期结果
[assistant] tool_use: read(auth.go)
[user] tool_result: "func Login(w http.ResponseWriter, r *http.Request) { ... }"
... 中间还有很多轮 ...
[assistant] tool_use: read(main.go) ← 最近 5 个之内
[user] tool_result: "package main..." ← 保留
变形后:
[assistant] tool_use: read(config.json)
[user] tool_result: "[Old tool result content cleared]" ← 内容被替换
[assistant] tool_use: read(auth.go)
[user] tool_result: "[Old tool result content cleared]" ← 内容被替换
...
[assistant] tool_use: read(main.go)
[user] tool_result: "package main..." ← 保留
{
"role": "user",
"content": [
{ "type": "tool_result", "tool_use_id": "toolu_latest", "content": "..." },
{
"type": "cache_edits",
"edits": [
{ "type": "delete", "cache_reference": "toolu_01" },
{ "type": "delete", "cache_reference": "toolu_02" }
]
}
]
}
{
"type": "clear_thinking_20251015",
"keep": "all"
}
{
"type": "clear_thinking_20251015",
"keep": { "type": "thinking_turns", "value": 1 }
}
{
"type": "clear_tool_uses_20250919",
"trigger": { "type": "input_tokens", "value": 180000 },
"clear_at_least": { "type": "input_tokens", "value": 140000 },
"clear_tool_inputs": ["bash", "glob", "grep", "read", "web_fetch", "web_search"]
}
对话进行中,工具调用不断累积...
────── 第 1 阶段:正常积累 ──────
轮 1~20:token 从 0 增长到 100K
- 期间产生了 3 个可折叠序列(连续 grep/read 调用)
- 这 3 个序列进入暂存队列
- 发给 API 的 messages 不变(暂存不影响任何东西)
────── 第 2 阶段:达到 90%(162K),首次提交 ──────
轮 21:token 达到 162K
- 一次性提交暂存队列中的所有 3 个序列
- 规则生成摘要(不调 LLM,即时完成)
- 下次投影:3 组 tool_use+tool_result 被替换为 3 条计数摘要
- 投影后 token 降到约 140K
────── 第 3 阶段:继续工作,再次积累 ──────
轮 22~40:模型继续工作,产生新的可折叠序列
- 新序列进入暂存队列
- token 再次增长
- 再次到 162K → 再次提交 → 再次降下来(可循环多次)
────── 第 4 阶段:折叠不够用,达到 95%(171K) ──────
对话文本本身膨胀(不可折叠),90% 提交后降不了多少
→ token 涨到 171K
→ 阻塞主对话,派生子 agent
→ 子 agent 用 LLM 重新生成所有已提交序列的摘要(语义级,更紧凑)
→ 投影后 token 降下来,主对话恢复
────── 第 5 阶段:兜底降级 ──────
如果 95% 之后仍然不够:
→ API 返回 413
→ Drain(强制提交所有暂存序列 + 重新投影)
→ 重试
→ 仍然 413(没有可折叠的了)
→ Reactive Compact(全量 LLM 压缩,最终兜底)
[assistant] thinking: "我需要搜索几个文件来理解项目结构"
[assistant] tool_use: grep("handleAuth")
[user] tool_result: "src/auth.go:42: func handleAuth..."(200 行匹配结果)
[assistant] tool_use: grep("middleware")
[user] tool_result: "src/middleware.go:15: func Apply..."(150 行)
[assistant] tool_use: read("src/auth.go")
[user] tool_result: "package auth\n\nimport (...)\n\nfunc handleAuth..."(500 行完整文件)
[assistant] "根据分析,auth 模块的结构如下..."
[assistant] thinking: "我需要搜索几个文件来理解项目结构"
[user] "Searched 2 patterns, read 1 file" ← 合成摘要
[assistant] "根据分析,auth 模块的结构如下..."
用户发来消息
↓
[每轮迭代开始]
→ snip(如启用)
→ microcompact
→ context collapse(如启用)
→ autocompact 检查 ← 在这里
→ 向 API 发请求
→ 接收流式响应
→ 执行工具调用
[回到下一轮迭代开始]
用户 input:「帮我分析这个项目」
轮1:发请求 → 模型 tool_use(read A) → 执行 → tool_result(累计 120K)
轮2:检查 120K < 167K → 发请求 → 模型 tool_use(read B) → 执行 → tool_result(累计 155K)
轮3:检查 155K < 167K → 发请求 → 模型 tool_use(read C) → 执行 → tool_result(累计 170K)
轮4:检查 170K > 167K → 触发 compact → 压完(变成 60K)→ 发请求 → 模型继续工作
[user] "This session is being continued from a previous conversation...
Summary:
1. Primary Request and Intent
...
8. Current Work
正在分析工具注册机制,刚读完 tools.go(包含 tool_result 里的内容)
9. Optional Next Step
..."
[attachment] 最近读过的文件(最多 5 个,总预算 50K token)
[attachment] plan 文件(如果有)
[attachment] invoked skills(如果有)
CRITICAL: Respond with TEXT ONLY. Do NOT call any tools.
If you call any tools, the conversation cannot continue.
You have one turn to provide a summary. Failure to provide a summary
in this turn means the conversation cannot continue.
Your response MUST start with <analysis> tags.
Before providing your final summary, wrap your analysis in <analysis> tags,
then provide your summary. Go through the conversation messages chronologically,
tracking what was asked, what decisions were made, and what errors were fixed.
Pay special attention to user feedback and explicit corrections.
Your summary must include these nine sections:
1. Primary Request and Intent — all explicit requirements the user stated
2. Key Technical Concepts
3. Files and Code Sections — enumerate specific files examined/modified,
include full code snippets where applicable
4. Errors and fixes
5. Problem Solving — steps taken in chronological order
6. All user messages (non tool-results)
7. Pending Tasks
8. Current Work — detailed description of the most recent work
9. Optional Next Step — must be DIRECTLY in line with the user's most
recent explicit requests
REMINDER: Do NOT call any tools. Respond with plain text only.
group 0: [user("帮我分析这个项目")]
group 1: [assistant(含 tool_use_A), tool_result_A]
group 2: [assistant(含 tool_use_B), tool_result_B]
group 3: [assistant, ...]
工具执行完成
↓
[返回原始工具结果]
↓
预算检查 ← 在这里拦截
↓
决定:原样返回 / 持久化到磁盘并返回预览
↓
消息进入 messages 数组
↓
下一轮迭代可能触发压缩管线(Snip/Microcompact/...)
完整的消息生命周期:
工具执行 → 工具结果预算处理 → 消息进入 messages → 上下文压缩管线 → API 调用
关键区别:
- 工具结果预算处理的是"原始工具输出",决定它能否完整进入上下文
- 上下文压缩处理的是"已经在上下文中的消息",决定如何缩减它们