读 memory 项目,如果一开始就盯着 embedding、graph、BM25、RRF,很容易变成名词对名词。 更稳的读法,是跟着一次请求往下走:这条新消息会不会保存成长期事实;旧事实要不要被覆盖、保留还是降权; 哪些工具结果只属于当前任务;最后进入 prompt 的,是原文、摘要、实体关系,还是几条已经排好序的记忆。

这篇是系列入口。它先把“写入”和“召回”拆成可以逐步检查的过程,再把 Mem0、Letta、 Graphiti、LangMem、TencentDB-Agent-Memory、 OpenViking、Cognee、Supermemory 放到同一张地图里。这样后面读源码时,读者看到的就不是一堆孤立功能,而是每个系统把哪一段责任拿到自己手里。

读完后你应该能回答。 读完入口篇,应该能回答四件事:一条对话怎样先被提取成 memory record; 一次查询怎样经过召回、排序和裁剪进入本轮模型输入;为什么分词、embedding、BM25、RRF、图遍历只是这个过程里的不同工具; 以及后面几篇分别把 memory 的哪段责任拿出来深挖。

先用一条偏好理解基本过程:用户说“以后分析报错时,先给复现步骤,再给修复建议”。 系统需要判断这句话值不值得保存、保存成什么形状、下次什么时候取出来。进入具体章节后,例子会换成最能说明该系统工作方式的记忆单位: Letta 看 state block,Graphiti 看带时间的关系,TencentDB-Agent-Memory 看工具日志和长期事实, OpenViking 看资源、记忆、技能怎样进入同一棵上下文树,Cognee / Supermemory 看文档怎样变成平台化上下文。

Agent memory 的两条基本路径

写入侧:
  原始对话 / 文件 / 工具结果
  -> 抽取可复用信息
  -> 规范化成 record / block / edge / document
  -> 去重、更新、标时间或分配 owner
  -> 存入带 scope 的 memory store

读取侧:
  当前问题或当前任务
  -> 检索、过滤、排序
  -> 选出少量证据、画像或任务结构
  -> 放入本轮模型输入
  -> 必要时按引用恢复原文
读者要追踪的对象 它回答的问题 后续章节看哪里
一条用户偏好 怎样从对话变成持久记录,又怎样回到本轮模型输入。 Mem0、LangMem、Supermemory
一个 agent state block 哪些信息从创建 agent 起就被拥有,而不是回答前临时检索。 Letta
一条会过期的关系 历史事实怎样退出当前视图,同时保留来源和时间窗口。 Graphiti
一段厚重工具日志 当前任务材料怎样离开 prompt,又能按引用恢复原文。 TencentDB-Agent-Memory
一棵跨资源、记忆与技能的上下文树 怎样统一寻址、分层读取,并只把选中的少量内容放进模型输入。 OpenViking
一份知识文档 文件或 URL 怎样被处理成可检索知识,并按用户或租户隔离。 Cognee、Supermemory

本系列依据。 本系列会以公开仓库、官方文档、论文和项目博客为准。入口篇只使用项目公开定位和 README 级别的材料做路线划分; 后续源码篇再进入具体数据结构、调用顺序、索引与检索逻辑。服务端托管产品的内部实现不可见时,只描述公开 API 能观察到的行为,不把推测写成源码事实。

一、从一次请求看起:memory 到底帮 Agent 做什么

假设一个 coding agent 连续陪你做三周项目。第一周你告诉它“这个仓库不要 mock 数据库”;第二周它从测试失败里学到 “账单模块依赖真实租户配置”;第三周你只问一句“继续修上次那个报销问题”。如果没有 memory,它只能重新翻聊天记录和工具输出; 如果把所有历史都塞进 prompt,上下文很快被日志和旧结论淹没。

长期记忆系统介入的位置就在这里:写入时把对话里的可复用事实变成 memory record, 读取时从 memory store 里挑出少量证据放回本轮上下文。 前者常被叫做 extraction、add、upsert 或 memory write;后者常被叫做 search、retrieve、recall 或 context assembly。

记忆写入与读取:历史材料经过提取整理存入长期记忆,当前问题驱动检索筛选,选中的内容进入本轮上下文。
先看写入和读取这两个过程,再看各项目差异:写入决定什么值得长期保存,读取决定这一轮模型真正能看到什么。

1.1 写入:从原始对话里提取可长期复用的事实

写入不是把整段聊天原封不动塞进数据库。一次对话里有闲聊、临时指令、工具输出、用户偏好、项目事实和时间线变化; memory 系统要先判断哪些内容以后还可能用得上,再把它变成更稳定的记录。这个过程通常会经过五步:

步骤 系统在判断什么 产物长什么样
观察 这轮输入、回复、工具结果里哪些内容值得检查。 一段 conversation / event / tool trace。
抽取 哪些句子以后还可能影响回答或行动。 几条 candidate memory,例如用户偏好、项目事实、约束。
归并 候选事实和旧记忆是重复、补充、冲突,还是新事实。 ADD、UPDATE、NOOP,或只追加一条新记录并保留旧证据。
落库 这条事实归哪个 user、agent、workspace、project 或 entity。 带 source、scope、time、confidence、entity links 的 memory record。
建索引 以后应该通过关键词、语义、实体还是时间把它找回来。 关键词索引、向量、图边、metadata 字段。

分词和 embedding 是这个过程的基础工具。中文关键词检索需要分词,例如用 GSE 这类 tokenizer 把连续中文切成可索引的词; embedding 把文本转成向量,方便后面按语义召回相似旧记忆。它们常参与建索引、去重和相似记忆查找; “这句话值不值得长期保存”这个判断,还要看 LLM 抽取、规则、schema、时间字段,以及应用允许保存哪些信息。 在一些系统里,LLM 会先抽 candidate memory,再用 embedding / vector search 找相似旧记忆做归并;在另一些系统里,抽取、索引和归并会被拆成异步任务。

放到源码里看会更直观。 以 Mem0 当前 OSS 代码为例,add() 先把 user_id、agent_id、run_id 变成存储 metadata 和查询 filters, 再把消息送进 _add_to_vector_store(..., infer)。默认 infer=True 时, _add_to_vector_store() 会取最近消息、召回相似旧记忆,再让 LLM 做一次抽取。

processed_metadata, effective_filters = _build_filters_and_metadata(...)
vector_store_result = self._add_to_vector_store(
    messages, processed_metadata, effective_filters, infer, prompt=prompt
)

last_messages = self.db.get_last_messages(session_scope, limit=10)
existing_results = self.vector_store.search(..., top_k=10, filters=search_filters)
response = self.llm.generate_response(...)

这几行说明两件事:写入入口已经带着 scope,LLM 看到的是新消息、最近消息和一小批相似旧记忆, 不是整座 memory store。embedding / vector search 在这里承担“找相似旧记忆”的职责,LLM 才负责把候选事实写成新的 memory record。

1.2 召回:从记忆库里组装本轮模型输入

召回面对的是相反的压力:memory store 可以越积越大,prompt 只能容纳很少一部分。用户问“继续修上次那个报销问题”时, 系统不能把三周历史全部拿出来,只能先限定范围,再用几路检索信号召回候选,最后按 token 预算选出本轮模型能看到的少量内容。

步骤 系统在做什么 为什么不能省
范围过滤 先按 user、agent、workspace、project、时间段或权限缩小搜索面。 防止把别的用户、别的项目或已经失效的事实带进来。
多路召回 BM25 找精确词,vector search 找语义相近,entity / graph 找相关对象。 单一路径很容易漏掉“同义不同词”或“词相同但对象不同”的情况。
融合排序 用 RRF、reranker、时间衰减或业务权重合并候选。 候选多了以后,顺序本身就会影响模型注意到什么。
预算裁剪 只保留 top-k 或 token 预算内的一小批证据。 模型需要的是当前任务的工作集,不是完整记忆库。
组装上下文 把记忆记录改写成 prompt 能读的 bullet、引用、profile 或 tool result。 memory record 的存储形态通常不适合直接塞给模型。

BM25、RRF、reranker、hybrid search 就是在召回和排序过程里发挥作用。BM25 给关键词匹配打分; vector search 给语义相似度打分;RRF 可以按名次融合多路结果;reranker 会在候选集上做更精细的相关性判断。 它们共同服务于一个目标:memory store 可以很大,但进入本轮模型输入的内容必须小、准、和当前问题有关。

再看 Mem0 的召回路径。search() 先校验 top_k、threshold 和 filters,再调用 _search_vector_store()。 后者把 query 同时送进语义检索、关键词检索和实体增强,最后由 score_and_rank() 输出预算内结果。

query_lemmatized = lemmatize_for_bm25(query)
query_entities = extract_entities(query)
embeddings = self.embedding_model.embed(query, "search")

internal_limit = max(limit * 4, 60)
semantic_results = self.vector_store.search(..., top_k=internal_limit, filters=filters)
keyword_results = self.vector_store.keyword_search(..., top_k=internal_limit, filters=filters)

scored_results = score_and_rank(
    semantic_results=candidates,
    bm25_scores=bm25_scores,
    entity_boosts=entity_boosts,
    threshold=threshold,
    top_k=limit,
)

这样讲,读者不用靠猜来判断“有没有 BM25、RRF 或 rerank”。这条 OSS 路径里能直接看到 BM25 侧的关键词分数、vector 侧的语义候选和 entity boost; rerank 是 search() 外层的可选分支,RRF 不是这段路径里显式出现的融合步骤。

1.3 把基础概念放回写入和召回过程

很多概念单独看都像“检索技术”,放回写入和召回过程以后,位置就清楚了。下面这张表不是词汇表, 而是一张读源码时的定位表:看到某个机制,先判断它在帮系统保存事实、找候选、合并候选,还是控制最后进入模型的内容。

概念 在哪个过程里出现 读源码时看什么
分词 / GSE 写入建索引,读取处理 query 它通常服务关键词索引,不代表系统已经理解了事实。
embedding 写入建向量,读取做 vector search 它负责语义相似度,常用来找相似旧记忆或候选证据。
BM25 读取召回或排序 它偏字面匹配,适合项目名、函数名、专有名词这类精确线索。
vector search 读取召回或写入归并 它偏语义相似,适合“说法变了但意思接近”的线索。
entity / graph 写入抽实体,读取扩展关系 它让人、项目、地点、任务之间的连接参与检索。
RRF / rerank 读取排序 它决定候选进入模型前的先后顺序和去重结果。
top-k / 本轮模型输入 读取结束 它按最后的 token 预算,决定模型这一轮真正看见什么。

有了这层基础,再看后面的项目会清楚很多:有的项目强在提取,有的项目强在召回, 还有的项目重点解决保存位置:原始历史、用户画像、关系、任务状态和本轮模型输入分别由哪个组件处理。

二、用五个盒子比较项目

回到前面的报销例子,agent 手里其实有五个盒子。第一个盒子装原始证据:消息、文件、事件、工具结果; 第二个盒子装稳定画像:用户偏好、团队习惯、项目约束;第三个盒子装关系:人、项目、文档、任务怎样连接; 第四个盒子装时间线:哪些事实以前成立、现在成立、未来计划;第五个盒子装这一轮 prompt 里真正出现的少量材料。

判断一个项目,不妨先看它把哪个盒子当主战场。Mem0 把写入、归并和召回放到应用与模型之间的一层 memory service; Letta V1 把 profile、tools、messages 和 agent state 绑在同一个有状态 agent 里,当前 Letta Code 本地后端则会把 Git 已提交记忆编译进上下文;Graphiti 把实体、关系、episodes 和时间有效期组织成 temporal graph; LangMem/LangGraph 把记忆动作接进工作流运行时,区分 hot path 和 background path;TencentDB-Agent-Memory 先处理当前任务里的上下文卸载, 随后扩展到按权限装配的团队记忆资产;OpenViking 把 resource、memory、skill 与 session 统一到 viking:// 上下文树; Cognee 与 Supermemory 更像把企业知识、连接器、文件处理和用户画像一起产品化。

Agent Memory 阅读目录:依次介绍 Mem0、Letta、Graphiti、LangMem、TencentDB、OpenViking,以及同篇比较的 Cognee 与 Supermemory。
入口篇先做路线图。每个项目最值得读的地方,是它把原始证据、用户画像、图关系、时间线和本轮模型输入分别放在哪里。

三、七篇文章,八条 memory 路线

Mem0:从可更新记忆到 ADD-only 写入

Mem0 的公开定位是给 AI assistants 和 agents 提供 memory layer。它最适合作为系列第一篇: 写入、去重、实体链接、多信号检索、Temporal Reasoning、Memory Decay 都集中在“应用和模型之间的一层记忆服务”里。

Letta / Letta Code:Agent 恢复运行时,哪些记忆会一起回来

Letta 篇先用已归档的 V1 解释 AgentState、memory blocks 与恢复,再沿当前 Letta Code 本地后端查看: 哪些 Git 已提交文件会进入核心记忆正文,哪些只进入目录,以及记忆变化后何时重新编译。

Graphiti / Zep:事实变化后,怎样保留历史与当前状态

Graphiti 把事实、实体、关系和 episodes 组织成带时间有效期的 context graph。它关心的不只是能不能搜到一段文本, 而是事实什么时候成立、什么时候被替代、能不能追回来源。

LangMem / LangGraph:记忆何时立刻写,何时后台整理

LangMem 提供 memory tools、background manager,并和 LangGraph store 集成。它的看点在于记忆动作是否发生在 agent 当前推理路径里, 还是在后台整理。调用用了 async、任务已入队、状态能够重启恢复,是三个需要分别核对的条件。

TencentDB Agent Memory:从本地记忆,到团队记忆服务器

TencentDB-Agent-Memory 先用 Context Offload 和 L0-L3 处理单个 Agent 的上下文压力, 再用 Memory Hub、Memory Proxy、ACL 和 Loadout 把经验变成团队资产。

OpenViking:Memory、Resource 与 Skill 进入同一棵上下文树

OpenViking 把 resources、memories、skills 与 sessions 放进 viking:// 命名空间, 用 L0/L1/L2 控制阅读深度。list 返回候选,context 按预算组装材料;session commit 的 accepted 与 .done 分别对应持久交接和后台处理完成。

Cognee:把多源文档处理成可检索知识图

Cognee 强调 ingest data、build self-hosted knowledge graph、vector embeddings、graph reasoning 和 ontology generation。 它更像把企业或个人知识库先变成一套可追踪、可检索、可连接的长期知识层。

Supermemory:通过 context API 为多个 Agent 提供记忆

Supermemory 把 memory、user profiles、hybrid search、connectors、file processing 放到同一套 context engine 里。 写入时可选择只建搜索索引,或同时抽取事实、更新画像;部署时也要区分本地 Memory API 与平台的连接器、托管 MCP 等能力。

四、为什么第一篇先讲 Mem0

Mem0 很适合做第一篇,不只是因为它热度高,也因为它的算法演进足够清楚。早期 paper mem0 会让 LLM 对相似旧记忆做 ADD、UPDATE、DELETE、NOOP 判断;mem0g 再把实体和关系放进图结构;到 2026 年 4 月的新算法, 公开材料强调 single-pass ADD-only、entity linking、hybrid retrieval、Temporal Reasoning 和 Memory Decay。

这条线能帮读者建立一个基础问题:如果写入时修改旧记忆,系统能维持较小 memory set,但会把“是否过时”的判断压到写入阶段; 自动抽取采用 ADD-only,不等于管理 API 不能更新或删除。对这些追加写入的记录,系统能保留历史变化,检索阶段就要承担更多排序、时间解释和去噪责任。理解这个取舍以后,再去看 Letta 的 agent state、 Graphiti 的 temporal graph、LangMem 的 hot path/background memory、TencentDB-Agent-Memory 的 Context Offload、OpenViking 的 context tree, 就不会把所有项目都看成“向量库外面包一层 SDK”。

已完成。 Mem0 记忆算法怎么演进 已经作为系列第一篇发布, 重点讲 paper mem0、mem0g、v3、Temporal Reasoning 和 Memory Decay。

五、这一组文章按什么顺序读

这一组是七篇文章、八个项目路线:最后一篇把 Cognee 与 Supermemory 放在一起,是因为它们都把 memory 做成可供多个 Agent 使用的服务。 文章顺序不按项目热度排,而按“记忆保存位置从外到内、内容从文本到结构、服务对象从单个 Agent 到多个 Agent”的顺序排。这样每一篇都能接住上一篇留下的问题, 而不是平铺罗列功能清单。

顺序 项目 主线问题
1 Mem0 自动抽取怎样从可更新记忆走向 ADD-only,以及过期过滤与多信号检索怎样控制可见结果。
2 Letta / Letta Code 当 agent state 成为核心抽象,memory block、persona、human profile 和工具调用怎样一起工作。
3 Graphiti / Zep 时间、来源和关系进入图以后,长期记忆怎样回答“以前”和“现在”这类问题。
4 LangMem / LangGraph 记忆立即写入会增加多少回答延迟,放到后台后又如何安排触发和失败重试。
5 TencentDB Agent Memory 当前任务里的工具日志怎样移出模型输入但保留原文,同时把跨会话信息整理成 L0 到 L3 长期记忆。
6 OpenViking 资源、记忆、技能和会话怎样统一寻址、分层读取,并在 session 中断后继续提交未完成记忆。
7 Cognee / Supermemory 当记忆扩展成知识层或 context API,检索、连接器、文件处理和用户画像怎样被打包。

六、选型时先问历史存在哪里

读完这些项目后,最可复用的判断不是“哪一个 benchmark 更高”,而是四个具体问题: 原始历史由哪个组件保存,长期画像由哪个组件更新,关系和时间变化在哪里解释,本轮模型输入由哪个组件组装。 如果这些位置没有先定清楚,后面无论接多少 embedding、BM25、graph traversal 或 rerank,都很容易把问题推迟到 prompt 里。

这个系列会尽量沿着源码和公开 API 行为往下读:先看入口 API,再看数据模型,再看写入和检索路径,最后回到工程取舍。 对读者来说,目标不是记住每个项目的宣传语,而是能在自己的 agent 里判断:这份记忆应该是证据、画像、关系、任务状态, 还是只属于当前这轮模型上下文。

参考资料