乐于分享
好东西不私藏

解读claude code源码中的压缩和记忆机制

解读claude code源码中的压缩和记忆机制

在讨论具体压缩和记忆机制之前,先理解一个核心矛盾:编码 Agent 对上下文的消耗速度和依赖程度,都远超普通对话 AI。

普通聊天场景中,用户和 AI 各发一段文字,每轮增长几百 token,上下文增长是线性的、可预期的。但编码 Agent 完全不同:

  • 工具输出是 token 黑洞。
     一次 grep 返回 300 行代码,一次编译报错带完整 stack trace,一次 API 调用返回整个 JSON 响应。这些输出在生成的瞬间有诊断价值,但 5 轮之后 LLM 可能只需要其中 1-2 行,剩下的就是纯噪声——而它们占据的 token 不会自己消失。
  • 任务有极强的状态依赖。
     LLM 忘了之前在改哪个文件,就可能读错文件甚至覆盖已有改动;忘了上一步遇到的编译错误,就会重复尝试已经失败的方案。这不是一般的”信息丢失”,而是会直接破坏任务连续性的问题。
  • 会话天然很长。
     一个真实的编码任务(比如”给 auth 模块加错误处理,跑通测试,更新文档”)可能需要 30-50 轮交互,涉及十几次文件读取、编辑、命令执行。190K 的 context window 听起来很大,但在这种消耗速度下,一个正常的编码 session 很容易在 20 分钟内把 context 吃满。

所以 Claude Code 面临的核心设计问题是:如何在有限的 context window 里,持续保住”让任务能继续推进”的最小信息集?

这个问题拆成两个子问题:

  1. session 内:context 不够了,怎么既保留会话关键信息,又能不撑爆上下文窗口(或节省Token)→ 压缩机制
  2. 跨 session:历史会话积累的知识怎么带到下次?→ 记忆机制

这两套机制不是各自独立的——它们共享同一个稀缺资源(context window),必须协同设计。后文会先分别介绍实现细节,最后回到它们的协同关系。

压缩机制

CC的上下文管理非常精细,拥有四种不同粒度的压缩机制同时工作,并且按严格顺序触发,前面能搞定就不动后面。

我在源码里找到了四个相关的 Feature Flag:

HISTORY_SNIP          → 第一层:裁工具输出CACHED_MICROCOMPACT   → 第二层:缓存式 LLM 摘要CONTEXT_COLLAPSE      → 第三层:结构化归档REACTIVE_COMPACT      → 第四层:后台自动压缩

第一层:HISTORY_SNIP — 删噪声

最容易膨胀的不是用户输入,也不是LLM的回复,而是工具输出。绝大多数情况下工具输出占据了大多数的token。一个API调用或一次搜索返回100行的结果,一次编译报错可能带完整的 stack trace,这些输出在生成的瞬间有用,5轮之后就是纯噪声。这和聊天 AI 的 context 膨胀方式完全不同。

HISTORY_SNIP 做的事情:遍历历史消息中所有 role: "tool" 的结果,如果内容超过阈值(默认值是 30,000 字符,上限是 150,000 字符),替换成精简版。精简策略是保留前MAX_OUTPUT_LENGTH字符,用 [N lines truncated] 替代。

之前:  [tool result] (482 lines)  src/auth.py:12: import jwt  src/auth.py:34: jwt.decode(token, SECRET)  src/auth.py:56: jwt.encode(payload, SECRET)  ... (479 more lines of grep results)之后:  [tool result] (snipped to 6 lines)  src/auth.py:12: import jwt  src/auth.py:34: jwt.decode(token, SECRET)  src/auth.py:56: jwt.encode(payload, SECRET)  [snipped 476 lines]  src/utils/crypto.py:89: verify_jwt(token)  src/utils/crypto.py:102: refresh_jwt(token)

这是成本最低的压缩——不需要调 LLM,不会丢失关键信息(头尾保留),效果立竿见影。一次 grep 的输出从 2000 token 压到 100 token,什么都没丢。

注意:Claude Code 源码中 HISTORY_SNIP feature flag 存在且被多处引用,但具体实现模块(snipCompact.js 等)在公开/泄露的版本中并未包含,属于内部 A/B 测试。Claude Code 目前公开的截断逻辑是 BashTool/utils.ts 的 head-only 截断。

第二层:CACHED_MICROCOMPACT — 花钱压缩

如果用户在一个 session 里已经进行了 20 轮交互,每轮都包含有效的文件编辑和讨论,即使工具输出都被裁过了,20 轮对话本身的 token 量依然非常可观。这时候需要对对话内容本身进行压缩,而不仅仅是裁工具输出。但调 LLM 做摘要是有成本的,所以这一层引入了缓存机制——同一段历史不会被重复摘要。

claude  code源码定义了当token超过70%,第二层启动。

这一层拿老的对话片段(旧轮次+已经snip过的工具输出),发给 LLM 做一次专门的摘要调用:

System: "把这段对话压缩成关键信息。         保留:文件路径、做出的决策、遇到的错误、当前任务状态。         丢弃:冗长的工具输出、重复的讨论、格式化的代码。"User: [10 轮旧对话的拼接文本]

摘要完成后保留最近 8 条消息不动,其余旧轮次被摘要替换,消息列表结构仍在。

为什么摘要 prompt 要显式列出”保留文件路径、决策、错误”?

这是编码场景的特殊需求。普通聊天的摘要丢掉一些细节问题不大,但编码任务中,丢了文件路径意味着 LLM 不知道之前编辑了哪些文件,可能重复读取或覆盖;丢了决策(”用户说不要改 config.yaml”)意味着 LLM 可能违反用户指令;丢了错误信息意味着 LLM 会从头排查已经定位过的 bug。摘要 prompt 里的保留清单,本质上是在定义编码任务中**”最小可继续信息集”**的边界。

这一层还利用了 Anthropic API 的 cache_deleted_input_tokens 能力——在 API 的缓存层面标记某些 token 为”已删除”,不占用 cached token 配额。等于是在不改变消息内容的情况下,把缓存里的空间也省出来了。

第三层:CONTEXT_COLLAPSE — 结构化归档

用户在一个超长会话里处理了多个不同子任务——先改了 auth 模块,又修了测试,再更新了文档,然后开始处理一个新 feature。前两层是局部的、渐进的压缩,它们能把单个工具输出或几轮旧对话压小,但对话历史的总量仍然在增长。当 token 超过 90% 上限时,局部压缩已经无法腾出足够空间,需要一次彻底的、全局性的替换。

CONTEXT_COLLAPSE 和前二层的区别:前两层(HISTORY_SNIP 和 CACHED_MICROCOMPACT)是局部的、渐进的压缩,而从第三层开始是一次整体替换,压缩是全量的、不可逆的:旧消息列表被清空,取而代之的是摘要加上压缩后需要恢复的上下文。

这里有一个核心问题:这份”代表全局状态的摘要”从哪来?

第三层需要一份能代表”整个会话到目前为止所有状态”的摘要,这和第二层不同——第二层只摘要旧轮次,最近 8 条还在;第三层是整个历史都要被替换,摘要必须覆盖全局。这份摘要有两种来源,压缩触发时按顺序尝试:快速路径:直接读 Session Memory 文件

Session Memory 文件是后台持续维护的九个章节的结构化笔记,内容已经是提炼好的摘要,不需要再调 LLM 处理。目前这部分源码中默认是禁用的,有anthropic绝对开启的用户。

慢速路径:临时调 LLM 现场生成

快速路径返回 null 时走这里,把整个消息历史发给 LLM,让它现场生成摘要。

getCompactPrompt() 生成的 prompt 由三部分拼接:

第一部分:强制禁止工具调用

CRITICAL: Respond with TEXT ONLY. Do NOT call any tools.- Do NOT use Read, Bash, Grep, Glob, Edit, Write, or ANY other tool.- Tool calls will be REJECTED and will waste your only turn.- Your entire response must be plain text: an <analysis> block followed by a <summary> block.

放在最前面,因为源码注释说 Sonnet 4.6 在继承了主对话工具列表的情况下有时会尝试调工具,这会导致 maxTurns: 1 白白浪费掉唯一的机会。

第二部分:核心摘要指令

要求 LLM 先写 <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            下一步,必须和用户最近的明确请求对齐                                 包含原文引用,防止任务漂移

第三部分:再次强调禁止工具

REMINDER: Do NOT call any tools. Respond with plain text only.Tool calls will be rejected and you will fail the task.

摘要生成后如何处理

LLM 返回的原始摘要包含 <analysis> 和 <summary> 标签,formatCompactSummary() 做两件事:

// 1. 直接删掉 <analysis> 块——这是思考草稿,不需要保留formattedSummary.replace(/<analysis>[\s\S]*?<\/analysis>/, '')// 2. 把 <summary> 标签替换成可读的标题formattedSummary.replace(/<summary>[\s\S]*?<\/summary>/, `Summary:\n${content}`)

然后 getCompactUserSummaryMessage() 把摘要包装成最终插入消息列表的内容:

This session is being continued from a previous conversation that ran out of context.The summary below covers the earlier portion of the conversation.[格式化后的摘要]If you need specific details from before compaction (like exact code snippets, error messages,or content you generated), read the full transcript at: [transcript路径]Recent messages are preserved verbatim.Continue the conversation from where it left off without asking the user any further questions.Resume directly — do not acknowledge the summary, do not recap what was happening.Pick up the last task as if the break never happened.

两条路径最终都走 getCompactUserSummaryMessage() 包装成同样格式插入消息列表,但内容丰富度有差异——慢速路径包含完整代码片段和所有用户消息原文,快速路径更精炼但细节更少。

上下文已折叠 - 已汇总30轮对话"第1-5轮:读取认证模块,识别出3个缺少错误处理的函数第6-12轮:为 verify_token()、refresh_token()、decode_payload() 添加了 try/except第13-15轮:运行测试,发现 test_expired_token 存在回归问题第16-20轮:修复测试,47个测试全部通过第21-25轮:更新了新错误响应的 API 文档第26-30轮:应用了代码审查建议已修改文件:src/auth.py、tests/test_auth.py、docs/api.md当前状态:所有更改已提交,准备发起 PR

压缩完成后,系统还会并行重建压缩后的上下文:把最近读取过的文件(最多 5 个)、本次会话用过的 skill、plan 模式指令、工具变更通知重新注入,确保 Claude 在摘要之后还能正常继续工作。

第四层:REACTIVE_COMPACT 

REACTIVE_COMPACT 和第三层调用的是同一个底层函数 compactConversation(),区别只在触发方式。 第三层是主动的——token 用量接近上限时系统提前介入;第四层是被动的——API 已经返回了 prompt_too_long 错误,会话无法继续,此时紧急调用同一个压缩函数,读取 Session Memory 里现成的摘要,替换历史消息后重新发起请求。同一个错误只尝试一次,避免死循环。

压缩的工程权衡

做上下文压缩最难的不是”怎么压”,而是”压什么”。读完上下文管理这部分代码,我最大的感触是:不同信息的保质期不一样,应该用不同的策略处理。

工具的中间输出几轮之后就没用了。但用户描述的需求背景可能整个会话都要保留。LLM 上一轮的思考过程可以扔掉,但它做出的决策(”我选择用 try/except 而不是 if/else”)应该保留。

Claude Code 的四层策略本质上就是在按”保质期”分级处理:第一层丢最短命的(冗长的工具输出),最后两层才动最长命的(对话结构和决策历史)。

什么信息绝对不能丢?

文件路径——LLM 需要知道之前编辑了哪些文件,不然可能重复读取或覆盖。做出的关键决策——”用户说不要改 config.yaml”这种指令如果被压掉了,LLM 就可能违反。未解决的错误——正在处理的 bug 信息如果丢了,LLM 会从头排查。

Claude Code 的摘要 prompt 里明确列出了这些保留项。

摘要本身占多少 token?

如果摘要写太长,压缩就白做了。Claude Code 通过 max_tokens 参数限制摘要调用的输出长度。CoreCoder 把摘要调用的 prompt 限制在 15000 字符以内。

压缩后 LLM 会不会”忘记”承诺?

这是最大的风险。你说”帮我改完 auth 模块后跑一次全量测试”,前半句被 LLM 做完了,后半句在压缩时被摘要吸收了,LLM 可能就忘了跑测试。

Claude Code 的策略是在摘要 prompt 里强调”保留用户明确要求的操作和约束”。但这不是 100% 可靠的——摘要 LLM 本身也可能犯错。这是一个已知的不完美,目前没有完美解。

用什么模型做摘要?

Claude Code 用的是同一个模型。一次摘要调用的成本 ≈ 一次正常对话的成本。如果你想省钱,可以用一个便宜的模型专门做摘要(比如用 DeepSeek 做主力但用 GPT-4o-mini 做摘要)。

记忆

Auto Memory

自动记忆不是用规则或正则提取,而是采用 forked agent 模式——启动一个独立的 Claude agent 来做这件事。这个 forked agent 和主对话共享 prompt cache,所以读取对话历史几乎不花额外 token 成本。

四种记忆类型

1. user — 用户画像

记录用户是谁,目的是让 Claude 调整沟通方式。

保存时机:学到用户的角色、偏好、职责、知识背景时使用时机:回答需要考虑用户水平时例:用户说"我是数据科学家,在看日志监控"→ 保存:用户是数据科学家,当前关注可观测性用户说"我写 Go 十年了,但第一次碰这个项目的 React"→ 保存:Go 深度专家,React 新手,用后端类比解释前端

有一条明确的限制:不能保存对用户的负面评价,只保存对工作有帮助的信息。


2. feedback — 行为指导

记录用户对 Claude 工作方式的纠正和确认,目的是让同样的指导不需要说第二遍。

保存时机:  纠正:"别这样"、"不要做X"、"停止XX"  确认:"对就是这样"、"完美继续这么做"(接受了非显而易见的选择)文件结构:  规则本身  Why:用户给的原因(往往是一次事故或强烈偏好)  How to apply:什么时候适用这条规则例:"别 mock 数据库,上季度 mock 测试过了但 prod 迁移挂了"→ 保存:集成测试必须打真实数据库。Why:mock/prod 差异曾掩盖过迁移失败"别在每次回复结尾总结你做了什么,我能看 diff"→ 保存:这个用户要简洁回复,不要结尾总结

一个关键设计:成功也要记,不只记失败。如果只记纠正,Claude 会避开错误但偏离已验证的好做法,变得过于保守。


3. project — 项目上下文

记录代码和 git 历史里推导不出来的信息:正在做什么、为什么做、截止时间。

保存时机:学到谁在做什么、为什么、什么时候特殊规则:相对日期必须转成绝对日期  "周四之前" → "2026-05-08"(否则记忆过期后无法解读)文件结构:  事实或决策本身  Why:动机(约束、截止日、利益相关方要求)  How to apply:如何影响建议例:"周四之后冻结非关键合并,移动端要切 release branch"→ 保存:合并冻结从 2026-05-08 开始,标记该日期后的非关键 PR

4. reference — 外部系统指针

记录信息在哪里找,不记录信息本身。

保存时机:学到外部系统里有什么资源例:"pipeline 的 bug 都在 Linear 的 INGEST 项目里"→ 保存:pipeline bug 追踪在 Linear INGEST 项目"grafana.internal/d/api-latency 是 oncall 看的面板"→ 保存:改请求路径代码前检查这个 Grafana 面板

明确不记什么

代码规范、架构、文件路径、项目结构   → 可以 grep/读代码推导git 历史、谁改了什么                → git log/blame 更权威调试方案、修复步骤                  → 答案在代码里,原因在 commit message 里CLAUDE.md 里已有的内容              → 重复临时任务状态、进行中的工作           → 会过期,没有持久价值

有一条特别反直觉的规则:即使用户明确要求保存,这些也不记。如果用户说”记住这周的 PR 列表”,Claude 应该反问”这里面有什么是让你意外的或者不明显的?”——那个部分才值得记。

记忆的使用规则

防漂移:记忆是”当时的快照”,不代表现在。用记忆里的文件路径或函数名之前,必须先验证它们现在还存在。

记忆说 X 存在 ≠ X 现在存在

防污染:用户说”忽略关于 X 的记忆”,就完全当 MEMORY.md 是空的,不能”承认然后覆盖”,不能提及。

主动验证:如果记忆里说某个函数或文件存在,推荐给用户之前必须先 grep 确认。

写入流程

触发的条件是对话的一个完整轮次结束,启动后的写入流程分两步走,有明确的顺序要求:

Step 1: 写主题文件user_role.md、feedback_testing.md 等每个主题一个文件,带 frontmatterStep 2: 更新 [MEMORY.md](http://memory.md/) 索引每条一行,150 字符以内格式:- [Title](https://www.notion.so/file.md) — one-line hook[MEMORY.md](http://memory.md/) 只是指针,不写实际内容

去重规则

写之前先查已有记忆清单能更新已有文件就不新建发现错误或过时的记忆要删除或修正

显式记忆指令

用户说"记住X" → 立刻保存,选最合适的类型用户说"忘掉X" → 找到对应条目删除

MEMORY.md 的限制

超过 200 行会被截断(因为它会被注入 system prompt)所以索引必须保持简洁

Extract Memories Agent 在每轮对话结束后从对话中提取信息,写入 Auto Memory 目录形成持久化记忆文件

Claude.md——你自己写的

claude.md存在多个位置,它有层级结构,可以存在于多个位置:

~/.claude/CLAUDE.md          ← 全局,所有项目都生效~/myproject/CLAUDE.md        ← 项目根目录,当前项目生效~/myproject/src/CLAUDE.md    ← 子目录,只对该目录生效

Claude Code 会从上到下全部读取,内容叠加。其中全局记录的是个人偏好、通用规范,由自己定义,写完后放到对应目录会自动加载。

项目级claude.md记录了这个项目的关键信息,可以通过/init让模型扫描代码推断,但是背景、注意事项这些通常需要人来自己编写。

背景:背景决定了技术架构的设计思路技术栈:技术栈决定了了架构的组成,模型会基于当前技术栈相兼容的方式来实现。命令:他帮你在执行任务的时候直接使用,而不用每次都问你怎么跑项目项目结构:知道结构才知道怎么寻找代码,并把新的代码放到正确位置代码规范:这是最重要的部分。没有这些约束,它生成的代码风格会跟你现有代码不一致,review 起来很痛苦。注意事项:记录历史踩过的坑