Claude Code 101|09|Memory
Claude Code 的 Memory 不是更长的 system prompt,而是由 MEMORY.md、topic files、相关性召回和 AutoDream 组成的文件化长期记忆系统。
Agent 的长期记忆最容易被做成一个看似有效、实际危险的功能:把用户偏好、项目事实、历史结论不断追加到 system prompt 后面。这个方案在 demo 里很直观,因为下一轮模型确实能“记住”刚才写入的东西;但一旦进入真实工程,它很快会同时触发三个问题:上下文预算被旧信息占用,过期事实持续污染判断,临时任务进度和长期知识混在一起。
Claude Code 的 Memory 更像一个外部化的长期状态系统。它不是“把历史塞回 prompt”,而是把长期状态放到文件化结构中,用入口索引、主题文件、相关性召回和后台整理共同维护。模型看到的不是全部记忆,而是当前任务所需的一小部分投影。
这一篇要回答的工程问题是:一个长期工作的 Coding Agent 如何把历史经验变成可召回、可修正、可压缩的外部状态,而不是把上下文窗口变成越来越脏的垃圾堆。
工程问题
简单的 run(prompt) 不需要 Memory。它只处理一次请求,没有长期偏好,没有跨会话项目约定,也不需要从过去错误中学习。但 Claude Code 面对的是长期工程工作:用户可能反复在同一个仓库里修改功能,可能有固定代码风格,可能有测试命令约定,可能已经发现某个工具链坑点,可能明确要求某些目录不要碰。
这些信息如果每次都靠用户重新说明,Agent 的使用成本会很高;如果全部写进 prompt,Agent 又会越来越难控制。真正的问题不是“如何保存更多文本”,而是:
- 哪些信息值得长期保存。
- 保存在哪里,如何避免入口文件无限膨胀。
- 当前任务应该召回哪些记忆。
- 记忆过期后如何被修正或删除。
- 历史会话如何被压缩成稳定知识。
Claude Code 的答案是把 Memory 放到 runtime 外部,作为一种可读写、可索引、可整理的持久状态。上下文窗口只拿到被选中的片段,Memory 系统本身负责存储和治理。
概念边界
前一篇讨论 File、Bash、Web、LSP 四类工具如何连接真实工程边界。工具让 Agent 能观察和改变世界,Memory 则解决另一个问题:这些观察中哪些应该跨会话留下来。
后一篇 Session Resume 会讨论 transcript 如何重放成运行时状态。Resume 处理的是“这场会话进行到哪里”;Memory 处理的是“跨很多会话仍然有用的稳定知识”。二者都使用持久化,但语义完全不同:transcript 是事件日志,Memory 是整理后的长期状态。
在 Claude Code 这个案例里,Memory 并不是一个单点功能,而是一组围绕长期状态生命周期的机制:写入、扫描、召回、提示、压缩、去重和清理。
机制一:入口索引与主题文件分层
长期记忆不能只靠一个不断增长的文件。单文件方案一开始很省事,但随着内容增加,它会变成一个没有边界的混合物:用户偏好、项目架构、临时 TODO、旧 bug、过期命令、一次性计划全部挤在一起。模型每次读取它,都会被迫在大量无关信息中寻找当前任务需要的部分。
Claude Code 的文件化 Memory 使用入口索引与主题文件分层。入口文件承担导航作用,主题文件承载更细的长期内容。这个结构的关键不是“多几个文件”,而是把长期记忆从一段 prompt 文本变成一个可维护的信息空间。
这种分层的重点不是文件名,而是信息空间的形状:Memory 需要入口索引、主题内容、扫描逻辑、结构化记录、漂移提示和使用约束。尤其重要的一点是,记忆只能作为历史上下文,不能替代当前观察。
这个分层解决的是上下文预算问题。入口索引帮助 Agent 知道“可能有什么”,主题文件帮助 Agent 在需要时读取“具体是什么”。如果所有内容都平铺到入口文件,索引和内容会混成一体;如果只有主题文件没有入口,召回又会变成盲扫。
机制二:相关性召回,而不是全量注入
Memory 的价值不在于模型能看到多少历史,而在于它能看到多少相关历史。Claude Code 的记忆召回不是把整个 memory 目录都塞入上下文,而是先扫描候选,再基于当前任务选择少量内容。
这与检索增强生成的思想类似,但它服务的是 Agent Runtime:当前 query、最近工具使用、记忆类型、主题标题、manifest 或 header 等信号共同决定哪些记忆进入本轮上下文。src/memdir/memoryScan.ts、src/memdir/memoryAge.ts 和 src/services/extractMemories/extractMemories.ts 展示了扫描、年龄、抽取和相关性处理的边界;src/memdir/memoryTypes.ts 中对不同记忆类别和漂移风险的建模,也说明召回并不只是字符串匹配。
这里有一个容易忽略的工程点:召回系统需要抑制“看似相关但价值低”的内容。例如当前任务正在使用某个工具,系统不应该因此反复召回普通 API 文档;但如果记忆是 warning、gotcha、项目特殊约束,则应该有更高优先级。长期记忆不是文档搜索,它必须知道哪些历史事实会影响行动风险。
对于 Coding Agent,这一点尤其关键。旧的构建命令、曾经失败的 migration、用户明确禁止的目录、某个测试环境的非直觉行为,都可能影响后续工具调用。如果召回太宽,模型会被噪声干扰;如果召回太窄,Agent 会重复踩坑。
机制三:写入边界由提示协议约束
Memory 写入不是“模型觉得重要就保存”。成熟 Agent 必须告诉模型什么可以进入长期记忆,什么只能留在当前会话中。否则 Memory 会迅速膨胀为任务日志。
Claude Code 通过 memory prompt、remember skill 和相关工具约定来表达边界。src/skills/bundled/remember.ts 体现了“保存长期可用信息”的入口;src/commands/memory/memory.tsx 说明 Memory 也需要用户可操作的管理界面;src/utils/memoryFileDetection.ts 则从工程侧处理记忆文件识别。
一个可用的写入边界大致是:
- 用户稳定偏好可以保存,例如语言、风格、工作流偏好。
- 项目约定可以保存,例如测试命令、目录规则、部署约束。
- 工具坑点可以保存,例如某命令必须带特定参数。
- 稳定事实可以保存,但需要在未来使用前验证。
- 临时计划、当前 TODO、已完成步骤、一次性调试过程不应保存为长期记忆。
这里的关键是“未来复用价值”。Memory 不是为了完整记录过去,而是为了降低未来任务的协调成本。对 Agent 来说,任务进度属于 session state,行动历史属于 transcript,长期知识才属于 Memory。
机制四:AutoDream 是后台 compaction
长期记忆如果只追加不整理,最终会失败。它需要像数据库一样做 compaction:把原始日志中的稳定信号提取出来,合并到主题文件,删除重复或过期内容,重建入口索引。
Claude Code 的 AutoDream 就承担这个角色。它不是普通命令,而是后台整理流程:受配置控制,有触发门槛,有 lock 防止并发整理,并通过任务机制运行。
AutoDream 的工程含义可以类比 log-structured storage:
- transcript 和 daily log 是 append-only 原始信号。
- Memory topic files 是压缩后的状态。
- 入口索引是可快速定位的 manifest。
- consolidation lock 是并发控制。
- 后台任务避免在用户交互路径上做重整理。
这解释了为什么 Memory 不能只依赖即时写入。即时写入适合用户明确要求“记住这个”;后台整理适合从多次会话中归纳稳定模式。二者结合,长期状态才不会既缺失又臃肿。
机制五:记忆必须承认漂移
代码仓库会变化,用户偏好会变化,工具链会变化。长期记忆如果被当成真理,就会制造错误。Claude Code 在 src/memdir/memoryTypes.ts 中明确表达了 memory drift caveat:记忆可能过期,使用前应根据当前文件或资源验证;如果召回记忆与当前观察冲突,应相信当前观察,并更新或删除过期记忆。
这条规则对 Coding Agent 非常重要。比如记忆里保存了“项目使用 pnpm test”,但仓库后来迁移到 uv 或 bun;或者记忆里保存了“API 在 v1 目录”,但代码已经重构。Agent 如果把旧记忆当作事实,会比没有记忆更危险,因为它会自信地执行错误操作。
因此 Memory 系统必须同时支持 recall 和 invalidation。能记住只是第一步,能忘掉和修正才是工程能力。
TypeScript-style pseudocode
下面的伪代码不复刻 Claude Code 的真实函数名,而是抽象它的架构形状:
type LongTermMemory = {
entryIndex: MemoryIndexFile
topicFiles: MemoryTopicFile[]
rawSignals: TranscriptLog[]
policies: MemoryPolicy
}
type RecallRequest = {
userIntent: string
projectContext: string
recentTools: string[]
riskHints: string[]
}
async function recallForTurn(memory: LongTermMemory, request: RecallRequest) {
const manifest = await scanIndexAndHeaders(memory.entryIndex, memory.topicFiles)
const candidates = rankMemoryCandidates(manifest, {
query: request.userIntent,
cwdContext: request.projectContext,
recentTools: request.recentTools,
preferWarnings: true,
penalizeGenericDocs: true,
})
const selected = candidates.slice(0, memory.policies.maxFilesPerTurn)
const records = await readSelectedTopics(selected)
return records.map(record => ({
...record,
caveat: 'verify against current repository state before acting',
}))
}
async function writeMemory(signal: CandidateMemory, memory: LongTermMemory) {
if (!hasFutureReuseValue(signal)) return { saved: false }
if (isTemporaryTaskState(signal)) return { saved: false }
const target = chooseTopicFile(signal, memory.topicFiles)
await mergeWithoutDuplicating(target, signal)
await refreshEntryIndex(memory.entryIndex, memory.topicFiles)
return { saved: true, target }
}
async function compactMemory(memory: LongTermMemory) {
if (!passedTimeGate()) return
if (!passedSessionGate()) return
if (!(await acquireConsolidationLock())) return
try {
const signals = await gatherStableSignals(memory.rawSignals)
const grouped = groupByLongTermTopic(signals)
await mergeTopics(grouped, memory.topicFiles)
await pruneStaleAndDuplicateRecords(memory.topicFiles)
await rebuildIndex(memory.entryIndex, memory.topicFiles)
} finally {
await releaseConsolidationLock()
}
}
这个抽象里有三个分离:召回与写入分离,即时保存与后台整理分离,历史事实与当前验证分离。缺少任何一个分离,Memory 都会退化为 prompt 追加器。
工程启发
第一,长期记忆应该是外部状态,而不是上下文窗口的一部分。上下文窗口是执行时投影,Memory 目录才是持久存储。把二者混在一起,会导致 prompt 越来越长,也会让记忆无法被治理。
第二,Memory 要先设计信息生命周期,再设计存储格式。一个成熟系统至少需要创建、读取、更新、删除、合并、去重、过期和验证。只提供 append API,等于把维护成本推给未来的模型。
第三,入口索引很重要。很多团队会直接把向量库当 Memory,但没有可读入口和主题组织,调试会非常困难。文件化 Memory 的好处是人和模型都能检查它,发现污染后也能修正。
第四,后台整理应该脱离用户交互路径。用户发起请求时,Agent 应尽快完成当前任务;大规模整理历史适合放在后台,受锁和门槛控制。AutoDream 的意义就在于把记忆维护变成 runtime housekeeping,而不是每轮对话的负担。
第五,必须显式处理漂移。Memory 里保存的不是永恒事实,而是“某时刻观察到的事实或偏好”。Coding Agent 在行动前仍要读取当前仓库状态。好的 Memory 会减少重复探索,而不是取代观察。
小结
Claude Code 的 Memory 可以概括为“文件化长期状态 + 相关性召回 + 后台 compaction + 漂移治理”。它解决的不是模型记性差,而是长期 Agent 如何管理跨会话知识的问题。
这一层把工具使用中沉淀下来的稳定信号外部化,让未来会话可以按需取回。但 Memory 不负责恢复一场具体会话进行到哪里;那是下一篇 Session Resume 的问题。Resume 会把 transcript 当作事件源,从历史日志中重建 runtime state。Memory 记住长期知识,Resume 恢复运行位置,两者共同构成 Coding Agent 的持久性基础。