原始码面前,没有秘密。今天我们一起拆解 53k+ Star 的 AI 记忆层项目——mem0。
前言
嗨大家好,我是小歪!昨天我们聊了 browser-use,今天换个口味——来看一个更底层、更「infrastructure」级别的项目:mem0ai/mem0。
你有没有想过,为什么 ChatGPT 每次对话都是「从零开始」?为什么你的 AI 助手记不住你上周说过的偏好?记忆,是当前 AI Agent 最大的短板之一。mem0 就是要解决这个问题——它为 AI Agent 提供了一个通用的、持久的记忆层。
一句话概括:mem0 是 AI Agent 的记忆中间件,让对话不再金鱼脑。
⭐ GitHub Stars: 53k+
🔧 核心语言: Python
📦 项目地址: github.com/mem0ai/mem0
一、架构总览:分层抽象的艺术
先上个全局视角,mem0 的模块结构是这样的:
mem0/
├── memory/ # 核心:记忆的增删改查、实体链接、历史记录
├── configs/ # 配置系统 + Prompt 工程(宝藏!)
├── llms/ # LLM 抽象层(16 个 Provider)
├── embeddings/ # 向量嵌入抽象层
├── vector_stores/ # 向量数据库抽象层(20+ 种!)
├── reranker/ # 重排序抽象层
├── client/ # 云端 API 客户端
├── utils/ # 工厂模式、实体提取、评分
└── exceptions.py # 结构化异常体系
这个架构有一个核心设计模式:ABC + Factory。
每个可插拔组件(LLM、Embedding、VectorStore、Reranker)都遵循同一个套路:
- 定义抽象基类(ABC)
- 各种具体实现继承基类
- Factory 根据字符串名称动态加载对应实现
# 工厂模式的核心:importlib 动态导入
def load_class(class_type):
module_path, class_name = class_type.rsplit(".", 1)
module = importlib.import_module(module_path)
return getattr(module, class_name)
这种设计让配置变得超简单——你只需要传一个字符串就能切换底层实现:
config = {
"llm": {"provider": "openai", "config": {"model": "gpt-4o"}},
"embedder": {"provider": "openai"},
"vector_store": {"provider": "qdrant"}
}
二、核心流程:记忆是怎么被「记住」的?
这是整篇文章最重要的部分。当你调用 memory.add(messages, user_id="alice") 时,背后发生了什么?
mem0 V3 版本采用了一个 8 阶段的批处理管道(Phased Batch Pipeline),这是整个项目最精妙的设计:
Phase 0:上下文收集
从 SQLite 历史库中取出最近 10 条消息,作为会话上下文。这样即使你分多次调用 add(),系统也能感知到之前的对话。
Phase 1:现有记忆检索
把完整对话做 embedding,在向量库里搜索 top_k=10 的相似已有记忆。这些已有记忆在后面有两个用途:
- 去重参考:避免重复提取
- 链接参考:新记忆可以关联到已有记忆
Phase 2:LLM 提取(核心!)
调用 LLM,使用 ADDITIVE_EXTRACTION_PROMPT 系统提示词 + generate_additive_extraction_prompt() 用户提示词,只做 ADD 操作。
等等——只做 ADD?之前的版本不是还要做 UPDATE 和 DELETE 吗?
没错!这是 V3 最大的架构决策:抛弃了复杂的 ADD/UPDATE/DELETE 决策树,改为纯增量提取模型。
为什么?因为让 LLM 同时做「提取 + 决策」太难了。LLM 经常搞混 UPDATE 和 DELETE,不如让它只专注于提取新记忆,去重和更新交给确定性算法来处理。
# LLM 返回的 JSON 结构
{
"memories": [
{
"memory": "用户喜欢用 Python 写后端",
"linked_memory_ids": ["0", "3"], # 关联到已有记忆
"attributed_to": "user",
"occurred_at": "2024-01-15"
}
]
}
Phase 3:批量嵌入
把所有提取出的记忆文本一次性做 embedding。如果批量调用失败,优雅降级到逐条嵌入。
Phase 4:元数据处理
为每条记忆创建元数据:原始数据、MD5 哈希、词形还原后的文本、时间戳等。
Phase 5:哈希去重
计算记忆文本的 MD5 哈希,与已有记忆的哈希比对。新记忆如果在已有集合中存在相同哈希,直接跳过。这就是为什么 V3 可以放心做纯 ADD——哈希去重保证了不会插入完全相同的记忆。
Phase 6:批量持久化
批量插入向量数据库 + 批量写入 SQLite 历史记录。同样有逐条降级方案。
Phase 7:实体链接
这是 mem0 最酷的功能之一——实体-记忆知识图谱!
系统会从每条记忆文本中提取实体(人名、地名、概念等),然后在独立的实体向量库({collection_name}_entities)中搜索相似实体:
- 匹配到已有实体(相似度 ≥ 0.95)→ 把新记忆 ID 追加到该实体的
linked_memory_ids - 没匹配到 → 创建新实体
这形成了一个二部图(Bipartite Graph):实体 ↔ 记忆,多条记忆可以通过共享实体关联起来。
Phase 8:保存消息
把原始消息写入 SQLite 的 messages 表,完成整个流程。
三、搜索机制:不只是向量检索
当你调用 memory.search(query, user_id="alice") 时,背后是一个 9 步搜索管道:
| 步骤 | 操作 |
|---|---|
| 1 | 查询预处理(词形还原 + 实体提取) |
| 2 | 查询向量化 |
| 3 | 语义搜索(过度获取:max(top_k×4, 60)) |
| 4 | BM25 关键词搜索 |
| 5 | BM25 分数归一化(Sigmoid 函数) |
| 6 | 实体增强(提取查询实体→搜索实体库→关联记忆获得 Boost) |
| 7 | 构建候选集 |
| 8 | 综合评分排序 |
| 9 | 格式化结果 |
最有趣的是实体增强评分(Entity Boost):
boost = similarity × ENTITY_BOOST_WEIGHT × memory_count_weight
memory_count_weight = 1.0 / (1.0 + 0.001 × (num_linked - 1)²)
这个「扩散衰减」公式会惩罚关联了太多记忆的热门实体——一个实体关联了 1000 条记忆时,它的增强效果会被大幅削弱,避免「 hubs 实体」垄断搜索结果。
四、Prompt 工程:真正的宝藏
mem0/configs/prompts.py 有 1062 行,是整个项目最密集的知识结晶。让我拆解几个绝妙的技巧:
技巧 1:UUID → 整数反幻觉
# 把真实 UUID 映射成简单整数再喂给 LLM
uuid_map = {real_uuid_1: "0", real_uuid_2: "1", ...}
# LLM 返回 {"linked_memory_ids": ["0", "3"]}
# 再映射回真实 UUID
LLM 看到的是简单的整数 ID,它只能在给定的 ID 中选择,彻底杜绝了幻觉 UUID 的问题。超级聪明的做法!
技巧 2:时间锚定
Prompt 中同时提供 Observation Date(对话发生时间)和 Current Date(当前时间)。这样当对话中说「昨天」「上周」时,LLM 能正确推断出具体日期。
技巧 3:反「首题主导」
Prompt 中明确写道:
Beware of "first topic dominance" — you must scan ALL messages before extracting any memories. Do not fixate on the first topic discussed.
这是对 LLM 一个已知缺陷的直接对抗——LLM 容易被对话开头的话题「锚定」,忽略后面的内容。
技巧 4:近期记忆去重窗口
维护一个滑动窗口(最多 20 条)的最近提取记忆,防止在同一次会话的多次 add() 调用中反复提取同样的信息。
五、向量数据库层:20+ 种适配
mem0 支持 20 多种向量数据库,包括 Qdrant、FAISS、Pinecone、Milvus、Chroma、Redis、PostgreSQL (pgvector)、Elasticsearch、Weaviate……几乎你能想到的都支持。
以默认的 Qdrant 为例,它支持混合检索(Hybrid Search):
- 稠密向量(Dense Embedding):语义检索
- BM25 稀疏向量(Sparse Embedding):关键词检索
- 使用 Qdrant 的
query_points()API +prefetch实现两路召回 + RRF 融合
# Qdrant 混合检索核心代码
query_points(
collection_name=self.collection_name,
query=query_vector,
using="default", # 稠密向量
prefetch=[
Prefetch(query=query_vector, using="default", limit=limit),
Prefetch(query=sparse_vector, using="bm25", limit=limit),
],
query=Query(fusion=rrf), # Reciprocal Rank Fusion
limit=top_k
)
六、设计模式总结
| 模式 | 应用场景 | 亮点 |
|---|---|---|
| ABC + Factory | 所有可插拔组件 | 字符串配置驱动,零代码切换实现 |
| 实体-记忆二部图 | 记忆关联 | 独立实体库 + linked_memory_ids |
| 阶段批处理管道 | 记忆写入 | 8 阶段 + 逐级降级 |
| 哈希去重 | 防止重复记忆 | MD5 + 词形还原双重保障 |
| 懒加载 | 实体存储 | 首次使用时才创建,节省资源 |
| 优雅降级 | 所有外部调用 | 批量→逐条,失败→跳过 |
| 安全感知配置 | API Key 处理 | 运行时白名单 + 拒绝名单 + 后缀匹配 |
七、吐槽时间
没有完美的项目,来说几个我觉得可以改进的地方:
-
同步/异步代码大量重复:
Memory和AsyncMemory是近乎相同的两份代码(3220 行!),async 版本只是用asyncio.to_thread()包装了一下。理想情况下应该用单一代码库 + 同步适配器。 -
记忆类型只实现了 Procedural:
enums.py定义了 Semantic、Episodic、Procedural 三种记忆类型,但只有 Procedural 被实际使用。另外两种还在路上。 -
实体提取质量依赖 LLM:实体-记忆知识图谱的质量高度依赖 LLM 的提取能力。如果 LLM 提取的实体不一致(比如「Python」和「Python语言」被当作两个实体),图谱质量会下降。
-
配置 Pydantic 模型用了
Any:MemoryConfig中的embedder、llm、vector_store字段类型都是Any,失去了类型检查的优势。
八、快速上手
from mem0 import Memory
m = Memory()
# 添加记忆
m.add("我叫小歪,我每天研究开源项目", user_id="xiaowai")
# 搜索记忆
results = m.search("我叫什么?", user_id="xiaowai")
print(results) # → [{memory: "用户叫小歪", score: 0.95, ...}]
# 查看所有记忆
all_memories = m.get_all(user_id="xiaowai")
# 更新记忆
m.update(memory_id="xxx", data="我叫小歪,最喜欢AI项目")
# 删除记忆
m.delete(memory_id="xxx")
结语
mem0 解决了一个 AI Agent 领域的刚需问题——持久化记忆。它的 V3 架构用「纯增量提取 + 哈希去重」替代了复杂的 ADD/UPDATE/DELETE 决策,用「实体-记忆知识图谱」增强了搜索质量,用「UUID→整数映射」杜绝了 LLM 幻觉——这些都是在生产环境中摸爬滚打出来的经验。
如果你在构建 AI Agent,mem0 值得一试。它不是最完美的,但绝对是当前最成熟的 AI 记忆方案。
小歪每日开源研读,我们明天见! 🚀