犀牛鸟 2026 研究笔记 estelledc.github.io

Ch15: Claude Code 记忆——索引驱动的实时学习

Part 4: 生产级 Coding Agent 记忆系统 前置章节:Ch14: MemOS——记忆操作系统与 Dream Pipeline 后续章节:Ch16: Codex 记忆——AI 驱动的批量整合


15.1 一句话定位

Claude Code 是 Anthropic 的 Coding Agent,采用文件索引 + LLM 语义检索两层架构,以”实时学习”为哲学——每次对话结束就写入记忆,每次新对话就从索引选相关记忆注入上下文,形成一个持续学习的闭环。

15.2 日常类比:个人图书馆 + 图书管理员

想象你有一个个人图书馆:

关键洞察:管理员挑书不是靠关键词匹配,而是靠理解你的问题语义后做判断。这就是”LLM 做语义筛选”——和传统向量搜索不同,不是算余弦相似度,而是让大模型自己判断”哪些记忆和当前任务相关”。

类比的边界

图书馆类比 对应物 类比失效之处
书架目录 MEMORY.md 目录是静态的,MEMORY.md 由 AI 自动维护
具体的书 记忆文件 (.md) 书不自动更新,记忆文件会由 autoDream 整合
图书管理员 Sonnet 模型 管理员不会写新书,Sonnet 会实时写入新记忆
借书限额 最多 5 个文件/轮 借书限额是经济考量,这里是认知负载考量

15.3 读取路径详解

Claude Code 的记忆读取分为两个阶段:启动时全量注入索引每轮对话按需注入记忆文件。这不是全量把所有记忆塞进去(太占上下文),也不是纯向量搜索(精度不够),而是”索引 + 按需检索”的混合模式。

15.3.1 启动阶段:MEMORY.md 全量注入

当你启动 Claude Code 时(比如 claude 命令或新开一个 session),系统会:

  1. 读取项目根目录下的 MEMORY.md 文件
  2. 将其全量注入到 system prompt 中
  3. 这个文件是索引,包含所有记忆文件的标题、路径和一行摘要

约束条件:

参数 限制 原因
行数 不超过 200 行 system prompt 空间有限
文件大小 不超过 25KB 同上
记忆文件总数 不超过 200 个 索引行数限制的自然结果

MEMORY.md 的典型结构:

# Memory Index

## User Preferences
- [pnpm-preference](.claude/memory/pnpm-preference.md): 项目使用 pnpm,不用 npm
- [dark-theme](.claude/memory/dark-theme.md): 用户偏好深色主题

## Project Conventions
- [api-error-format](.claude/memory/api-error-format.md): API 错误响应统一用 {code, message, data} 格式
- [test-location](.claude/memory/test-location.md): 测试文件放在 src/__tests__/ 下

## Feedback History
- [review-2026-06-20](.claude/memory/review-2026-06-20.md): code review 要求函数注释加 @example

## Reference
- [internal-api-doc](.claude/memory/internal-api-doc.md): 内部 API 文档地址和认证方式

为什么全量注入索引而不是全量注入内容? 因为 200 个记忆文件如果全量注入,每个文件平均 200 行就是 40000 行,远超上下文窗口。而索引只有 200 行,相当于”先看目录再选书”。

15.3.2 每轮对话:LLM 语义检索注入

在每轮对话中,Claude Code 的读取流程是:

  1. 用户发送消息
  2. Sonnet 模型同时处理用户消息和 MEMORY.md 索引
  3. Sonnet 从不超过 200 个记忆文件中选出不超过 5 个最相关的文件
  4. 被选中的文件全量注入到当前对话的上下文中
  5. Sonnet 基于用户消息 + 注入的记忆生成响应

约束条件:

参数 限制 原因
每轮选入文件数 不超过 5 个 控制认知负载,5 个是经验值
单文件行数 不超过 200 行 单个记忆粒度不宜过大
单文件大小 不超过 4KB 同上
Session 累计记忆大小 不超过 60KB 上下文窗口预算分配

语义筛选 vs 向量搜索:传统 RAG 系统用 embedding 向量做余弦相似度搜索,优点是快(毫秒级),缺点是语义理解浅——”pnpm” 和 “package manager” 可能向量距离远。Claude Code 让 Sonnet 自己做筛选,理解更深,但代价是每轮对话多一次 LLM 推理调用。

15.3.3 “索引 + 按需检索” 模式定位

在记忆检索的设计空间中,有三种典型方案:

方案 代表 优点 缺点
全量注入 Codex 的 memory_summary.md 简单,零延迟 内容量受限于上下文窗口
纯向量搜索 传统 RAG 快,可扩展 语义理解浅,recall 不稳定
索引 + LLM 按需检索 Claude Code 语义精度高 每轮多一次 LLM 调用,成本高

Claude Code 选了第三种,本质是用 LLM 的理解能力换检索精度。代价是每轮对话的 Sonnet 调用成本增加(筛选 5 个文件也需要一次推理),但好处是不会把无关记忆塞进上下文浪费 token。

15.4 写入路径详解

Claude Code 的记忆写入是”实时”的——每次对话结束后就写入,不等批量整合。这是”Claude Code 记忆哲学”的核心:学习不能等,每次交互都是学习机会

15.4.1 extractMemories:Forked Agent 机制

写入不是由主对话的 Sonnet 完成的,而是由一个 forked agent 完成:

主对话(Sonnet)
  │
  ├─ 用户消息 → 生成响应 → 返回用户
  │
  └─ 响应中无工具调用时
       │
       └─ 异步启动 extractMemories agent
            │
            ├─ 共享主对话的 prompt cache(不重复计算)
            ├─ maxTurns = 5(最多 5 轮工具调用)
            └─ 分析对话内容 → 写入记忆文件 → 更新 MEMORY.md 索引

什么是 forked agent? 它是从主对话 fork 出来的一个独立 Sonnet 实例,共享主对话的 prompt cache(前面已经计算的 KV cache 可以复用,省 token),但有自己的工具调用权限和回合限制。

为什么 fork?

  1. 不阻塞主对话:用户看到响应后就可以继续输入,记忆写入在后台默默进行
  2. 共享 prompt cache:复用主对话的 KV cache,写入 agent 不需要重新理解整个对话上下文
  3. 限制回合数:maxTurns=5 防止写入 agent 无限循环(比如反复读写文件)

15.4.2 Fire-and-Forget 模式

extractMemories 是 fire-and-forget 的:

这意味着:

  1. 记忆写入失败不影响主对话——最坏情况是这轮对话的内容没被记住,下一轮还能正常工作
  2. 记忆写入延迟不影响用户体验——用户不需要等”记忆写入完成”才能继续
  3. 记忆写入可能和用户的下一轮输入并发——如果用户很快回复,写入还没完成,新消息照样处理

15.4.3 四类型分类

extractMemories 会将提取的记忆分为四类:

类型 语义 示例 存储位置
user 用户偏好和习惯 “我喜欢用 vim 快捷键” .claude/memory/user/
feedback 显式反馈(纠正、偏好声明) “不要用 var,用 const” .claude/memory/feedback/
project 项目级约定和发现 “这个项目用 Next.js App Router” .claude/memory/project/
reference 参考信息(URL、文档位置) “API 文档在 internal.dev/api” .claude/memory/reference/

分类的目的是什么? 为了让 MEMORY.md 索引有结构——按类型分组,Sonnet 做语义筛选时能更快定位。同时也影响记忆的淘汰优先级:feedback 类(用户显式声明)通常优先级最高,reference 类(可能过时的 URL)优先级最低。

15.4.4 明确排除规则

不是所有对话内容都值得记忆。extractMemories 有明确的排除规则:

排除 1:可推导信息

如果一条信息可以从已有代码或配置推导出来,不需要记忆。比如:

排除 2:CLAUDE.md 已有内容

CLAUDE.md(项目指令文件)已经包含的信息不需要重复记忆。比如:

为什么排除? 因为记忆空间有限(200 个文件),每存一条无用记忆就挤占一条有用记忆的位置。排除可推导信息和已有信息,是保证记忆信噪比的核心策略。

15.5 autoDream 整合机制

记忆写入是实时的,但实时写入会积累碎片——同一主题可能分散在多个文件中,过时信息可能和最新信息矛盾。autoDream 就是定期整理的机制。

15.5.1 类比延伸:图书馆的年度盘点

如果 extractMemories 是”每天把新书放上架”,autoDream 就是”每年盘点一次,把过时的书淘汰,把同主题的合并,更新目录”。

15.5.2 触发条件

autoDream 不是每轮都跑,而是有明确的触发门槛:

条件 原因
距上次 Dream 时间 超过 24 小时 太频繁浪费算力,太稀疏碎片积累
最少 session 数 至少 5 次 确保有足够新记忆值得整合

两个条件同时满足才触发。这意味着:

15.5.3 整合操作

autoDream 做两件事:

  1. 整合去重:扫描所有记忆文件,找到内容重叠或主题相同的,合并为一个文件
  2. 合并相似记忆:比如 pnpm-preference.mdpackage-manager.md 可能说的同一件事,合并后只保留一个

整合完成后会更新 MEMORY.md 索引,确保索引和实际文件一致。

15.5.4 autoDream 的局限

15.6 Freshness Caveat:时间衰减警告

记忆不是越老越可信。Claude Code 对超过 1 天的记忆加 freshness caveat(新鲜度警告)。

15.6.1 机制

当 Sonnet 从记忆索引中选中一个文件准备注入时,会检查该记忆的最后修改时间

警告格式类似:

[注意:此记忆最后更新于 2026-06-20,信息可能已过时]

15.6.2 为什么是 1 天?

1 天的阈值偏保守(很多记忆一周后仍然有效),但这是刻意的设计选择:

15.6.3 Freshness Caveat 的局限

15.7 团队记忆:多层路径安全设计

Claude Code 的记忆不仅服务个人,还支持团队共享。但共享带来了安全问题——你不能让项目的记忆覆盖个人的偏好,也不能让队友的敏感信息泄露给你。

15.7.1 三层记忆路径

~/.claude/memory/          ← 用户全局记忆(跨项目共享)
  ├── user/
  │   └── editor-vim.md    ← "我喜欢 vim 快捷键"
  └── feedback/

项目根/.claude/memory/     ← 项目记忆(团队共享)
  ├── project/
  │   └── api-format.md    ← "API 用 {code, message, data}"
  └── reference/

.claude/memory/local/      ← 本地记忆(不提交 git)
  └── temp-workaround.md   ← "CI 第 3 步要加 --force"

15.7.2 读取优先级

当多层有同主题记忆时,优先级从高到低:

  1. 项目记忆:项目约定优先于个人偏好(在项目语境下)
  2. 用户全局记忆:个人偏好跨项目生效
  3. 本地记忆:最低优先级,通常是不重要的临时信息

15.7.3 安全设计

安全措施 说明
路径隔离 项目记忆和全局记忆在不同目录,不会互相覆盖
Git 忽略 .claude/memory/local/.gitignore 中,临时记忆不提交
权限检查 extractMemories 写入前检查目标路径是否在允许列表中
敏感信息排除 写入时检测并排除 token、密码、API key 等敏感信息
跨项目隔离 全局记忆只存用户偏好,不存项目特定信息,防止项目 A 的记忆污染项目 B

15.7.4 团队共享的实际挑战

Claude Code 当前的方案偏保守——项目记忆由 git 管理版本(可回滚),全局记忆仅限用户偏好,没有跨用户的记忆共享机制。这避免了冲突但限制了团队级知识的积累。

15.8 核心权衡:精度 vs 成本

Claude Code 记忆系统的设计选择处处是权衡,核心矛盾是:

LLM 语义检索精度高,但成本高;实时写入保证不丢信息,但增加后台开销。

15.8.1 语义检索的成本分析

每轮对话的记忆筛选都需要一次 Sonnet 推理调用。假设:

单次筛选成本约 $0.006,看起来不多,但一个 session 可能 20 轮对话,一天可能 5 个 session:

$0.006 × 20 轮 × 5 session = $0.6/天

一个月下来就是 ~$18 的额外成本,全部花在”选哪 5 个记忆文件”上。这是纯向量搜索不需要的开销。

15.8.2 实时写入的开销分析

extractMemories 的开销更隐蔽:

一天 5 个 session,每个 session 假设 10 次无工具响应(触发 10 次 extractMemories):

10 次 × 5 session = 50 次/天的后台写入

50 次后台 Sonnet 推理,每次约 500 output tokens,成本约 $0.375/天。加上 autoDream 的定期整合,记忆相关的总成本可能占 Claude Code 日常使用成本的 10-15%。

15.8.3 精度收益

这些成本换来的是什么?

15.8.4 权衡总结

维度 选择 收益 代价
检索方式 LLM 语义筛选 高精度 每轮多一次推理调用
写入时机 实时 fire-and-forget 不丢信息 后台 Sonnet 实例开销
索引方式 MEMORY.md 全量注入 简单可靠 200 文件上限
新鲜度 1 天二元判断 实现简单 不区分稳定/临时记忆
整合频率 24h + 5 session 避免碎片 整合本身消耗算力

15.9 与传统记忆方案对比

特性 Claude Code 传统 RAG 简单全量注入
检索方式 LLM 语义筛选 向量余弦相似度 无检索,全量塞入
检索精度 N/A(全量)
检索延迟 ~1s(LLM 推理) ~10ms 0
写入方式 实时异步 离线 embedding 实时
写入延迟 <2s 分钟级(建索引) <1s
上下文效率 高(只注入 5 个) 中(top-K 可能含噪音) 低(全量占空间)
成本 高(每轮额外推理) 低(向量搜索便宜) 中(token 成本)
可扩展性 受 200 文件限制 强(百万级向量) 弱(上下文窗口限制)

15.10 实战观察:Claude Code 记忆的行为模式

15.10.1 记忆注入的”命中”与”未命中”

在实际使用中,语义筛选并非完美:

命中场景(记忆被正确注入):

未命中场景(记忆应该注入但没注入):

15.10.2 记忆写入的”过度记忆”问题

extractMemories 有时会写入不该记的内容:

这些”噪音记忆”会挤占索引空间,降低整体信噪比。autoDream 的整合可以部分缓解,但不完美。

15.10.3 MEMORY.md 的”漂移”问题

MEMORY.md 索引和实际记忆文件之间可能出现不一致:

Claude Code 没有内置的索引一致性检查,这是当前架构的一个弱点。

15.11 章节小结

Claude Code 的记忆系统是一个索引驱动的实时学习方案,核心设计可以概括为三句话:

  1. 读时精选:MEMORY.md 索引全量注入,Sonnet 每轮从 200 个记忆中选 5 个注入——用 LLM 的理解能力换检索精度,代价是每轮多一次推理调用。
  2. 写时即记:extractMemories 在每轮对话后异步启动,fire-and-forget 地写入新记忆——保证不丢信息,代价是后台 Sonnet 实例开销。
  3. 定期整理:autoDream 在 24h + 5 session 后整合去重——解决碎片问题,但整合本身消耗算力且可能丢失细节。

这三个环节形成闭环:实时写入积累记忆 → 索引驱动精准检索 → 定期整合防止碎片 → 检索精度保持高位。闭环的代价是额外的 LLM 调用成本(约占 10-15%),换来的收益是不丢失任何学习机会的高精度记忆系统。

下一章我们将看 Codex 的截然不同方案——不是实时写入,而是批量整合;不是索引 + LLM 筛选,而是全量注入摘要。两种哲学的对比会揭示记忆设计空间的核心权衡。


导读目录:README.md 上一章:Ch14: MemOS——记忆操作系统与 Dream Pipeline 下一章:Ch16: Codex 记忆——AI 驱动的批量整合