Claude Code 记忆系统深度解析:工作原理与机制
一个经常被使用者忽略的关键问题是——Claude Code 的记忆系统究竟如何运作?表面上它似乎只是简单地将信息存储起来,但背后实际上蕴含着一系列精巧的设计。从分类维度、层级结构,到检索与提取的并发机制,这套系统远不止“写文件、读文件”这么简单。接下来,我们将从底层出发,逐步拆解整个记忆生命周期。
一、从两种维度理解记忆类型
Claude Code 的记忆系统由两个核心维度构成,掌握这两个维度是理解整个系统运作的基石。
维度 A:内容语义类型(记忆“表达了什么信息”)
| 类型 | 含义 | 衰减速度 |
|---|---|---|
user | 用户角色、目标、背景知识 | 极慢,几乎永不过期 |
feedback | 对 Claude 行为的纠正或认可 | 慢,偏好稳定 |
project | 进行中工作、决策、里程碑、截止日期 | 快,源码注释标注为 “decay fast” |
reference | 外部系统资源指针 | 中,外部系统相对稳定 |
维度 B:存储范围类型(记忆“存在于何处”)
前三种属于 CLAUDE.md 指令体系(用户手写规则),后三种属于 AutoMem 体系(由 Fork Agent 自动维护)。
| 类型 | 体系 | 存储路径 | 跨项目 |
|---|---|---|---|
User | CLAUDE.md | ~/.claude/CLAUDE.md | ✓ |
Project | CLAUDE.md | 或 .claude/CLAUDE.md | ✗ |
Local | CLAUDE.md | .claude/CLAUDE.md.local | ✗ |
Managed | CLAUDE.md | 外部注入(frontmatter 标记) | — |
AutoMem | AutoMem | ~/.claude/projects/ | ✓ |
TeamMem | AutoMem | ~/.claude/projects/ | ✓ |
两个维度的关系
维度 A(内容语义)× 维度 B(存储范围)构成了正交的两个轴。AutoMem 是四种内容类型的主要载体——Fork Agent 在提取记忆时,会根据内容语义进行分类,并写入对应的 topic 文件,文件名即可反映其类型(例如 user_role.md、feedback_testing.md)。
Session Memory:独立于分类体系的特殊机制
文档中的 Session Memory 并不属于上述任何一种分类,它所扮演的角色完全不同:
| AutoMem / CLAUDE.md | Session Memory | |
|---|---|---|
| 本质 | 提炼出的知识点 | 当前对话的压缩摘要 |
| 目的 | 跨会话传递知识 | 解决 context window 快满的问题 |
| 生命周期 | 长期持久 | 仅服务于当前会话 |
| 触发时机 | 每轮对话结束后 | context 累计 token ≥ 10,000 时 |
Session Memory 是 Compact 机制的重要组成——当对话内容过长时,它会用结构化的摘要替换原始对话历史,确保对话能够顺利进行。它解决的是“本次对话太长如何处理”的实时问题,而不是“下一次对话如何记住本次学会的内容”。
二、完整闭环:一次对话的生命周期
我们先看完整的时序图,再逐阶段展开详细说明。
sequenceDiagram
participant 用户
participant Claude主线程
participant SystemPrompt
participant SonnetRanker
participant MemoryFS
participant ForkAgent
Note over 用户,ForkAgent: ── 会话 A 启动 ──
Claude主线程->>SystemPrompt: 请求加载跨会话记忆摘要
SystemPrompt->>MemoryFS: 读取记忆索引(≤200行/25KB)
MemoryFS-->>SystemPrompt: 返回 AutoMem 摘要索引
SystemPrompt-->>Claude主线程: 将长期记忆注入 System Prompt(每次必加载)
Claude主线程->>SystemPrompt: 请求加载用户指令规则
SystemPrompt->>MemoryFS: 读取 CLAUDE.md / rules/*.md(从当前目录向上遍历)
MemoryFS-->>SystemPrompt: 返回用户手写的行为约束规则
SystemPrompt-->>Claude主线程: 将用户指令注入 User Context
Note over 用户,ForkAgent: ── 用户发消息 ──
用户->>Claude主线程: 发送 prompt
par 并发执行(不阻塞主对话响应)
Claude主线程->>SonnetRanker: 异步启动相关记忆预取(不等结果)
SonnetRanker->>MemoryFS: 轻量扫描所有记忆文件元数据(只读前30行)
MemoryFS-->>SonnetRanker: 返回文件名、描述、修改时间清单
Note over SonnetRanker: 去除本轮已注入过的文件(防重复)
SonnetRanker->>SonnetRanker: 语义评分,精选最相关的 ≤5 个文件
SonnetRanker->>MemoryFS: 读取选中文件完整内容(≤200行/4KB)
MemoryFS-->>SonnetRanker: 返回文件内容(超1天附加过期提醒)
and
Claude主线程->>Claude主线程: 执行工具调用(Bash/Read/Edit...)
end
SonnetRanker-->>Claude主线程: 将相关记忆作为上下文注入本轮推理
Note over 用户,ForkAgent: ── Claude 推理(含记忆上下文)──
Claude主线程->>Claude主线程: 结合记忆与工具结果生成响应
Claude主线程-->>用户: 输出响应
Note over 用户,ForkAgent: ── 对话结束,异步触发记忆沉淀 ──
Claude主线程->>ForkAgent: 对话结束后异步派生子 Agent 提取记忆
Note over ForkAgent: fire-and-forget,主线程不等待
ForkAgent->>MemoryFS: 扫描已有记忆文件,获取现有内容清单
MemoryFS-->>ForkAgent: 返回已有 topic 文件列表(防止重复创建)
ForkAgent->>ForkAgent: 分析新增消息,按内容语义分类
ForkAgent->>MemoryFS: 将新记忆写入对应分类文件
ForkAgent->>MemoryFS: 更新记忆索引,保持摘要最新
Note over ForkAgent: 推进游标,下次只处理新增消息
Note over 用户,ForkAgent: ── 会话 B 启动(闭环完成)──
Claude主线程->>SystemPrompt: 请求加载跨会话记忆摘要
SystemPrompt->>MemoryFS: 读取已含上轮提取结果的记忆索引
MemoryFS-->>SystemPrompt: 返回最新记忆索引(包含上轮对话产生的新记忆)
SystemPrompt-->>Claude主线程: 注入 System Prompt → 循环往复
各阶段设计意图
① 会话启动:为何需要同时加载两套内容?
MEMORY.md(AutoMem)和 CLAUDE.md 是完全独立的两套体系,分别承担不同的职责:前者是 Claude 从历史对话中自动学到的知识,后者是用户主动告知 Claude 的行为规则。两者在会话启动时全量加载,是因为它们都属于“无论对话内容是什么,都应当知晓的背景信息”——不经过相关性筛选,直接注入 System Prompt。
② 记忆预取:为何不等待预取完成再开始推理?
预取(prefetch)和工具执行是并发进行的。当 Claude 接收到用户消息后,会立即启动记忆预取,同时开始执行工具调用。工具调用本身存在 I/O 等待时间,而预取正是利用这段时间在后台默默完成。待第二轮 API 请求时,记忆已经准备就绪,可以连同工具结果一起发给模型。这样一来,用户感知到的延迟仅来自推理过程本身,记忆检索的耗时被完全隐藏在工具执行的等待时间内。
③ 语义评分:为什么 Sonnet 只看 description 而不读文件全文?
扫描阶段仅读取每个文件的前 30 行(frontmatter),将所有文件的 description 字段汇总成一份清单,通过一次 sideQuery 即可完成选择。如果让 Sonnet 阅读完整的文件内容,就需要先将所有文件读取进来,不仅速度慢,而且消耗大量 token。description 字段正是为此设计——写记忆时要求 description 精准描述内容,使评分阶段得以在不读取正文的情况下准确判断相关性。
④ 记忆注入时机:为什么记忆在两次 API 请求之间注入?
Claude Code 的核心是一个 while(true) 循环,每次循环对应一次 API 请求。推理(thinking)和响应(response)属于同一次 API 请求中流式输出的两个阶段,无法在其中插入新内容。因此,记忆只能在两次请求之间注入——第一轮请求触发工具调用,工具执行期间预取完成,第二轮请求时记忆与工具结果一同发给模型。对于没有工具调用的简单问答,第一轮即 break,可能无法获取记忆。
⑤ 记忆提取:为什么采用 fire-and-forget(异步记忆提取)而不等待结果?
提取记忆的目的仅是将新记忆写入磁盘,主线程无需知道“写了什么”即可继续——用户已获取响应,对话已结束,主线程没有理由继续等待。因此,系统在后台异步写入文件,主线程立即释放,用户不会感知到任何额外延迟。
同时,系统内置了两层保护机制,防止多次提取相互冲突:
- 互斥:若主 Agent 在本轮已自行写入过记忆,后台提取将跳过该范围,避免重复写入。
- 排队:若上一次提取仍在运行,新的提取请求会先进入等待队列,待前一次完成后才触发。
⑥ 游标机制:为何要记录上次提取到哪条消息?
每次对话结束后,Fork Agent 只需分析“上次提取之后新增的消息”,而无需重新扫描整个对话历史。游标记录了上次提取的边界,确保实现增量处理。如果没有游标,每次提取都需要重新分析全部历史,既浪费 token,又可能导致重复记忆生成。
§1 会话启动 — 两套内容同步加载
每次会话启动时,Claude 会同步加载两套完全独立的内容,它们各自承担不同的职责:
第一套:跨会话长期记忆(AutoMem 索引)
从 ~/.claude/projects/<项目哈希>/memory/MEMORY.md 中读取记忆摘要索引,并注入到 System Prompt。这是 Claude 从历史对话中自动积累的知识——用户的角色背景、工作偏好、项目决策等。索引文件有大小限制(≤200行/25KB),超出时会自动截断。
第二套:用户手写指令规则(CLAUDE.md 体系)
从当前工作目录开始向上遍历目录树,依次读取 CLAUDE.md、.claude/CLAUDE.md、.claude/rules/*.md 等文件,并注入到 User Context。这些是用户主动告知 Claude 的行为约束——代码风格、禁止操作、项目规范等。
两套内容的本质区别:
| 跨会话长期记忆 | 用户手写指令 | |
|---|---|---|
| 来源 | Claude 从对话中自动提炼 | 用户手动编写 |
| 存储位置 | ~/.claude/ 用户目录 | 项目目录树 |
| 更新方式 | 每次对话结束后异步写入 | 用户手动编辑 |
| 一句话概括 | Claude 自己记住的信息 | 用户告诉 Claude 的规则 |
两套内容均为全量加载,不进行相关性筛选——它们属于“无论聊什么都应该知道的背景”,直接进入上下文,无需经过 Sonnet 评分。
§2 用户发消息 — 相关记忆并发预取
用户发消息后,相关记忆预取立即并发启动,不会阻塞主对话响应。预取与工具执行同步进行,利用工具 I/O 等待时间在后台完成,用户完全感知不到额外延迟。
检索流程分为 6 个步骤:
- 扫描:递归读取 memory 目录下所有
.md文件,每个文件仅读取前 30 行(frontmatter),按修改时间倒序排列,最多扫描 200 个文件。 - 去重:过滤掉本 session 中已经注入过的文件,避免重复注入。
- 构建清单:将所有文件的
description字段汇总成一份清单,格式为- [type] filename (时间戳): description。 - Sonnet 语义评分:将清单发送给 Sonnet 进行一次独立的侧边请求,Sonnet 仅查看文件名和 description,不读取文件内容,从中选出最相关的 ≤5 个文件。
- 防幻觉校验:将 Sonnet 返回的文件名与实际存在的文件列表进行交集处理,过滤掉不存在的文件名。
- 读取内容:读取选中文件的完整内容(每个文件 ≤200 行且 ≤4KB),若修改时间超过 1 天,则附加过期提醒。
为什么评分只用 description 而不读文件内容?
如果让 Sonnet 阅读完整的文件内容,就需要先将所有文件读取进来,既慢又耗费 token。扫描阶段仅读取 frontmatter,将所有文件的 description 汇总成一个清单,一次请求即可完成选择。这也是写记忆时要求 description 精准描述内容的原因——description 的质量直接决定了记忆能否被正确召回。
各层限制:
| 限制项 | 数值 |
|---|---|
| 每轮最多召回文件数 | 5 个 |
| 每个文件最大行数 | 200 行 |
| 每个文件最大字节 | 4 KB |
| 每轮最大注入量 | ≈20 KB |
| session 累计上限 | 60 KB |
| 扫描文件上限 | 200 个 |
§3 推理 — 记忆注入时机
预取完成的记忆以 的形式追加在用户消息之前,作为推理上下文呈现给 Claude。
关键细节:记忆注入发生在两次 API 请求之间。
Claude Code 的核心是一个持续循环,每次循环对应一次 API 请求。推理(thinking)和响应(response)是同一次请求中流式输出的两个阶段,无法在中间插入新内容。因此,记忆只能在两次请求之间注入:
| 轮次 | 记忆状态 |
|---|---|
| 第 1 轮(用户发消息,预取刚启动) | 无记忆 |
| 工具执行期间(预取在后台完成) | 注入记忆 |
| 第 2 轮(工具结果 + 记忆一起发给模型) | 有记忆 |
| 无工具的简单问答(第 1 轮直接结束) | 可能无记忆 |
这意味着对于需要工具调用的复杂任务,记忆一定会在第二轮推理前准备就绪;而对于简单的直接问答,记忆可能来不及注入。
§4 对话结束 — 异步记忆提取(fire-and-forget)
每轮对话结束后,系统会异步触发两套独立的提取机制,主线程不等待结果:
长期记忆提取(AutoMem):
- 每轮对话结束后通过 stop hook 无条件触发,在后台异步运行。
- 互斥保护:若主 Agent 在本轮已自行写入过记忆文件,后台提取将跳过该范围。
- 排队机制:若上一次提取仍在运行,新请求将进入等待队列,完成后触发补充运行。
会话摘要提取(Session Memory):
- 触发条件:context window 累计 token ≥ 10,000(首次),之后每增长 ≥ 5,000 token 且满足工具调用或对话断点条件。
- 用途:生成结构化会话摘要,供 Compact 时替代原始对话历史。
Fork Agent 提取流程(5 步):
- 预扫描:读取现有 topic 文件列表,生成已有记忆清单并注入提示词,避免重复创建文件。
- 构建提示词:包含新增消息数量、已有记忆清单、四类内容定义(user/feedback/project/reference)及写入规则。
- 派生子 Agent 执行:子 Agent 与主对话共享 System Prompt 前缀(命中 Prompt Cache,复用缓存),工具权限受限(只读 + 仅限记忆目录的写入),最多执行 5 轮。
- 写入记忆文件:按内容语义分类写入对应的 topic 文件,并更新
MEMORY.md索引。 - 推进游标:记录本次提取处理到的最后一条消息,下一次提取只处理新增消息,避免重复分析。
三、文件格式详解
指令类(CLAUDE.md 体系)— 纯 Markdown,用户手写
# ~/.claude/CLAUDE.md
不要使用 any 类型。
永远先写测试。
回复用中文。
@./docs/coding-standards.md
支持 @include 引用其他文件,从 cwd 向上遍历目录树,越近优先级越高。
AutoMem 类(topic 文件体系)— 带 frontmatter,自动维护
topic 文件格式:
---
name: 回复风格偏好
description: 不要在回复末尾总结刚做了什么
type: feedback
---
不要在回复末尾用“总结”段落重复已完成的操作。
**Why:** 用户说“我能看到 diff,不需要你再重复”
**How to apply:** 所有回复结尾直接停,不加总结段
MEMORY.md 索引格式:
- [用户角色](user_role.md) — 研发架构师,侧重系统设计
- [回复风格](feedback_style.md) — 不要在末尾总结
- [项目状态](project_auth.md) — auth 重构由合规驱动
两套体系对比:
| 维度 | 指令类(CLAUDE.md) | AutoMem 类(topic 文件) |
|---|---|---|
| 文件格式 | 纯 Markdown,无 frontmatter | 带 YAML frontmatter |
| 谁写 | 用户手写 | Extract Agent 自动生成 |
| 加载时机 | 每次会话启动,全量加载 | MEMORY.md 必加载;topic 文件按相关性每轮最多召回 5 个 |
| 跨项目 | User 类全局生效;Project/Local 限当前项目 | 始终项目隔离 |
四、检索 vs 提取:并发机制对比
| 维度 | 检索记忆(Retrieval) | 提取记忆(Extraction) |
|---|---|---|
| 触发点 | 用户发消息时 | 对话结束时 |
| 并发机制 | 非阻塞轮询 | 异步 |
| 取消机制 | [Symbol.dispose]() 显式中止 | feature gate 控制 |
| 错误处理 | .catch() 返回空数组 | 错误被吞掉,不影响主对话 |
| 何时完成 | 工具执行期间 | 对话结束后 |
五、新鲜度机制
| 文件年龄 | 处理 |
|---|---|
| ≤ 1 天 | 直接注入,无提醒 |
| > 1 天 | 附加:“This memory is X days old. Memories are point-in-time — verify against current code before asserting as fact.” |
六、存储路径速查
| 文件 | 路径 | 何时读取 | 何时写入 |
|---|---|---|---|
MEMORY.md | ~/.claude/projects/ | 每次会话启动 | Extract Agent 完成后 |
topic *.md | ~/.claude/projects/ | 每轮对话 prefetch(≤5个) | Extract Agent(按内容语义类型分文件) |
CLAUDE.md | 项目根 / 父目录 / ~/.claude/CLAUDE.md | 每次会话启动 | 用户手动编辑 |
team/*.md | ~/.claude/projects/ | 同 topic 文件(TEAMMEM flag 开启时) | Extract Agent(scope=team) |
session-memory | ~/.claude/projects/ | auto-compact 触发时 | Session Memory subagent |
七、设计哲学
整个记忆系统背后,蕴含着一套清晰的设计哲学:
1. 不阻塞主流程。 检索和提取均为异步操作,用户感知到的延迟仅限于推理本身。这是最核心的原则——任何额外等待都会削弱使用体验。
2. 精准而非全量。 每轮最多注入 5 个文件,通过 Sonnet 进行语义评分,而非简单的关键词匹配。宁缺毋滥,因为灌入过多不相关的记忆只会稀释有效的上下文。
3. description 是记忆能否被找回的唯一入口。 评分阶段 Sonnet 仅查看 description,这意味着在写入记忆时,description 的质量直接决定了记忆能否被正确召回。这并非理论问题——在实际使用中,description 写得模糊的记忆与没有记忆几乎毫无区别。
4. 两套体系各司其职。 CLAUDE.md 承载用户的意图表达,AutoMem 负责 Claude 的学习积累。两者互不干扰,加载路径也完全分离。如果用户希望某个规则永久生效,应写在 CLAUDE.md 中;如果希望 Claude 从对话中自动总结规律,则依赖 AutoMem 的自动提取能力。
5. 游标机制保证增量。 确保每次提取仅处理新增消息,避免重复分析历史内容。这一设计的好处在于,记忆提取的成本与对话总长度无关,只与单轮新增的消息量成正比。
