乐于分享
好东西不私藏

AI智能体100讲|第3讲:Claude Code 如何治理长期记忆

AI智能体100讲|第3讲:Claude Code 如何治理长期记忆

长期记忆的核心,不是“记更多”,而是“可治理”

上一讲我们看了 Claude Code 的 System Prompt。

它解决的是:

Agent 当前怎么行动;哪些动作可以做;哪些动作要谨慎;工具应该怎么用;输出应该怎么收束。

这一讲看 Durable Memory。

它解决的是另一个问题:

换一个会话之后,哪些信息还应该继续影响 Agent?

很多人一听到 Agent memory,第一反应是:

把聊天记录存起来;下次再塞给模型;这样模型就不会失忆。

Claude Code 不是这么做的。

它的 Durable Memory 不是聊天记录仓库,也不是一个越写越长的对话摘要。

它更像一个被 Prompt 规则治理的小型知识库:

memory/├── MEMORY.md├── user_role.md├── feedback_testing.md├── project_release_context.md└── reference_ci_dashboard.md

这个设计最有意思的地方,不是“它会记住东西”。

而是 Claude Code 把长期记忆拆成了一套完整的治理链路:

system 参数里放规则;side query 里做召回选择;messages 里注入相关记忆;请求结束后再用 forked agent 做后台兜底提取。

也就是说,Durable Memory 不是一个单独的 Memory Tool。

它是一套由四个 prompt/template 位置协同完成的小型知识库治理系统。

1. Memory Lines   主 Agent System Prompt 里的长期记忆制度。2. Relevant Memory Selector Prompt   独立 side query 里的召回路由器。3. Relevant Memory Injection Template   把选中的记忆片段注入 messages 的包装模板。4. Memory Extraction Prompt   请求结束后 forked agent 的后台提取任务说明。

这四个位置,是理解 Claude Code Durable Memory 的主线。

这一讲就沿着这四个位置展开。

全文约 10000 字,预计阅读 25 分钟。

如果你时间有限,先看第二章的全局流程和第十章的设计提炼。

不过在进入四个位置之前,先把 Durable Memory 和其他几种“保存信息”的机制分清。


一、先分清边界:Durable Memory 不是什么

在讲机制之前,先把边界分清。

Claude Code 里不止一种“保存信息”的机制。

如果把它们混在一起,很容易误解 Durable Memory。

Durable Memory:  跨会话长期知识。未来多个 conversation 仍然可能有用。Session Notes:  当前 session 的工作现场。主要服务当前会话继续推进。Compact Summary:  当前上下文被压缩后,重新进入 messages 的接续摘要。Plan:  当前任务打算怎么做。Task / Todo:  当前任务拆成哪些步骤。

Durable Memory 关注的是:

未来会话还应该知道什么?

Session Notes 和 Compact 更关注的是:

当前会话怎么接着干?

Plan 和 Todo 更关注的是:

当前任务怎么推进?

这几个边界一旦混淆,问题会很明显。

比如,当前任务里的临时状态不应该写进 Durable Memory:

今天临时修到某个测试失败;刚刚 checkout 到某个分支;当前有一个还没提交的中间改动;

这些信息一旦跨会话保存,下一次新会话就可能被过期现场污染。

反过来,长期协作偏好也不应该只留在 Session Notes 里:

用户更喜欢先看风险,再看实现;用户要求不要自动提交;某个项目的发布流程需要先走内部审批;

这些信息如果只保存在当前 session,下一次打开就丢了。

所以 Claude Code 把 Durable Memory 单独拆出来治理。

它不是“所有历史都值得保存”。

它问的是:

哪些信息值得在未来会话继续影响 Agent?


二、全局流程:四个 prompt/template 各管什么

先看完整链路。

用户发起 query  ↓主 Agent 的 System Prompt 中包含 Memory Lines  告诉主 Agent:什么该记、什么不该记、怎么保存、怎么读取、怎么验证  ↓startRelevantMemoryPrefetch  针对当前 query 异步扫描 memory 文件  ↓Relevant Memory Selector Prompt  根据 memory manifest 选择最多 5 个相关 topic memory files  ↓Relevant Memory Injection Template  读取选中文件正文,并包装成 role=user + isMeta + <system-reminder>  ↓主 Agent 正常回答 / 使用工具  如果用户明确要求 remember / forget,或主 Agent 判断需要保存  可以直接 Write / Edit memory 文件  ↓query 结束后的 stop hooks  如果主 Agent 已经写过 memory,后台 extraction 跳过  如果主 Agent 没写,启动 forked memory extraction agent 兜底

这个流程里最关键的是四个 prompt/template 位置:

Memory Lines  所在层:主 Agent 的 system 参数,属于 System Prompt 的动态 system section  作用:规定主 Agent 如何使用、保存、删除、更新、验证 memorySelector Prompt  所在层:独立 side query 的 system prompt + user prompt  作用:从 memory manifest 里选出当前 query 明确需要的 memory 文件Injection Template  所在层:主 Agent 的 messages  作用:把选中的 memory 正文包装成 meta user contextExtraction Prompt  所在层:forked agent 的 messages  作用:请求结束后分析最近新增消息,兜底更新 memory

这套说法要和上一讲的 request 分层对齐。

system 参数:  放 Claude Code 拼装后的 System Prompt。  Memory Lines 属于这里。messages:  放 conversation history、tool_result、current user message,  也会放 role=user + isMeta=true 的 meta user context。  relevant memory 正文属于这里。side query:  Claude Code 额外发起的小型模型请求。  Selector Prompt 在这里运行,不是主 Agent 的 System Prompt。forked agent:  query 结束后 fork 出来的后台 Agent。  Memory Extraction Prompt 是发给它的 user prompt。

所以不要把这些都混成一句“Claude Code 有 Memory Prompt”。

更准确的说法是:

Memory Lines 管制度;Selector Prompt 管召回;Injection Template 管上下文注入;Extraction Prompt 管后台提取和更新。

三、Prompt 1:Memory Lines,主 Agent 的长期记忆治理制度

第一个位置,是主 Agent System Prompt 里的 Memory Lines。

它通过动态 system section 加入主 Agent 的 System Prompt:

systemPromptSection('memory', () => loadMemoryPrompt())

这里要先分清:Memory Lines 不等于 memory 文件正文。它是一套治理制度,告诉主 Agent:

什么类型的信息可以保存;什么信息不能保存;用户要求 remember / forget 时怎么处理;什么时候应该访问 memory;召回 memory 后怎么验证;memory 文件应该怎么组织;

它大致由这些部分组成:

# auto memory  持久 memory 文件系统路径如果用户明确要求 remember / forget:  保存或删除对应 memory## Types of memory  user / feedback / project / reference## What NOT to save in memory  规定哪些东西不要进长期记忆## How to save memories  规定 topic files、frontmatter、MEMORY.md index 等格式## When to access memories  什么时候必须读 memory,什么时候要忽略 memory## Before recommending from memory  使用 memory 前要验证当前事实

这一章分五步讲完:

3.1 四类 memory type 和文件组织方式3.2 MEMORY.md 为什么只是索引3.3 什么不能记,也就是排除规则3.4 使用前为什么要验证3.5 主 Agent 怎么写入和更新

先看类型,再看结构,再看边界,再看验证,最后看写入。

3.1 四类 memory type 是封闭分类

Claude Code 源码里定义的 memory type 只有四类:

userfeedbackprojectreference
user:  用户是谁、用户的长期偏好、协作方式。feedback:  用户对 Agent 工作方式给过的反馈。project:  代码里看不出来、但对项目协作有长期意义的背景。reference:  外部系统、文档、仪表盘、服务入口等长期可复用引用。

容易误解的是:type 只有四类,但文件不是只有四个

同一个 type 下面可以有多个 topic memory files:

memory/├── MEMORY.md├── user_collaboration_style.md        # type: user├── user_review_preference.md          # type: user├── feedback_testing_process.md        # type: feedback├── project_release_context.md         # type: project└── reference_ci_dashboard.md          # type: reference

所以:

type 是分类标签;file 是具体主题记忆;MEMORY.md 是入口索引。

3.2 MEMORY.md 是索引,不是正文仓库

MEMORY.md 不是把所有长期记忆都塞进去的大文件,而是入口索引。它有明确读取限制:

MAX_ENTRYPOINT_LINES = 200MAX_ENTRYPOINT_BYTES = 25_000

所以 MEMORY.md 的职责应该是:

这里有哪些 topic memory files;每个文件大概讲什么;什么情况下应该打开哪个文件。

真正的记忆正文放在独立 topic memory file 里。每个 topic file 需要 frontmatter,尤其是:

namedescriptiontype

其中 description 非常关键。

后面的 Selector Prompt 不是先读取所有正文再判断相关性,而是主要看 memory manifest:

filenametypedescriptionmtime

所以 description 不是装饰字段,而是召回路由器判断相关性的核心输入。

3.3 真正关键的是规定什么不能记

长期记忆最危险的地方,不是记得太少,而是把会过期、会误导、可从当前状态重新读取的信息写进去。

Claude Code 的 Memory Lines 里有明确的 What NOT to save in memory

核心思想可以概括成一句话:

能从当前代码库、当前文件系统、CLAUDE.md、git 状态里重新获得的信息,不要写进长期记忆。

典型不该保存的内容包括:

代码模式、架构结构、文件路径;某个函数当前在哪里;最近谁改了什么;git history / 当前分支状态;刚刚修过的某个 bug 的临时过程;当前任务的中间状态;CLAUDE.md 已经明确写过的内容;

这些信息短期内可能有用,但很容易过期。一旦写进 Durable Memory,未来会话可能会把过期事实当成当前事实。

所以即使用户说“记住这个”,Claude Code 的规则也不是无脑写入。

它会要求 Agent 判断:

这是不是长期有用?未来会话真的需要吗?是否已经可以从项目文件里重新读到?是不是只是当前任务现场?

这就是 Prompt 软规则治理的特点:代码提供工具和边界,Prompt 负责语义判断。

3.4 使用 memory 前要验证当前事实

Memory Lines 里还有一个容易被忽略的动作:

## Before recommending from memory

也就是:

在基于 memory 给建议之前,先确认它仍然符合当前事实。

例如:

memory 说某个文件存在:  先检查文件是否还存在。memory 提到某个函数、配置或 flag:  先 grep 当前代码库。memory 和当前状态冲突:  以当前状态为准,并考虑更新或删除旧 memory。

这里先点到为止,后面第七章会单独讲这个信任边界。

3.5 主 Agent 怎么写入和更新 memory

Claude Code 没有一个神秘的 Memory Tool。

主 Agent 更新 memory,本质上还是用普通文件工具:

ReadWriteEditGrepGlob

典型流程是:

新增 memory:  1. 先检查是否已有相关 topic file  2. 如果没有,Write 新文件  3. 必要时 Edit / Write 更新 MEMORY.md index更新 memory:  1. Read 原 topic file  2. 用 Edit 修改已有内容  3. 避免重复创建相似文件删除 / forget:  1. 先定位相关 memory  2. 删除或修改对应内容  3. 必要时更新 MEMORY.md index

这也是这个模块有意思的地方:它不是硬编码一个数据库 CRUD 流程,而是让主 Agent 在 Prompt 规则约束下,像维护小型知识库一样维护 memory 文件。


四、Prompt 2:Selector Prompt,召回路由器

第二个位置,是 Relevant Memory Selector Prompt。

它解决的问题是:

当前 query 进来时,应该召回哪些 memory 文件?

Claude Code 不是每轮把所有 memory 文件都塞进上下文。

它会先做一次轻量召回选择。

流程大致是:

每个用户 turn 进来  ↓startRelevantMemoryPrefetch()  ↓扫描 memory directory  ↓读取每个 .md 文件的 frontmatter / header  ↓构造 memory manifest  ↓用 Selector Prompt 发起 side query  ↓返回 selected_memories: string[]

这里有几个重要细节。

4.1 它不是每次都强制召回

源码里会尝试在每个用户 turn 启动 prefetch。

但不是所有输入都会进入召回。

常见 gate 包括:

auto memory 是否开启;feature gate 是否开启;是否是最后一条真实 user message;输入是否太短;当前 session 已经注入的 relevant memory 是否超过阈值;

也就是说,它不是“每次 query 都把 memory 拿出来”。

更准确地说:

每次用户 turn 都可能触发异步 prefetch,但会经过多道 gate,符合条件才做 selector。

这个 prefetch 是异步的,而且主 Agent 不会为了它阻塞等待。

源码里的注释很直接:

Fired once per user turnConsume point polls settledAt (never blocks)

也就是说,Claude Code 在 query loop 入口启动一次 memory prefetch。

后面到了消费点,只检查它是否已经完成:

如果 side query 已经完成:  消费结果,把 relevant memory attachment 加入 messages,供后续模型调用使用。如果 side query 还没完成:  不等待,直接跳过。  如果主 Agent 后面还有下一轮 loop iteration,再重新检查。

所以这不是:

用户 query  ↓等待 Selector Prompt 完成  ↓主 Agent 第一次 LLM call

而更像:

用户 query  ↓启动 Selector side query  ↓主 Agent 继续跑  ↓后续消费点检查 side query 是否已经完成  ↓完成则注入;没完成则 zero-wait 跳过

4.2 Selector 看的是 manifest,不是所有正文

memory 扫描会递归读取 memory 目录下的 .md 文件。

但它不会把所有正文都交给 selector。

扫描阶段主要提取:

filenamefilePathmtimeMsdescriptiontype

并且有上限:

MAX_MEMORY_FILES = 200FRONTMATTER_MAX_LINES = 30

MEMORY.md 会被排除在 topic memory files 扫描之外。

最后会形成类似这样的 manifest:

- [user] user_collaboration_style.md (saved 2026-xx-xx): User prefers risk-first answers.- [project] project_release_context.md (saved 2026-xx-xx): Release process and approval context.- [reference] reference_ci_dashboard.md (saved 2026-xx-xx): CI dashboard and build reference links.

Selector Prompt 就基于这份 manifest 做选择。

这是一种很轻量的 RAG。

它不是 embedding top-k。

它更像:

小型知识库 topic files  ↓frontmatter / description 形成 manifest  ↓LLM selector 判断相关性  ↓只读取被选中的少量文件正文

4.3 Selector Prompt 的结构

Selector 运行在一个独立 side query 里。

它不是主 Agent 的 System Prompt。

这个 side query 大致包含三层:

system prompt:  你是 memory selector。  根据用户 query 和可用 memory 文件,选择有帮助的 memory。  最多选择 5 个。  不确定就不要选。user prompt:  Query: 当前用户请求  Available memories: memory manifest  Recently used tools: 最近成功使用过的工具output schema:  {    "selected_memories": ["filename.md"]  }

关键限制包括:

最多返回 5 个 filename;可以返回空列表;无效 filename 会被过滤;side query 使用较小输出预算,例如 max_tokens = 256;

这里的目标不是让 selector 总结 memory。

它只做一件事:

从一组 memory 文件名和 description 里,挑出当前 query 明确需要的文件。

原 prompt 里有几句规则很关键:

Only include memories that you are certain will be helpful.If unsure, do not include it.If none are clearly useful, return an empty list.

这三句决定了它的召回风格:

宁可少选;不确定就不选;允许空召回。

4.4 Recently used tools 是召回去噪参数

Selector Prompt 里还有一个容易被忽略的输入:

Recently used tools

它表示最近成功使用过的工具。

这个参数的作用,不是告诉模型“我很会用工具”。

它是召回去噪。

如果 Agent 最近已经成功使用过某个工具,那么普通的工具用法文档,召回价值就会下降。

例如 Agent 刚刚已经成功使用过某个 CLI。

这时再召回一篇“这个 CLI 怎么调用”的 memory,可能只是噪声。

但有一类内容仍然值得召回:

warningsgotchasknown issues限制条件容易踩坑的地方

所以 Recently used tools 的作用可以概括为:

抑制普通用法文档;保留风险、坑点和已知问题。

这也是 Claude Code 做 memory 召回时比较精细的一点。

它不是只问“相关不相关”。

还会考虑“这个信息对当前 Agent 还有没有边际价值”。


五、Prompt 3:Injection Template,把记忆注入 messages

Selector 只返回文件名。

真正进入主 Agent 上下文的,是 Injection Template 包装后的 memory 正文。

流程是:

Selector 返回 selected_memories  ↓读取对应 memory topic files  ↓给每个 memory 加 header / freshness 提醒  ↓包装成 role=user + isMeta=true 的 meta message  ↓外层使用 <system-reminder>  ↓插入主 request 的 messages

这里最重要的概念区分是:

relevant memory 不是放进 system 参数。

它进入的是 messages

具体形态是 role=user 的 meta message。

可以抽象成:

role: userisMeta: truecontent:  <system-reminder>  Memory (saved yesterday)  ...memory file content...  </system-reminder>

如果用接近 API 请求的形式表示,就是:

{"role""user","content""<system-reminder>\nMemory (saved yesterday)\n\n...memory file content...\n</system-reminder>"}

如果一次召回多个 memory 文件,就会形成多个类似的 role=user meta messages。

所以它和上一讲的概念要严格对齐:

Memory Lines  在主 Agent 的 system 参数里,是长期记忆治理规则。relevant memory 正文  在主 Agent 的 messages 里,是 meta user context。<system-reminder>  是 meta message 内部的提醒包装,不等于 system role。Read 工具结果  是工具调用返回的 tool_result,不是自动召回 memory。Selector Prompt  是独立 side query,不是主 Agent System Prompt。

5.1 从 attachment 到 meta user message

这里还要把注入路径说清楚。

Relevant memory 正文不是一开始就作为普通 message 直接插进 message list。

源码里的路径更像这样:

prefetch side query 完成  ↓createAttachmentMessage({ type: 'relevant_memories', ... })  ↓追加到当前 query loop 的 toolResults 数组  ↓下一轮状态变成:  messages = [    ...messagesForQuery,    ...assistantMessages,    ...toolResults]

也就是说,relevant memory 在内部先是一个 attachment

等发送 API 前,normalizeMessagesForAPI() 会把这个 attachment 展开成 role=user 的 meta message,并用 <system-reminder> 包住正文。

所以最终给模型看到的,才是类似这样的结构:

{"role""user","content""<system-reminder>\nMemory (saved yesterday)\n\n...memory file content...\n</system-reminder>"}

这里的关键不是它在整个 message list 里的绝对位置,而是它的语义:

它是对模型可见的上下文资料;它不是用户真实输入;它不是 system 参数;它也不是 tool_result。

源码里还有一次 attachment reorder:

reorderAttachmentsForAPI()

它会让 attachment 尽量贴近合适的位置。

但这个移动不是无限制的。

遇到 assistant message 或 tool_result 这类会影响对话顺序和工具协议的边界时,reorder 会停止。

原因很简单:assistant 输出和 tool_result 有严格的前后关系,不能为了移动一条 memory attachment 去打乱它们。

举一个简化后的例子。

假设内部状态里已经有:

user:  帮我修一下测试失败的问题assistant:  我先运行测试。tool_result:  测试失败日志...attachment:  relevant memory

发送 API 前,attachment 会被展开成 meta user message:

user:  帮我修一下测试失败的问题assistant:  我先运行测试。tool_result:  测试失败日志...user, isMeta=true:  <system-reminder>  Memory (saved yesterday)  ...memory file content...  </system-reminder>

它不会为了“更靠近 user query”而跨过 assistant / tool_result,变成这样:

user:  帮我修一下测试失败的问题user, isMeta=true:  <system-reminder>  Memory...  </system-reminder>assistant:  我先运行测试。tool_result:  测试失败日志...

因为这样会破坏 assistant 与 tool_result 的相对顺序。

所以更准确的说法是:

Relevant memory 先作为内部 attachment 进入当前 query loop。发送 API 前,它会被展开成 role=user + isMeta=true 的 meta message。正文被 <system-reminder> 包住。reorder 会尽量把 attachment 放到合适位置,但不会跨越 assistant message 或 tool_result 这类边界。

5.2 注入内容有限制

被选中的 memory 文件不会无限制注入。

每个文件读取有上限:

MAX_MEMORY_LINES = 200MAX_MEMORY_BYTES = 4096

如果文件被截断,提示会告诉模型:

如果需要完整内容,可以使用 Read 工具继续读取。

同时,当前 session 里自动注入的 relevant memory 也有累计上限:

MAX_SESSION_BYTES = 60 * 1024

这些限制说明 Claude Code 并不把 Durable Memory 当成“上下文越多越好”。

它只把当前 query 可能用到的少量记忆片段带回来。

5.3 freshness 提醒

Injection Template 里还会根据 memory 保存时间加 header。

大致会出现这样的信息:

Memory (saved today)Memory (saved yesterday)Memory (saved N days ago)

如果 memory 已经不是今天或昨天保存的,还会有 stale / freshness 相关提醒。

这背后是一个很重要的原则:

memory 是过去某个时刻的观察,不是当前实时事实。

这点后面会单独讲。

5.4 新写入的 memory 为什么下一轮还能生效

这里先给一个结论:

新写入的 memory 不依赖 System Prompt 立刻刷新,而是通过下一轮 prefetch + injection 进入 messages。

完整的缓存机制放到第八章讲。


六、Prompt 4:Extraction Prompt,forked agent 的后台兜底维护

第四个位置,是 Memory Extraction Prompt。

它不在主 Agent 的正常回答流程中。

它发生在 query 结束后的 stop hook 阶段。

目标是:

如果主 Agent 没有主动写 memory,就启动一个 forked agent 兜底分析最近新增消息,判断是否有值得长期保存的信息。

简化流程如下:

主 Agent 完成当前 query  ↓stop hooks 阶段  ↓检查本轮主 Agent 是否已经写过 auto memory  ↓如果写过:  跳过后台 extraction  更新处理游标  ↓如果没写过:  扫描 existing memories manifest  构造 Memory Extraction Prompt  启动 forked agent  允许它在 memory directory 内 Read / Edit / Write

这不是“每轮都做聊天总结”。

它有明确的 gate 和互斥逻辑。

常见跳过条件包括:

当前是 subagent;auto memory 关闭;feature gate 没开;remote mode;主 Agent 本轮已经通过 Write / Edit 写过 memory;

其中最关键的是:

hasMemoryWritesSince(...)

它会检查 transcript 里,主 Agent 在上次 extraction 之后是否已经对 auto memory path 做过 Write / Edit。

如果主 Agent 已经写了,后台 forked agent 就不再重复提取。

这让系统形成了一个双层写入机制:

主 Agent:  用户明确要求 remember / forget,或者当前任务中判断需要保存时,直接更新 memory。forked agent:  主 Agent 没写时,在请求结束后兜底检查最近新增消息。

6.1 Extraction Prompt 的角色设定

Extraction Prompt 会明确告诉 forked agent:

你现在是 memory extraction subagent。分析最近约 N 条消息。只从这些消息中提取值得长期保存的信息。不要为了提取 memory 再去调查代码库。不要 grep 源码。不要查 git。不要验证外部事实。

这个限制很重要。

forked agent 的任务不是继续解决用户问题。

它只是做长期记忆维护。

所以它只应该基于最近新增的对话内容提取 memory。

6.2 它能用哪些工具

forked extraction agent 的工具权限被压得很窄。

允许的大致是:

ReadGrepGlob只读 Bash:ls / find / cat / stat / wc / head / tailEdit / Write:仅限 memory directory 内

不允许的是:

MCP 工具Agent 工具write-capable Bashmemory directory 外的 Edit / WriteBash rm

并且它有 turn budget:

maxTurns = 5

Prompt 里还会建议它高效完成:

第一轮并行 Read 需要检查的 memory 文件;第二轮并行 Write / Edit 更新 memory;不要一边读一边写来回交错。

这说明 Claude Code 把后台 extraction 当成一个受限、短程、目的明确的维护任务。

6.3 它怎么避免重复 memory

Extraction Prompt 会带上 existing memories manifest。

forked agent 在写之前需要先看已有 memory。

原则是:

能更新已有文件,就不要新建重复文件;写入 topic file 时带 frontmatter;必要时更新 MEMORY.md index;遵守同一套 memory type 和 What NOT to save 规则。

这也是为什么 Memory Lines 和 Extraction Prompt 里会有大量重复规则。

这不是坏味道。

因为它们约束的是两个不同执行位置:

Memory Lines:  约束主 Agent。Extraction Prompt:  约束后台 forked agent。

两者共享同一套质量协议:

typeswhat not to savefrontmatterMEMORY.md indexdedupe使用前验证

这样主 Agent 主动写和 forked agent 兜底写,才不会写出两套风格完全不同的 memory。


七、横切原则:召回后不盲信

Durable Memory 最容易被忽视的一条原则是:

memory 不是当前事实。

它只是过去某个时刻保存下来的观察。

所以 Claude Code 不要求模型盲信 memory。

相反,Memory Lines 里会明确要求:

Before recommending from memory

也就是在根据 memory 给建议之前,需要验证当前状态。

典型做法包括:

memory 说某个文件存在:  先检查文件是否真的存在。memory 提到某个函数 / flag:  先 grep 当前代码库。memory 提到某个工具用法:  如果要基于它行动,先确认当前版本和当前环境。memory 和当前状态冲突:  相信当前状态,并考虑更新或删除旧 memory。

这条原则贯穿整个 Durable Memory 设计。

在 Memory Lines 里,它表现为使用前验证。

在 Injection Template 里,它表现为 freshness 提醒。

在 What NOT to save 里,它表现为不要保存容易过期的代码事实。

这也是为什么 Claude Code 要排除很多看起来“有用”的信息:

文件路径;当前架构;最近修改;git history;调试过程;

这些东西短期内确实有用。

但它们太容易过期。

存得越多,未来越容易误导 Agent。

举一个反面例子。

假设某个 Agent 把这些内容写进了 durable memory:

src/api/user.ts 里有登录逻辑;项目使用 React;测试命令是 npm test。

三个月后,项目已经重构:

登录逻辑移到了 src/auth/handler.ts;前端换成了 Vue;测试命令改成了 pnpm test。

如果 Agent 盲信旧 memory,它就可能建议修改一个已经不存在的文件,用一个已经不适用的框架,跑一个已经失效的命令。

这就是为什么 Claude Code 要排除代码事实、文件路径、架构模式。

不是因为它们没用,而是因为它们太容易过期。

所以 Durable Memory 的信任边界可以概括为:

memory 可以提供线索,但当前文件系统、当前代码、当前用户输入,才是当前事实。


八、System Prompt 缓存与 Memory 召回为什么可以解耦

现在回到前面那个问题:

同一个会话里,如果上一次更新了 MEMORY.md 或新增了一个 memory 文件,下一次对话会立刻更新到 System Prompt 的动态模块里吗?

答案是:

不一定会立刻更新到 System Prompt 的 memory section,但最新 memory 文件仍然可能被召回并注入 messages。

原因在于 Claude Code 的动态 system section 有缓存。

systemPromptSection('memory', () => loadMemoryPrompt()) 这一类 section 会被计算一次,然后缓存起来。

缓存通常到这些场景才会清掉:

/clear/compact新的 session / prompt 重建

而且不只是 memory,很多动态 system sections 都走类似缓存机制。

这是为了让 System Prompt 稳定,也有利于 prompt cache。

但 Claude Code 没有把最新 memory 内容完全依赖在 System Prompt 里。

它把职责拆开了:

system 参数:  放稳定规则。  Memory Lines 告诉 Agent 怎么治理 memory。filesystem:  放长期知识库本体。  topic memory files 可以被新增、更新、删除。prefetch / selector:  每轮用户 query 进来时,可以重新扫描 memory directory。messages:  放本轮真正需要的 relevant memory 正文。

所以即使 System Prompt 里的 memory section 没有及时刷新,新写入的 memory 文件仍然可以通过下面链路生效:

下一轮 user query  ↓startRelevantMemoryPrefetch()  ↓重新扫描 memory directory  ↓Selector Prompt 看到最新 filename / description  ↓选中相关 topic file  ↓Injection Template 把正文注入 messages

这不是补丁式设计。

这是上下文治理策略:

稳定制度放 system;易变内容走 messages;长期本体放 filesystem;按需召回靠 prefetch。

这让 Claude Code 同时拿到两个好处:

System Prompt 稳定,prompt cache 更容易命中;memory 内容仍然可以在下一轮 query 中被最新扫描和召回。

九、关键参数速查

为了把前面的细节收束一下,这里把关键参数按流程列出来。

Memory Lines / MEMORY.md  MAX_ENTRYPOINT_LINES = 200    MEMORY.md 入口索引最多读取 200 行  MAX_ENTRYPOINT_BYTES = 25_000    MEMORY.md 入口索引最大读取字节Memory file scan  MAX_MEMORY_FILES = 200    扫描最多 200 个 memory topic files  FRONTMATTER_MAX_LINES = 30    每个文件只读取前 30 行 frontmatter / headerSelector Prompt  最多选择 5 个文件    避免把 memory 全量塞入上下文  max_tokens = 256    selector 只输出小型 JSON 结果  selected_memories: string[]    结构化输出文件名列表Injection Template  MAX_MEMORY_LINES = 200    每个被选中文件最多注入 200 行  MAX_MEMORY_BYTES = 4096    每个被选中文件最多注入 4096 bytes  MAX_SESSION_BYTES = 60 * 1024    当前 session 自动注入 memory 的累计上限Extraction Prompt  maxTurns = 5    forked extraction agent 的最大轮数  只允许 memory directory 内 Edit / Write    防止后台 agent 修改项目文件  lastMemoryMessageUuid    记录上次 extraction 处理到哪条消息

这些参数共同表达了一个设计取向:

Durable Memory 是按需召回的小型知识库,不是无限上下文扩展器。


十、这个模块真正值得学什么

Durable Memory 真正有价值的地方,不只是源码里有 memory 目录。

而是它提供了一套很好的 Agent 长期知识治理范式。

10.1 小型知识库,而不是聊天记录仓库

Claude Code 没有把历史对话原样保存下来。

它把长期有用的信息整理成 topic memory files。

这比聊天摘要更像知识库。

对自研 Agent 的启示是:

先定义知识生命周期。不要先问怎么存,先问什么值得跨会话继续影响 Agent。

10.2 用 Prompt 软规则治理知识库

很多系统会把记忆管理做成硬编码逻辑:

触发关键词;分类器;固定 schema;数据库写入;

Claude Code 更有意思的地方是:

代码负责边界、路径、工具权限和参数限制;Prompt 负责语义判断、排除规则、写入质量和验证原则。

比如:

什么值得保存;什么不该保存;应该更新旧文件还是新建文件;当前 query 该召回哪些 memory;memory 是否需要验证后再用;

这些主要靠 Prompt 规则约束模型完成。

这就是 Durable Memory 的“软治理”。

10.3 主 Agent + forked agent 的双层更新

只靠主 Agent 容易漏记。

只靠后台总结又不够及时。

Claude Code 用两层机制折中:

主 Agent:  能立即响应用户 remember / forget。forked agent:  请求结束后兜底检查有没有漏掉的长期信息。互斥逻辑:  主 Agent 写过,后台跳过;  主 Agent 没写,后台补上。

这比“每轮都总结一遍聊天记录”更克制。

也比“完全靠主 Agent 自觉保存”更可靠。

10.4 System 缓存和 memory 更新解耦

第八章已经讲过具体机制。

这里真正值得借鉴的是设计取舍:

不要把所有变化都压到 System Prompt 上。

稳定规则放 system 参数,易变内容走 messages,长期本体放 filesystem。

这比“每次变更都重建 System Prompt”更适合长期运行的 Agent。

10.5 Durable Memory 和 Skill 的关系

这个设计确实有一点像 Skill。

相似点是:

都有索引;都不是全量加载;都按需选择;都把相关内容带入当前上下文;

但两者解决的问题不同。

Skill:  召回的是能力。  重点是“遇到这类任务时,应该怎么做”。Durable Memory:  召回的是上下文知识。  重点是“未来会话还应该知道什么”。

可以说,它们都使用了“索引 + 按需加载”的技术路线。

但 Skill 偏能力治理,Durable Memory 偏长期上下文治理。

10.6 自研 Agent 可以怎么借鉴

如果自己做 Agent memory,可以直接借鉴这个顺序:

1. 先定义长期记忆边界:记什么,不记什么。2. 设计封闭 type taxonomy,但允许每类多个 topic files。3. 为每个 topic file 写 frontmatter description。4. 用 index file 做入口,不要把所有内容堆进一个文件。5. 在主 Agent system prompt 中写清 memory 使用和更新规则。6. 每轮 query 用 selector prompt 做轻量召回。7. 把召回内容作为 meta context 注入 messages。8. 请求结束后用 forked agent 做兜底提取。9. 对旧记忆保持不盲信,必要时回到当前文件系统验证。

这套设计的核心不是“让 Agent 记住一切”。

而是:

可治理;可召回;可更新;可验证。

如果只做一个最小可用版本,其实不用一开始就上向量数据库。

最小版本只需要:

1. 一个 memory/ 目录。2. 一个 MEMORY.md 索引。3. 若干 topic memory files,每个文件有 name / description / type。4. 主 Agent system prompt 里有一段 memory 使用规则。5. 每轮 query 用一个轻量 selector 选择相关文件。6. 选中文件内容作为 meta context 注入 messages。

先把这六步跑通,再考虑后台 extraction、双层更新和更复杂的信任验证。


结尾:长期记忆真正的难点是治理

如果自己做 Agent memory,最容易先想到的是存储:

用什么数据库;怎么做向量检索;历史记录怎么切片;摘要怎么生成。

Claude Code 这个模块提醒我们,存储不是第一步。

第一步应该先问四个问题:

什么信息值得跨会话保存?本轮 query 应该召回哪些记忆?召回内容应该放进请求的哪个位置?旧记忆和当前事实冲突时,应该相信谁?

这些问题不解决,memory 存得越多,Agent 越容易被过期信息误导。

所以长期记忆真正难的不是“记住”。

而是让长期知识始终保持四个属性:

值得保存;容易找到;能够更新;不会被盲信。

这也是 Claude Code Durable Memory 最值得借鉴的地方。

它不是从“存更多历史”开始,而是先设计治理规则、召回方式、注入位置和信任边界。

如果现在就想试试,最简单的一步是:

给你的 Agent 加一个 memory/ 目录,写一段“什么该记、什么不该记”的规则,然后观察它在几次对话之后,是否开始区分“值得长期保存的信息”和“只是当前任务现场”。