Claude Code 实现了一套基于文件的长期记忆系统,其机制可分为五个部分:
- 把长期知识保存为本地 Markdown 文件;
- 用
MEMORY.md维护简短索引; - 必要时用额外模型调用从主题文件中选择相关记忆;
- 允许主 Agent 继续用
Read、Grep、Glob主动查找; - 在对话结束后,由一个受限的后台 Agent 抽取新记忆。
一、记忆机制的分类
Claude Code 中至少存在三种经常被统称为 memory、但功能不同的机制。
1. CLAUDE.md:人工维护的持久化指令
CLAUDE.md、CLAUDE.local.md 和 .claude/rules/*.md 由人维护,用于告诉 Claude:
- 项目应该如何构建和测试;
- 代码风格与架构约束;
- 哪些工作流必须遵循;
- 哪些目录需要特殊处理。
它们属于指令记忆,用于规定工作方式,而非 Claude 自动提取的知识。
2. Auto memory:Claude 生成的跨会话知识
Auto memory 由 Claude 自动读写,默认位于:
~/.claude/projects/<project>/memory/
它保存的是未来会话仍可能有用、但无法简单从当前代码推导出来的信息:用户角色与偏好、用户对工作方式的纠正、项目背景与决策原因,以及 Linear 项目或监控面板这类外部系统入口。本文主要讨论该层机制。
3. Transcript 与 compaction:会话内状态
Claude Code 还会把消息、工具调用和结果保存为 JSONL,并在上下文过长时进行 compaction。它们解决的是:
- 如何恢复同一个会话;
- 如何在长对话中压缩上下文;
- 如何回滚或续接之前的工具调用。
这类数据并非默认注入每个新会话的长期知识库,应与 auto memory 区分。
二、记忆文件的存储结构
典型的 auto-memory 目录结构如下:
memory/
├── MEMORY.md
├── release-merge-freeze.md
├── postgres-conn-pool.md
└── latency-dashboard.md
其中,MEMORY.md 用于存储索引,而非记忆正文:
- [发布期间的主干冻结](release-merge-freeze.md) — 应用商店审核期间冻结主干,新版本上线并稳定观察一天后解除
- [Postgres 连接池](postgres-conn-pool.md) — 线上连接上限及其决策原因
- [延迟看板](latency-dashboard.md) — 线上延迟看板入口与关键面板
源码中的记忆提示词明确要求:
- 每条索引占一行;
- 每条约 150 字符以内;
- 仅保留标题、文件链接和单行摘要(one-line hook);
- 详细内容必须存入独立主题文件;
- 不得将记忆正文直接写入
MEMORY.md。
主题文件则带 YAML frontmatter:
---
name: release-merge-freeze
description: 移动端版本提交应用商店审核后冻结主干,新版本上线并稳定观察一天后解除
type: project
---
# 发布期间的主干冻结
移动端版本提交应用商店审核后,主干进入冻结状态,直至新版本上线并稳定观察一天。
**Why:** 此前曾在审核期间合入改动,导致紧急修复被一并打包送审,最终需要回滚发布。
**How to apply:** 安排合并前,应确认当前是否处于冻结窗口;冻结期间仅允许合入经批准的 hotfix。
这条规则之所以适合写入 memory,是因为它由团队流程约定和实际事故经验共同形成,无法从代码中判断当前是否允许向主干分支合并;反过来,如果它已经写进 CLAUDE.md 或由 CI 强制执行,就不该再存一份可能过时的副本。
MEMORY.md 的容量限制
索引本身也有容量限制:最多加载前 200 行或 25 KB,以先达到的上限为准。超过限制时,Claude Code 仅加载上限以内的内容,并附加警告,要求 Claude:
- 缩短每一条索引;
- 把细节移入主题文件;
- 合并重复项;
- 删除过期项。
三、记忆召回通路及其启用条件
从功能上看,Claude Code 有三种取得旧记忆的方式:
- 通路 A:注入
MEMORY.md索引; - 通路 B:自动相关性预取主题文件;
- 通路 C:主 Agent 主动调用文件工具。
但在源码中,通路 A 与通路 B 并非始终同时启用,而是由同一个 tengu_moth_copse feature flag 切换:
也就是说,A 与 B 是二选一的两种自动召回模式,C 则在两种模式下都可用。由此带来一个容易被忽略的推论:磁盘上存在 MEMORY.md 并不表示它一定会进入本轮上下文;同样,缺少索引条目也不表示主题文件一定无法被检索。
四、通路 A:MEMORY.md 索引注入
1. MEMORY.md 的上下文注入过程
Auto memory 开启时,Claude Code 会读取:
~/.claude/projects/<project>/memory/MEMORY.md
并把它和 CLAUDE.md 一起汇总进本轮会话的用户上下文。
进入 system prompt 的是“如何保存、何时读取、不要保存什么”这类行为规则;
MEMORY.md的正文属于 system prompt 之后的用户上下文。
换句话说,memory 是提供给模型的材料,而不是不可覆盖的系统级配置。
2. 索引召回的主要特性
当 tengu_moth_copse 未启用且索引文件存在时,加载 MEMORY.md 无需额外的模型调用:
读文件 → 截断 → 拼入上下文
该过程具有三个特点:
- 确定性:无需 embedding、关键词搜索或 LLM 选择;
- 低延迟:没有独立 API 请求;
- 信息粒度有限:模型通常仅能看到单行摘要,而非主题文件全文。
例如用户问“在向主干分支合并前,是否发生过与版本发布相关的事故?”,模型此时只知道“存在 release-merge-freeze.md,它与商店审核期间的主干冻结有关”。如果单行索引恰好包含完整答案,模型可能直接作答;但从设计目的看,索引主要用于提供后续读取主题文件的路径。
3. 索引模式的召回缺口
如果主题文件已经写入磁盘,但抽取 Agent 忘了更新 MEMORY.md:
memory/
├── MEMORY.md
├── indexed-memory.md
└── orphan-memory.md
那么 orphan-memory.md 就不会通过通路 A 进入上下文——除非主 Agent 主动扫描目录,这条记忆在索引模式下等同于不存在。这也是相关性预取模式直接扫描全部主题文件、而不完全依赖索引的重要原因。
五、通路 B:自动相关性预取
相关性预取是更接近检索系统的一条通路,但它既不建立 embedding,也不对正文执行向量搜索。其流程如下:
扫描 frontmatter
↓
生成候选 manifest
↓
额外模型调用选择文件
↓
读取选中文件开头
↓
作为 relevant_memories 附件注入
1. 触发条件
startRelevantMemoryPrefetch() 的启动条件大多是例行检查(flag 已开启、存在真实用户消息、会话注入预算未耗尽),其中只有一项值得单独一提:用户输入去除首尾空白后若不含任何空白字符,会被判定为“单词查询”并跳过预取。这条英文启发式规则落到 CJK 输入上,意味着 合并主干前是否发生过发布事故 这类不含空格的中文提问通常不会触发自动预取。
2. 扫描候选文件
scanMemoryFiles() 递归列出 memory 目录中的 .md(排除 MEMORY.md),每个文件只读前 30 行用于解析 frontmatter 中的 description 和 type,再按修改时间从新到旧排序,最多保留 200 个候选。
随后,formatMemoryManifest() 生成每行对应一个文件的清单:
- [project] release-merge-freeze.md (2026-07-15T...): 移动端版本提交应用商店审核后冻结主干,新版本上线并稳定观察一天后解除
- [project] postgres-conn-pool.md (2026-07-10T...): 线上连接池上限及其决策原因
- [reference] latency-dashboard.md (2026-07-02T...): 线上延迟看板入口
检索模型看到的是文件名、类型、时间和 description,而不是主题文件正文。frontmatter 描述的具体程度会直接影响记忆能否被选中。
3. 通过额外的 LLM 调用选择文件
selectRelevantMemories() 把用户问题和这份清单拼成一次独立的模型调用:
Query: <用户问题>
Available memories:
<manifest>
返回值被约束为:
{
"selected_memories": [
"release-merge-freeze.md"
]
}
最多选择 5 个文件。选择提示词还包含一个特殊规则:
- 如果某个工具最近已经成功使用,不要召回它的普通 API 文档;
- 但仍应召回该工具的警告(warning)、注意事项(gotcha)和已知问题(known issue)。
原因在于,成功调用已经表明模型掌握了工具的使用方式,重复注入 API 手册会造成上下文浪费;在工具使用期间,历史故障与风险提示具有更高的参考价值。
4. 文件读取、截断与注入
文件选定后,readMemoriesForSurfacing() 会限制正文读取量:每个文件最多注入前 200 行或前 4096 字节,并在任一上限达到时截断。
单轮最多注入 5 个文件,因此每轮的理论上限约为 20 KB。本会话累计注入上限约为 60 KB,超过该值后不再启动新的 memory prefetch。发生 compaction 后,旧附件会离开当前消息历史,累计值随之重置,相关记忆可以再次注入。
如果文件被截断,附件尾部会提示模型使用 Read 查看全文。最终内容以 relevant_memories attachment 的形式,经 <system-reminder> 包装后进入消息上下文。
5. 去重
Claude Code 从两个维度避免重复注入:
alreadySurfaced:过滤本会话之前已经自动召回过的路径;readFileState:过滤主 Agent 已经通过Read、Write或Edit接触过的文件。
系统会在 selector 调用前后分别执行过滤,尽可能将 5 个名额用于尚未注入的文件,同时避免多个 memory 目录或并发状态重新引入重复项。
6. 预取的非阻塞执行方式
这是理解实际召回效果的关键:startRelevantMemoryPrefetch() 在用户轮次(turn)开始时启动,但采用零等待策略,主模型不会等待 selector 返回,而是立即开始首次生成。
查询循环只会在一次工具执行结束、准备进入下一轮模型调用时检查预取是否完成:
这种设计隐藏了 selector 的延迟,代价是预取并不是一个“回答前必须完成”的同步 RAG 阶段:只有首轮产生了工具调用,预取内容才有机会进入下一次 agent-loop 迭代。因此,把通路 B 描述为“主模型开始生成前一定已经读取主题文件”并不准确。
六、通路 C:主 Agent 的主动工具检索
前两条自动通路仅向模型提供索引线索或部分正文。主 Agent 仍可自主决定是否调用以下工具:
Read release-merge-freeze.md
Grep "merge freeze" ~/.claude/projects/<project>/memory/
Glob **/*.md
该过程包含两个层面:
Grep、Glob和文件读取本身是确定性工具;- 决定何时调用、搜索什么关键词,仍然是主对话 LLM 的行为。
源码的 memory prompt 还会要求:
- 用户明确说“检查、回忆、记住”时必须访问 memory;
- memory 中的路径、函数和 flag 只是过去某个时间点的声明;
- 在给出当前建议前,应重新检查文件是否存在、函数是否仍能搜到;
- memory 与当前代码冲突时,以当前代码为准,并更新或删除旧记忆。
七、完整执行流程示例
把三条通路放回同一个问题上。假设 memory 目录中只有 MEMORY.md 和 release-merge-freeze.md,用户问“在向主干分支合并前,是否发生过与版本发布相关的事故?”
tengu_moth_copse=false 时走 A + C:索引里的那行摘要随会话上下文注入,主 Agent 可能据此直接作答,也可能再调用一次 Read 补齐事故原因与解冻条件。仅凭最终回答无法判断它究竟读没读主题文件。
tengu_moth_copse=true 时走 B + C:filterInjectedMemoryFiles() 先去掉 AutoMem 的 MEMORY.md,预取扫描主题文件并由 selector 选中 release-merge-freeze.md。若主 Agent 首轮产生了工具调用,正文的前 200 行/4 KB 会作为附件进入下一轮,不足则再由主 Agent 读取全文;若首轮直接给出答案,这份附件就赶不上本次回答。
两种模式下还有一条共同的兜底路径:只要用户把问题改成“检查长期记忆:……”,memory prompt 就会要求主 Agent 走通路 C 主动搜索。此时自动预取有没有触发——中文提问不含空格、会话预算耗尽或 flag 未开启——都不再影响结果。
八、记忆写入机制
除召回机制外,Claude Code 还需要确定记忆的写入主体与执行时机。写入有两条路径:主 Agent 在对话中直接写,或在 turn 结束后由后台 extraction Agent 补充写入。
1. 主 Agent 直接写入
loadMemoryPrompt() 会将完整的保存规则注入 system prompt。当用户明确提出持久化记忆要求时,主 Agent 先读取已有主题文件避免重复,再新建或更新主题文件;索引模式下还要同步更新 MEMORY.md,而 tengu_moth_copse=true 时的 skipIndex 模式省去了这一步。
2. 后台 extraction Agent
如果主 Agent 未写入 memory,查询循环结束后仍可触发 extractMemories:
主 Agent 输出最终回复
↓
handleStopHooks
↓
检查最近消息中是否已执行 memory Write/Edit
↓
没有则 runForkedAgent
↓
抽取并写入主题文件
该路径同样受 feature flag 和运行环境限制,并非在每个版本、每个账户或每个 turn 中都必然执行。
该后台 Agent 运行在权限受到严格限制的沙盒中:它可以使用 Read、Grep、Glob 和只读 Bash 查看内容,但 Write、Edit 只能作用于 auto-memory 目录;MCP、子 Agent 和具有写入能力的 shell 均不可用。它与主 Agent 的直接写入互斥,同一时刻仅运行一个抽取任务,最多执行 5 个 Agent turn。
3. 四类记忆
抽取提示词定义了一个封闭的分类体系(taxonomy):
| 类型 | 保存内容 | 示例 |
|---|---|---|
user | 用户角色、目标、偏好、知识水平 | “用户熟悉 Java,但刚接触前端” |
feedback | 用户对工作方式的纠正与肯定 | “无需在每次改动后重复总结 diff” |
project | 无法从代码推导的项目背景和决策 | “移动端新版本上线后方可解除 merge freeze” |
reference | 外部系统的入口 | “线上延迟看板位于某 Grafana dashboard” |
源码尤其强调不要保存:
- 代码模式、目录结构和架构快照;
- Git 历史和最近改动;
- 调试修复步骤;
- 已经写在
CLAUDE.md中的内容; - 当前任务进度和临时状态。
其目标不是节省磁盘,而是减少记忆漂移:代码是活的,几周前保存的文件路径可能已经变成错误信息。
九、总结
从 Agent 设计角度看,Claude Code Memory 机制的主要参考价值并非某个固定阈值,而是以下三个原则:
- 索引与正文分离,控制常驻上下文成本;
- 自动召回与显式工具互补,不把成功完全押在单一检索器上;
- 只保存无法从当前状态推导的知识,减少长期记忆漂移。