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 历史不记。这些信息随时可以从代码库实时获取,存成记忆是冗余。

这个约束在做梦时同样生效。子代理整理记忆的时候,如果发现某条记忆记的是可以从当前代码推导出来的东西,应该删掉。

做梦不只是把散落的便签整理成笔记本,也是把不再需要的便签扔进垃圾桶。在这件事上花的工程量,不比决定"记住什么"少。