Agent 的记忆:从无状态模型到持久心智
✨文章摘要(AI生成)
本文从大语言模型无状态这一根本约束出发,系统拆解 Agent 记忆的 抽取、存储、检索、更新与遗忘 四阶段生命周期,并比较向量、图数据库与文件式记忆等主流架构。文章结合 Mem0、Letta、Graphiti、Codex、Claude Code 等实践,分析被动抽取与主动编辑、数据库派与文件派、长期画像与原始工作记忆之间的关键取舍。最终指出,真正决定记忆系统能否长期运行的,不只是召回能力,而是可审计的维护机制、合并优先策略与基于真实使用的遗忘。
TL;DR
- 记忆的本质是对「有限上下文窗口」的工程绕行。 LLM 本身无状态,所谓「Agent 有记忆」不过是一套外部系统在恰当时机把恰当的过去重新喂回上下文。任何记忆系统都可以拆进同一条生命周期四环——抽取 → 存储 → 检索 → 更新/遗忘,你看到的所有框架差异,本质都是这四环上的不同工程选择。
- 三大根本取舍贯穿全程。 抽取上是「被动 LLM 抽取 vs 主动 agent 自编辑」(省 token vs 判断细腻);存储上是「向量 vs 图 vs 文件」,再粗一层是「数据库派 vs 文件派」(容量无限但人看不见 vs 人能读能管但受上下文约束);定位上是「记忆中间件用数据库派、个人/coding agent 用文件派」。
- 主流产品正收敛到同一套范式:「压缩的长期画像 + 原始工作记忆」。 ChatGPT、Gemini、Claude 在没有互相抄的情况下走到一起,因为它们面对同一个物理约束。范式趋同,但产品哲学分野——无摩擦 / 单一真相源 / 透明可控。
- 第四环(更新/遗忘)才是「玩具」与「能长期运行的记忆系统」的真正分水岭, 业界正收敛出两条共识:「合并优先于新增」(别指望主对话 agent 顺手维护,要么写入门控、要么离线重组)与「原始记录与压缩视图分离」(逻辑遗忘而非物理删除)。
- 别太信 benchmark。 LoCoMo 等基准可被激进检索策略刷分,各家报数口径不一、连同一家不同页面都对不上。Letta 用纯文件 + GPT-4o-mini 拿到 74.0%,高过 Mem0 报告的最佳图变体 68.5%——结论是记忆质量更取决于 agent 如何管理上下文,而非具体检索机制。用你自己的真实数据评估,是唯一靠谱的做法。
先从一个容易被忽略、却决定了整个领域形态的事实说起:大语言模型本身是无状态的。
一次 LLM 调用,本质是 tokens-in-tokens-out 的纯函数。你这一轮对话里教会它的东西——你的名字、你偏好 pnpm 而不是 npm、你上次踩过的那个坑——在下一轮、下一个会话、明天,它一概不知道。模型权重在推理时是冻结的,它「记得」的唯一通道,是你把过去的信息重新塞进这一次的上下文窗口里。换句话说,所谓「Agent 有记忆」,从来不是模型真的记住了什么,而是有一套外部系统,在恰当的时机,把恰当的过去重新喂了回去。
那有人会说:上下文窗口现在不是越来越大了吗?Claude 都有 1M token 的窗口了,那我把全量历史一股脑塞进去不就行了?
这恰恰是理解记忆系统的关键反例。笔者的资料里给了一组很有说服力的数据:即便窗口足够大,把整段历史做 full-context 塞入,在成本和延迟上都会爆炸——Mem0 实测 full-context 方案的 p95 延迟高达 17.12 秒,单次请求约 26K token(26031)。而换用记忆系统后,同样的任务只需注入约 1764 token 的精选事实,p95 搜索延迟约 0.2 秒,相对 full-context 节省了约 90% 的 token、降低约 91% 的延迟(数据据 Mem0 ECAI 2025 论文,arXiv:2504.19413)。
所以可以给「记忆系统」下一个不太浪漫但很准确的定义:
记忆系统本质上是对「有限上下文窗口」的工程绕行。窗口再大也是有限且昂贵的资源;记忆系统做的事,是用约 1.8K token 的「精选事实」去置换掉 26K token 的「全量历史」,在容量、成本、延迟之间找一个能落地的平衡点。

把这件事想透,后面所有的设计选择——存什么、怎么存、怎么召回、怎么忘——都能归结到一个母问题上:在上下文预算有限的前提下,如何让 Agent 表现得像「记得」一切。 这篇文章接下来要做的,就是沿着这个母问题,一层层把「记忆」拆开。
在拆之前,得先有一张地图。下面这一节,笔者先把几个最基础、也最容易被新手搞混的概念立起来。
概念地图:先把基础术语立起来
短期记忆 vs 长期记忆
最粗、也最重要的一刀,是把记忆切成短期和长期两类。
短期记忆(也叫工作记忆,working memory),指的是当前会话/线程内的上下文:最近几条消息、刚刚返回的工具结果、中间推理的草稿、还没执行完的计划。它的生命周期就是这一次会话——会话结束,通常就清空了。它对应的是「此刻 Agent 脑子里正在转的东西」。
长期记忆(long-term memory),则是跨会话、跨线程持久化下来的东西,存在数据库、向量库、图存储或文件里,用于个性化和持续学习。你上周告诉它的偏好,今天新开一个对话它还能调出来,靠的就是长期记忆。
这两者听起来界限清晰,但在工程实现上极易混为一谈。笔者认为讲这个区分最干净的例子是 LangGraph——它干脆把这条分界线做成了框架的「一等公民」,用两个完全独立的持久化钩子来承载:
| checkpointer(短期) | store(长期) | |
|---|---|---|
| 作用域 | thread(线程/会话)级 | user/app(跨线程)级 |
| 存什么 | 图执行状态、state["messages"]、会话内多轮上下文 | 任意 namespace 下的跨会话 KV,用户偏好、画像 |
| 生命周期 | 换一个 thread_id 就消失 | 跨会话、跨线程持久存在 |
| 类比 | 进程的内存 | 进程外的磁盘 |
这个例子之所以值得记住,是因为它顺带点破了一个新手最常踩的坑:
混淆短期与长期,是 Agent 记忆领域最常见的架构错误。 一个典型翻车现场是:把「用户偏好」这种本该长期持久的信息,错存进了 thread 级的 checkpointer——结果用户一换会话(换
thread_id),偏好就凭空蒸发了,表现就是经典的「这 AI 怎么又不记得我了」。反过来,把转瞬即逝的会话草稿塞进长期 store,又会让长期记忆库迅速腐烂、污染上下文。
记住这条边界:短期记忆解决的是「这一次对话内的连贯」,长期记忆解决的是「跨越对话的延续」。 它们的存储介质、写入时机、清理策略几乎完全不同,应当分两套机制来设计——这是整张地图的第一根坐标轴。
CoALA 四分法与它的分歧
把记忆切成「短期/长期」只是第一刀。要再往细里分,目前业界用得最广的一套词汇,来自 2023 年 Princeton 的 CoALA 论文(Cognitive Architectures for Language Agents,Sumers 等,arXiv:2309.02427)。它把 Agent 记忆分成「工作记忆 + 三类长期记忆」共四类:
| 类别 | 它存什么 | 一句话理解 |
|---|---|---|
| 工作记忆(Working) | 当前上下文、最近观察、中间推理、部分计划 | 短期 scratchpad,对应人类有限容量的缓冲区 |
| 情景记忆(Episodic) | 过去发生的具体经历与事件(「上次试方案 X 结果如何」) | 让 Agent 能从交互历史里学习,对 copilot 类产品至关重要 |
| 语义记忆(Semantic) | 关于世界/用户的事实性知识 | 大部分已被 LLM 预训练覆盖,企业/用户专属事实才是真正的缺口 |
| 程序记忆(Procedural) | 学到的规则、技能、how-to | 可从反馈中习得,对应「会做某件事」的能力 |
这套四分法并非凭空发明,它的源头是认知心理学的几篇奠基性工作:Tulving(1972,区分情景记忆与语义记忆)、Squire(1987,程序记忆)、以及 Baddeley & Hitch(1974,工作记忆模型)。也正因为有这层认知科学的背书,这套词汇被广泛采纳——IBM、MongoDB、LangChain、Letta、Mem0 都用了它的某种变体。在很长一段时间里,CoALA 几乎成了讨论 Agent 记忆的「普通话」。
但到了 2025 年底,这套话语体系开始出现明显的分裂,笔者觉得这个分歧本身比四分法更值得读者留意。
一派的代表是 Letta 的 Sarah Wooders,她公开反对这种「大脑类比」,主张回到工程本质看问题——「LLM 是 tokens-in-tokens-out 的函数,不是大脑」,因此与其套用 episodic/semantic/procedural 这套人类认知的标签,不如纯粹从「信息如何进出上下文窗口」的工程视角去设计记忆。这不是抠字眼:如果你信的是这一派,你会更关心「这条信息此刻该不该在窗口里」,而不是「它属于情景还是语义」。
另一派则在尝试给分类法做「版本更新」。2025 年 12 月的综述《Memory in the Age of AI Agents》(arXiv:2512.13564)就指出该领域正在分裂,并提出了一套更精简的新三分法:事实记忆(Factual)、经验记忆(Experiential)、工作记忆(Working)。它某种程度上是对 CoALA 四分法的重新折叠——把「语义」收进 Factual,把「情景 + 程序」并进 Experiential,工作记忆原样保留。
笔者的建议是:不必在两种视角里站队,知道它们并存即可。 CoALA 四分法的价值在于给你一套讨论问题的共同词汇,方便你判断「我这个场景缺的到底是哪类记忆」;而 Letta 那派的工程视角则提醒你别被认知类比带跑——真正决定 Agent 体验的,往往不是你给某条记忆贴了什么标签,而是它在恰当的时机有没有进到上下文窗口里。后文谈具体机制时,笔者会更多站在工程视角,但这套分类词汇仍会作为讨论的脚手架反复用到。
有了「短期 vs 长期」这根纵轴,和「working/episodic/semantic/procedural」这套横向标签,我们就有了一张最基础的记忆概念地图。接下来,笔者会沿着一条记忆从「产生」到「消亡」的完整生命周期,把每个环节的工程取舍逐一摊开。
记忆的生命周期:一个统一抓手
前面铺垫了那么多概念、那么多项目,要是逐个对比很容易看花眼。笔者建议读者从现在起,脑子里只装一根主线——记忆的生命周期。前面提到的 CoALA 论文和那篇《Memory in the Age of AI Agents》在分类法上吵得不可开交,但在生命周期这件事上倒是出奇地一致。它就四个环节:
抽取(Extraction)→ 存储(Storage)→ 检索(Retrieval)→ 更新/遗忘(Update/Forgetting)
- 抽取:决定「记什么」——从滚滚而过的对话里把值得留下的东西挑出来。
- 存储:决定「存哪、存成什么形态」——向量、图、KV、关系库,还是一个纯文本文件。
- 检索:决定「什么时候、怎么把它喂回去」——语义检索、关键词、图遍历、还是直接读进 system prompt。
- 更新/遗忘:决定「老记忆怎么办」——冲突怎么消解、重复怎么合并、过时的怎么失效或衰减。
这四个环节就是本文后续所有讨论的骨架。笔者想强调一个判断:你看到的所有框架差异,本质上都是这四个环节上的不同工程选择,没有第五种花样。mem0 和 Letta 之所以体感天差地别,不是因为一个「更先进」,而是它俩在「抽取」环节一个选了被动 LLM 抽事实、一个选了让 agent 自己 tool call 编辑;Graphiti 之所以独树一帜,是因为它把功夫几乎全砸在了「更新/遗忘」上,用双时态模型把删除变成了失效。把任何一个号称「记忆系统」的东西拆到这四格里,它的取舍就一目了然,营销话术也就不攻自破了。

接下来四节,笔者就沿着这四个环节一路走下去。先从最前端、也最容易被低估的一环——抽取——讲起。
第一环:抽取——记什么、谁来记
抽取要回答两个问题:谁来写记忆,以及抽什么。这两个问题听起来朴素,却是整套系统的源头——抽取这一关丢掉的信息,后面再强的检索也捞不回来。
谁来写记忆:四种模式
把视野放到一批热门开源项目上,「谁来写记忆」大致只有四种答案:
| 模式 | 谁动手 | 代表实现 | 一句话特征 |
|---|---|---|---|
| 直接 append | 系统自动 | MetaGPT、Pi | 来什么存什么,不做任何提炼 |
| LLM 抽取 | 一次额外的 LLM 调用 | mem0、crewAI、Codex、Graphiti | 把对话蒸馏成事实/三元组/经验条目 |
| Agent 自编辑 | 主模型在对话中调工具 | Letta、Claude Code auto memory | 模型自己决定改写哪条记忆 |
| 人工维护 | 人手写 | CLAUDE.md、AGENTS.md | 记忆就是人写的指令文件,Agent 只读不写 |
直接 append 是最粗暴的一档。MetaGPT 在角色 observe/react 时把收到和产出的消息原样塞进一个 list,去重都只是「对象是否已存在」级别;Pi 则把会话存成 append-only 的 JSONL 树,从不提炼。好处是零成本、零信息损失,坏处是这堆原始流水迟早膨胀到没法直接喂回模型。
LLM 抽取 是数据库派的主流。它用一次额外的 LLM 调用,把一段对话蒸馏成结构化的「事实 / 三元组 / insight / 经验条目」。mem0、crewAI、Codex、Graphiti 都走这条路——区别只在抽成什么形态(下一小节展开)。
Agent 自编辑 最优雅。Letta(MemGPT 后身)把一组记忆操作直接做成工具交给主模型调用——core_memory_append、memory_replace、memory_insert、memory_rethink——模型在推理循环里像编辑文档一样维护自己的 core memory。Claude Code 的 auto memory 同理,模型判断「这条值得未来复用」后,界面会显示 "Writing memory"。它的妙处是把「什么重要」的判断权交给了那个最懂当前上下文的主模型,但代价是每次记忆操作都要烧主模型的 inference token。
人工维护 则干脆把抽取这一步外包给人。Claude Code 的 CLAUDE.md、Codex/opencode 的 AGENTS.md 就是人手写的指令文件,Agent 只读不写。它质量最高、最可控,但显然不 scale——指望用户持续手填记忆是不现实的。
现实里很少有项目是纯种。Codex 就同时挂着三层:人工的
AGENTS.md、LLM 蒸馏的~/.codex/memories/、以及 append-only 的会话日志,叠在一起用。Claude Code 同样是「人工CLAUDE.md+ 模型自写 auto memory」双轨。所以上面这张表读的是「主力机制」,不是非此即彼。
把「存哪」(文件派 vs 数据库派)和「谁来写」这两个轴叠起来,一批项目大致落在下面这张矩阵里:

这张图最值得玩味的是两个空角。文件派几乎没人纯靠 append——因为文件是给人读的,不提炼就成了一堆没法看的流水;数据库派几乎没人纯靠人工维护——机器召回的容量人手根本填不过来。于是两派各自从两端往中间挪,最后都向「LLM 抽取」收敛。这也解释了为什么 LLM 抽取是目前最常见的写入方式——它恰好卡在「人填不动」和「append 没法看」之间的那个甜区。
抽什么:取向的分野
确定了「谁来写」,下一个问题是「抽成什么」。同样是 LLM 抽取,各家的取向差得很远,这直接决定了记忆库长什么样、能干什么。
mem0:抽通用个人事实/偏好。 mem0 的 FACT_RETRIEVAL_PROMPT 把对话压成一句句短陈述——Name is John、Favourite movies are ... 这种。它面向的是 C 端用户画像场景,追求的是「跨会话稳定、可独立成立」的个人事实。值得一提的是 mem0 在新版本里默认管线已经倒向 ADD-only 抽取(一次 LLM 调用、不再 UPDATE/DELETE、累积不覆盖),它的抽取 prompt 甚至明确举例:「有只狗叫 Max」和「和 Max 去露营」是两条记忆而非重复——也就是说连 mem0 自己都在重新权衡「激进合并 vs 保留细节」。
Codex:抽四类高信号。 Codex 在「抽什么」上最讲究。它的 stage-one 抽取模板明确只收四类:用户偏好信号、可复用知识、失败护盾(symptom → cause → fix)、repo 地图。更关键的是它对「证据来源」立了两条硬规矩——underindex 助手自述、overindex 用户原话与代码证据。这一条直击长期记忆最隐蔽的失败模式:把 agent 自己的臆测当成既定事实存进去,下次召回又被当真。Codex 的选择是宁可漏抽,也不让助手的自说自话污染记忆库。
AutoGen:从失败里「学」insight。 AutoGen 的 task-centric memory 走了条独特的路——它的 learn_from_failure 把同一个任务反复交给 agent,一旦答错,就让 LLM 针对这次失败提炼一条 insight 存起来。这里的记忆不是被动「记」下来的,而是从错误里主动学出来的,更接近 CoALA 框架里的程序记忆(procedural memory)。
把这三家并排看,差异一目了然:mem0 关心「你是谁、你喜欢什么」,Codex 关心「这个仓库怎么干活、哪里踩过坑」,AutoGen 关心「这类任务怎么才能做对」。同一个「抽取」动作,三种世界观。
被动 vs 主动:本环节的根本分野
抽取这一环最底层的取舍,其实可以收敛成一句话:被动抽取,还是主动自编辑。 这也是整份概念框架里反复出现的那条主线。
- 被动抽取(mem0、crewAI 这类 LLM 抽取,以及 append):写入由系统在对话之外触发,主模型甚至感知不到记忆正在被写。好处是省 token——记忆操作不占主对话的 inference 预算;坏处是它对「这一刻上下文里什么最重要」的判断不如主模型细腻,模型没抽到的,就永远丢了。
- 主动自编辑(Letta、Claude Code auto memory):由主模型在推理循环里自己决定写什么、改什么。好处是判断细腻——最懂当前语境的那个模型亲自下手;坏处是每次都要烧 inference token,且记忆质量完全押在模型的自觉上,它不主动存,照样丢。
| 被动抽取 | 主动自编辑 | |
|---|---|---|
| 触发方 | 系统(对话之外) | 主模型(推理循环内) |
| token 成本 | 低 | 高(每次推理都算) |
| 判断细腻度 | 较粗(独立 LLM 凭 prompt 判断) | 高(最懂上下文的模型亲自判断) |
| 典型失败 | 抽漏即永久丢失 | 模型不主动存即丢失 |
| 代表 | mem0、crewAI | Letta、Claude Code |
两条路没有谁更优,只有适不适配。给 C 端产品做无人值守的用户记忆,被动抽取的省 token 和一致性是刚需;做一个要「越用越懂你」的自主 agent,主动自编辑的细腻才撑得起体验。记住这个分野,后面看存储和检索的选择会顺很多——抽取阶段押了哪一边,往往就决定了整条管线的性格。
第二环:存储——记在哪
抽取决定了「记什么」,存储决定了「记在哪种载体上」。这一环看似只是个数据库选型问题,实则是整篇文章里最容易暴露设计哲学的地方——你把记忆存成什么,决定了它能长多大、谁能看见它、出了错怎么改、迁移时疼不疼。
三大存储流派
笔者综合两份资料,把当下的存储后端归为三大流派。
| 流派 | 载体 | 召回靠什么 | 代表实现 |
|---|---|---|---|
| 向量库 | embedding + 相似度索引 | 语义检索(常配 BM25) | mem0、LlamaIndex、Cursor 的 Turbopuffer |
| 时序知识图谱 | 实体-关系图 + 时间元数据 | 图遍历 + 向量 + 全文 | Zep/Graphiti、cognee |
| 纯文件 / KV | Markdown / JSONL / 键值 | 读进上下文、grep | Claude memory tool、Letta filesystem、CLAUDE.md |
向量库是最主流的选择。把每条记忆 embed 成向量塞进索引,召回时用查询向量做近邻搜索。它的强项是语义模糊匹配——你问「我喜欢看什么电影」,能召回当初存的「Favourite movies are ...」,哪怕字面不一样。mem0 就坐在 App 与 LLM 之间当一层「记忆引擎」,底层是向量库;Cursor 的代码库索引则把代码按函数/类语义切分后存进 serverless 的 Turbopuffer。
时序知识图谱走得更远——它不存离散的「事实条目」,而存「实体 + 实体间的关系」,并给每条关系边挂上时间元数据。Zep 的 Graphiti 引擎是这一派的标杆,它在 Neo4j 上维护三层子图(情景子图存原始事件、语义实体子图存抽取出的实体关系、社区子图做高层聚类),召回时融合语义、全文 BM25、图算法三路搜索。它最大的卖点是双时态模型,这一点笔者放到下一环「更新与遗忘」里细说。cognee 也属此派,但它走的是统一三层存储。
纯文件 / KV则朴素到近乎反直觉:记忆就是一个 Markdown 文件,召回就是把它读进 system prompt。Claude API 的 memory tool 让模型用文件系统命令(create / str_replace / insert)自管理一个 /memories 目录;Claude Code 是 CLAUDE.md 级联文件;Letta 在 2025 年起也转向了 git-backed 的 Context Repositories,每次记忆变更自动版本化提交。
需要强调的是,这三派并非互斥。mem0 和 cognee 都明确走「混合」路线:mem0 的存储后端是向量库(Pinecone / Qdrant / Weaviate / Chroma / pgvector)+ 可选图数据库(Neo4j / Memgraph)存实体关系 + 常规 DB 存元数据;cognee 则用一套 ECL pipeline 把数据同时灌进关系、向量、图三层。换句话说,「向量 vs 图 vs 文件」是按主力载体归类,真到工程里往往是叠着用的。
一个更本质的分类轴:数据库派 vs 文件派
三派分类有用,但笔者读完两份资料后觉得,真正决定开发者体验的,是一条更粗的轴:这堆记忆,是存进一个机器才看得懂的数据库,还是存成一个人也能打开的文件?
把向量库和知识图谱合并成「数据库派」,把纯文件/KV 单列为「文件派」,整个图景一下子清晰了。这两派的取舍几乎是镜像对称的:
| 维度 | 数据库派(向量库 / 知识图谱) | 文件派(Markdown / JSONL) |
|---|---|---|
| 容量 | 近乎无限 | 受上下文预算约束 |
| 召回 | 语义检索强(向量 / 图遍历 / hybrid) | 读进 system prompt、grep |
| 人能看见吗 | 看不见(向量是一堆浮点数) | 能直接读 |
| 人能手改吗 | 难,得走 API | 能,直接编辑 |
| 可追溯 | 不能手动 diff | git 可追溯,可 blame |
| 外部依赖 | 要维护一套基础设施 | 零外部依赖 |
| 迁移成本 | 高(绑定具体后端) | 几乎为零(就是文本) |
数据库派的代价集中在「人」这一侧:记忆变成了向量索引或图节点,你打不开、看不懂、不能像 review 代码那样 diff 它写了什么,换一套后端还要做数据迁移。而且你得额外养一套基础设施——Qdrant、Neo4j、pgvector、FalkorDB、LanceDB,跑起来、监控好、不丢数据,都是运维账。Zep/Graphiti 的运维成本高(要跑 Neo4j)在两份资料里都被反复点名。
文件派的代价则集中在「容量」这一侧:上下文窗口就那么大,一个 MEMORY.md 不可能无限膨胀。这一派靠「索引 + 按需读」来扩展容量——常驻一个精简索引,明细文件按需 fetch——但这套技巧本身就是为了绕开「人能读的文件装不下太多东西」这个硬约束。
这里有个常被忽略的前提:文件派之所以「人能读」,是因为它存的是经过提炼的自然语言,而不是原始流水账。文件派几乎没人纯靠 append——人要读,就得先提炼;否则一个塞满原始对话的 JSONL 谈不上「可读」,文件派的核心优势就名存实亡了。
趋势:个人 agent 选文件派,记忆中间件选数据库派
把这条轴套到具体项目上,会浮现一个相当一致的规律:
几乎所有 2025 年之后火起来的「个人 / coding agent」都选了文件派——Codex 的 AGENTS.md + ~/.codex/memories/、Claude Code 的 CLAUDE.md、opencode、Pi、Hermes 的 MEMORY.md、OpenClaw 的 MEMORY.md,无一例外。Cursor 那套 .cursor/rules、Copilot 的 copilot-instructions.md、Windsurf 的 .windsurfrules、以及跨工具事实标准 AGENTS.md,本质上也都是文件式配置。
而把记忆当成独立产品卖的「记忆中间件」才用数据库派——mem0、Letta、Zep 都是。这很好理解:它们的定位是「给别人的 agent 当一层可插拔的记忆服务」,要的就是大容量、强语义检索、多租户隔离,至于「人能不能用肉眼读懂这条记忆」根本不在它们的产品诉求里。
为什么个人 / coding agent 集体倒向文件派?原因不难理解——它们的用户是开发者。开发者对「记忆」的要求和 C 端用户截然不同:他们要的是「记忆我能看见、能管、能进版本控制」。一个 CLAUDE.md 可以被 code review、可以 git blame 出某条规则是谁哪天加的、可以在 PR 里被讨论和回滚——这些都是开发者日常工作流里天然就有的东西。让 agent 的记忆复用这套工作流,远比让开发者去查一个向量库里到底存了什么要顺手。Letta 自家那个著名实验也从侧面印证了这条路的性价比:一个用 GPT-4o-mini 的简单文件系统 agent 在 LoCoMo 上拿到 74.0%,反而高于 Mem0 报告的其最佳图变体 68.5%——它的结论是「记忆质量更多取决于 agent 如何管理上下文,而非具体检索机制」。对开发者场景而言,「能管」往往比「检索多花哨」更重要。
各家后端选型一览
最后把数据库派各家的具体后端选型摊开,方便对照——这也能看出「混合」到底混了些什么:
| 项目 | 主力后端 | 备注 |
|---|---|---|
| mem0 | Pinecone / Qdrant / Weaviate / Chroma / pgvector(向量)+ 可选 Neo4j / Memgraph(图)+ 常规 DB(元数据) | 典型混合:向量主存、图存关系、DB 存时间戳与 user/agent/session ID |
| Graphiti(Zep) | Neo4j | 单一图后端,每条边带时序元数据 |
| cognee | 默认 LanceDB | 用一套 LanceDB 统一关系 + 向量 + 图三层 |
| LangGraph | 生产用 PostgreSQL + pgvector | 默认 cosine 相似度;也支持 SQLite、InMemory(仅 demo) |
可以看到,即便同属数据库派,选型也分两种气质:mem0 是「按职责分库」——向量、图、关系库各司其职,灵活但要同时养好几套基础设施;cognee 则反其道而行,用 LanceDB 一个后端兜住三层,少一个依赖少一份运维。LangGraph 走的是另一条务实路线——直接复用最普通的 PostgreSQL + pgvector,把记忆设计的活儿全压给开发者,但好处是不引入任何陌生的新基础设施。这本身就是「数据库派内部」的又一层取舍:是用多个专用后端换能力上限,还是用单一通用后端换运维省心。
第三环:检索——怎么把记忆喂回去
存和检索是一枚硬币的两面。前一节聊了「记忆存在哪、长什么样」,这一节聊「轮到要用的时候,怎么把对的那几条捞出来、塞进上下文」。一个值得先说破的规律是:召回方式基本跟着存储载体走——你选了向量库,召回路径几乎注定是语义检索;你选了 Markdown 文件,召回路径几乎注定是 index-then-fetch。所以这一节也顺着上一节的「数据库派 vs 文件派」两条路分别讲。
数据库派:从单路语义检索,到 hybrid 多路融合
数据库派的召回清一色是语义检索——把 query embedding 算出来,到向量库里做 top-K 相似度匹配。但只靠向量有它的硬伤:纯语义检索对「精确实体名 / 罕见专有名词 / 数字日期」不敏感(embedding 会把「pnpm」和「npm」拉得很近),对「这条事实是不是当下还成立」也无感。于是这一派近一两年的共同演化方向是 hybrid——多路召回各取所长,再融合排序。
几个有代表性的实现,可以摆在一起对比:
| 代表实现 | 召回路数 | 融合 / 排序 | 特别之处 |
|---|---|---|---|
| Graphiti (Zep) | 向量 + BM25 全文 + 图 BFS 三路 | RRF 融合后再上 cross-encoder 重排 | 图遍历能补「多跳关系」,语义+全文补不了 |
| mem0(新算法) | 向量 + BM25 + 实体 boost 三路并行打分 | 多信号分数融合 | 额外带时间感知检索,给「当前状态 / 过去事件 / 未来计划」排正确的时间实例 |
| crewAI (RecallFlow) | 向量召回 | 按置信度路由 | 结果置信度低于阈值就 explore_deeper,把当前结果喂回 LLM 再搜一轮 |
Graphiti 是三路里最「全」的:向量负责语义相近、BM25 负责关键词精确命中、图 BFS 负责沿实体关系往外跳,三路结果用 RRF(Reciprocal Rank Fusion)合并,最后再过一遍 cross-encoder 做精排。mem0 的新算法思路类似——语义、BM25 关键词、实体匹配三路并行打分融合;它额外有一层时间感知检索,专门解决「我去年的职位」和「我现在的职位」这种同一实体不同时间实例该返回哪个的问题(这恰好对应下一节 Graphiti bi-temporal 软失效在召回端的回报:旧事实不删,但召回时按时间挑对的版本)。
crewAI 的 RecallFlow 则给召回加了一层自适应控制:它不是「搜一次就完事」,而是先看返回结果的置信度,低于阈值就触发 explore_deeper,把已有结果喂回 LLM 重新组织 query 再搜一轮。这等于把「检索」从一次性操作变成了一个带反馈的小循环。
一句 caveat:hybrid 这套虽然召回质量更高,但代价是每次召回要跑多路检索 + 融合 + 重排,延迟和工程复杂度都上去了。Mem0 自家测得,纯记忆方案相比 full-context 已经能把 p95 搜索延迟压到约 0.2 秒、单次约 1764 token(对比 full-context 的 p95 17.12 秒、约 26K token)——但那是「比 full-context 省」,hybrid 内部各路怎么取舍仍是工程活。
文件派:index-then-fetch 分层召回
文件派没有向量库,召回靠的是「把文件读进 system prompt」。但文件可以无限长,上下文预算却有限,怎么办?答案是 index-then-fetch——常驻一份「索引」,明细按需 grep + 打开。
以 Codex 为代表,这条链路是这样的:
- 把
memory_summary.md注入系统提示,当作常驻索引(它知道「有哪些记忆、大概讲什么」,但不含明细); - agent 用关键词 grep
MEMORY.md,定位到相关条目; - 命中后,按指针打开明细文件读全文;
- 必要时回到原始 rollout jsonl 查证据(「这条结论当初是从哪段对话蒸馏出来的」);
- 全程限 4–6 步预算——不让 agent 无节制地翻文件。
Claude Code 的 auto memory 是同构的一套:MEMORY.md 的前 200 行 / 25KB 当索引,每会话注入;具体 topic 文件按需读。两者的共性是「索引常驻、明细按需取」。

这是文件派能跟数据库派比「容量」的关键技巧——不靠向量,靠分层 + 按需 fetch,常驻成本恒定、明细按需取。记忆库本身可以长到几十上百个文件,但每会话固定注入的就那一份索引,明细只在 agent 主动 grep 命中时才进上下文。这也解释了为什么 2025 年之后那批 coding agent 敢用纯文件做记忆而不担心「装不下」。
一个特例:Hermes 用全文检索而非语义
数据库派 = 语义、文件派 = 分层,这个对应关系有个值得记一笔的例外:Hermes 的跨会话召回用的是 FTS5 全文检索,而不是语义检索。它把历史消息存进 SQLite,用 FTS5(含 trigram CJK 索引,对中文友好)做关键词全文搜索。
这个选择的妙处在于它故意放弃了语义:
- 零 LLM 成本——全文检索是纯数据库操作,不需要算 embedding、不需要 LLM 介入;
- 返回真实消息原文——召回的是当初说过的原话,不是被摘要过的二手版本;
- 代码注释里特意写明 "no summary LLM path"——把语义摘要从召回主路径里彻底拿掉。
换句话说,当你的诉求是「精确找回某句话说过什么」而不是「找回语义相近的概念」时,朴素的全文检索反而又快又准又便宜。这是对「召回一定要上向量」这个默认假设的一个有力反例。
检索机制可能没那么决定性
讲完三条召回路线,笔者想在这里埋一个伏笔,提醒读者别把「检索」这一环看得太重。
一个反直觉的实验结论是:召回机制本身,可能没有大家想象的那么决定性。Letta 自家做过一组对照——一个用 GPT-4o-mini 的纯文件系统 agent,几乎没怎么调提示,就在 LoCoMo 上拿到 74.0%,反而高过不少专用记忆库(包括 Mem0 报告的其最佳图变体 68.5%)。它得出的结论是:「记忆更多关乎 agent 如何管理上下文,而非具体的检索机制。」
这话听起来像是在拆自己的台,但它指向一个真问题:那些漂亮的 hybrid / RRF / cross-encoder 召回流水线,到底有多少分数是「检索做得好」挣来的,又有多少是 benchmark 本身可以被激进检索策略刷出来的?这背后牵扯到一整套 benchmark 水分的问题——这里先点到为止,留到后面专门聊评测的章节再展开。
第四环:更新与遗忘——记忆库怎么不腐烂
如果说前三环(抽取、存储、检索)决定了记忆系统「能不能用」,那这一环决定的是「能不能一直用」。一个只进不出、来什么存什么的记忆库,跑上几百轮对话之后必然腐烂:同一个事实被记三遍、旧偏好和新偏好打架、过时信息混在召回结果里把模型带偏。所以笔者认为,更新与遗忘才是区分「玩具」和「能长期运行的记忆系统」的真正分水岭——而它恰恰是绝大多数项目做得最潦草、甚至干脆不做的一环。
把记忆生命周期摊开看,前三环大家做法虽有差异但都「做了」;唯独到了第四环,会出现一个断崖:相当多的项目在这里直接交白卷。下面分两块讲,先说去重/合并(写入侧的「防腐」),再说遗忘(时间维度的「排毒」)。
去重与合并:脏数据是该「进库前拦」还是「召回时绕」
先看现状。在笔者翻过的这批项目里,大多数根本不做实质性的去重/合并:AutoGen 的记忆库是单调自增 ID 的纯 append,来一条加一条;MetaGPT 稍好一点,但也只是对象级的精确去重(if message in self.storage 那种),语义上重复的两条照样并存。这类系统的隐含假设是「召回时再想办法」——靠检索排序、靠重排把重复和过时的内容压下去。问题是脏数据已经进库了,你只能绕着它走,绕不干净。
少数把去重当核心的项目,思路可以归成四种。
① 写入时相似度门控 + LLM 决策(crewAI / mem0)。 这一类的精髓是把「整理」从召回时提前到了写入时。crewAI 的做法堪称教科书:每条新记忆入库前先做一次向量检索查相似项,若 top 相似度 ≥ consolidation_threshold(默认 0.85)就触发一次 LLM 调用,让它输出一个 ConsolidationPlan——对命中的已有记录逐条给出 keep/update/delete 动作,并决定这条新记忆 insert_new 是否还有必要。mem0 经典的 DEFAULT_UPDATE_MEMORY_PROMPT 思路一致:检索 top-10 旧记忆,让 LLM 判每条该 ADD / UPDATE / DELETE / NONE。这里有个细节笔者觉得特别妙——mem0 会把记忆的 UUID 先映射成连续的整数 id 再喂给 LLM,防的是 id 幻觉:让模型生成或引用一长串 UUID,它很容易编一个出来,而连续整数它编错的概率低得多。

这套机制的关键,一句话概括就是:去重/合并发生在「进库前」而不是「召回时」,脏数据根本进不来。 这跟 append-only 派「先全进来再说」是两种完全相反的哲学。
② 图结构天然去重(cognee / Graphiti)。 图存储有个免费的好处:实体本身就是节点,同名实体天然该塌缩成同一个。cognee 把这一点用到了极致——它直接用 uuid5(NAMESPACE, name 规范化) 作为节点 id,也就是说「张三」这个实体不管被写入多少次,规范化后算出来的 id 都一样,自动落到同一个节点上,再用一次 LLM 调用把这个节点累积的多条描述合并成一条。Graphiti 走得更细:先用 MinHash + LSH + Jaccard 做模糊匹配快速筛候选,再用 LLM 兜底判定,而且 prompt 里写了一条硬约束——数字、日期、关键限定词不同的,绝不算重复。这条约束很重要,因为「2024 年的预算」和「2025 年的预算」字面高度相似,但合并掉就是灾难。
③ 离线 LLM 重组(Letta / Codex / Hermes)。 前两种是在写入路径上同步做,这一种则是把整理彻底剥离成后台异步任务,由一个专门的 agent 来干。Letta 的 sleep-time agent 在后台读对话 transcript,被明确要求保证 memory block「综合、可读、最新」,用 memory_rethink 这种工具把整块记忆重写一遍。Codex 的 consolidation 把「合并优先于新增」直接写成了硬规则——prompt 里有 no-op preferred、aggressively merge 这样的字眼,并且巧妙地拿 git workspace 的 diff 当路由信号判断哪些记忆需要重新整理。Hermes 的 curator 则做「umbrella 合并」,它有句话笔者印象很深:「一会话一 skill 是失败」——意思是如果每次会话都新长出一个独立技能而不去归并到已有的伞状技能下,那这个记忆系统就退化成了流水账。
这三类(写入门控、图去重、离线重组)的共同点是:它们都不指望主对话 agent 在干活的间隙顺手维护记忆。这也引出后面小结里那个判断。
不过这里有一个值得警惕的反转。笔者克隆到的 mem0 快照里,默认管线其实已经从「LLM 判 UPDATE」倒向了 V3 的「ADD-only +
linked_memory_ids链接 + md5 哈希去重」——也就是说不再让 LLM 激进地覆盖旧记忆,而是只增不改、靠链接关联、靠哈希挡掉完全相同的内容。它的ADDITIVE_EXTRACTION_PROMPT里有个很说明问题的例子:「养了一条叫 Max 的狗」和「和 Max 去露营」会被当成两条记忆而不是重复项合并掉。连 mem0 自己——这个最早把「smart consolidation」当卖点的项目——都在重新权衡「激进合并 vs 保留细节」的取舍。原因不难理解:合并太狠会丢信息,把两条本该独立的事实揉成一条,未来就再也拆不开了。所以「合并优先」不是越激进越好,它本身是个需要校准的旋钮。
遗忘:大部分系统其实压根不会忘
如果说去重还有不少项目认真做,那遗忘这件事就更惨了——笔者的发现相当反直觉:绝大部分记忆系统根本没有真正的遗忘机制。 mem0、cognee、AutoGen 都是只增不减;Letta 的 archival storage 文档里写得明明白白,「persists indefinitely」(永久保留),删除只能靠显式调 API。换句话说,这些系统的「容量管理」全压在召回侧(检索时少取一点)和上下文侧(塞不下就截断),记忆库本身是个只涨不跌的池子。
真正做了遗忘的,思路可以分四类,而且一个比一个有意思。
① 软失效 / 版本化(Graphiti,笔者认为最优雅)。 Graphiti 给每条事实边都带了 bi-temporal 四个时间戳:created_at / expired_at 记的是「系统何时录入、何时让这条记录失效」(录入轴),valid_at / invalid_at 记的是「这个事实在现实中何时开始成立、何时终止」(现实轴)。当一条新事实进来、与某条旧事实矛盾时,旧边不删,而是给它打上 expired_at=now(系统现在认为它过期了)和 invalid_at=新事实的 valid_at(现实中它从新事实生效那一刻起就不再成立)。
| 时间轴 | 字段 | 语义 |
|---|---|---|
| 系统录入轴 | created_at / expired_at | 系统何时录入这条边、何时把它标记为失效 |
| 现实成立轴 | valid_at / invalid_at | 这个事实在现实世界中何时开始、何时终止 |
这套设计的回报是两种查询都能做:既能查「现在为真的事实」(过滤掉已失效的边),又能做 point-in-time 的时间旅行——「系统在某个历史时刻以为什么是真的」。这对处理「用户偏好会演化」这类场景特别干净:包管理从 npm 换成 pnpm,删除式更新会直接抹掉「曾经用过 npm」这段历史,而软失效让记忆变成一个可回溯的版本库。值得一提的是,AutoGPT 的新记忆层干脆直接复用了 Graphiti 这一整套。

② 使用计数驱动(Codex / OpenClaw)。 这一类换了个判断维度:一条记忆该不该留,不看它写入时被打了多高的「重要性」分,而看它实际被用到的频次。Codex 的 stage1_outputs 表带了 usage_count 和 last_usage 两列,consolidation 时会忽略那些 last_usage 落在 max_unused_days 窗口之外的记忆——很久没被召回过的,就当它过期了。OpenClaw 同理,按真实召回流量来决定记忆的去留与晋升。笔者很认同这个取向:写入当下的「重要性」是主观臆测,而被反复召回是客观证据,后者显然更可信。
③ 时间衰减 / 状态机(OpenClaw / Hermes / crewAI)。 这是最朴素也最直接的一类——给记忆加个随时间自然变淡的分数。OpenClaw 用指数衰减,而且是多层的——不同层半衰期不等(如 30 天 / 14 天),让不同性质的记忆按各自的速率变淡;crewAI 的召回打分里则直接含一项 decay = 0.5^(age_days/30)。Hermes 则把它做成了显式状态机:skill 30 天没用 → 标记 STALE,90 天没用 → archive(移进 .archive/ 目录,可恢复,绝不物理删除)。注意这里有个反复出现的设计共识——衰减归衰减,归档归归档,但很少有人真的把数据物理抹掉。
④ tombstone 逻辑遗忘(OpenHands,笔者认为最有工程美感)。 OpenHands 的 condenser 把整个会话历史建模成一份 append-only 的事件日志。当上下文需要压缩时,它不删任何原始事件,而是写一条 Condensation tombstone(墓碑)事件,标记「这一批事件已经被这条 summary 替代了」;之后重放会话视图(View)时,过滤器看到墓碑就跳过被替代的原始事件,只呈现 summary。这就实现了物理保留、逻辑遗忘——原始证据一条没丢(可审计性拉满),但模型看到的上下文是精简过的(不超上限)。它的高明之处在于把「可审计」和「省上下文」这两个本来打架的目标彻底解耦了:一份数据,两套视图。
这四类放一起看,能拼出一张「遗忘策略全景」:
| 思路 | 代表项目 | 触发依据 | 是否物理删除 |
|---|---|---|---|
| 软失效 / 版本化 | Graphiti(AutoGPT 复用) | 新事实与旧事实矛盾 | 否(打失效戳,支持时间旅行) |
| 使用计数驱动 | Codex / OpenClaw | 实际召回频次 / max_unused_days | 否(归档,靠频次淘汰) |
| 时间衰减 / 状态机 | OpenClaw / Hermes / crewAI | 距上次使用的时长 | 否(衰减分数 / STALE→archive) |
| tombstone 逻辑遗忘 | OpenHands | 上下文需要压缩 | 否(写墓碑,重放时过滤) |
一个贯穿全表的 caveat:你会发现「是否物理删除」这一列几乎全是「否」。这不是巧合——成熟的记忆系统几乎一致地选择了「逻辑遗忘而非物理删除」,因为物理删除会永久丢掉演化史和审计轨迹,而这两样在事后排查「为什么 agent 当时这么干」时往往是救命的。真正的稀缺能力不是「删得干净」,而是「忘得可逆」。
小结:别指望主对话 agent 顺手维护记忆
把这一环的观察收一下:真正把「合并优先于新增」做扎实的项目,路径无非两条——要么在写入时就上门控(crewAI / mem0),把脏数据挡在库外;要么用一个离线的、专用的 agent 去定期重组(Letta / Codex / Hermes),把整理变成一个独立的后台任务。 这两条路看起来不同,但背后是同一个判断:记忆维护是一件需要「专门花算力、专门设约束」去做的事,绝不能指望正在干活的主对话 agent 在间隙里顺手维护——它注意力在任务上,顺手记下来的东西既不去重也不失效,攒几轮就把记忆库喂脏了。第四环之所以是分水岭,正是因为它逼着你把「记忆维护」当成一等公民来设计,而不是 append 之后的事后补救。
架构取舍:四种写入哲学
读完前面那么多机制,你大概已经有个直觉:记忆系统真正的分水岭不在「存哪儿」,而在「谁、在什么时候、按什么规则把东西写进去」。存储后端可以换(向量库换成图谱、文件换成 SQLite),但写入哲学一旦定下来,几乎决定了整个系统的体验、成本和可控性。笔者把这件事压缩成一张极精炼的对比表——它把四种主流写入哲学放在七个维度上横切:
| 维度 | 被动抽取(Mem0) | 主动自编辑(Letta) | 时序图谱(Zep) | 文件式(Claude / Letta FS) |
|---|---|---|---|---|
| 写入决策 | LLM 自动抽取 | agent tool call | LLM 抽实体关系 | agent 写文件 |
| 一致性 / 可预测 | 高 | 取决于模型 | 高 | 高 |
| token 成本 | 低 | 高(每次推理) | 中 | 中 |
| 冲突消解 | LLM 判断 ADD/UPDATE/DELETE | agent 重写 | 双时态失效 | 覆写文件 |
| 可审计 | 中 | 高(ADE) | 高(时间点查询) | 最高(纯文本) |
| 运维成本 | 低-中 | 中 | 高(Neo4j) | 最低 |
| 最适场景 | 给现有 agent 加用户记忆 | 复杂自主 agent | 企业动态 / 合规 | coding agent |
这张表值得逐列读,但笔者更想把它读成「四对核心矛盾」。
第一对矛盾:成本 vs 可控。 被动抽取(Mem0)把「记什么」交给一次独立的、廉价的 LLM 抽取调用,主对话 agent 完全不知情,所以 token 成本最低、行为最可预测;代价是它无法对「当前上下文里什么真正重要」做细腻判断——抽取 prompt 没覆盖到的信号就永久丢了。主动自编辑(Letta)正好相反:模型在推理循环里自己调 core_memory_append/memory_replace 决定写什么,理论上最贴近「agent 当下认为重要的东西」,但每一次记忆操作都烧主模型的 inference token,且记忆质量完全锁死在模型判断上——一致性那一栏写的是「取决于模型」,这五个字就是这条路线最大的不确定性。
第二对矛盾:冲突消解放在哪个时间点。 这是四种哲学差异最大的地方。Mem0 在写入时用 LLM 判 ADD/UPDATE/DELETE/NOOP——脏数据进库前先决策;Letta 让 agent 在后续对话里直接重写整块 core memory;文件式干脆就是「覆写文件」,最朴素也最暴力,旧值直接没了;而 Zep/Graphiti 的双时态失效是唯一一个不丢历史的方案——矛盾来了不删旧边,只打失效戳,因此它的「可审计」是「时间点查询」级别的,能回答「系统在某个历史时刻以为什么是真的」。这一列其实暗示了一个分级:覆写 < LLM 判断 < 双时态失效,越往后历史保真度越高,代价是机制越重。
第三对矛盾:可审计 vs 无运维。 注意「可审计」和「运维成本」这两列几乎是镜像的——文件式同时拿下「可审计最高」和「运维成本最低」,看起来是免费的午餐。但这背后有个没写进表格的隐藏约束:文件式的容量受上下文预算硬约束,它能做到零运维 + 纯文本可 diff,是因为它把「扩容」的难题转嫁给了 index-then-fetch 这套分层召回技巧,而不是真的没有代价。相对地,Zep 用 Neo4j 换来了时序推理和审计能力,运维成本那一栏老老实实写着「高」。
第四对矛盾,也是最该记住的一条:没有银弹,只有场景。 表格最后一行其实是整张表的结论——四种哲学不是优劣关系,而是各自咬定了一类场景:给现成 agent 快速加用户记忆选 Mem0,从零构建复杂自主 agent 选 Letta,企业合规 / 动态环境选 Zep,coding agent 选文件式。更有意思的是 Letta 自家那个反直觉实验:纯文件系统 + GPT-4o-mini 在 LoCoMo 上拿到 74.0%,反而高过 Mem0 论文里报告的最佳图变体 68.5%(arXiv:2504.19413)。
Letta 由此得出的结论是:「当前记忆基准可能意义不大,记忆更多关乎 agent 如何管理上下文,而非具体检索机制。」换句话说,写入哲学的选择,比你用的是向量还是图谱重要得多。这也提醒读者,上面表格里的「最适场景」是工程取舍的结果,不是 benchmark 分数的排名——benchmark 本身水分就很大,口径还各不相同(这一点下文专门展开)。
主流产品在收敛什么
把视线从开源项目转到消费级产品,会看到一个比开源世界清晰得多的趋势:ChatGPT、Gemini、Claude 三家,加上一票 coding agent,正在收敛到同一套范式——「压缩的长期画像 + 原始工作记忆」。
这句话值得拆开。所谓「压缩的长期画像」,是一份跨会话持久、被精炼过的用户/项目摘要,常驻或按需注入;所谓「原始工作记忆」,是一段未经压缩的近期对话窗口,用来给那份滞后的画像打补丁(因为摘要往往不实时更新)。为什么三家在没有互相抄的情况下不约而同走到这里? 因为它们面对的是同一个物理约束——上下文窗口有限、full-context 既贵又慢(Mem0 测得 full-context p95 延迟 17.12 秒、单次约 26K token),而用户的真实历史是无限增长的。要在「个性化」和「成本/延迟」之间找平衡点,「一份压缩画像兜底 + 一段原始近期对话补 delta」几乎是唯一的工程解。下面逐个看四家是怎么把同一个范式落到不同取舍上的。
ChatGPT 走的是双层 always-on,把「无摩擦」做到极致。 它有两层:一层是 Saved memories,显式、用户可编辑的事实列表(姓名、偏好、目标),底层是 bio tool,存在系统提示的 "Model Set Context" 区,带时间戳(如 [2025-05-02] The user likes ice cream)——这是三家里唯一对用户完全可见、可审计的清单。另一层是 Reference chat history,2025 年 4 月 10 日上线的隐式画像,注意它并不真正「搜索」过去对话,而是维护一份近期聊天的滚动历史、随时间构建用户画像,用户无法直接查看、不可审计。ChatGPT 的取舍很鲜明:一切自动加载进每个对话,换来「零等待个性化」,代价是牺牲了可审计性和可预测性——你说不准它到底从哪条历史里捞出了什么。
Gemini 走的是单一真相源,是三家里最克制的一个。 2025 年 8 月上线的 Personal Context,核心是一份 user_context 结构化文档:固定 schema 的分段类型化大纲,每条记忆是一个简短事实 bullet,带 Statement + Rationale + 时间戳(如 "User explicitly stated on June 18, 2025…"),再配一个「最近对话轮次」的原始消息窗口当 delta。它最聪明的设计是把冲突消解完全压在时间戳上——「当前角色」是一年前更新的、今天用户说「我换了新公司」,系统就能合理覆盖旧职位;又能把「2024 年 6 月考虑搬去 SF」解读成时间限定的历史而非永久身份。Shlok Khemani 评价 Gemini 这套「比 ChatGPT 和 Claude 都简单」,是单一真相源。坐拥最多数据的 Google 反而在「何时触发个性化、存什么」上最保守,这本身就是个值得玩味的取舍。
Claude 走的是透明可控、工作场景导向,但它的复杂在于「其实是两套系统」。 (A) Search and reference chats(2025 年 9 月)是实时、按需的 RAG:模型主动调 conversation_search 和 recent_chats 两个 tool 去检索原始聊天历史,仅付费用户可用,带引用链接回原聊天。(B) Generate memory from chat history(后加)则是存储的合成画像:Claude 自动摘要对话、跨聊天历史合成关键洞察、每 24 小时更新一次,用户能在 "View and edit memory" 里查看和编辑,编辑立即生效;2026 年 3 月这一层扩展到所有用户(含免费)。Claude 还有个独有设计——project 隔离:每个 project 有独立的 memory 空间和专属 summary,且跨聊天的 memory summary 明确排除 project 内的聊天,让「产品路线图」和「创意写作」互不串味。
这里有个极易混淆、必须讲清的点:消费版 Claude.ai 的 chat memory、开发者 API 的 memory tool、以及 Claude Code 的 CLAUDE.md,是三套完全不同的系统。API 的 memory tool 是
/memories文件目录,模型用create/str_replace/insert命令自管理(beta headercontext-management-2025-06-27,2025-09-29 上线 beta);Claude Code 是按优先级级联加载的 CLAUDE.md 文件;它们都不等于 Claude.ai 网页版那套合成画像 + RAG 搜索。看到 "Claude memory" 四个字,先问清是哪一套。
Coding agent 收敛得更彻底——「代码库索引 + rules 文件 + 自动 memories」成了事实标准三件套。 Cursor 是最完整的样本:codebase indexing 把代码按函数/类语义切分后算 embedding 存进 Turbopuffer 向量库,用 Merkle 树做增量更新(每 5–10 分钟查 hash,只重传改动文件),且只上传 embedding 不传原始代码;rules 存在 .cursor/rules/ 或根目录 AGENTS.md;v1.0(2025 年 6 月)的 Memories 则是一个 sidecar model 观察对话、建议保存条目供开发者批准,且刻意做成 per-project、不跨项目共享。GitHub Copilot Memory(2026 公开预览,3 月对 Pro 默认开)的取舍也很典型——它把记忆分成 repository-level facts(带引用指向支撑代码,用前先校验当前分支是否仍成立,只用 validated facts)和 user-level preferences(跨仓库),并用「28 天未使用自动删除、成功使用重置计时器」做被动遗忘。而把整个生态串起来的,是 CLAUDE.md / AGENTS.md / .windsurfrules / copilot-instructions.md 这套文件式配置——它已经成了跨工具的事实标准。
把这四类放一起看,收敛的不只是「双层结构」这个形态,更是三个共识:其一,长期画像必须被压缩,没人敢把全量历史塞进上下文;其二,画像需要一段原始近期对话来补实时性,因为摘要天然滞后;其三,冲突消解越来越依赖时间维度——Gemini 的时间戳、Copilot 的「用前校验是否仍成立」、Claude 的 24 小时刷新周期,本质都是在回答同一个问题:「这条旧记忆,现在还算数吗?」 真正区分各家的,反倒是那条贯穿始终的产品价值观——ChatGPT 押「无摩擦」、Gemini 押「单一真相源」、Claude 押「透明可控把权交给用户」。范式趋同,哲学分野,这才是这一节最值得记住的洞察。
别太信 benchmark
聊了这么多机制,到了该泼冷水的环节:当前这套记忆 benchmark,可能没你想的那么有参考价值。
最常被引用的 LoCoMo 已经被证明可以靠激进检索策略刷分——只要召回时把候选答案塞得足够多、足够准,分数就上去了,跟记忆系统本身的「理解」关系不大。而真正让笔者意识到「基准失真」的,是 Letta 自家那篇博客《Benchmarking AI Agent Memory: Is a Filesystem All You Need?》里的一个反直觉实验:
他们用一个纯文件系统 + GPT-4o-mini,再加上「极少提示调优」,在 LoCoMo 上拿到了 74.0%——明显高于 Mem0 在自家论文(arXiv:2504.19413)里报告的最佳图变体 68.5%。
一个没有向量库、没有知识图谱、没有任何花哨检索机制的「破文件夹」,反而赢了精心设计的图记忆。Letta 由此给出的结论很扎心:当前的记忆基准可能意义不大,记忆质量更多取决于「agent 怎么管理自己的上下文」,而不是用了哪种具体的检索机制。换句话说,你纠结向量还是图、RRF 还是 cross-encoder 的那些功夫,大概率不如把「什么时候读、读多少、读进来怎么用」这件事想清楚来得值钱。
caveat:横向比较 benchmark 分数基本是个陷阱。 各家报告 LoCoMo 分数时用的 eval-LLM 和检索设置都不一样,不可直接横比。更尴尬的是连 Mem0 自己不同页面的报数都对不上:博客里写 LoCoMo 92.5% / LongMemEval 94.4%(约 6,900 token/query),另一些页面却给的是 91.6% / 93.4%,而且托管版还包含「专有优化、开源版达不到」的部分。所以别拿别人 README 里的数字当真——用你自己的真实数据去评估,这是唯一靠谱的做法。
落地:四条路线,没有银弹
如果你看完这一切,想给自己的 Agent 上一套记忆系统,笔者的第一句话是:先确认你属于哪条路线,再选机制。 这四条路线对应四类截然不同的诉求,参考实现也完全不同:
| 你的场景 | 推荐路线 | 参考项目 |
|---|---|---|
| 给 SaaS / C 端产品做用户记忆,要无人值守 | 数据库派 + 写入时合并门控 | mem0、crewAI |
| 事实会频繁演化、需要时间旅行查询 | 知识图谱 + bi-temporal | Graphiti、Zep |
| coding / 个人 agent,要人能看、能管、能进 git | 文件派 + index-then-fetch | Codex、Claude Code |
| 想要「用得越久越聪明」的自我改进 | 文件派 + 双速离线整理 | Hermes、OpenClaw |
这四条路线没有高下之分,错配才是灾难——给一个 coding agent 上 Neo4j,或者让一个 C 端产品指望用户手动维护 Markdown,都是自找麻烦。
选定路线之后,无论走哪条,下面这几条通用原则都建议刻进 DNA,它们是从十几个项目的踩坑里反复验证出来的:
- 不要过早上图数据库。 向量库 + LLM 抽取能覆盖绝大多数场景,只有当「关系推理」真正成为核心需求时,图才值回它那份不低的运维成本(一套 Neo4j 不是免费的)。
- 给每条记忆打时间戳。 这是冲突消解和时序查询的前提。反面教材很现成——OpenAI 早期的记忆不给条目打时间戳,直接导致在 LoCoMo 的时序类问题上惨败(Mem0 图变体在时序题上 58.13% vs OpenAI 21.71%,差距大头就出在这)。时间戳几乎零成本,但缺了它,「我上周三在干嘛」这类问题你永远答不对。
- 隔离 user / session / project 作用域。 这是相关性和安全的基础。Claude 的 project 隔离、Cursor 的 per-project memories 都印证了这一点——别让 A 项目的记忆污染 B 项目的上下文。
- benchmark 当参考、不当真。 理由上一节已经说透了。
如果一定要笔者推一个「集大成的工程范例」,那是 Codex 的两阶段记忆维护管道——它几乎把「怎么自动维护一个文件记忆库」这件事标准化了:

Phase 1 并行地把每个历史会话蒸馏成 SQLite 结构化记录;Phase 2 则由一个跑在无网、无审批沙箱里的专用 consolidation 子 agent,用 ~/.codex/memories/.git 的 baseline-to-worktree diff 当作唯一的脏数据检测与 ingestion / forgetting 路由信号。它把「记忆整理」本身做成了一个受沙箱约束、可增量、可遗忘、带使用计数反馈的 agent 任务,而不是简单 append 或撒手不管。想自己搭一套文件式自维护记忆库的人,从这个管道开始抄准没错。
收尾:正在收敛的共识,与两个没人解决的缺口
把十几个项目和三家主流产品摊在一起看,会发现喧嚣之下其实有两条共识正在悄悄收敛:
其一,「合并优先于新增」。 无论你存在数据库还是文件里,激进的 append 都会让记忆库慢慢腐烂——重复、过时、自相矛盾的条目越堆越多,最后召回质量崩盘。区别只在于「合并这件事在什么时候做」:crewAI、mem0 选择在写入时用相似度门控 + LLM 决策把脏数据挡在库外;Letta、Codex、Hermes 则交给离线的专用 agent 去做重组。但方向是一致的——别指望主对话 agent 顺手维护,把合并当成一等公民去设计。
其二,「原始记录与压缩视图分离」。 成熟方案都不会用 summary 直接覆盖掉原始证据:OpenHands 用 tombstone 逻辑遗忘但物理保留事件、Codex 把 rollout 原始会话日志与蒸馏后的 summary 分开存、Graphiti 的失效边只打时间戳从不删除。日常召回时看精炼后的视图(省 token、降噪),需要查证或回溯时还能翻到原始记录。可审计性和上下文预算这对矛盾,靠「分层」而非「二选一」来化解。
但话不能说满。两个全行业至今都没真正解决的缺口,笔者必须点出来:
第一,写入前的人工 review,没有一个项目完整做到。 十几个项目里,记忆写入前有人工确认门的一个都没有——全是 LLM 抽取后直接落库。最接近的两个也只是局部:AutoGPT 的删除走两步确认(找候选 → 用户确认 → 执行),但写入仍全自动;Hermes 的 curator 有 dry-run 模式产出 REPORT.md 让人批准,但只覆盖 skill 整理这一块。「记忆质量靠 prompt 硬约束 + 自动化兜底,不靠人」是当前的主流哲学,本质是为了无人值守的全自动体验而放弃人工把关——这个取舍是否划算,留待时间检验。
第二,也是更要命的一个:安全。 多个来源都指出,记忆系统天然易受间接 prompt injection 攻击——攻击者可以在一段看似无害的内容里埋入指令,诱导 agent 在用户毫不知情的情况下读取、甚至写入记忆。一旦被污染的记忆进了库,它会在未来每一次召回时反复影响 agent 的行为,且极难察觉。这不是某一家的实现 bug,而是所有自动记忆方案的共性隐患。在你给 Agent 接上「会自己记东西」的能力之前,请务必把这条风险放在心上。
记忆让 Agent「用得越久越聪明」,但也让它「被污染一次就长期受影响」。这件事远没到尘埃落定的时候——共识在收敛,缺口也明明白白地摆在那里。对想动手的人来说,这恰恰是最好的入场时机。