mem0 是一个开源记忆层(Memory Layer),为 AI Agent 提供长期记忆能力。
用户消息
│
▼
Memory.add() / Memory.search() [memory/main.py]
│
├── LLM 层 (18+ 提供方) [llms/]
│ └── 事实提取 / 记忆合并
├── Embedding 层 (12+ 提供方) [embeddings/]
│ └── 文本 → 向量
├── Vector Store 层 (24+ 提供方) [vector_stores/]
│ └── 向量存储 / 检索
├── Entity Store [memory/main.py]
│ └── 实体抽取 + 记忆-实体双向链接
├── SQLite 存储 [memory/storage.py]
│ └── 历史记录 / 消息缓存
└── Reranker 层 (5 提供方) [reranker/]
└── 可选重排序
把 LLM 的职责压缩到最小(只做事实提取),其余全部用确定性算法(hash 去重、spaCy 实体抽取、向量搜索)完成。
核心流程一:add() — 写入记忆
add(messages, user_id, agent_id, metadata)
│
├── 参数校验
│ ├─ 至少一个身份标识 (user_id/agent_id/run_id)
│ └─ 防止 metadata 注入身份字段
│
└── V3 Phased Batch Pipeline (infer=True)
│
├── Phase 0: 上下文收集
│ └─ 从 SQLite 取最近 10 条消息作为上下文
│
├── Phase 1: 已有记忆检索
│ ├─ 对新消息做 embedding
│ ├─ 向量搜索 top-10 相关已有记忆
│ └─ UUID → 整数映射 (防幻觉,见下文)
│
├── Phase 2: LLM 事实抽取 (单次调用)
│ ├─ 输入: 新消息 + 已有记忆 + 上下文
│ ├─ 输出: 结构化 JSON (只做 ADD,不做 UPDATE/DELETE)
│ └─ 这是 V3 的核心简化——旧版让 LLM 做 CRUD 决策,新版只提取
│
├── Phase 3: 批量 Embedding
│ ├─ 优先用 batch API 一次编码所有文本
│ └─ 失败则逐条回退
│
├── Phase 4: 单条 CPU 处理
│ ├─ BM25 词形还原 (lemmatize)
│ └─ MD5 hash (用于去重)
│
├── Phase 5: Hash 去重
│ └─ 与已有 hash + 本轮已见 hash 比较,跳过重复
│
├── Phase 6: 批量持久化
│ ├─ vector_store.insert() 写入向量
│ └─ SQLite 记录变更历史
│
├── Phase 7: 批量实体链接
│ ├─ spaCy 抽取实体 (零 LLM 成本)
│ ├─ 实体去重 → embed → 搜索已有实体
│ └─ 新增或更新实体-记忆关联
│
└── Phase 8: 保存消息 + 返回
├─ SQLite 保存消息 (按 session 保留最近 10 条)
└─ return [{"id", "memory", "event":"ADD"}, ...]
参数校验
校验三步:user_id、agent_id、run_id至少传一个,这三个会被拆成租户标识用于后续所有搜索的隔离;防止调用者通过 metadata 偷偷注入身份信息来跨越租户边界;消息格式自动归一化——传字符串自动转消息列表,传字典自动包一层。
Phase 0:上下文收集(滑动窗口)
从 SQLite 取最近 10 条消息作为 LLM 提取时的上下文。把 user_id、agent_id、run_id 拼成确定性字符串作为查询 key,按时间倒序取最近 10 条再升序返回。之所以需要这一步,是因为用户说”它很快”,没有上文 LLM 就不知道”它”指什么。
Phase 1:已有记忆检索(向量检索)
搜索与新消息相关的已有记忆,供 LLM 判断哪些是新事实、哪些已经知道了。把新消息做 embedding 后向量搜索取 top-10。关键一步是 UUID → 整数映射:真实记忆 ID 是长 UUID,传给 LLM 时替换成 "0"、"1" 这种短整数。因为 LLM 看到真实 UUID 可能编造不存在的 ID(幻觉),短整数让它只能引用我们给的 ID。
Phase 2:LLM 事实抽取
一次性把所有信息发给 LLM,让它从新消息中提取事实。输入包括系统指令(纯增量提取,只提取新事实)、新消息、已有记忆列表、最近上下文;输出为结构化 JSON,每条记忆包含文本和归属人。之所以叫”增量提取”,LLM 只管提取,去重交给后面的 hash 比较,更新交给外部逻辑。
Phase 3:批量 Embedding
把 LLM 提取的所有记忆文本一次性编码成向量。优先一次 API 调用处理全部,失败则逐条回退。如果 LLM 提取了 20 条记忆,逐条就是 20 次 API 调用,batch 只需要 1 次。
Phase 4:生成指纹 + Hash 去重
对每条记忆做两个纯 CPU 操作:词形还原,如把 “running”→”run”、”cars”→”car”,让后续关键词搜索时搜 “run” 能匹配到存储的 “running”;MD5 哈希,生成 32 位指纹用于去重。同时组装元数据:原文、词形还原后文本、哈希、时间戳、归属人。
跳过已经存在的记忆,分两层:跨批次去重,和已有记忆的 hash 比较;同批次去重,和本轮已处理的 hash 比较。命中任何一个就跳过。用 hash 而不是向量相似度,是因为 hash 是确定性算法——相同文本必定相同指纹,零误判,且比较是 O(1),比向量搜索快几个数量级。
Phase 5:批量持久化
把去重后的记录写入两个存储:向量数据库负责语义检索,批量写入向量和元数据;SQLite 负责审计追踪,记录谁在什么时候加了什么记忆。两者职责分离,各自失败各自逐条回退。
Phase 6:批量实体链接
从记忆文本中抽取实体,建立记忆和实体的双向链接。分五步:用 spaCy (开源的 Python NLP 库)批量抽取人名、组织、地点等实体(零 LLM 成本);标准化去重——”阿里巴巴”和”阿里”归一化为同一个 key;把实体文本编码成向量;搜索是否已有这些实体,精确匹配或语义相似度 ≥ 0.95 都算命中;命中则合并关联记忆列表,新实体则批量插入。这一步让搜索时能通过实体反向找到关联记忆——搜”阿里”,不是靠语义碰运气,而是直接找到链接到这个实体的所有记忆。
每条 entity 的数据结构:
Text
{
"data": "阿里巴巴", // 实体文本
"entity_type": "PROPER", // 实体类型 (PROPER/QUOTED/TOPIC/IDENTIFIER)
"linked_memory_ids": [ // 关联的记忆 ID 列表
"uuid-1",
"uuid-2",
"uuid-3"
],
"user_id": "u1", // 租户隔离字段
"agent_id": "a1"
}
加上向量本身(由 embed(entity_text) 生成),用于语义搜索。
核心操作:
写入(upsert):先精确匹配(同一 normalized key),再语义匹配(向量相似度 ≥ 0.95)。命中则往 linked_memory_ids 追加新记忆 ID;未命中则新建一条 entity 记录。
删除关联:当一条记忆被删除时,遍历记忆的所有 entity,从 linked_memory_ids 中移除该记忆 ID。如果移除后列表为空,整个 entity 记录删除。
搜索:search() 时从查询中抽实体 → 搜 entity store → 拿到 linked_memory_ids → 给这些记忆加分。
真正的知识图谱(比如 Neo4j、NebulaGraph)不会遍历所有节点,靠的是索引 + 图遍历剪枝。
新增节点/边时:通过索引定位。比如新增"辛放 → 工作于 → 阿里巴巴",先在节点索引里查"辛放"和"阿里巴巴"是否存在(B-tree 或哈希索引,O(log n) 或 O(1)),存在就复用,不存在就新建。然后插入边。全程不扫全表。
删除节点时:只需要处理直接关联的边。图数据库存了邻接表——每个节点知道自己连着哪些边。删"辛放"这个节点,只需要遍历它的出边和入边(通常几条到几十条),删掉这些边,再检查相邻节点是否变成孤立节点。不需要碰图中不相关的节点。
更新关系时:只影响路径上的节点。比如"辛放从阿里巴巴跳槽到腾讯",只改一条边的指向,其他几百万个节点完全不受影响。
真正需要遍历的场景是图查询——比如"找到辛放的所有二度人脉",这时才从"辛放"出发,沿着边做 BFS,但也是按需遍历,碰到目标深度就停,不会扫全图。
核心流程二:search
检索流程主要围绕向量检索、BM25检索和实体检索。
search(query, user_id, top_k=20, threshold=0.1)
│
├── 参数校验
│ ├─ 至少一个身份标识
│ └─ 高级过滤 (eq/ne/gt/lt/in/contains/AND/OR/NOT)
│
└── 混合检索 (Hybrid Search)
│
├── Step 1: BM25 预处理
│ └─ 查询词形还原
│
├── Step 2: 实体抽取
│ └─ spaCy 抽取查询中的实体
│
├── Step 3: 语义 Embedding
│ └─ 查询文本 → 向量
│
├── Step 4: 向量搜索 (Over-fetch 4×)
│ └─ 先取 top_k × 4 条候选,再精排
│
├── Step 5: BM25 关键词搜索
│ └─ 全文检索 (仅 Qdrant/ES/PGVector 支持)
│
├── Step 6: BM25 归一化
│ └─ sigmoid 自适应归一化 (参数根据查询词数动态调整)
│
├── Step 7: 实体 Boost
│ └─ 搜索 entity_store,匹配实体的关联记忆加分
│
├── Step 8: 混合评分
│ └─ combined = (semantic + BM25 + entity_boost) / max_possible
│
└── Step 9: 格式化返回
└─ return {"results": [MemoryItem, ...]}
关键组件
实体系统 (Entity Store)
独立的向量集合,存储 (entity_text, entity_type, linked_memory_ids):
- 抽取:spaCy NLP(非 LLM),零 API 成本,抽取 4 类实体(专有名词、引号文本、名词复合、技术标识符)
- 去重:同一实体只存一份,多条记忆链接到同一个实体
- 双向链接:记忆 → 实体、实体 → 记忆,支持”这个实体关联了哪些记忆”的反向查询
- 搜索增强:search() 时匹配实体为关联记忆加分
Session 隔离
通过 user_id + agent_id + run_id 三重标识实现租户隔离:
user_id:不同用户数据完全隔离agent_id:同一用户的不同 Agent 实例隔离run_id:同一 Agent 的不同会话隔离_build_session_scope()生成确定性 scope key,用于 SQLite 查询过滤
SQLite 存储
两张表:
history:记忆变更历史(ADD/UPDATE/DELETE),用于回溯messages:最近 N 条消息(按 session 保留最近 10 条),作为 add() 的上下文
线程安全:threading.Lock 保护所有写操作。