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 配置文件。但问题在于:
- 用户懒得写。大部分人不会主动去更新配置文件
- 粒度不对。有些东西只在当前会话有用("我刚跑的测试命令是
npm test -- --grep auth"),不值得写进永久配置 - 记忆会过时。三个月前的项目结构笔记,今天可能已经完全不对了
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:
- 先扫描 memory 目录里最多 200 个
.md文件的 frontmatter(不含MEMORY.md) - 用一个 side query 让 Sonnet 从候选里挑出最多 5 个当前 query 明显相关的 memory(还会过滤掉已经在当前会话中展示过的文件
alreadySurfaced,以及当前正在使用的工具的 API 文档——避免噪声) - 把这几个文件以 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 到 10K | Session 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 次会话,距首次 >24h | Auto Dream 触发:整合 5 个会话的记忆 |
注意 Day 2 开始时——新会话已经能拿到 Day 1 提取出来的持久记忆,用户什么都不用做。差别只在于:有的分支是直接带上 MEMORY.md 索引,有的分支是等 query 来了再动态召回最相关的 memory 文件。
五、和其他方案的对比
| 维度 | Claude Code | Cursor | Windsurf | 手动 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),每层的成本和时间尺度不同,但共同构成了从"一次性工具"到"长期搭档"的进化路径。