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 保护所有写操作。