Context 的生死抉择:四层压缩、截断算法与 Session Memory
《Claude Code 架构解密》精读笔记 · 第11篇 · 第7章前半(7.1-7.4)
Agent 系统的灵魂不在算法,而在上下文------模型"知道"什么,决定了它能"做"什么。但上下文窗口是有限的,Agent 的对话却是无限的。本篇解析 Claude Code 如何在"有限窗口"与"无限对话"之间做出生死抉择:能不压就不压,能少压就少压,必须压就精准压。
一、导语:一个"简单"请求就吃掉 44K Token
用户说"帮我重构这个模块的错误处理",一次交互的 token 消耗:
| 操作 | 消耗 |
|---|---|
| Agent 调用 Glob 搜索相关文件 | ~2K |
| Agent 调用 Read 读取 5 个文件 | ~15K |
| Agent 调用 Grep 搜索错误模式 | ~3K |
| Agent 思考并制定方案 | ~2K |
| Agent 调用 Edit 修改 3 个文件 | ~6K |
| Agent 调用 Bash 运行测试 | ~4K |
| 测试失败,读取错误日志 | ~3K |
| 修正代码并重新测试 | ~8K |
| 汇报结果 | ~1K |
| 合计 | ~44K |
一个"简单"请求,一个来回就吃掉 44K tokens。十几轮交互后,200K 窗口捉襟见肘。
这背后有三个 Agent 特有的矛盾:
- 工具结果的信息密度问题:Read 一个 500 行文件,Agent 可能只关心 10 行,但 500 行全进上下文
- 历史的不可丢弃性:Agent 之前的文件修改、测试结果、发现的错误------是后续决策的关键依据,盲目丢弃导致"失忆"
- Token 计费的双重压力:上下文越长,每次 API 调用越贵,响应越慢
Claude Code 的设计哲学:能不压就不压,能少压就少压,必须压就精准压。这就是四层递进压缩策略的核心思想。
二、架构图解:12 个文件、3900 行代码的压缩体系
整个压缩系统由 12 个文件、约 3900 行代码组成,形成清晰的调用层级:
调用入口:query loop · /compact · REPL · swarm
│
┌─────────┼──────────┐
▼ ▼ ▼
autoCompact.ts compact microCompact.ts
(351行) command (530行)
│ │
▼ ▼
自动触发决策 轻量级清理
│
├──── sessionMemoryCompact.ts (630行)
│ 零LLM成本压缩 ──→ 失败回退
│
└──── compact.ts (1705行)
核心压缩引擎
full / partial compact
│
┌─────────┼──────────┐
▼ ▼ ▼
prompt.ts grouping.ts postCompactCleanup.ts
提示词工程 消息分组 压缩后状态清理
核心设计原则:越往上层,成本越低、触发越频繁;越往下层,效果越强、代价越高。
三、核心点拆解
3.1 第一层:API 原生上下文管理(零成本)
直接利用 Anthropic API 的 context_management 参数,在服务端清理,本地不修改消息历史。
typescript
type ContextEditStrategy =
| { type: 'clear_tool_uses_20250919'
trigger?: { type: 'input_tokens'; value: number } // 180K 时触发
exclude_tools?: string[] }
| { type: 'clear_thinking_20251015' }
最巧妙的设计 :exclude_tools 区分了"只读工具"和"写操作工具"------
| 可清理结果(只读) | 不可清理(写操作) |
|---|---|
| Shell, Glob, Grep | FileEdit |
| FileRead, WebFetch | FileWrite |
| WebSearch | NotebookEdit |
核心原则:只清理"看过的东西",绝不清理"做过的事情"。如果清理了 Edit 的调用记录,Agent 就不知道自己改过这个文件------可能重复修改甚至做出矛盾操作。
优势 :完全免费(零额外 API 调用)。局限:只能清理工具结果和 thinking 块,无法做语义级压缩。
3.2 第二层:微压缩(零 LLM 成本)
当 API 原生管理不够用,本地做更精细的清理。两条互补路径:
路径 A:时间触发微压缩
利用 Prompt Cache TTL(~1 小时)的工程洞察:用户离开超过 60 分钟再回来,缓存已过期------此时清理旧工具结果不造成额外缓存未命中。
- 触发条件:距上次 assistant 消息 > 60 分钟
- 保留最近
keepRecent=5个工具结果 - 旧
tool_result替换为"[old tool result content cleared]" - 5 是经验值------Agent 最近 5 次工具调用最可能被后续引用
路径 B:缓存微压缩(Cached Microcompact)
更精巧:利用 API 的 cache_edits 能力,不修改本地消息,让服务端在下次请求时删除指定工具结果。
关键:为什么"不修改本地消息"?
→ Prompt Cache 的 key 是请求前缀
→ 修改消息历史中间部分 → 整个缓存失效 → 反而更贵
→ cache_edits 方案:告诉服务端"使用缓存时删除这些内容"
→ 既减少 token 消耗,又保持缓存前缀有效性
安全约束 :只有主线程可以注册 cached microcompact 状态。子 Agent 共享进程但生命周期不同,产生的 cache_edits 可能导致主对话丢失不该丢失的内容------跨生命周期状态污染是多 Agent 系统中最隐蔽的 bug 来源之一。
3.3 第三层:Session Memory 压缩(零 LLM 成本的创新)
最创新的设计。范式转变:从"紧急摘要"到"持续做笔记"------像一位尽职的会议记录员,持续将关键信息提取到结构化笔记中。
双阈值触发机制:
typescript
const shouldExtract =
(hasMetTokenThreshold && hasMetToolCallThreshold) || // 正常触发
(hasMetTokenThreshold && !hasToolCallsInLastTurn) // 自然断点触发
// 默认阈值:
// - 初始化阈值:10,000 tokens
// - 更新间隔:5,000 tokens + 3 次工具调用
为什么双阈值?单 token 阈值的问题:Agent 可能在大量工具调用中消耗了很多 token,但还没产生有意义的"决策结论"------此时提取只是"Agent 正在搜索文件"这样的过程信息。加入工具调用次数(3 次),等到 Agent 完成一组操作再提取。
9 章节结构化笔记模板:
| 章节 | 内容要求 | Token 限制 | 设计意图 |
|---|---|---|---|
| Session Title | 5-10 词的会话标题 | ~200 | 快速识别主题 |
| Current State | 当前进行中的工作 | ~2,000 | 恢复上下文最关键 |
| Task Specification | 需求与设计决策 | ~2,000 | 用户意图的持久记录 |
| Files and Functions | 关键文件概述 | ~2,000 | 代码导航的快速索引 |
| Workflow | 常用命令和工作流 | ~2,000 | 开发环境配置记忆 |
| Errors & Corrections | 错误修复记录 | ~2,000 | 避免重复踩坑 |
| Codebase Documentation | 系统组件文档 | ~2,000 | 项目架构理解 |
| Learnings | 方法总结与经验 | ~2,000 | 会话中学到的技巧 |
| Worklog | 工作日志 | ~2,000 | 按时间序的操作记录 |
| 总限制 | 12,000 |
三个深思熟虑的决策:
- 章节顺序即优先级:"Current State"最前(压缩恢复时"我正在做什么"最重要),"Worklog"最后(token 紧张时优先截断)
- 独立 Token 限制:防止某个章节挤占其他章节空间
- 为什么 9 个:9 × 2,000 ≈ 18K 上限------200K 窗口的 9%,实际消耗 6K-10K
工具权限隔离:Session Memory Agent 只能编辑指定的 memory 文件------不需要读文件、搜索代码、执行命令。即使行为偏离预期,也不影响用户环境。
并发控制 :sequential() 包装器确保单线程执行(防止两次提取基于相同旧笔记编辑导致覆盖);压缩前 await waitForSessionMemoryExtraction()(15 秒超时兜底)。
3.4 第四层:Full Compact(LLM 驱动的摘要生成)
最重的武器。compact.ts(1705 行),当前三层都无法满足时启动。
双路径摘要生成:
| 路径 | 机制 | 成本 |
|---|---|---|
| 优先:runForkedAgent | 复用主对话 system prompt → 命中 Prompt Cache;skipCacheWrite: true 只"蹭"缓存不"抢"缓存 |
~30% 折扣 |
| 回退:queryModelWithStreaming | 独立 API 调用;消息预处理(剥离图片+重注入附件);支持 2 次重试 | 全价 |
九段式摘要结构:
xml
<analysis> ← 思考草稿(后续被剥离)
...模型的分析过程...
</analysis>
<summary> ← 实际摘要(保留注入对话)
1. Primary Request and Intent
2. Key Technical Concepts
3. Files and Code Sections
4. Errors and fixes
5. Problem Solving
6. All user messages
7. Pending Tasks
8. Current work
9. Optional Next Step
</summary>
<analysis> 块的 Prompt 工程技巧:让模型先"思考"再"总结"(Scratchpad Thinking 效应)------分析阶段自由推理,最终只有 <summary> 保留在对话历史中,既提升质量又节省空间。
防工具调用防线 :Forked Agent 继承了完整工具集,但压缩 Agent 只需输出文本。Prompt 包含严厉禁令:"CRITICAL: Respond with TEXT ONLY. Do NOT call any tools."------即便如此,Sonnet 4.6+ 仍有约 2.79% 的概率尝试工具调用,所以还需 maxTurns: 1 兜底。
3.5 自动压缩决策引擎
autoCompact.ts(351 行)------四层压缩的"指挥官"。
阈值计算:
有效窗口 = 模型上下文窗口 - maxOutputTokens
自动压缩阈值 = 有效窗口 - 13,000 (AUTOCOMPACT_BUFFER_TOKENS)
13K 缓冲区:确保触发压缩到完成之间的"真空期"内,用户继续发消息不会撞上硬限制。
五层防护:防止递归压缩和竞争条件
| 检查 | 机制 | 目的 |
|---|---|---|
| 1. querySource 递归保护 | 'session_memory' / 'compact' → 直接返回 false |
压缩操作触发新压缩→无限递归 |
| 2. 功能开关 | DISABLE_COMPACT / DISABLE_AUTO_COMPACT / userConfig |
支持禁用 |
| 3. Reactive Compact 互斥 | 响应式压缩已启用→主动压缩退让 | 避免两种策略同时操作 |
| 4. Context Collapse 互斥 | 实验性策略互斥 | 避免竞争 |
| 5. 熔断器 | 连续失败 3 次→静默停止 | 防止无限浪费 API 调用 |
压缩策略优先级:Session Memory Compact(零 LLM 成本,最优选)→ 失败降级到 Full Compact(LLM 驱动,兜底)→ 再失败递增 consecutiveFailures 触发熔断器。
3.6 消息分组算法:在正确的地方"切一刀"
核心问题 :消息不能一条一条丢弃------它们之间存在语义依赖。tool_use 和 tool_result 必须成对出现,否则 API 直接报错。
旧方案失败:按"人类输入"分组在 Agent 场景崩溃------单轮 Agentic 场景中,用户只发一条消息,Agent 执行数百次工具调用,所有消息都在同一组中无法分割。
新方案 (grouping.ts,63 行):用 assistant.message.id 变化作为分组边界------同一 API 响应的流式块共享 message.id,新 message.id 出现 = 新的请求-响应周期。
PTL 重试的头部丢弃策略:
当对话历史膨胀到连压缩请求都放不下(Prompt Too Long),Claude Code 从最旧的消息组开始丢弃:
| 模式 | 策略 | 适用场景 |
|---|---|---|
| 精确模式 | 根据 API 告知的 tokenGap,累加丢弃直到释放足够 token |
API 返回具体超出量 |
| 粗暴模式 | 丢弃 20% 的组 | API 只返回泛化错误 |
不可避免的损失:被丢弃的旧消息组永久丢失,不出现在最终摘要中。这也是 Session Memory 如此重要------即使在消息被丢弃后,笔记中的记录仍然存在。
3.7 后压缩清理与附件重建
状态清理(postCompactCleanup.ts,77 行)------重置所有因压缩而失效的缓存:
| 无条件重置(所有压缩) | 仅主线程重置 |
|---|---|
| resetMicrocompactState | getUserContext.cache.clear |
| clearSystemPromptSections | resetGetMemoryFilesCache |
| clearClassifierApprovals | |
| clearSpeculativeChecks | |
| clearBetaTracingState | |
| clearSessionMessagesCache |
关键细节 :子 Agent 压缩时不能清理主线程状态(共享进程,清理会导致主线程行为不一致)。sentSkillNames 故意不重置------Skill 内容约 4K tokens,压缩间保持可避免重复注入。
附件重建 (并行 Promise.all):
| 附件类型 | Token 预算 | 作用 |
|---|---|---|
| 最近 5 个文件内容 | 50K(单文件 5K 上限) | 恢复工作上下文 |
| Skill 指令 | 25K(单 Skill 5K) | 恢复已调用 skill |
| 计划文件引用 | --- | 保持规划连续性 |
| 后台 Agent 状态 | --- | 恢复子任务追踪 |
| deferredTools/MCP delta | --- | 工具/指令变更增量 |
粗略估算 vs 精确计数 :使用 roughTokenCountEstimation(~4 字符/token)而非 tokenizer,接受小范围误差以换取延迟优化。
极简警告状态(compactWarningState.ts,18 行):
typescript
export const compactWarningStore = createStore<boolean>(false)
export function suppressCompactWarning() { compactWarningStore.setState(() => true) }
export function clearCompactWarningSuppression() { compactWarningStore.setState(() => false) }
3 行代码解决一个 UX 问题:压缩成功后到下一次 Token 计数更新之间的"真空期",旧估算值可能错误显示"上下文即将耗尽"警告------压缩成功后抑制,Token 更新后清除。
四、横向对比:上下文管理策略的范式差异
| 维度 | Claude Code | LangChain Memory | OpenAI Assistants |
|---|---|---|---|
| 核心哲学 | 四层递进,渐进加压 | 声明式 Memory 模块 | 服务端自动管理 |
| 压缩触发 | 主动+被动双路径 | 手动配置 | 不可控(黑盒) |
| 零成本层 | API 原生 + 微压缩 + Session Memory | 无 | 无(全靠服务端) |
| 摘要质量 | 9 段式结构化 | 自由格式 | 未知 |
| 用户控制 | /compact 手动触发 | Memory 模块选择 | 无 |
| 透明度 | 完全可见 | 部分可见 | 黑盒 |
Claude Code 的独特优势:
- Session Memory 零成本压缩------平时持续做笔记,压缩时零 LLM 调用
cache_edits不修改消息------保持 Prompt Cache 前缀有效- PTL 头部丢弃------极端场景下的生存策略
五、实战启示:三个可复用设计模式
模式一:渐进式压缩------成本从低到高的阶梯
四层递进不是一个"优化",而是一个架构决策:
- 每一层都是独立可选的,失败可降级
- 绝大多数场景在前三层就能解决,Full Compact 是最后的兜底
- "能不压就不压"不是偷懒,而是成本最优
模式二:后台 Agent 做笔记------从"紧急摘要"到"持续记录"
Session Memory 的范式转变值得推广:
- 传统方式:等到上下文快满了才紧急摘要------像考前突击
- 创新方式:后台持续记录------像上课做笔记
- 双阈值触发(token + 工具调用次数)确保提取时机合理
- 最小权限 Agent(只能编辑指定文件)确保安全隔离
模式三:消息分组------语义完整性是第一约束
"切一刀"不是在任意位置截断,而是在语义完整的边界截断:
- 同一 API 请求-响应周期的产物不可分割(
message.id天然分组) adjustIndexToPreserveAPIInvariants保证切面两侧都 API 合法- PTL 头部丢弃是极端生存策略,但 Session Memory 文件笔记不受影响------这是"持续记录"对"紧急摘要"的根本优势
六、下期预告
第 11 篇完成了第 7 章前半------四层递进压缩 + Session Memory + 消息分组算法,解决了"对话太长"的问题。但上下文管理不只是"做减法",还有"做加法"------在每次 API 调用前,精心组装行为指令。
第 12 篇:Agent 的灵魂装配------System Prompt 三层管线 + CLAUDE.md 四级覆盖链
- 7.5 System Prompt 三层组装管线------静态层 / 动态章节层 / 用户指令层
- 7.6 CLAUDE.md 四级记忆覆盖链------从企业到个人的配置层叠
- 7.7 上下文管理的设计模式提炼
- 7.8 章节小结
本文是《Claude Code 架构解密》精读笔记系列第 11 篇,覆盖第 7 章前半(7.1-7.4)。系列共 20 篇,持续更新中。