2.2 Memory System¶
本节摘要
Claude Code 的Memory 系统有一个核心理念:用 Markdown 文件作为「外部大脑」,用 LLM 做检索引擎。本节按「记了什么 → 怎么写入 → 怎么召回」的主线展开,重点解析 CLAUDE.md 五层体系的内容设计和四种Memory 类型的自动提取机制。
一、CLAUDE.md 五层体系:记了什么¶
CLAUDE.md 是 Claude Code 的全部持久化Memory 载体——没有数据库,没有向量索引,就是 Markdown 文件。五层从组织级到自动生成,形成一个完整的知识覆盖体系。
五层结构总览¶
| 优先级 | 层级 | 路径 | 记录什么 | 谁来写 |
|---|---|---|---|---|
| 1(最低) | Managed | /etc/claude-code/CLAUDE.md + .claude/rules/*.md |
组织安全策略:「禁止执行 rm -rf」「必须使用公司 VPN」 | IT 管理员 |
| 2 | User | ~/.claude/CLAUDE.md + ~/.claude/rules/*.md |
个人全局偏好:「我偏好 TypeScript」「使用 Vim 键位」 | 用户手动 |
| 3 | Project | 从项目根到 CWD 逐级 CLAUDE.md |
项目约定:「使用 pnpm」「测试覆盖率 > 80%」「API 风格用 RESTful」 | 团队共享(committed) |
| 4 | Local | CLAUDE.local.md(gitignore) |
个人项目配置:「我负责 auth 模块」「本地 DB 端口 5433」 | 用户手动(不提交) |
| 5(最高) | AutoMem | ~/.claude/projects/<slug>/memory/MEMORY.md |
对话中自动提取的 Memory(详见下节「四种Memory 类型」) | Agent 自动写入 |
优先级规则:高层覆盖低层同名配置。AutoMem 优先级最高但内容最不可靠(机器生成),Managed 优先级最低但最权威(管理员设定)。
Project 层:逐级加载¶
Project 层的「逐级加载」在 monorepo 中尤为重要。以 repo/packages/web/ 为工作目录时,依次加载:
repo/CLAUDE.md ← 全仓库约定("用 pnpm workspace")
→ repo/packages/CLAUDE.md ← packages 层约定("共享组件在 @repo/ui")
→ repo/packages/web/CLAUDE.md ← web 子包约定("用 Next.js App Router")
内层覆盖外层。每一级同时检查 <dir>/CLAUDE.md、<dir>/.claude/CLAUDE.md、<dir>/.claude/rules/*.md 三个位置。
截断保护¶
为防止过大的 CLAUDE.md 挤占Context Window,truncateEntrypointContent() 做双重限制——先限行再限字节:
| 常量 | 值 | 说明 |
|---|---|---|
MAX_ENTRYPOINT_LINES |
200 | 硬性行数上限 |
MAX_ENTRYPOINT_BYTES |
25,000 | 25KB 硬性字节上限 |
MAX_MEMORY_CHARACTER_COUNT |
40,000 | 单文件建议最大字符数 |
为什么需要双重限制?源码注释揭示了原因:p100 observed: 197KB under 200 lines——有用户在 CLAUDE.md 里塞了 197KB 的超长行。
二、四种 Memory 类型:怎么写入¶
CLAUDE.md 前四层(Managed → Local)由人工维护。第五层 AutoMem 由后台 Fork Agent 自动提取——这是 Claude Code 最「AI-native」的设计。
提取机制:后台 Fork Agent¶
每次模型产生最终响应后,后台启动一个独立的 Fork Agent(extractMemories.ts),分析刚结束的对话片段。整个过程不阻塞主对话——用户感知到的是模型已经回复完毕。
graph LR
A["模型完成回复"] --> B["后台启动 Fork Agent\nmaxTurns=5, 超时 60s"]
B --> C["分析对话片段\n判断是否有值得保存的信息"]
C --> D["按四种类型分类\n写入 AutoMem 目录"]
| 参数 | 值 |
|---|---|
maxTurns |
5(最多 5 轮对话) |
drainTimeout |
60,000ms(60 秒超时) |
| 节流 | GrowthBook tengu_bramble_lintel(默认每 1 个合格 turn 触发) |
| 工具权限 | FileRead/Grep/Glob(无限制)、Bash(只读)、FileWrite 仅限Memory 目录 |
四种 Memory 类型¶
Fork Agent 将提取的信息按语义分为四种类型(memoryTypes.ts),写入 AutoMem 目录下的对应文件:
| 类型 | 记录什么 | 来源示例 | 写入示例 |
|---|---|---|---|
user |
用户角色、目标、技能背景 | 对话中用户自述 | "用户是 web 前端开发者,3 年经验,偏好 TypeScript" |
feedback |
用户对 Agent 行为方式的反馈 | 用户纠正或明确要求 | "不要在 commit message 里加 emoji""先问再改,不要自作主张" |
project |
项目架构、正在进行的工作 | 对话中涉及的项目信息 | "正在从 Express 迁移到 Fastify""monorepo,pnpm workspace" |
reference |
不容易从代码推导的外部知识指针 | 用户提到的地址、文档位置 | "staging 环境:https://staging.example.com""API 文档在 docs/api/" |
什么不应该保存¶
同样重要的是 WHAT_NOT_TO_SAVE——一份即使用户明确要求也会拒绝保存的负面清单:
| 不保存的信息 | 原因 |
|---|---|
| 代码模式 / 架构 | 可以通过读代码获得 |
| 文件路径 | 会变化,可以通过搜索获得 |
| API 签名 | 可以通过阅读源码获得 |
| Git 历史 | 已有版本控制系统 |
| 调试方案 | 一次性的,下次场景不同 |
| 已在 CLAUDE.md 中的内容 | 避免重复 |
设计原则:只记住 Agent 无法从环境中重新获得的信息。
三、Memory 召回:Sonnet 异步预取¶
Memory 写入了文件,下一个问题是——怎么在需要时找回来?
Claude Code 的做法是:每个用户 turn,用 Sonnet 并行预取最相关的Memory 文件,与主 LLM 查询同时执行,不增加用户感知延迟。
三步流水线¶
graph LR
A["Step 1: 扫描\n≤200 个文件\n每文件前 30 行\n~5ms"] --> B["Step 2: Sonnet 选择\n独立 Sonnet 调用\nmax_tokens=256\n选 ≤5 个文件\n~200ms"]
B --> C["Step 3: 注入\n每文件 ≤4KB\n会话累计 ≤60KB\n~10ms"]
| 步骤 | 做什么 | 关键参数 |
|---|---|---|
| 扫描 | 读取 memory 目录文件的 frontmatter | 最多 200 个文件,每文件前 30 行,按修改时间排序 |
| Sonnet 选择 | 用 Sonnet 判断哪些文件与当前查询最相关 | JSON schema 输出,最多选 5 个文件 |
| 注入 | 将选中文件内容追加到用户消息 | 每文件 ≤4KB / 200 行,会话累计 ≤60KB |
整个过程与主 LLM 查询并行(GrowthBook flag tengu_moth_copse 控制),用户感知延迟 = max(召回, 主查询),几乎免费。
为什么用 Sonnet 而不用向量检索?
传统 RAG 用 Embedding 做向量检索。Claude Code 用 Sonnet 读 frontmatter 判断相关性——精确度高,不需要维护向量索引。代价是每 turn 消耗一次 Sonnet 调用,但对 Anthropic 而言边际成本极低。
Memory 信任验证¶
召回后还有一道防线——TRUSTING_RECALL 在使用 Memory 前验证引用的有效性:
- Memory 提到文件路径 →
fs.existsSync检查是否仍存在 - Memory 提到函数名 →
grep确认函数是否还在代码中
防止「幻觉 Memory」误导模型(如Memory 写着"项目用 Express",但实际已迁移到 Fastify)。
四、辅助机制¶
Session Memory:会话级摘要¶
除了跨会话的长期 Memory(CLAUDE.md),Claude Code 还在会话内维护 Session Memory(sessionMemory.ts)——后台 Fork Agent 周期性提取当前对话要点,写入 summary.md。与Context 压缩联动:L4 压缩会利用已提取的 Session Memory 替代完整历史。
Sub-agent 独立 Memory¶
不同 Agent 类型维护独立的Memory 目录(agentMemory.ts),按 user / project / local 三种 scope 映射到不同路径,互不干扰。
KAIROS 夜间 Memory 整理¶
KAIROS 助理模式下的 DreamTask 在空闲时自动整理 Memory,经过 orient → gather → consolidate → prune 四阶段(类似人类睡眠时的 MemoryMemory 巩固固)。跨会话锁 consolidationLock 确保不会重复执行。
关键文件索引¶
| 文件 | 职责 |
|---|---|
src/utils/claudemd.ts |
CLAUDE.md 五层加载、合并、@include 解析 |
src/memdir/memdir.ts |
MEMORY.md 截断算法(行 + 字节双重限制) |
src/memdir/memoryTypes.ts |
四种Memory 类型定义 + WHAT_NOT_TO_SAVE |
src/services/extractMemories/extractMemories.ts |
后台 Fork Agent 自动提取 |
src/memdir/memoryScan.ts |
召回 Step 1:扫描 |
src/memdir/findRelevantMemories.ts |
召回 Step 2:Sonnet 选择 |
src/utils/attachments.ts |
召回 Step 3:注入 |
src/services/SessionMemory/sessionMemory.ts |
会话级摘要 |
src/tools/AgentTool/agentMemory.ts |
Sub-agent 独立 Memory |
src/tasks/DreamTask/DreamTask.ts |
KAIROS 夜间整理 |