Mem0 源码解析:为 AI Agent 构建智能记忆层
深入分析 Mem0 的架构设计与核心实现——如何通过向量数据库、知识图谱和 LLM 驱动的记忆决策,为无状态的大语言模型赋予持久记忆能力。
引言:为什么 AI 需要记忆?
大语言模型(LLM)有一个根本限制——无持久记忆。每次对话都是从零开始。如果你告诉 ChatGPT “我喜欢吃披萨”,下次对话它就忘了。
用软件架构的类比来理解:LLM 就像一个无状态的 Web 服务,每次请求独立处理,不记住之前的交互。而 Mem0(读作 “mem-zero”)就是给这个无状态服务加上的分布式缓存 + 持久化存储系统——就像 Redis 之于 Spring Boot 应用,让无状态变成有状态。
| 问题 | 软件开发类比 | Mem0 的方案 |
|---|---|---|
| LLM 不记得用户偏好 | Session 每次请求都丢失 | 向量数据库持久化 + 语义检索 |
| 上下文窗口有限 | HTTP Body 有大小限制 | 只检索相关记忆注入 prompt |
| 知识不能更新 | 配置写死在 jar 包里 | 记忆可增删改查 + 版本历史 |
| 关系知识难表达 | 只有 KV 存储没有关系型 DB | 知识图谱存实体关系 |
Mem0 在 LOCOMO benchmark 上比 OpenAI Memory 准确率高 26%,响应速度快 91%,Token 消耗降低 90%。这篇文章将深入分析它是如何做到的。
整体架构:分层设计
Mem0 的架构非常像一个标准的 Java 分层架构:
┌─────────────────────────────────────────────────────────────────┐
│ API 层 — Memory (同步) / AsyncMemory (异步) / MemoryClient (云端) │
│ ↓ 继承自 MemoryBase (ABC 抽象基类) │
├─────────────────────────────────────────────────────────────────┤
│ 核心处理层 — LLM Provider + Embedder Provider + Graph Store │
│ 事实提取 / 语义编码 / 实体关系 │
├─────────────────────────────────────────────────────────────────┤
│ 存储层 — VectorStore (Qdrant) + GraphDB (Neo4j) + SQLite │
│ 记忆向量 / 知识图谱 / 变更历史 │
└─────────────────────────────────────────────────────────────────┘
对应到 Java 世界:
memory/= Controller + Service 层(业务逻辑)configs/=application.yml配置llms/+embeddings/+vector_stores/= DAO 层(数据访问适配器)utils/factory.py= Spring 的 BeanFactory
核心设计模式
| 设计模式 | Mem0 中的应用 | Java 类比 |
|---|---|---|
| 抽象工厂 | LlmFactory, VectorStoreFactory, EmbedderFactory | Spring BeanFactory |
| 策略模式 | 各种 Provider 可互换 (OpenAI/Anthropic/Ollama) | JDBC Driver 切换 |
| 模板方法 | MemoryBase 定义接口, Memory 实现具体逻辑 | AbstractService → ServiceImpl |
| 配置驱动 | MemoryConfig 用 Pydantic 模型定义所有配置 | @ConfigurationProperties |
工厂使用 importlib 动态加载类,完全解耦了接口与实现——和 Java 的 ServiceLoader 异曲同工:
class LlmFactory:
provider_to_class = {
"openai": ("mem0.llms.openai.OpenAILLM", OpenAIConfig),
"anthropic": ("mem0.llms.anthropic.AnthropicLLM", AnthropicConfig),
"ollama": ("mem0.llms.ollama.OllamaLLM", OllamaConfig),
# ... 16 种 LLM Provider
}
切换 LLM 提供商只需改一行配置,不用改任何业务代码。
三级存储体系
Mem0 采用三种互补的存储方式,各有分工:
┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐
│ 向量数据库 (主存储) │ │ 图数据库 (关系存储) │ │ SQLite (历史存储) │
│ │ │ │ │ │
│ 存: 记忆文本+向量 │ │ 存: 实体关系三元组 │ │ 存: 变更历史 │
│ 查: 语义相似度 │ │ 查: 图遍历+BM25 │ │ 查: 按 memory_id │
│ 默认: Qdrant │ │ 默认: Neo4j (可选) │ │ 默认: ~/.mem0/ │
│ 支持 16 种 │ │ 支持 3 种 │ │ │
└──────────────────┘ └──────────────────┘ └──────────────────┘
向量数据库:语义搜索
传统数据库用 SQL WHERE name = 'xxx' 做精确匹配。向量数据库做的是语义匹配——即使措辞不同,意思相近也能匹配:
传统搜索: "我喜欢披萨" → 只能匹配包含 "披萨" 的记录
向量搜索: "我喜欢披萨" → 也能匹配 "最爱的食物是意大利饼"
原理是把文本转成高维向量(如 1536 维浮点数数组),语义相近的文本在向量空间中距离近,然后用余弦相似度(Cosine Similarity)找最近邻。
所有 16 种向量数据库(Qdrant、Chroma、PGVector、Pinecone、FAISS 等)都实现统一的 VectorStoreBase 接口——就像 Java 的 JpaRepository,切换数据库只改配置。
图数据库:结构化关系
向量搜索擅长”模糊语义匹配”,但不擅长处理结构化关系:
向量搜索: "Alice 的工作是什么?" → 可能找到 "Alice 是工程师"
图搜索: Alice --works_at--> Google
Alice --is_a--> engineer
Google --located_in--> California
图数据库能回答多跳关系问题(如 “Alice 工作的公司在哪个州?”),这是向量搜索难以做到的。类比:向量数据库像 Elasticsearch(全文搜索),图数据库像真正的关系型数据库(JOIN 查询),二者互补。
SQLite:变更审计
SQLite 用 Event Sourcing 模式记录每条记忆的完整生命周期——从创建到更新到删除。类似 Git 的 commit history,可追溯、可审计。
记忆生命周期:核心创新
Memory.add() 是 Mem0 最核心的方法,它的流程远比简单的”存起来”复杂得多:
用户消息 → 事实提取 → 向量编码 → 相似检索 → LLM 记忆决策 → 持久化
↑ 这是关键步骤
整个流程并行执行向量存储和图存储两条路径:
with concurrent.futures.ThreadPoolExecutor() as executor:
future1 = executor.submit(self._add_to_vector_store, messages, ...)
future2 = executor.submit(self._add_to_graph, messages, ...)
concurrent.futures.wait([future1, future2])
类似 Java 的 CompletableFuture.allOf() 并行执行两个独立的数据库写入。
Step 1:事实提取
调用 LLM 从对话中提取关键事实:
输入: "Hi, my name is John. I am a software engineer."
输出: {"facts": ["Name is John", "Is a Software engineer"]}
LLM 在这里充当信息提取器——从自然语言对话中提取结构化事实,而非简单地存储原始文本。
Step 2:相似检索
对每条新事实做向量搜索,找到已有的相似记忆:
for new_mem in new_retrieved_facts:
embeddings = self.embedding_model.embed(new_mem, "add")
existing_memories = self.vector_store.search(
query=new_mem, vectors=embeddings, limit=5, filters=filters,
)
Step 3:LLM 记忆决策(最精妙的设计)
这一步让 LLM 扮演”记忆管理员”,对比新事实和已有记忆,决定四种操作之一:
| 场景 | 已有记忆 | 新事实 | 决策 |
|---|---|---|---|
| 新信息 | ”是软件工程师" | "名字是 John” | ADD |
| 信息更新 | ”喜欢奶酪披萨" | "喜欢鸡肉披萨” | UPDATE → “喜欢奶酪和鸡肉披萨” |
| 矛盾信息 | ”喜欢奶酪披萨" | "不喜欢奶酪披萨” | DELETE |
| 重复信息 | ”名字是 John" | "名字是 John” | NONE |
这不是简单的字符串比较,而是 LLM 做语义级别的理解和决策。比如 UPDATE 场景中,LLM 理解”喜欢鸡肉披萨”不是替代”喜欢奶酪披萨”,而是补充——于是合并为”喜欢奶酪和鸡肉披萨”。
防止 UUID 幻觉的技巧
一个值得关注的工程细节——代码用整数 ID 替换 UUID 来和 LLM 交互:
temp_uuid_mapping = {}
for idx, item in enumerate(retrieved_old_memory):
temp_uuid_mapping[str(idx)] = item["id"]
retrieved_old_memory[idx]["id"] = str(idx)
LLM 在生成 JSON 时容易”幻觉”UUID(生成看似合法但不存在的 ID)。用简单的 “0”、“1”、“2” 替代后,LLM 就不容易出错。执行完再通过映射表转回真实 UUID。这类似于 API 设计中用整数 ID 而非 UUID 作为外部接口。
LLM 的三种角色
Mem0 中 LLM 不只用来”聊天”,它扮演了三种关键角色:
1. 信息提取器 (Extractor)
从对话消息中提取结构化事实。使用 FACT_RETRIEVAL_PROMPT 引导 LLM 输出 JSON 格式的事实列表。
2. 记忆管理员 (Memory Manager)
对比新事实和已有记忆,决定 ADD/UPDATE/DELETE/NONE。使用 UPDATE_MEMORY_PROMPT 和精心设计的 few-shot 示例。
3. 实体分析师 (Entity Analyst)
图数据库专用——从文本中提取实体和关系三元组。这里使用了 LLM 的 Function Calling 能力:
EXTRACT_ENTITIES_TOOL = {
"type": "function",
"function": {
"name": "extract_entities",
"parameters": {
"type": "object",
"properties": {
"entities": {
"type": "array",
"items": {
"properties": {
"entity": {"type": "string"},
"entity_type": {"type": "string"},
}
}
}
}
}
}
}
Function Calling 类似于定义一个 RPC 接口的 Schema(如 Protobuf/OpenAPI spec)——告诉 LLM “按照这个格式返回”,比让 LLM 自由输出 JSON 再解析要可靠得多。
图记忆:五步操作流程
当启用图数据库时,MemoryGraph.add() 执行一个带冲突检测的 “Upsert Pipeline”:
Step 1: 实体抽取 — LLM Function Calling → {entity, entity_type}
Step 2: 关系建立 — LLM 在实体间建立三元组 (source, relationship, destination)
Step 3: 已有查找 — 向量相似度在 Neo4j 中召回已有关系
Step 4: 冲突检测 — LLM 决定哪些旧关系需要删除
Step 5: 写入执行 — DELETE 旧关系 + MERGE 新关系
两个值得注意的设计:
两步抽取:实体抽取和关系建立拆成两步,第二步把第一步的结果(实体列表)传给 LLM 作为”白名单”,减少幻觉关系。类似两阶段提交的”准备阶段”。
双阈值策略:查询用 0.7 的宽松阈值(召回优先),写入用 0.9 的严格阈值(去重优先)。这和搜索引擎的 recall vs precision 权衡一致。
检索策略:混合搜索
搜索时并行执行向量搜索和图搜索:
with concurrent.futures.ThreadPoolExecutor() as executor:
future_memories = executor.submit(self._search_vector_store, ...)
future_graph = executor.submit(self.graph.search, ...) if self.enable_graph else None
图搜索内部还有一个 BM25 重排序——先用向量相似度在 Neo4j 中召回候选集,再用经典 BM25 算法做关键词相关性重排。类似 Java 中先从 DB 查粗选列表,再在内存中做精排。
最终返回合并两个来源的结果:
{
"results": [...], # 向量搜索结果(语义匹配的记忆)
"relations": [...] # 图搜索结果(结构化的实体关系三元组)
}
记忆注入:Mem0 只是中间件
Mem0 不自动注入记忆到 prompt——这留给应用层。它的设计哲学是做一个”记忆层”中间件,专注于记忆的存取:
# 1. 检索相关记忆
relevant = memory.search(query=message, user_id=user_id, limit=3)
memories_str = "\n".join(f"- {m['memory']}" for m in relevant["results"])
# 2. 注入到 System Prompt
system_prompt = f"You are a helpful AI.\nUser Memories:\n{memories_str}"
# 3. 调用 LLM
response = openai_client.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "system", "content": system_prompt}, {"role": "user", "content": message}]
)
# 4. 从对话中提取新记忆
memory.add(messages, user_id=user_id)
就像 Redis 只负责缓存,不负责告诉你怎么用缓存。
优缺点总结
优点
- 架构设计优秀:严格分层 + 工厂 + 策略模式,添加新 Provider 只需实现一个类
- 智能记忆管理:通过 LLM 理解语义来决定 ADD/UPDATE/DELETE,不是简单 KV 存储
- 双存储互补:向量搜索解决语义匹配,图搜索解决关系推理
- 开箱即用:只需一个
OPENAI_API_KEY,零配置运行 - Event Sourcing:完整变更历史,可追溯可审计
- 并行处理:向量和图的读写都并行执行
缺点
- 强依赖 LLM 质量:事实提取和记忆决策完全依赖 LLM,没有人工 Review 机制
- 成本不低:每次
add()至少 2 次 LLM 调用,启用图还要额外 2-3 次 - 记忆类型区分不足:Semantic/Episodic/Procedural 三种枚举,但前两者实现没有差异
- 缺少衰减机制:所有记忆平等对待,没有”遗忘曲线”或重要性权重
- SQLite 不适合生产:历史存储使用全局锁,不支持高并发写入
适用场景
- 适合:个人 AI 助手、客服系统、需要用户画像的聊天应用
- 不太适合:需要精确记忆管理的金融/医疗场景、超高频写入场景
小结
Mem0 的核心创新在于用 LLM 驱动记忆的全生命周期管理——不是简单地”存”和”取”,而是让 LLM 理解语义后做出智能的增删改决策。这种设计把传统数据库的 CRUD 操作提升到了语义层面。
从架构角度看,Mem0 展示了一个优秀的中间件设计范式:通过抽象工厂 + 策略模式实现了对 16 种 LLM、16 种向量数据库、3 种图数据库的统一适配,让应用开发者只需关心业务逻辑。
在 AI Memory 系列的下一篇文章中,我们将对比其他记忆框架的实现方案。敬请期待。