🌐 每日开源研读 #002 — mem0:给AI Agent装上「记忆大脑」

3 阅读1分钟

原始码面前,没有秘密。今天我们一起拆解 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)都遵循同一个套路:

  1. 定义抽象基类(ABC)
  2. 各种具体实现继承基类
  3. 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))
4BM25 关键词搜索
5BM25 分数归一化(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.py1062 行,是整个项目最密集的知识结晶。让我拆解几个绝妙的技巧:

技巧 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 处理运行时白名单 + 拒绝名单 + 后缀匹配

七、吐槽时间

没有完美的项目,来说几个我觉得可以改进的地方:

  1. 同步/异步代码大量重复MemoryAsyncMemory 是近乎相同的两份代码(3220 行!),async 版本只是用 asyncio.to_thread() 包装了一下。理想情况下应该用单一代码库 + 同步适配器。

  2. 记忆类型只实现了 Proceduralenums.py 定义了 Semantic、Episodic、Procedural 三种记忆类型,但只有 Procedural 被实际使用。另外两种还在路上。

  3. 实体提取质量依赖 LLM:实体-记忆知识图谱的质量高度依赖 LLM 的提取能力。如果 LLM 提取的实体不一致(比如「Python」和「Python语言」被当作两个实体),图谱质量会下降。

  4. 配置 Pydantic 模型用了 AnyMemoryConfig 中的 embedderllmvector_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 记忆方案。


小歪每日开源研读,我们明天见! 🚀