AI 的记忆系统:四种记忆类型与语义召回

上一篇文章聊了上下文压缩——它解决的是"当前会话能记住多少"的问题。但编程助手还需要另一种记忆:跨会话的长期记忆。

你关闭 Claude Code,明天重新打开。它应该知道什么?

  • 你偏好用 TypeScript 而不是 JavaScript
  • 你上次说"不要用 underscore 库"
  • 这个项目用 Vitest 而不是 Jest
  • 你曾经纠正过它的一个理解错误

上下文压缩管不了这些。会话结束了,对话历史清空了,明天是全新的窗口。如果 AI 每次都要你重新解释一遍,它就不是助手,是负担。

Claude Code 的记忆系统要回答的问题是:AI 应该记住什么、存在哪里、怎么找到、什么时候用。

Claude Code 把记忆分成四类,每类对应不同的"记住什么":

用户记忆(User Memory)——关于用户个人的偏好和习惯。“我喜欢简洁的代码风格"“我用 macOS"“我的邮箱是 xxx”。这类记忆跟随用户,不跟随项目。换一台电脑、换一个项目,这些记忆仍然有效。

反馈记忆(Feedback Memory)——用户纠正或表扬的记录。“上次你理解错了,我不是要说 A 而是 B"“你上次写的测试很好,保持这种风格”。这类记忆是 Agent 的自我改进燃料——它让 AI 从历史互动中学习。

项目记忆(Project Memory)——关于当前项目的约定和状态。“这个项目用 Next.js App Router"“错误处理统一用 toApiError()““部署用 Vercel”。这类记忆跟随项目,不跟随用户。同一个人换项目,记忆跟着变。

引用记忆(Reference Memory)——用户明确标记为"重要参考"的信息。“记住这个文档链接,以后经常要用"“这个 API 的认证方式比较特殊,记下来”。这类记忆是用户主动创建的"书签”。

四分类的本质是记忆的作用域:用户记忆是全局的(所有项目共享),项目记忆是局部的(特定项目),反馈记忆是交叉的(用户+项目的组合),引用记忆是手动的(用户显式控制)。

Claude Code 的记忆不是存在数据库里,而是存在文件系统上——每个记忆条目是一个带 YAML frontmatter 的 Markdown 文件。

一个记忆文件可能长这样:

---
type: user
tags: [programming, preference]
created: 2026-06-15
lastAccessed: 2026-07-01
---

用户偏好使用函数式编程风格,避免可变状态。

用文件系统存记忆,看似原始,实际有几个好处:

  • 可版本化。 Git 可以追踪记忆的变更历史,你知道这条记忆是什么时候创建的、谁创建的、改过几次。
  • 可审查。 用户可以随时打开文件看 AI 记住了什么、删掉不想被记住的。
  • 可移植。 记忆文件可以随项目一起 clone,不需要额外的数据库或云服务。
  • 可搜索。grep 就能搜记忆内容,不需要专门的查询接口。

这是一种"文件即数据库"的设计哲学——和 CLAUDE.md 一脉相承。不引入额外的存储层,用现有文件系统解决一切。

记忆存了怎么找?如果靠关键词匹配,“我喜欢简洁代码"这条记忆在用户说"帮我写个函数"时可能匹配不上——因为没有共同的关键词。

Claude Code 用的是语义召回(Semantic Recall)——通过嵌入模型(Embedding)将记忆条目和当前查询都转化为向量,计算余弦相似度,找到语义上最相关的记忆。

具体流程:

  1. 用户发起请求时,系统提取查询的语义向量
  2. 在记忆库中计算每条记忆与查询的相似度
  3. 取 Top-K 最相关的记忆
  4. 将这些记忆注入 system prompt 的"记忆"层

这意味着即使用户说"写点代码”,系统也能召回"用户偏好简洁代码风格"这条记忆——因为语义上相关,即使关键词不匹配。

语义召回的精度取决于嵌入模型的质量。好的嵌入模型能捕捉细微的语义差异;差的嵌入模型会把不相关的记忆拉进来,或者漏掉相关的记忆。Claude Code 使用侧查询(sideQuery)机制——在主要任务之外,额外调用一次嵌入模型来查找相关记忆。

语义召回有一个性能问题:每次用户请求都要查一遍记忆库,如果记忆库有几百条,每次都要计算几百次向量相似度。

Claude Code 的优化策略是预加载(Prefetch)——在会话启动时,预先加载与当前项目最相关的记忆子集,缓存起来。后续请求先查缓存,只在缓存不够时才做完整的语义搜索。

预加载的逻辑是:项目记忆优先(当前项目相关的记忆最可能用到),其次是用户记忆(用户偏好总是相关),最后是反馈记忆和引用记忆。

这本质上是一个缓存策略问题——和 CPU 的 L1/L2 缓存、浏览器的 HTTP 缓存是同一类问题。热点数据提前加载,冷门数据按需获取。

记忆会过期。你半年前说"偏好用 Redux”,现在可能已经迁移到 Zustand 了。如果 AI 还在引用那条过期记忆,它会做出错误的判断。

Claude Code 的记忆系统有一个新鲜度警告机制——当注入的记忆条目超过一定年龄(比如 90 天),系统会在 prompt 中标注"这条记忆可能已过时”。模型看到这个标注后,会谨慎使用这条记忆,或者主动向用户确认。

这揭示了一个深层问题:记忆的价值随时间衰减。 不是所有记忆都 equally 持久——用户的基本偏好(“我用 macOS”)可能几年不变,但项目状态(“用 Redux”)可能几周就过时。

理想的方案是对不同记忆类型设置不同的 TTL(Time To Live),或者用置信度衰减函数代替硬阈值。但当前实现用的是简单的时间阈值——它不完美,但它让模型知道"这条信息可能不可靠”。

Claude Code 的记忆系统不是万能的。它有几个明确的边界:

它不记住代码内容。 记忆系统存的是"元信息”(偏好、约定、反馈),不是代码本身。代码存在文件里,需要就读文件。

它不记住完整的对话历史。 对话历史是短期记忆,由上下文压缩管理。记忆系统是长期记忆,只存摘要级别的结论。

它不跨用户共享。 用户记忆是个人私有的,项目记忆是项目级别的。两个不同的人在同一项目上使用 Claude Code,有不同的用户记忆,共享项目记忆。

这些边界不是技术限制,是设计选择。记忆系统如果什么都记,就变成了一个混乱的日志文件。有边界的记忆才是有用的记忆。

Claude Code 的记忆系统背后有一条设计哲学:

AI 的记忆不应该是黑盒。 用户应该知道 AI 记住了什么、能查看、能修改、能删除。记忆存在文件系统的明文文件里,而不是某个不可见的云端数据库。

这和传统软件的"用户数据"设计一脉相承——你的文件在你的硬盘上,你的设置在可读取的配置文件中。AI 的记忆也不例外。

这也意味着记忆系统有一个天然的安全模型:文件系统权限即记忆权限。 谁能访问项目目录,谁就能看到项目记忆。谁能访问用户目录,谁就能看到用户记忆。不需要额外的访问控制层。

BYF 没有采用 Claude Code 的文件系统记忆,而是选择了事件溯源(Event Sourcing) 作为跨会话持久化的核心手段。

BYF 的所有状态变更操作都以 JSONL 格式记录到 wire.jsonl——用 BYF 自己的话说,“Wire Records 是事件溯源持久化层”。每个 turn 的每一步(用户输入、模型回复、工具调用、工具结果)都是独立的记录。会话恢复时,回放这些记录即可重建内存状态。BYF 的 AgentRecords 模块就是"回放引擎”,支持协议版本迁移,确保旧会话在新版 BYF 上仍然可恢复。

这种设计的优势是可调试性:BYF 专门有一个 vis 工具(可视化调试器),读取 $BYF_HOME/sessions 下的 wire records,渲染为可浏览的时间线/树形视图。你可以回看每一次 tool call 的请求和响应、每一步的 token 消耗、每一轮压缩的触发点——这不是记忆,这是审计线索

BYF 还利用 wire records 实现了会话分叉(Fork)——从现有会话创建新会话,原会话不变。实现为完整目录复制 + state.json 重写。与 Git 分支不同,BYF 操作的是会话记录而非工作树文件。这让用户可以在不丢失历史的前提下探索不同的修改路径——“如果我在这里换一种方案会怎样?”

BYF 的选择揭示了一个事实:记忆系统不只是"AI 记住了什么”,还有"人的哪些操作被记录、可回溯、可探索"。记忆服务于 AI,记录服务于人。

记忆让 AI 记住"谁在用它"和"它在干什么项目"。但光有记忆还不够——AI 还需要专业技能来处理特定类型的任务。下一篇文章我们要拆解 Claude Code 的 Skill 系统:可复用的专业知识封装。为什么 Skill 不是插件而是"提示词模块"?按需加载和全量注入之间到底该怎么取舍?


本系列基于 Claude Code 官方源码项目进行架构分析,聚焦设计思想而非代码实现。

相关内容