Claude做梦机制:后台记忆碎片整理
Claude Code 内部有一个叫 autoDream 的模块。它的 Prompt 标题是"Dream: Memory Consolidation"。
这不是什么隐喻。Claude Code 确实会在后台启动一个子代理,回顾过去多个会话的记录,把零散的记忆整理、去重、纠错,然后写回磁盘。整个过程你看不到,除非刻意去翻后台任务列表。
记便签和整理笔记本
Claude Code 有两套记忆机制,分工很明确。
extractMemories 在每轮对话结束后跑。它只看当前会话最近新增的消息,觉得有什么值得长期记住的就写一个记忆文件。你说"我们项目用 bun 不用 npm",它就把这条偏好存下来。很快,2-4 轮结束,像随手在便签上记一笔。如果你在对话里已经自己让 Claude 写了记忆,它就跳过,不重复干活。
autoDream 完全是另一回事。它不看单次对话,而是攒够了好几个会话之后才启动,通读过去的记录和已有的记忆文件,把分散在多个文件里的信息合并,把过时的记忆删掉,把索引控制在合理的长度。
一个记,一个整理。
三道门
autoDream 不是随便就能跑的。它有三层检查,从便宜到贵依次过,任何一层没过就跳过:
+------------------+ +------------------+ +------------------+
| Gate 1: Time |---->| Gate 2: Sessions |---->| Gate 3: Lock |
| | | | | |
| >= 24h since | | >= 5 sessions | | No other dream |
| last dream? | | touched since? | | in progress? |
| | | | | |
| Cost: 1 stat() | | Cost: dir scan | | Cost: file I/O |
+------------------+ +------------------+ +------------------+
| | |
No: skip No: skip No: skip
这个排列顺序本身就是一个设计选择。第一道门读一次文件修改时间,成本几乎是零。24 小时内做过梦就直接跳过,后面两道门不用看。第二道门要扫目录,稍微贵一点,所以放第二。扫描还做了 10 分钟的节流,避免时间门通过但会话门没过的情况下反复遍历目录。
第三道门最有意思。同一个项目可能开了好几个 Claude Code
实例,不能让它们同时做梦。锁的设计是这样的:一个
.consolidate-lock
文件,身兼三职。文件的修改时间就是"上次做梦是什么时候",省掉了额外的时间戳存储。文件内容写着持有锁的进程
PID,其他进程看到 PID 还活着就让出。锁有 1 小时的
TTL,超时强制回收,防止 PID
被操作系统重用后造成永久死锁。用伪代码表示:
def try_acquire_lock():
lock_path = memory_dir / ".consolidate-lock"
# 读取现有锁的 mtime 和持有者 PID
mtime, holder_pid = read_lock(lock_path)
# 锁还没过期,且持有者还活着 —— 让出
if mtime and (now - mtime) < 1_hour:
if holder_pid and is_process_running(holder_pid):
return None
# 写入自己的 PID
write(lock_path, str(my_pid))
# 二次确认:两个进程同时写,最后一个写入的赢
if read(lock_path) != str(my_pid):
return None # 竞争失败
return mtime or 0 # 返回旧 mtime,用于失败时回滚
这个 write-then-verify 不能完全防竞态。但做梦这件事本身是幂等的,最坏情况就是两个进程同时做了一次梦,一个的结果被覆盖。下次做梦会重新整理,所以丢一次没什么损失。
这种"因为操作幂等,所以锁可以不完美"的思路贯穿了整个 autoDream 的设计。做梦失败了?回滚锁文件的修改时间,下次会话还会再试。子代理把记忆文件写乱了?下次做梦会重新审视。不追求每一步都正确,而是保证系统整体能收敛到正确状态。
Prompt 里藏着的取舍
三道门都过了之后,autoDream 启动一个子代理,Prompt 分四个阶段:
+-------------------------------------------------------+
| Dream Prompt |
| |
| Phase 1: Orient |
| ls memory dir, read MEMORY.md index, |
| skim existing topic files |
| | |
| v |
| Phase 2: Gather |
| 1. Check daily logs (highest priority) |
| 2. Find drifted memories |
| 3. Grep transcripts for narrow terms (last resort) |
| | |
| v |
| Phase 3: Consolidate |
| Merge related memories, fix stale facts, |
| convert relative dates to absolute |
| | |
| v |
| Phase 4: Prune |
| Update MEMORY.md index (keep < 200 lines), |
| remove stale pointers, resolve contradictions |
+-------------------------------------------------------+
Phase 1 和 Phase 4 都挺常规的。Phase 1 看看记忆目录里现在有什么,Phase 4 把索引文件控制在 200 行、25KB 以内。
Phase 2 才是有意思的地方。它要求子代理去找新信息,但给了一个明确的优先级:日志文件排第一,已有记忆和代码库现状的矛盾排第二,通读会话记录排最后。Prompt 原文说的是"不要从头到尾读记录,只搜你认为重要的关键词"。
这是一个很实际的 token 预算取舍。会话记录可能非常长,让子代理从头读一遍会烧掉大量输入 token。但完全不看又可能漏掉重要信息。折中方案是让子代理自己判断搜什么,用 grep 按关键词精准捞。这把"读多少"的决策权交给了模型本身,在成本和覆盖率之间找平衡。
Phase 3 有一条不起眼但很关键的规则:"上周""昨天"这类相对时间要转成绝对日期。记忆文件可能在写入后几周甚至几个月才被再次读到,那时候"上周"指的是哪周就完全不确定了。
子代理的权限也值得一提。它能读任意文件、搜索代码库、用 grep 翻会话记录,但只能编辑记忆目录下的文件。不能跑 git、npm,不能调 MCP 工具,不能启动其他子代理。不能 rm 文件,但可以通过 Write 清空内容。这保证了做梦不会对项目代码产生任何副作用。它跟主对话共享 Prompt Cache,缓存读取只要十分之一的成本,所以做一次梦实际花不了多少钱。
做错梦
做梦的子代理本身就是一个 LLM,而 LLM 整合信息的时候会幻觉。合并两条记忆时可能引入原始记录里不存在的细节,"纠错"的时候可能把对的改成错的。
但回到前面说的那个核心思路:做梦是幂等的。这次整理出了问题,下次做梦会重新审视同一批文件。一条被错误修改的记忆,有机会在下个周期被纠正。记忆文件是纯文本,你随时可以打开看看它到底记了什么,觉得不对直接改。
这跟人类记忆倒是有点像。你确定小时候那件事是那样发生的吗?大脑每次回忆都在重新建构。区别在于,你没法打开自己的海马体检查 diff。
学会忘
记忆系统有一段明确的规范:代码模式不记,架构不记,文件结构不记,git 历史不记。这些信息随时可以从代码库实时获取,存成记忆是冗余。
这个约束在做梦时同样生效。子代理整理记忆的时候,如果发现某条记忆记的是可以从当前代码推导出来的东西,应该删掉。
做梦不只是把散落的便签整理成笔记本,也是把不再需要的便签扔进垃圾桶。在这件事上花的工程量,不比决定"记住什么"少。