Claude Code 的三层记忆系统:从"一次性对话"到"有记性的搭档"

6 阅读15分钟

AI Agent 的记忆不是一个功能,而是三个:每轮对话内的工作笔记、跨轮次的持久记忆、跨会话的记忆整合。Claude Code 用 Session Memory + Auto Memory Extraction + Auto Dream 构成了一条完整的记忆流水线。它不是单一的“记忆文件”,而是三条后台 sidecar 工作流。

一、先讲结论

Claude Code 的记忆系统比"往文件里追加几行笔记"复杂得多。它是三层架构,解决三个不同时间尺度的问题:

层级名称时间尺度核心机制是否阻塞主对话
短期Session Memory当前会话后台 forked agent 维护结构化笔记
中期Auto Memory Extraction每轮对话结束后后台 forked agent 提取持久记忆到磁盘
长期Auto Dream跨 5+ 次会话后后台 forked agent 整合/修剪记忆目录

几个关键判断:

  • 三层都尽量不阻塞主对话。它们用的是逻辑上的 forked agent / 分叉子代理,不是操作系统层面的 fork() 子进程;核心收益是能复用父级 prompt cache,把额外成本压到一次旁路 LLM 调用附近。但三层的触发机制有差异:Session Memory 注册在 postSamplingHook(每次模型采样后触发),extractMemories 和 autoDream 则从 handleStopHooks(query loop 结束时)fire-and-forget
  • 三层之间有协作关系。在 tengu_sm_compact 打开且条件满足时,Session Memory 会被 autocompact 优先复用;Auto Memory Extraction 写入的文件会被 Auto Dream 整合
  • 主 Agent 和后台 Agent 互斥。更准确地说,这是 Auto Memory Extraction 的互斥策略:如果主 Agent 这一轮已经手动写了记忆文件,后台提取会跳过这一段——不会重复写
  • 持久记忆的读取不只有一种路径。基础形态是把 MEMORY.md 作为索引加载进上下文;实验分支则会在每轮 query 前按需召回最多 5 个相关 memory 文件

如果你在造自己的 Agent,这套设计最值得借鉴的不是具体阈值,而是 "后台 fork + prompt cache 共享 + 权限沙箱"这个模式——它让你在不增加用户感知延迟的前提下,给 Agent 加任何"旁路"能力。

二、它在解决什么问题

用过 ChatGPT 或任何 LLM 对话产品的人都知道这个痛点:昨天的对话,今天全忘了

对 Coding Agent 来说,这个问题更严重。你花了一小时教 Agent 理解项目的构建流程、测试命令、代码规范,结果第二天开个新会话,一切从零开始。更糟的是,即使在同一次会话中,当上下文被压缩(compact)后,很多细节也会丢失。

传统的解决方案是让用户手动维护一个 CLAUDE.md 配置文件。但问题在于:

  1. 用户懒得写。大部分人不会主动去更新配置文件
  2. 粒度不对。有些东西只在当前会话有用("我刚跑的测试命令是 npm test -- --grep auth"),不值得写进永久配置
  3. 记忆会过时。三个月前的项目结构笔记,今天可能已经完全不对了

Claude Code 的解决思路是:别让用户自己记,让 Agent 自己记。而且针对不同时间尺度的记忆需求,用不同的策略。

三、三层记忆的完整工作流

flowchart TD
    subgraph 短期["短期:Session Memory"]
        SM_T["触发条件:token增长≥5K<br/>且工具调用≥3次<br/>或对话自然间断"]
        SM_A["后台 forked agent<br/>用 Edit 工具更新笔记文件"]
        SM_F["输出:结构化 Markdown<br/>10 个固定 section"]
    end

    subgraph 中期["中期:Auto Memory Extraction"]
        AM_T["触发条件:每轮 query loop 收尾<br/>(最终回答、无 tool call)"]
        AM_A["后台 forked agent<br/>读对话 → 写记忆文件到 memory/"]
        AM_F["输出:topic files<br/>+ MEMORY.md 索引"]
    end

    subgraph 长期["长期:Auto Dream"]
        AD_T["触发条件:≥24小时<br/>且≥5个新会话"]
        AD_A["后台 forked agent<br/>读记忆 + 必要时 grep 转录 → 整合"]
        AD_F["输出:合并/修剪/更新<br/>记忆目录"]
    end

    SM_T --> SM_A --> SM_F
    AM_T --> AM_A --> AM_F
    AD_T --> AD_A --> AD_F

    SM_F -->|"被 Auto Compact 复用"| C["上下文压缩"]
    AM_F -->|"下次会话作为记忆上下文被加载"| SP["记忆上下文"]
    AD_F -->|"下次会话作为记忆上下文被加载"| SP

下面逐层拆解。

第一层:Session Memory —— 当前会话的自动笔记

一句话:在后台维护一份结构化的 Markdown 笔记文件,记录当前会话的关键信息。

模板结构(10 个固定 section):

# Session Title
# Current State        ← 当前正在做什么
# Task specification   ← 用户要求构建什么
# Files and Functions  ← 重要文件及其作用
# Workflow             ← 常用命令和执行顺序
# Errors & Corrections ← 遇到的错误和修复方式
# Codebase and System Documentation
# Learnings           ← 什么有效什么无效
# Key results         ← 用户要求的具体输出
# Worklog             ← 逐步操作日志

用户可以通过 ~/.claude/session-memory/config/template.md 自定义模板。

真正的会话笔记文件并不放在这个目录里,而是每个 session 一份,路径形态是 {projectDir}/{sessionId}/session-memory/summary.md~/.claude/session-memory/config/ 存的是模板和更新 prompt;后者也可以用 prompt.md 覆盖。

触发条件

DEFAULT_SESSION_MEMORY_CONFIG = {
  minimumMessageTokensToInit: 10_000,    // 首次提取:至少 10K token
  minimumTokensBetweenUpdate: 5_000,     // 后续更新:上次提取后至少增长 5K
  toolCallsBetweenUpdates: 3,            // 后续更新:至少 3 次工具调用
}

触发逻辑用人话说就是:token 增长够多 + 工具调用够多,或者 token 增长够多 + 对话出现自然间断(最后一轮 assistant 没有工具调用)。两个条件中 token 增长是必须条件——即使工具调用数达标,token 增长不够也不会触发,防止频繁提取。

更新方式:Session Memory 不是追加写入,而是让 forked agent 用 Edit 工具就地编辑笔记文件的每个 section。Prompt 中有严格约束:

"NEVER modify, delete, or add section headers... ONLY update the actual content that appears BELOW the italic section descriptions"

每个 section 有 ~2K token 上限,总文件上限 12K token。超标时 prompt 会加入 CRITICAL 级别的压缩指令。

和上下文压缩的协作:这是 Session Memory 最巧妙的用法,但原理比“直接复用一下”更细一点。源码里有一条专门的 sessionMemoryCompact 分支:autocompact 触发后,会先等正在进行的 Session Memory 提取结束,再尝试直接用 summary.md 构造 compact summary。成功时,就不会再额外发一次 compact LLM 请求;如果 feature gate 没开、Session Memory 还是空模板、或者边界算不出来,才回退传统 compact。所以更准确的说法不是“永远零调用”,而是“优先走一条零额外 compact 调用的快路径”。

第二层:Auto Memory Extraction —— 持久化的长期记忆

一句话:每轮 query loop 收尾后,后台 forked agent 从对话中提取值得长期保留的信息,写入 ~/.claude/projects/<slug>/memory/ 目录。

和 Session Memory 的区别:Session Memory 只在当前会话有效,关掉就没了。Auto Memory Extraction 写入磁盘的文件是跨会话持久的。基础形态下,下次会话会把 MEMORY.md 这个索引加载进上下文;更激进的实验路径则会在 query 时动态召回相关 memory 文件。

运行时机:与 Session Memory 不同,它不是注册在 postSamplingHook 上(Session Memory 才是),而是 query loop 结束时由 handleStopHooks() fire-and-forget 触发。具体触发点是"模型给出最终回答、query loop 不再继续"。是否启用先看 isExtractModeActive() / tengu_passport_quail,频率再由 tengu_bramble_lintel 控制(默认每个 eligible turn 都可跑一次)。此外,它设置了 skipTranscript: true,不写入会话转录文件,避免与主线程的竞态条件。

光标追踪:用 lastMemoryMessageUuid 记录上次处理到哪条消息。每次只看新增的消息,避免重复提取。有一个重要的边界情况处理:

// 如果UUID没找到(比如被compaction删了),
// 回退到统计全部消息——不能返回0,那会永久禁用提取
if (!foundStart) {
  return count(messages, isModelVisibleMessage)
}

硬上限:forked agent 的 turn 数被限制为 maxTurns: 5。源码注释写得很直白:"Well-behaved extractions complete in 2-4 turns (read → write). A hard cap prevents verification rabbit-holes from burning turns." 这防止了提取 agent 陷入验证代码、反复 grep 源文件的死循环。

尾随提取:如果当前提取还在 inProgress 时又来了一个 eligible turn,不会丢弃也不会阻塞——而是把最新 context 暂存到 pendingContext,等当前提取结束后做一次 trailing run。游标已经推进了,所以尾随提取只处理两次调用之间新增的消息,不会重复。

主 Agent 互斥:如果主 Agent 在对话中已经手动写了记忆文件:

if (hasMemoryWritesSince(messages, lastMemoryMessageUuid)) {
  // 跳过提取,只推进游标
  lastMemoryMessageUuid = lastMessage.uuid
  return
}

这避免了主 Agent 和后台 Agent 写同一个文件的冲突。

权限沙箱:forked agent 不是全权操作——它的工具权限被严格限制:

允许限制
Read / Grep / Glob(无限制)Edit / Write 只能写 memory/ 目录
Bash 只能执行只读命令MCP / Agent 等全部拒绝
REPL(ant 模式下的统一入口)
if (tool === REPL) allow()  // REPL 模式下原始工具被隐藏,REPL 的 VM 会对每个内层操作重新走 canUseTool
if (tool === Read || tool === Grep || tool === Glob) allow()
if (tool === Bash && isReadOnly(input)) allow()
if ((tool === Edit || tool === Write) && isAutoMemPath(file_path)) allow()
deny()

值得注意的是 REPL 那行:当 REPL 模式开启时(ant-default),原始的读写工具被隐藏,forked agent 只能通过 REPL 调用它们。REPL 的 VM 上下文会对每个内层原始操作(Read / Bash / Edit 等)重新走上面的 canUseTool 检查。之所以不给 fork 一个不同的工具列表,是因为工具列表是 prompt cache key 的一部分(见 CacheSafeParams),换列表就破坏了缓存共享。

Memory Manifest 预注入:提取前会先扫描 memory 目录,把现有文件列表(最多 200 个)作为 manifest 注入 prompt,让 forked agent 知道已有哪些记忆文件,避免创建重复文件。

MEMORY.md 的截断保护

MAX_ENTRYPOINT_LINES = 200
MAX_ENTRYPOINT_BYTES = 25_000   // ~125 chars/line × 200 lines

先按行截断(自然边界),再按字节截断(兜底)。超标时会在末尾附上一段 warning,明确告诉模型:因为行数或字节超限,只加载了部分索引内容。

一个容易漏掉的实现:它不只靠 MEMORY.md 全量注入

如果只看目录结构,你很容易以为 Claude Code 的持久记忆读取方式就是“把 MEMORY.md 整包塞进 prompt”。源码里其实已经多走了一步。

tengu_moth_copse 打开时,filterInjectedMemoryFiles() 会把 AutoMem / TeamMem 的 MEMORY.md 从常驻上下文里过滤掉,改成 query-time recall:

  1. 先扫描 memory 目录里最多 200 个 .md 文件的 frontmatter(不含 MEMORY.md
  2. 用一个 side query 让 Sonnet 从候选里挑出最多 5 个当前 query 明显相关的 memory(还会过滤掉已经在当前会话中展示过的文件 alreadySurfaced,以及当前正在使用的工具的 API 文档——避免噪声)
  3. 把这几个文件以 user message attachment 的形式注入,而不是把整个索引常驻在上下文里

同时,tengu_moth_copse 还会影响 extractMemories 的行为:提取 prompt 切换到 skipIndex 模式,不再要求后台 agent 更新 MEMORY.md 索引——因为 recall 系统不再依赖索引,直接走 frontmatter 扫描 + Sonnet 选择。

这很关键,因为它说明 Claude Code 已经不满足于“加载一个索引文件”,而是在往按 query 动态召回记忆这条路走。索引更省 token,真正展开的是当前问题最相关的那几条记忆。

第三层:Auto Dream —— 跨会话的记忆整合

一句话:当积累了足够多的会话后,自动触发一次"梦境"——回顾所有记忆文件和会话转录,合并重复、修正过时信息、修剪冗余。

这是最"高级"的一层。名字叫"Dream"是因为它的行为确实像人类睡眠中的记忆巩固——不是新增信息,而是整理和优化已有记忆

四步 gating(cheap-to-expensive 顺序检查)

源码文件顶部注释只列了三步(Time → Sessions → Lock),但实际实现有四步——扫描节流是后加的优化,防止 time-gate 通过但 session-gate 不通过时每轮都扫目录:

// ① 时间门:距上次整合 ≥ 24 小时(一次 stat 调用)
const hoursSince = (Date.now() - lastAt) / 3_600_000
if (hoursSince < cfg.minHours) return

// ② 扫描节流:距上次扫描 ≥ 10 分钟(纯内存比较,避免每轮都扫目录)
if (sinceScanMs < SESSION_SCAN_INTERVAL_MS) return

// ③ 会话门:≥ 5 个新会话(需要扫描转录目录)
sessionIds = await listSessionsTouchedSince(lastAt)
// 排除当前会话
sessionIds = sessionIds.filter(id => id !== currentSession)
if (sessionIds.length < cfg.minSessions) return

// ④ 锁门:防止多个进程同时做 consolidation
const priorMtime = await tryAcquireConsolidationLock()
if (priorMtime === null) return

为什么是这个顺序?因为 ① 只做一次 stat() 系统调用,② 是纯内存比较,③ 需要扫描磁盘目录,④ 才是真正加锁。最便宜的检查先做,不通过就不做后面的检查

锁机制:用文件锁(memory/.consolidate-lock)防止多个进程同时触发 Dream。锁的 mtime 就是上次整合时间,锁的内容是持有者的 PID。获锁失败(mtime 新鲜且 PID 存活)→ 静默跳过;PID 已死或超过 1 小时(HOLDER_STALE_MS)→ 直接接管。Dream 出错 → 回滚 mtime 让下次检查能重新触发;但用户主动取消 → 不回滚(DreamTask.kill 自行处理)。autoDream 同样设置了 skipTranscript: true,不写入主对话的转录文件。

整合 Prompt 的四个阶段

Phase 1 — Orient(定位)
  ls 记忆目录 → 读 MEMORY.md → 浏览已有 topic 文件

Phase 2 — Gather(采集新信号)
  读每日日志 → 找过时的记忆 → grep 转录(窄搜索)

Phase 3 — Consolidate(整合)
  合并新信号到已有文件 → 把相对日期转绝对日期 → 删除被推翻的旧事实
  
Phase 4 — Prune and Index(修剪索引)
  更新 MEMORY.md → 保持 ≤200 行 ≤25KB → 删除过时指针

Prompt 特别强调:不要穷举式阅读转录(JSONL 文件很大),只在有明确线索时 grep 特定关键词。这是成本控制的关键——否则一次 Dream 可能消耗几十万 token。

如果继续深挖源码,还会看到一个 KAIROS / assistant-mode 变体:新的记忆不一定直接写 topic files,而是先 append 到 logs/YYYY/MM/YYYY-MM-DD.md 这种按天切分的 log,再由后续的 dream 过程蒸馏成 MEMORY.md + topic files。这个分支不是本文主线,但它暴露了 Anthropic 的另一个想法:写入时先走 append-only 日志,整理时再做 consolidation

用户可见性:Dream 运行时在 Tasks 面板中显示进度,用户可以随时取消。完成后会在主对话中显示 "Improved N memory files" 的系统消息。

四、一个完整的记忆生命周期

假设你连续三天用 Claude Code 做项目:

时间事件触发的记忆操作
Day 1 10:00开始新会话,token 到 10KSession Memory 首次初始化
Day 1 10:15完成一轮工具调用Session Memory 更新(5K delta + 3 calls)
Day 1 10:20模型给出最终回答(无 tool call)Auto Memory Extraction 触发,写 2 个记忆文件
Day 1 10:30上下文压缩触发优先尝试 Session Memory Compact;命中则直接用现成笔记构造摘要,否则回退普通 compact
Day 1 10:45会话结束
Day 2 09:00新会话开始加载持久记忆索引到上下文;必要时按 query 召回相关 topic files
Day 2 09:10对话进行中Session Memory 重新初始化(新会话新文件)
Day 3 下午第 5 次会话,距首次 >24hAuto Dream 触发:整合 5 个会话的记忆

注意 Day 2 开始时——新会话已经能拿到 Day 1 提取出来的持久记忆,用户什么都不用做。差别只在于:有的分支是直接带上 MEMORY.md 索引,有的分支是等 query 来了再动态召回最相关的 memory 文件。

五、和其他方案的对比

维度Claude CodeCursorWindsurf手动 CLAUDE.md
记忆层数3(短期+中期+长期)1(Rules)1(Memories)1(纯手动)
自动提取后台 forked agent简单追加
记忆整合Auto Dream(合并+修剪)手动维护
会话内复用Session Memory → Compact不适用
用户干预零(全自动)需手动写 Rules半自动全手动
成本每次提取约一次 LLM 调用不明
过时处理Dream 自动修剪手动手动

Claude Code 的优势在于全自动 + 多层次。短板是成本——每次 forked agent 提取都是一次 LLM 调用,虽然有 prompt cache 共享降低成本,但长时间使用的累计开销不可忽视。

六、局限与坑

1. Session Memory 的模板是刚性的

10 个固定 section 不一定适合所有工作流。比如做数据分析的用户可能不需要 "Workflow" section 但需要 "Dataset Schema" section。虽然支持自定义模板,但大部分用户不会去配置。

2. 记忆提取质量取决于模型能力

forked agent 本质上还是让 LLM 做"阅读理解+笔记整理"。更关键的是,提取 prompt 明确要求它只依据最近对话内容写 memory,不要再去 grep 代码或读源码验证。这让流程更便宜,但也意味着它写入的是“对话里被说过且模型认为值得记住的事实”,不一定是“再次核实后的事实”。一旦写错,后续会话里它就很容易再次进入上下文或被召回,放大效应很强。

3. Dream 不能跨项目整合

Auto Dream 只在项目级别运行(~/.claude/projects/<slug>/memory/)。如果你在多个相关项目间切换,每个项目的记忆是独立的。

4. 截断保护可能丢信息

MEMORY.md 的 200 行 + 25KB 硬限意味着记忆量有上界。对于积累了大量记忆的长期项目,截断不可避免。Auto Dream 的修剪设计可以缓解这个问题,但不能根本解决。

5. 后台 fork 的隐性成本

虽然 forked agent 共享 prompt cache,但每次提取仍然需要生成新内容(output tokens)。在高频对话场景下(每分钟多轮工具调用),Session Memory 的更新频率可能导致显著的 token 消耗。

七、最后总结

Claude Code 的记忆系统回答了一个根本问题:Agent 的记忆应该由谁来维护?

答案是三层:

  • 短期记忆由 Session Memory 自动维护,解决"压缩后不丢关键信息"的问题
  • 中期记忆由 Auto Memory Extraction 自动提取,解决"下次会话还记得上次做了什么"的问题
  • 长期记忆由 Auto Dream 自动整合,解决"记忆越来越乱越来越旧"的问题

这三层的默认实现都在复用同一个架构模式:后台 forked agent + prompt cache 共享 + 权限沙箱

如果让我总结一条最核心的设计洞察,那就是:好的 Agent 记忆系统不是一个功能,而是一条流水线。采集(Session Memory)→ 持久化(Extraction)→ 整合(Dream),每层的成本和时间尺度不同,但共同构成了从"一次性工具"到"长期搭档"的进化路径。