深入探究Mem0记忆库
本文部分内容由AI辅助生成,AI生成部分内容都已经过人工审核验证
GitHub: https://github.com/mem0ai/mem0
项目版本: v2.0.1(来自 pyproject.toml)
Git Commit:6d3486ca
代码行数: ~138,621 行
语言: Python + TypeScript(双 SDK 仓库)
目录
Repo 结构
这是一个 Polyglot Monorepo,包含 Python 和 TypeScript 两套 SDK,以及其他组件:
| 目录 | 说明 | 包名 |
|---|---|---|
mem0/ | Python SDK(同步 + 异步),核心实现 | mem0ai(PyPI) |
mem0-ts/ | TypeScript SDK(同步 + 异步),与 Python 功能对齐 | mem0ai(npm) |
cli/python/ | Python CLI | mem0-cli(PyPI) |
cli/node/ | Node CLI | @mem0/cli(npm) |
server/ | 自托管 FastAPI 服务器 | — |
openmemory/ | 自托管记忆平台(API + Next.js UI) | — |
vercel-ai-sdk/ | Vercel AI SDK Provider | @mem0/vercel-ai-provider |
openclaw/ | OpenClaw AI Agent 运行时平台记忆插件 | @mem0/openclaw-mem0 |
mem0-plugin/ | AI 编辑器插件(MCP 服务器) | — |
docs/ | Mintlify 文档站点 | — |
evaluation/ | 基准测试框架(LOCOMO 等) | — |
本文档聚焦:mem0/(Python SDK)和 mem0-ts/(TypeScript SDK)的核心实现。两者功能完全对等,以下代码示例以 Python 为主,TypeScript 对应 API 保持一致。
版本概述
Mem0 经历了几个重大版本演进:
| 版本 | 发布时间 | 核心变化 |
|---|---|---|
| v0.1.x | 2024-2025 | 早期版本,基础记忆存储功能 |
| v1.0.0 | 2025-10 | 正式版本,稳定 API,新增 Reranker、元数据过滤、Azure 向量存储 |
| v2.0.0 | 2026-04 | V3 内存管道升级:单次 ADD-only 提取、混合检索、实体链接(实体关联通过内置 Entity Collection 实现,替代默认依赖外部图数据库的方案) |
V3 说明:文档中提到的”V3 管道”是代码内部的管道版本号(# === V3 PHASED BATCH PIPELINE ===),并非产品版本号。产品版本号为 v2.0.1。
v2.0 核心变化:
- Single-pass ADD-only 提取:从 2 次 LLM 调用简化为 1 次,延迟降低约 50%。不再产生 UPDATE/DELETE 事件,只产生 ADD 事件,记忆通过
linked_memory_ids关联 - Multi-signal 混合检索:语义向量 + BM25 关键词 + 实体匹配三种信号融合
- 内置实体链接:替代 Graph Memory,无需外部图数据库
迁移注意:v2.0 后,add() 不再产生 UPDATE/DELETE 事件。如需更新或删除记忆,需手动调用 update() / delete() API。
项目背景
团队信息
Mem0 由 Taranjeet Singh(CEO)和 Deshraj Yadav(CTO)于 2024 年创立,公司总部位于旧金山。
| 创始人 | 背景 |
|---|---|
| Taranjeet Singh | 前 Paytm 工程师、Khatabook 高级产品经理。创建了 EmbedChain(8.9K GitHub Stars) |
| Deshraj Yadav | 前 Tesla Autopilot 高级工程师,负责 AI 平台后端架构设计。创建了 EvalAI(2K+ GitHub Stars) |
孵化背景:Mem0 是 Y Combinator Summer 2024 批次的创业公司,于 2025 年 10 月完成 $24M 融资(Seed + Series A)。
商业化模式
Mem0 采用 开源 + 托管平台 双模式:
| 模式 | 说明 | 适用场景 |
|---|---|---|
| 开源 SDK | Apache-2.0 许可,完全免费,自托管 | 数据敏感、需要完全控制、成本敏感 |
| 托管平台 | SaaS 服务,按量付费 | 快速迭代、生产环境、无需运维 |
托管平台定价:
| 方案 | 价格 | 记忆数量 | API 调用/月 | 特性 |
|---|---|---|---|---|
| Hobby | 免费 | 10,000 | 1,000 | 社区支持 |
| Starter | $19/月 | 50,000 | 5,000 | 社区支持 |
| Pro | $249/月 | 无限 | 50,000 | Graph Memory、高级分析 |
| Enterprise | 定制 | 无限 | 无限 | 私有部署、SSO、SLA |
维护模式
| 方面 | 说明 |
|---|---|
| 开源维护 | GitHub 社区贡献 + 核心团队审核合并 |
| 核心团队 | 分布在旧金山、印度等地 |
| 发布节奏 | 频繁迭代,平均每周 1-2 个版本 |
| 社区规模 | GitHub 54.7K+ Stars,100K+ 开发者使用平台 |
| 组织规模 | GitHub 组织 1.1K+ followers,9 个开源仓库 |
相关项目
Mem0 团队还维护了以下开源项目:
| 项目 | 说明 | GitHub Stars |
|---|---|---|
| EmbedChain | RAG 框架,简化 LLM 应用开发(Mem0 的前身项目,200万+ 下载) | 已归档,不再维护 |
| EvalAI | AI 模型评估平台,支持大规模 ML/AI 算法评测 | 2K+ |
什么是 Mem0?
Mem0(发音为 “mem-zero”)是一个专为 AI Agent 和助手设计的智能记忆层。它的核心价值在于:为 AI 应用提供持久化、个性化的记忆能力,让 AI 从”陌生人”变成”认识你的老朋友”。
快速上手
from mem0 import Memory
# 初始化记忆实例
memory = Memory()
# ========== 存储记忆 ==========
# 从对话中自动提取并存储记忆
memory.add(
messages=[{"role": "user", "content": "我叫小明,是一名程序员,最近在学习 Rust"}],
user_id="user_123"
)
# 返回: {"results": [{"id": "mem-uuid-1", "memory": "用户叫小明,是一名程序员,最近在学习 Rust", "event": "ADD"}]}
# ========== 检索记忆 ==========
# 语义搜索相关记忆
results = memory.search("用户是做什么的?", filters={"user_id": "user_123"})
# 返回: {"results": [{"id": "mem-uuid-1", "memory": "用户叫小明,是一名程序员...", "score": 0.92}]}
# ========== 获取所有记忆 ==========
# 列出用户的所有记忆
all_memories = memory.get_all(filters={"user_id": "user_123"})
# 返回: {"results": [{"id": "mem-uuid-1", "memory": "...", ...}, ...]}
# ========== 获取单条记忆 ==========
memory_item = memory.get(memory_id="mem-uuid-1")
# ========== 更新记忆 ==========
# 手动更新记忆内容(会产生 UPDATE 事件)
memory.update(memory_id="mem-uuid-1", data="用户叫小明,是一名高级程序员,精通 Rust")
# ========== 删除记忆 ==========
# 手动删除记忆(会产生 DELETE 事件)
memory.delete(memory_id="mem-uuid-1")
# ========== 清空记忆 ==========
# 删除用户的所有记忆
memory.delete_all(user_id="user_123")
# ========== 查看历史 ==========
# 查看记忆的变更历史
history = memory.history(memory_id="mem-uuid-1")
# 返回: [{"event": "ADD", "old_memory": null, "new_memory": "...", "created_at": "..."}]
核心能力:
| 方法 | 说明 |
|---|---|
add() | 智能提取并存储记忆(LLM 驱动) |
search() | 混合检索(语义 + 关键词 + 实体) |
get_all() | 获取所有记忆 |
get() | 获取单条记忆 |
update() | 手动更新记忆 |
delete() | 手动删除记忆 |
history() | 查看记忆变更历史 |
核心架构
Mem0 采用典型的分层架构,各组件职责清晰:
Memory 类
├── embedding_model → 向量化模型(文本 → 向量)
├── vector_store → 向量数据库(记忆存储)
├── llm → 大语言模型(理解记忆内容)
├── db → SQLite(历史消息)
└── reranker → 重排序模型(可选)
关键特点:
- 工厂模式:通过 Factory 类动态创建各种 Provider
- 配置驱动:使用 Pydantic 管理所有配置
- 懒加载:Entity Store 在首次使用时才初始化
数据存储
Mem0 采用双存储架构:关系型数据库管理历史消息和记忆变更历史,向量数据库存储记忆向量。
关系型存储(历史消息)
数据库支持:
| 数据库 | Python SDK | TypeScript SDK | 用途 | 生产环境 |
|---|---|---|---|---|
| SQLite | ✅ 默认 | ✅ 默认 | 历史消息 + 变更历史 | ✅ 推荐(本地部署) |
| Supabase (PostgreSQL) | ❌ | ✅ 可选 | 云端历史存储 | ✅ 推荐(云端部署) |
Python SDK:SQLite
Python SDK 目前仅支持 SQLite 作为关系型存储。配置项为 history_db_path:
# 本地文件(持久化)
config = {"history_db_path": "/data/mem0_history.db"}
# 内存数据库(临时,适合测试)
config = {"history_db_path": ":memory:"}
SQLite 管理两类数据:
1. history 表:记忆变更历史
CREATE TABLE IF NOT EXISTS history (
id TEXT PRIMARY KEY,
memory_id TEXT,
old_memory TEXT,
new_memory TEXT,
event TEXT,
created_at DATETIME,
updated_at DATETIME,
is_deleted INTEGER,
actor_id TEXT,
role TEXT
);
| 字段 | 类型 | 说明 |
|---|---|---|
id | TEXT (PK) | 记录唯一ID (UUID) |
memory_id | TEXT | 关联的记忆ID |
old_memory | TEXT | 变更前的记忆内容 |
new_memory | TEXT | 变更后的记忆内容 |
event | TEXT | 事件类型:ADD / UPDATE / DELETE |
created_at | DATETIME | 记忆创建时间 |
updated_at | DATETIME | 记录更新时间 |
is_deleted | INTEGER | 软删除标记 (0/1) |
actor_id | TEXT | 发言者标识(来自消息的 name 字段,用于多参与者场景) |
role | TEXT | 触发此操作的消息角色(user 或 assistant),记录记忆是由用户消息还是 AI 回复触发生成/修改的 |
事件类型说明:
| 事件 | 触发方式 | 说明 |
|---|---|---|
| ADD | memory.add() | 新增记忆,由 LLM 从对话中智能提取 |
| UPDATE | memory.update(memory_id, data) | 手动更新记忆内容,用户主动调用 API |
| DELETE | memory.delete(memory_id) | 手动删除记忆,用户主动调用 API |
V3 管道变化:
- V3 之前:
add()可能产生 ADD、UPDATE、DELETE 三种事件,LLM 会判断是否需要更新或删除已有记忆 - V3 之后:
add()只产生 ADD 事件,不再自动产生 UPDATE 和 DELETE。记忆通过linked_memory_ids建立关联,而非直接修改已有记忆
迁移注意:如需更新或删除记忆,必须手动调用 update() / delete() API。
2. messages 表:历史会话消息
| 字段 | 类型 | 说明 |
|---|---|---|
id | TEXT (PK) | 消息唯一ID |
session_scope | TEXT | 会话作用域标识 |
role | TEXT | 角色:user / assistant |
content | TEXT | 消息原文 |
name | TEXT | 发言者名称(如多轮对话中的发言人) |
created_at | DATETIME | 消息时间 |
session_scope 构建规则:
session_scope 是一个字符串,由 user_id、agent_id、run_id 组合而成,用于隔离不同会话的消息:
def _build_session_scope(filters):
"""
根据 user_id、agent_id、run_id 构建会话作用域标识。
任何一个参数不同,都会产生不同的 session_scope,
从而隔离不同会话的历史消息。
"""
parts = []
# 按 key 排序,确保相同参数组合产生相同的 session_scope
for key in sorted(["user_id", "agent_id", "run_id"]):
val = filters.get(key)
if val:
parts.append(f"{key}={val}")
return "&".join(parts) # 返回字符串,如 "user_id=user_123&agent_id=agent_456"
示例:
| 调用方式 | session_scope 值 |
|---|---|
memory.add(messages, user_id="user_123") | "user_id=user_123" |
memory.add(messages, user_id="user_123", agent_id="agent_456") | "user_id=user_123&agent_id=agent_456" |
memory.add(messages, user_id="user_123", agent_id="agent_456", run_id="run_789") | "user_id=user_123&agent_id=agent_456&run_id=run_789" |
关键点:"user_id=user_123" 和 "user_id=user_123&agent_id=agent_456" 是不同的 session_scope,它们的消息互不干扰。
隔离意义:
| 场景 | session_scope | 说明 |
|---|---|---|
| 同一用户,无 agent | user_id=user_123 | 用户的所有消息共享上下文 |
| 同一用户,不同 agent | user_id=user_123&agent_id=agent_456 | 不同 agent 的消息隔离 |
| 同一用户同一 agent,不同 run | user_id=user_123&agent_id=agent_456&run_id=run_789 | 不同运行实例的消息隔离 |
实际影响:
# 第一次调用:session_scope = "user_id=user_123"
memory.add([{"role": "user", "content": "我叫小明"}], user_id="user_123")
# 第二次调用:session_scope = "user_id=user_123&agent_id=agent_456"(不同的 session!)
memory.add([{"role": "user", "content": "我喜欢编程"}], user_id="user_123", agent_id="agent_456")
# 结果:第二次调用无法获取第一次的消息作为上下文,因为 session_scope 不同
核心作用:
messages 表不是用来存储记忆的,而是为 LLM 提供上下文理解的支撑:
- 代词消解:当用户说”他”、“它”、“那个”时,LLM 需要历史消息来理解指代对象
- 上下文连贯:理解”继续”、“还是那个问题”等需要前文背景的表达
- 时间推理:帮助 LLM 解析”昨天”、“上周”等相对时间
工作流程:
用户发送新消息
↓
Phase 0: 根据 session_scope 获取最近 10 条历史消息
↓
将历史消息 + 新消息一起传给 LLM
↓
LLM 提取记忆时可以理解完整上下文
↓
Phase 8: 将当前消息存入 messages 表(带 session_scope)
获取消息逻辑:
# 只获取相同 session_scope 的消息
def get_last_messages(self, session_scope: str, limit: int = 10):
query = """
SELECT role, content, name
FROM messages
WHERE session_scope = ?
ORDER BY created_at DESC
LIMIT ?
"""
return self.db.execute(query, (session_scope, limit)).fetchall()
自动清理机制:每次保存消息时,自动删除该 session_scope 超过 10 条的旧消息,保持每个会话只保留最近 10 条。这样既保证上下文足够,又避免存储膨胀。
TypeScript SDK:SQLite + Supabase
TypeScript SDK 提供了可插拔的历史存储提供者接口(HistoryManager):
// 默认使用 SQLite(本地 better-sqlite3)
const historyManager = new SQLiteManager({
dbPath: "./mem0_history.db",
});
// 或者使用 Supabase(云端 PostgreSQL)
const historyManager = new SupabaseHistoryManager({
client: createClient(supabaseUrl, supabaseKey),
});
HistoryManager 接口定义如下(位于 mem0-ts/src/oss/src/storage/base.ts):
interface HistoryManager {
addHistory(memoryId, previousValue, newValue, action, ...): Promise<void>;
getHistory(memoryId): Promise<any[]>;
reset(): Promise<void>;
close(): void;
// V3 可选方法 — 不需要的实现可以省略
saveMessages?(messages, sessionScope): Promise<void>;
getLastMessages?(sessionScope, limit?): Promise<...>;
batchAddHistory?(records): Promise<void>;
}
各实现类:
SQLiteManager:实现了全部 7 个方法,同时管理memory_history和messages两张表SupabaseHistoryManager:实现了 4 个必需方法(addHistory,getHistory,reset,close),通过memory_history表(Supabase PostgreSQL)存储历史MemoryHistoryManager:内存实现,同样只实现 4 个必需方法DummyHistoryManager:空实现,当disableHistory=true时使用
选择建议:
- 本地开发/单机部署:
SQLiteManager,零依赖,性能优秀,功能完整(含消息上下文管理) - 云端多实例部署:
SupabaseHistoryManager,支持水平扩展(注意:不支持消息管理,需自行处理会话上下文) - Python SDK:目前仅支持 SQLite,如需云端存储建议使用托管平台 API
向量数据库(记忆向量存储)
向量数据库是 Mem0 的核心存储,以 Qdrant 为例介绍其数据结构。
支持 30+ 向量数据库,包括:
| 分类 | 数据库 | 说明 |
|---|---|---|
| 托管云服务 | Pinecone, Qdrant Cloud, Weaviate Cloud, Milvus Cloud, Elasticsearch Cloud | 零运维,适合生产 |
| 自托管 | Qdrant, Milvus, ChromaDB, Weaviate, Faiss, Elasticsearch | Docker/K8s 部署 |
| 关系型扩展 | PGVector, Supabase, Azure MySQL | 基于现有数据库的向量扩展 |
| NoSQL 扩展 | MongoDB, Redis, DynamoDB, S3 Vectors | 多用途存储的向量能力 |
主记忆 Collection
默认名称:mem0(配置项:collection_name,可通过配置修改)
这是存储所有记忆的核心 Collection,每条记忆以 Point(Qdrant 的数据单元)形式存储。
Collection 配置:
| 配置项 | 英文配置名 | 默认值 | 说明 |
|---|---|---|---|
| Collection 名称 | collection_name | "mem0" | Collection 的唯一标识 |
| 向量维度 | embedding_model_dims | 1536 | 由 Embedding Model 决定(如 OpenAI text-embedding-3-small 为 1536) |
| 距离度量 | distance | COSINE | 支持:Cosine(余弦相似度)、Euclidean(欧氏距离)、Dot Product(点积) |
| 持久化 | on_disk | False | 向量存储位置,见下方详细说明 |
| 存储路径 | path | "/tmp/qdrant" | 本地数据库文件路径 |
on_disk 持久化说明:
| on_disk 值 | 存储位置 | 特点 | 适用场景 |
|---|---|---|---|
False(默认) | 内存 | 速度快,重启后数据丢失 | 开发测试、临时缓存 |
True | 磁盘 | 速度稍慢,数据持久保存 | 生产环境、长期存储 |
注意:on_disk 的 False/True 决定了向量数据是存内存还是磁盘。请对照上表选择:False 时重启数据丢失,True 时持久保存。Qdrant 本地模式会在 path 下创建 RocksDB/WAL 等元数据文件,但这不等于向量数据的持久化——只有 on_disk=True 才能保证向量数据重启后不丢失。
向量类型:
| 向量类型 | 英文名 | 用途 |
|---|---|---|
| Dense Vector | vectors | 语义搜索,由 Embedding Model 生成 |
| Sparse BM25 Vector | sparse_vectors_config | 关键词搜索,由 fastembed 的 BM25 模型生成 |
距离度量选择:
| 度量方式 | 英文名 | 适用场景 | 支持的向量库 |
|---|---|---|---|
| Cosine | Distance.COSINE | 文本语义相似度(推荐) | Qdrant, Pinecone, FAISS, Milvus… |
| Euclidean (L2) | Distance.EUCLID | 需要绝对距离度量 | Qdrant, Milvus, FAISS… |
| Dot Product | Distance.DOT | 已归一化向量 | Qdrant, FAISS… |
Point 结构:
Point 是 Qdrant 的核心数据单元,包含 ID、向量和元数据:
{
"id": "uuid-string",
"vector": {
"": [0.123, -0.456, ...], // Dense 向量(主向量,用于语义搜索)
"bm25": { // Sparse 向量(用于关键词搜索)
"indices": [12, 45, 89],
"values": [0.8, 0.6, 0.4]
}
},
"payload": {
"data": "User's name is Marcus and works as a Senior Engineer at Shopify",
"text_lemmatized": "user name be marcus and work as a senior engineer at shopify",
"hash": "a1b2c3d4...",
"user_id": "user_123",
"agent_id": null,
"run_id": null,
"actor_id": null,
"role": "user",
"created_at": "2026-05-03T10:30:00Z",
"updated_at": "2026-05-03T10:30:00Z",
"attributed_to": null
}
}
关键字段说明:
data:记忆的原始文本text_lemmatized:词形还原后的文本,用于 BM25 关键词搜索。将词语还原为词根形式,如:- 原文:
"User's name is Marcus and works as a Senior Engineer" - 词形还原后:
"user name be marcus and work as a senior engineer"(works → work, is → be)
- 原文:
hash:MD5 哈希值,用于精确去重user_id:用户标识,用于跨会话记忆共享agent_id:Agent 标识,用于区分不同 AI Agent 的记忆run_id:运行标识,用于区分同一 Agent 的不同运行实例actor_id:发言者标识,来自消息中的name字段,用于多参与者场景(如群聊、多角色对话)中区分不同发言人。例如:{"role": "assistant", "content": "...", "name": "Maria"}会将actor_id设为 “Maria”role:触发消息角色,记录此记忆是由user消息还是assistant消息触发生成的
实体 Collection
默认名称:mem0_entities(主 Collection 名称 + _entities 后缀)
什么是实体?
实体是从记忆文本中提取的命名实体,代表记忆中提到的具体对象。
实体提取方式:
Mem0 使用 spaCy NLP 模型(而非 LLM)进行实体提取,这是一种基于规则和词性标注的轻量级方法:
from mem0.utils.entity_extraction import extract_entities
entities = extract_entities("Marcus has a dog named Poppy and works at Shopify")
# 返回示例: [("PROPER", "Marcus"), ("PROPER", "Poppy"), ("PROPER", "Shopify")]
为什么使用 spaCy 而非 LLM?
| 对比项 | spaCy NLP | LLM |
|---|---|---|
| 速度 | 毫秒级 | 秒级 |
| 成本 | 无 API 调用 | 每次 API 调用有成本 |
| 准确性 | 对命名实体识别足够准确 | 更灵活但成本高 |
| 批量处理 | 支持 nlp.pipe() 高效批处理 | 需要多次 API 调用 |
spaCy 提取的四类实体:
| 实体类型 | 说明 | 示例 |
|---|---|---|
PROPER | 专有名词(人名、地名、品牌名) | “Marcus”, “Shopify”, “Osteria Francescana” |
QUOTED | 引号内的文本(书名、电影名、特定术语) | “‘The Nightingale’”, “‘Eternal Sunshine‘“ |
COMPOUND | 复合名词短语 | ”machine learning”, “senior engineer” |
NOUN | 单个名词(作为兜底) | “dog”, “cat”, “project” |
实体提取示例:
原文:"Marcus has a dog named Poppy and works at Shopify"
提取结果:
("PROPER", "Marcus")("PROPER", "Poppy")("PROPER", "Shopify")
实体 Collection 的 Point 结构:
{
"id": "entity-uuid",
"vector": [0.1, 0.2, ...], // 实体文本的向量
"payload": {
"data": "Marcus",
"entity_type": "PROPER",
"linked_memory_ids": ["mem-uuid-1", "mem-uuid-2"], // 关联的记忆 ID 列表
"user_id": "user_123",
"agent_id": null,
"run_id": null
}
}
实体的作用:
- 关系查询:通过
linked_memory_ids快速找到所有提到某实体的记忆 - 实体提升:搜索时如果查询包含实体,相关记忆的得分会被提升
- 知识图谱基础:为构建更复杂的知识图谱提供基础
实体链接机制:
当新记忆被添加时:
- 从记忆文本中提取所有实体
- 对每个实体,在实体 Collection 中搜索相似度 ≥ 0.95 的已有实体
- 如果找到匹配:更新已有实体的
linked_memory_ids,添加新记忆 ID - 如果未找到:创建新实体,
linked_memory_ids设为当前记忆 ID
混合检索
Mem0 v3 采用混合检索策略,结合三种信号进行记忆召回。
检索流程:
graph TD
A["用户查询"] --> B["Step 1: 查询预处理"]
B --> B1["词形还原"]
B --> B2["实体提取"]
B --> C["Step 2: 查询向量化"]
C --> D["Step 3: 语义检索<br>Dense Vector"]
C --> E["Step 4: 关键词检索<br>BM25 Sparse Vector"]
D --> F["语义结果<br>top_k * 4"]
E --> G["BM25 原始分数"]
G --> H["Step 5: BM25 分数归一化<br>Sigmoid → [0,1]"]
B2 --> I["Step 6: 实体提升计算"]
F --> J["Step 7: 构建候选集"]
H --> K["Step 8: 综合评分"]
I --> K
J --> K
K --> L["Step 9: 排序返回"]
三种检索信号:
| 信号类型 | 来源 | 作用 |
|---|---|---|
| 语义分数 | Dense Vector 相似度 | 捕捉语义相似性,“苹果手机” 和 “iPhone” 语义相近 |
| BM25 分数 | Sparse Vector 关键词匹配 | 精确关键词匹配,“iPhone 15” 能精确匹配 |
| 实体提升 | 实体 Collection | 查询包含实体时,相关记忆得分提升 |
Step 1-2: 查询预处理
# 词形还原:将词语还原为词根形式,用于 BM25 关键词匹配
# 例如:"works" → "work", "is" → "be", "running" → "run"
query_lemmatized = lemmatize_for_bm25(query)
# 实体提取:使用 spaCy NLP 模型提取命名实体
# 例如:"Marcus's dog Poppy" → [("PROPER", "Marcus"), ("PROPER", "Poppy")]
query_entities = extract_entities(query)
# 查询向量化:使用 Embedding Model 将查询文本转换为向量
# 用于后续的语义相似度检索
embeddings = embedding_model.embed(query)
各步骤执行者:
| 步骤 | 执行者 | 说明 |
|---|---|---|
| 词形还原 | lemmatize_for_bm25() | 内置函数,基于 spaCy 词性标注 |
| 实体提取 | extract_entities() | spaCy NLP 模型(非 LLM) |
| 查询向量化 | embedding_model.embed() | 配置的 Embedding Provider(如 OpenAI) |
Step 3: 语义检索
使用 Dense Vector 进行相似度检索:
internal_limit = max(limit * 4, 60) # 过采样:取 limit * 4 或 60 中的较大值
semantic_results = vector_store.search(
query=query,
vectors=embeddings,
top_k=internal_limit,
filters=filters
)
过采样原因:语义检索只是第一步,后续还要结合 BM25 和实体提升重新排序,需要足够的候选池。
Step 4-5: 关键词检索与 BM25 归一化
BM25 原始分数是无界的(通常 0-20+),需要归一化到 [0, 1]:
# 使用 Sigmoid 函数归一化
normalized = 1.0 / (1.0 + exp(-steepness * (raw_score - midpoint)))
Sigmoid 参数自适应:根据查询词数量调整
| 查询词数量 | midpoint | steepness |
|---|---|---|
| ≤ 3 | 5.0 | 0.7 |
| 4-6 | 7.0 | 0.6 |
| 7-9 | 9.0 | 0.5 |
| 10-15 | 10.0 | 0.5 |
| > 15 | 12.0 | 0.5 |
原因:长查询的 BM25 原始分数通常更高,需要调整参数保证归一化效果。
Step 6: 实体提升
如果查询中包含实体,从实体 Collection 中查找相关记忆并给予提升。
实体提升计算流程:
def _compute_entity_boosts(query_entities, filters):
# 1. 实体去重(最多处理 8 个)
deduped_entities = deduplicate(query_entities[:8])
memory_boosts = {}
for entity_type, entity_text in deduped_entities:
# 2. 将实体文本向量化
entity_embedding = embedding_model.embed(entity_text, "search")
# 3. 在实体 Collection 中搜索相似实体(阈值 >= 0.5)
matches = entity_store.search(
query=entity_text,
vectors=entity_embedding,
top_k=500,
filters=filters,
)
for match in matches:
if match.score < 0.5:
continue
# 4. 获取该实体关联的记忆 ID 列表
linked_memory_ids = match.payload.get("linked_memory_ids", [])
# 5. 计算衰减因子:关联记忆越多,单个记忆的提升越小
num_linked = len(linked_memory_ids)
memory_count_weight = 1.0 / (1.0 + 0.001 * ((num_linked - 1) ** 2))
# 6. 计算最终提升分数
# boost = 相似度 * 0.5 * 衰减因子
boost = match.score * 0.5 * memory_count_weight
# 7. 将提升分数分配给关联的记忆
for memory_id in linked_memory_ids:
memory_boosts[memory_id] = max(memory_boosts.get(memory_id, 0), boost)
return memory_boosts
衰减因子说明:
| 关联记忆数 | 衰减因子 | 说明 |
|---|---|---|
| 1 | 1.000 | 单记忆实体,无衰减 |
| 5 | 0.984 | 轻微衰减 |
| 10 | 0.917 | 中等衰减 |
| 50 | 0.310 | 显著衰减 |
| 100 | 0.091 | 强衰减 |
衰减原因:如果一个实体关联了太多记忆(如 “User” 可能关联所有记忆),则该实体对单个记忆的区分度较低,需要衰减以避免过度提升。
示例:
查询:"Marcus's dog Poppy"
提取实体:[("PROPER", "Marcus"), ("PROPER", "Poppy")]
实体 “Poppy” 在实体 Collection 中找到匹配(相似度 0.95),关联记忆 ["mem-uuid-1"]:
boost = 0.95 * 0.5 * 1.0 = 0.475
最终 entity_boosts = {"mem-uuid-1": 0.475}
Step 7-8: 综合评分与合并
这是混合检索的核心。候选集来自语义检索结果,然后叠加 BM25 分数和实体提升:
for candidate in semantic_results:
semantic_score = candidate.score # 语义分数(已归一化,0-1)
bm25_score = bm25_scores.get(mem_id, 0) # BM25 分数(已归一化,0-1)
entity_boost = entity_boosts.get(mem_id, 0) # 实体提升(0-0.5)
raw_combined = semantic_score + bm25_score + entity_boost
combined = raw_combined / max_possible # 归一化到 [0, 1]
max_possible 计算:
| 激活的信号 | max_possible | 说明 |
|---|---|---|
| 仅语义 | 1.0 | 只有语义分数 |
| 语义 + BM25 | 2.0 | 两种信号各占 50% |
| 语义 + 实体 | 1.5 | 实体提升权重 0.5 |
| 语义 + BM25 + 实体 | 2.5 | 完整混合检索 |
阈值过滤:语义分数低于 threshold(默认 0.1)的候选会被直接排除,即使 BM25 或实体提升很高也无法挽救。
Step 9: 排序返回
按综合分数降序排列,返回 top_k 结果。
示例:
查询:"Marcus's dog Poppy"
| 记忆 | 语义分数 | BM25 分数 | 实体提升 | 综合分数 |
|---|---|---|---|---|
| ”Marcus has a dog named Poppy” | 0.85 | 0.72 | 0.5 | (0.85+0.72+0.5)/2.5 = 0.828 |
| ”Marcus works at Shopify” | 0.70 | 0.30 | 0.25 | (0.70+0.30+0.25)/2.5 = 0.50 |
| ”User has a cat” | 0.45 | 0.0 | 0.0 | 0.45/1.0 = 0.45 |
索引与过滤
向量数据库为常见过滤字段创建了索引:
user_id(keyword)agent_id(keyword)run_id(keyword)actor_id(keyword)
支持丰富的过滤操作:eq, ne, in, nin, gt, gte, lt, lte, contains, icontains,以及逻辑组合 AND, OR, NOT。
重排序(Reranker)
Mem0 支持在搜索结果返回前进行二次重排序,通过专门的 Reranker 模型对候选记忆进行精细打分,提升检索精度。
工作流程:
混合检索(语义 + BM25 + 实体)
↓
候选记忆列表
↓
rerank=True 启用重排序?
↙ ↘
Yes No
↓ ↓
Reranker 直接返回
重新打分
↓
按 rerank_score 排序
↓
返回结果
支持的 Reranker:
| Reranker | 说明 | 依赖包 | 适用场景 |
|---|---|---|---|
cohere | Cohere Rerank API | cohere | 云端服务,高质量 |
sentence_transformer | 本地 CrossEncoder 模型 | sentence-transformers | 本地部署,无 API 成本 |
llm | LLM 对每个文档打分 | 需要 LLM 配置 | 灵活,但成本较高 |
zero_entropy | Zero Entropy Rerank API | zeroentropy | 云端服务 |
配置方式:
from mem0 import Memory
# 配置 Cohere Reranker
memory = Memory(config={
"reranker": {
"provider": "cohere",
"config": {
"model": "rerank-english-v3.0",
"top_k": 10
}
}
})
# 配置本地 Sentence Transformer Reranker
memory = Memory(config={
"reranker": {
"provider": "sentence_transformer",
"config": {
"model": "cross-encoder/ms-marco-MiniLM-L-6-v2",
"device": "cuda" # 或 "cpu"
}
}
})
使用方式:
# 搜索时启用重排序
results = memory.search(
"用户喜欢什么编程语言?",
filters={"user_id": "user_123"},
rerank=True # 启用重排序
)
# 返回结果会包含 rerank_score 字段
# {"results": [{"id": "...", "memory": "...", "score": 0.85, "rerank_score": 0.92, ...}]}
注意事项:
- 性能影响:重排序会增加额外延迟(Cohere API 约 100-300ms,本地模型取决于硬件)
- 成本:Cohere 和 LLM Reranker 会产生 API 调用成本
- 容错:如果重排序失败,会自动降级返回原始排序结果
- top_k:Reranker 的
top_k会覆盖搜索的top_k参数
记忆存储:add() 核心流程
add() 方法在 mem0/memory/main.py 文件中实现,内部调用 _add_to_vector_store() 方法执行 V3 分阶段批处理管道(V3 是内部管道版本号,非产品版本)。当调用 memory.add() 时,实际上发生了一件相当复杂的事情。
流程图
graph TD
A["add() 入口<br>messages 列表"] --> B{"infer=True?"}
B -->|"Yes"| C["V3 批处理管道"]
B -->|"No"| D["快速直存模式"]
C --> E["Phase 0: 上下文收集"]
D --> H["直接插入向量库"]
E --> F["Phase 1: 现有记忆检索"]
F --> G["Phase 2: LLM 智能提取"]
G --> I["Phase 3: 批量向量化"]
I --> J["Phase 4-5: 去重与哈希"]
J --> K["Phase 6: 批量持久化"]
K --> L["Phase 7: 实体链接"]
L --> M["Phase 8: 保存消息"]
M --> N["返回结果"]
H --> N
infer 参数说明
add() 方法接受一个 infer 参数(默认 True),决定记忆的处理方式:
| infer 值 | 模式 | 说明 |
|---|---|---|
True(默认) | V3 批处理管道 | 通过 LLM 智能提取记忆,支持去重、关联、实体提取 |
False | 快速直存模式 | 直接将消息内容作为记忆存储,不经过 LLM 处理 |
快速直存模式(infer=False):
当 infer=False 时,Mem0 会:
- 跳过所有 LLM 调用
- 直接将每条消息的
content作为记忆文本 - 为每条消息生成向量并插入向量库
- 返回插入的记忆列表
适用场景:
- 需要快速存储大量原始消息
- 不需要智能提取和去重
- 成本敏感场景(节省 LLM API 调用)
注意:快速直存模式下,不会进行:
- 语义去重
- 实体提取
- 记忆关联
- 上下文理解
Phase 0: 上下文收集
从 SQLite 获取最近 10 条历史消息,用于让 LLM 理解对话上下文。
Phase 1: 现有记忆检索
- 将新消息向量化 →
query_embedding - 从向量数据库搜索 Top 10 相关记忆
- UUID 映射为整数(防 LLM 产生幻觉 ID)
Phase 2: LLM 智能提取
这是 Mem0 最核心的步骤。Mem0 调用 LLM,让 AI 决定应该记住什么。
Prompt 构成
LLM 接收的 Prompt 由系统提示词 + 用户提示词组成:
系统提示词(ADDITIVE_EXTRACTION_PROMPT)定义角色和提取规则:
- 你是”记忆提取器”,负责从对话中提取丰富、上下文相关的事实
- 从 user 和 assistant 消息中提取信息
- user 消息揭示个人事实、偏好、计划、经历
- assistant 消息包含推荐、计划、建议和用户可能参考的可操作信息
任务提示词由 generate_additive_extraction_prompt() 动态构建,作为 LLM 的具体任务输入,包含以下部分:
| 组成部分 | 说明 |
|---|---|
Summary | 用户画像的叙述性摘要(从已有记忆中生成) |
Last k Messages | 最近 10 条历史消息(用于理解上下文和指代消解) |
Recently Extracted Memories | 本次会话中已提取的记忆(用于会话内去重) |
Existing Memories | 系统中现有相关记忆(来自向量数据库,用于语义去重) |
New Messages | 当前需要提取的新消息 |
Observation Date | 对话实际发生的日期(用于解析相对时间) |
Current Date | 当前系统日期 |
Custom Instructions | 用户自定义指令(如有) |
Prompt 完整内容
以下是 Mem0 使用的系统提示词 ADDITIVE_EXTRACTION_PROMPT 的核心部分(完整 Prompt 还包含 Summary、Recently Extracted Memories、Last k Messages、Current Date、Optional Inputs、Casual Topics、Incidental Facts、Shared Photos 等段落,此处为关键摘录):
英文原文(关键部分):
# ROLE
You are a Memory Extractor — a precise, evidence-bound processor responsible for
extracting rich, contextual memories from conversations. Your sole operation is ADD:
identify every piece of memorable information and produce self-contained, contextually
rich factual statements.
You extract from BOTH user and assistant messages. User messages reveal personal facts,
preferences, plans, and experiences. Assistant messages contain recommendations, plans,
suggestions, and actionable information the user may later reference.
Accuracy and completeness are critical. Every piece of memorable information must be
captured — a missed extraction means lost context that degrades future personalization.
# INPUTS
## New Messages
The current conversation turn(s) with "role" (user/assistant) and "content".
Both roles contain extractable information:
- **User messages**: Personal facts, preferences, plans, experiences, things done /
never done before, opinions, requests, implicit preferences revealed through questions
- **Assistant messages**: Specific recommendations given, plans or schedules created,
information researched, solutions provided, agreements reached
## Existing Memories
Memories currently in the system relevant to this conversation. Formatted as:
[{"id": "uuid-string", "text": "..."}, ...]
Use these ONLY for deduplication and linking — do NOT extract new memories from
Existing Memories. Your extractions must come exclusively from New Messages.
## Observation Date
When the conversation actually took place (e.g., "2023-05-24"). This is your ONLY
temporal anchor for resolving time references.
Resolve ALL relative references against Observation Date:
- "yesterday" → day before Observation Date
- "last week" → week preceding Observation Date
- "next month" → month following Observation Date
CRITICAL: "User went to Paris last week" is useless 6 months later.
"User went to Paris the week of May 15, 2023" is meaningful forever.
# GUIDELINES
## What to Extract
**From user messages:**
- Personal details, preferences, plans, relationships, professional context
- Health/wellness, opinions, hobbies, emotional states
- Entity attributes (breed, model, color, make, size)
- Implicit preferences revealed through requests
- **Shared content and reference material** — when a user shares documents, case studies,
articles, data, specifications, code, extract the key factual data FROM that content
- Firsts and milestones — 'first call-out', 'just started', 'recently joined', etc.
**From assistant messages (ONLY when genuinely new):**
- Specific recommendations given (books, restaurants, products, services)
- Plans or schedules created for the user
- Information researched or provided (facts, instructions, solutions)
- Agreements reached during conversation
Do NOT extract: greetings, filler, vague acknowledgments, or content too generic to be useful.
**When in doubt, extract.** A slightly redundant memory is far less costly than a missing one.
## Memory Quality Standards
### Contextually Rich, Not Atomic
Bad: "User has a dog"
Good: "User has a dog named Poppy and their morning walks together are the highlight of their day"
### Self-Contained
Every memory must be understandable on its own. Replace all pronouns with specific names or "User."
### Concise but Complete (15-80 words, up to 100 for detail-rich content)
1-2 sentences per memory (up to 3 for content with multiple proper nouns). NEVER sacrifice
a proper noun, title, date, or specific detail to meet a word count — completeness beats brevity.
### Temporally Grounded
Preserve exact dates, durations. Convert relative → absolute using Observation Date.
### Numerically Precise
"416 pages" stays "416 pages", not "about 400 pages."
### Preserve Specific Details — Never Generalize Concrete Information
Book titles, movie titles, restaurant names, brand names are the HIGHEST-VALUE details.
- "watched 'Eternal Sunshine of the Spotless Mind'" → KEEP the full title
- "tried the new restaurant Osteria Francescana" → KEEP "Osteria Francescana", NOT "a new restaurant"
## Memory Linking
When extracting a new memory, check if it relates to any Existing Memory. Add related
Existing Memory IDs to "linked_memory_ids". Link when:
- **Same entity/topic**: New fact about a person, place, or thing already mentioned
- **Updated preference**: A changed or evolved opinion on something previously captured
- **Continuation**: Follow-up event or next step in a previously captured narrative
- **Contradiction**: New information that conflicts with an existing memory
中文翻译版:
# 角色
你是一个记忆提取器——一个精确的、基于证据的处理器,负责从对话中提取丰富、上下文相关的记忆。你的唯一操作是 ADD:识别每一个值得记忆的信息,并生成自包含的、上下文丰富的事实陈述。
你从 user 和 assistant 消息中提取信息。user 消息揭示个人事实、偏好、计划、经历。assistant 消息包含推荐、计划、建议和用户可能参考的可操作信息。
准确性和完整性至关重要。必须捕获每一个值得记忆的信息——一次遗漏意味着失去上下文,会降低未来的个性化质量。
# 输入
## 新消息
当前对话轮次,包含 "role"(user/assistant)和 "content"。
两种角色都包含可提取的信息:
- **user 消息**:个人事实、偏好、计划、经历、做过/从未做过的事、意见、请求
- **assistant 消息**:给出的具体推荐、创建的计划或日程、研究过的信息、提供解决方案、达成的协议
## 现有记忆
系统中与当前对话相关的现有记忆。格式:[{"id": "uuid-string", "text": "..."}, ...]
仅将这些用于去重和关联——不要从现有记忆中提取新记忆。你的提取必须完全来自新消息。
## 观察日期
对话实际发生的日期(如 "2023-05-24")。这是你解析时间引用的唯一锚点。
将所有相对引用转换为绝对日期:
- "昨天" → 观察日期的前一天
- "上周" → 观察日期前的一周
- "下个月" → 观察日期后的一个月
关键:"User went to Paris last week" 在 6 个月后毫无用处。"User went to Paris the week of May 15, 2023" 永远有意义。
# 指南
## 提取什么
**从 user 消息:**
- 个人细节、偏好、计划、关系、专业背景
- 健康/健身、意见、爱好、情绪状态
- 实体属性(品种、型号、颜色、品牌)
- 通过请求透露的隐含偏好
- **共享内容和参考资料**——当用户分享文档、案例研究、文章、数据、规格、代码时,提取其中的关键事实数据
- 第一次和里程碑
**从 assistant 消息(仅当真正是新信息时):**
- 给出的具体推荐(书籍、餐厅、产品、服务)
- 为用户创建的计划或日程
- 研究或提供的信息(事实、指令、解决方案)
- 达成的协议
**不要提取**:问候、填充词、模糊确认、太笼统而无用的内容。
**有疑问时,提取。** 略显冗余的记忆远比遗漏的记忆代价小。
## 记忆质量标准
### 上下文丰富,非原子化
坏的:"User has a dog"
好的:"User has a dog named Poppy and their morning walks together are the highlight of their day"
### 自包含
每条记忆必须独立可理解。将所有代词替换为具体名字或 "User"。
### 简洁但完整(15-80 词,细节丰富时最多 100 词)
每条记忆 1-2 句。不要牺牲专有名词、标题、日期来满足词数——完整性优于简洁。
### 时间锚定
保留确切日期、持续时间。将相对时间 → 绝对时间(使用观察日期)。
### 数值精确
"416 pages" 保持 "416 pages",不是 "about 400 pages"。
### 保留具体细节——永远不要泛化具体信息
书名、电影名、餐厅名、品牌名是最高价值的细节。
- "watched 'Eternal Sunshine of the Spotless Mind'" → 保留完整标题
- "tried the new restaurant Osteria Francescana" → 保留 "Osteria Francescana",不是 "a new restaurant"
## 记忆关联
当提取新记忆时,检查是否与任何现有记忆相关。将相关现有记忆的 ID 添加到 "linked_memory_ids"。关联条件:
- **相同实体/主题**:关于已提及的人、地点或事物的新事实
- **更新的偏好**:先前捕获的偏好发生变化或演进
- **延续**:先前捕获的叙述的后续事件或下一步
- **矛盾**:与现有记忆冲突的新信息
LLM 调用方式
Mem0 调用 LLM 时,通过 API 参数强制 JSON 输出:
response = self.llm.generate_response(
messages=[
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_prompt},
],
response_format={"type": "json_object"}, # 强制 JSON 输出
)
response_format={"type": "json_object"} 是 OpenAI API 的特性,确保 LLM 输出合法的 JSON。
如果模型不支持 response_format:
Mem0 有多层容错机制:
# 1. 首先尝试直接解析 JSON
try:
extracted_memories = json.loads(response).get("memory", [])
except json.JSONDecodeError:
# 2. 如果失败,尝试从文本中提取 JSON
extracted_json = extract_json(response) # 查找 { } 之间的内容
extracted_memories = json.loads(extracted_json).get("memory", [])
except Exception:
# 3. 如果还是失败,返回空列表
extracted_memories = []
extract_json() 函数会:
- 尝试匹配
```json ... ```代码块 - 如果没有,查找第一个
{和最后一个}之间的内容 - 如果还是失败,返回原文本
LLM 返回格式
LLM 返回 JSON 格式的提取结果。输出格式通过 Prompt 中的 EXAMPLES 隐式定义,而非显式字段说明:
示例 1:多主题提取
{"memory": [
{"id": "0", "text": "User's name is Marcus and was promoted to Senior Engineer at Shopify around August 12, 2025"},
{"id": "1", "text": "Marcus has a wife named Elena and they celebrate special occasions at Osteria Francescana"},
{"id": "2", "text": "Marcus and his wife Elena are expecting their first baby in March 2026"}
]}
示例 2:带关联的提取
{"memory": [
{"id": "0", "text": "User's dog Poppy loves morning walks", "linked_memory_ids": ["uuid-existing-memory"]}
]}
示例 3:无内容可提取
{"memory": []}
输出字段说明:
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
memory | Array | 是 | 提取的记忆数组,可为空 |
id | String | 是 | LLM 生成的序号(“0”, “1”, “2”…),用于内部处理 |
text | String | 是 | 记忆文本内容 |
linked_memory_ids | Array | 否 | 关联的已有记忆 UUID 列表 |
提取规则要点
LLM 必须遵循的规则:
- 不要提取助手 Echo:如果 assistant 只是重复 user 已说过的内容,不重复提取
- 提取附带事实:用户问问题时透露的背景信息同样重要
- 时间锚定:将相对时间(“昨天”、“上周”)转换为绝对日期
- 保留具体细节:书名、电影名、产品型号、数量等必须完整保留
- 多维度提取:一次对话可能包含多个话题,每个话题都要单独提取
- 语义去重:如果新信息与现有记忆语义等价,跳过提取
- 记忆关联:相关记忆要建立链接关系
Phase 3: 批量向量化
批量将提取的记忆文本向量化,减少 API 调用次数。
# mem0/memory/main.py - Memory._add_to_vector_store()
def _add_to_vector_store(self, messages, metadata, filters, infer, prompt=None):
# ... Phase 2: LLM 提取完成后 ...
# Phase 3: Batch embed all extracted memory texts
mem_texts = [m.get("text", "") for m in extracted_memories if m.get("text")]
try:
# 批量向量化:一次 API 调用处理所有文本
mem_embeddings_list = self.embedding_model.embed_batch(mem_texts, "add")
# 构建文本 -> 向量的映射
embed_map = dict(zip(mem_texts, mem_embeddings_list))
except Exception:
# 容错:如果批量失败,逐个向量化
embed_map = {}
for text in mem_texts:
try:
embed_map[text] = self.embedding_model.embed(text, "add")
except Exception as e:
logger.warning(f"Failed to embed memory text: {e}")
批量向量化的优势:
| 方式 | API 调用次数 | 适用场景 |
|---|---|---|
| 批量向量化 | 1 次 | 正常情况,效率高 |
| 逐个向量化 | N 次 | 批量失败时的容错方案 |
注意:embed_batch() 的 "add" 参数表示这是用于存储(而非搜索)的向量,某些 Embedding Model 会根据此参数选择不同的编码策略。
Phase 4-5: 去重与哈希
Mem0 使用 MD5 哈希进行精确去重:
# mem0/memory/main.py - Memory._add_to_vector_store()
# Phase 4: Per-memory CPU processing + Phase 5: Hash dedup
# 构建已有记忆的哈希集合
existing_hashes = set()
for mem in existing_results:
h = mem.payload.get("hash") if hasattr(mem, "payload") and mem.payload else None
if h:
existing_hashes.add(h)
records = [] # (memory_id, text, embedding, payload)
seen_hashes = set() # 当前批次去重
for mem in extracted_memories:
text = mem.get("text")
if not text or text not in embed_map:
continue
# 计算哈希
mem_hash = hashlib.md5(text.encode()).hexdigest()
# 两层去重:已有记忆 + 当前批次
if mem_hash in existing_hashes or mem_hash in seen_hashes:
logger.debug(f"Skipping duplicate memory (hash match): {text[:50]}")
continue
seen_hashes.add(mem_hash)
# 词形还原(用于 BM25 搜索)
text_lemmatized = lemmatize_for_bm25(text)
# 构建元数据
memory_id = str(uuid.uuid4())
mem_metadata = deepcopy(metadata)
mem_metadata["data"] = text
mem_metadata["text_lemmatized"] = text_lemmatized
mem_metadata["hash"] = mem_hash
mem_metadata["created_at"] = datetime.now(timezone.utc).isoformat()
mem_metadata["updated_at"] = mem_metadata["created_at"]
records.append((memory_id, text, embed_map[text], mem_metadata))
Phase 6: 批量持久化
将向量、ID、元数据打包后批量插入向量数据库,并记录历史。
# mem0/memory/main.py - Memory._add_to_vector_store()
# ... Phase 4-5: 去重完成后,records 包含所有待插入的记忆 ...
# Phase 6: Batch persist
all_vectors = [r[2] for r in records] # 提取向量
all_ids = [r[0] for r in records] # 提取 ID
all_payloads = [r[3] for r in records] # 提取元数据
try:
# 批量插入向量数据库
self.vector_store.insert(
vectors=all_vectors,
ids=all_ids,
payloads=all_payloads,
)
except Exception:
# 容错:逐个插入
for mid, vec, pay in zip(all_ids, all_vectors, all_payloads):
try:
self.vector_store.insert(vectors=[vec], ids=[mid], payloads=[pay])
except Exception as e:
logger.error(f"Failed to insert memory {mid}: {e}")
# 批量记录历史(ADD 事件)
history_records = [
{
"memory_id": r[0],
"old_memory": None,
"new_memory": r[1],
"event": "ADD",
"created_at": r[3].get("created_at"),
"is_deleted": 0,
}
for r in records
]
try:
self.db.batch_add_history(history_records)
except Exception:
# 容错:逐个添加历史
for hr in history_records:
try:
self.db.add_history(hr["memory_id"], None, hr["new_memory"], "ADD", created_at=hr.get("created_at"))
except Exception as e:
logger.error(f"Failed to add history for {hr['memory_id']}: {e}")
批量持久化的优势:
| 方式 | 向量库操作次数 | 适用场景 |
|---|---|---|
| 批量插入 | 1 次 | 正常情况,效率高 |
| 逐个插入 | N 次 | 批量失败时的容错方案 |
Phase 7: 实体链接
Mem0 v2 引入了实体存储机制,将记忆中的实体单独管理:
- 从记忆文本中批量提取实体
- 全局去重(不同记忆中的同一实体只保留一个)
- 搜索已有实体(相似度 ≥ 0.95 则复用)
- 新实体批量插入,已存在实体更新
linked_memory_ids
实体存储的好处:
- 支持实体级别的关系查询
- 加速实体相关搜索
Phase 8: 保存消息
将原始消息存入 SQLite(用于未来上下文理解),返回新增的记忆列表。
最终返回
{
"results": [
{"id": "mem-uuid-1", "memory": "用户叫小明,是一名程序员", "event": "ADD"},
{"id": "mem-uuid-2", "memory": "用户最近在学习 Rust", "event": "ADD"}
]
}
高级过滤操作符
Mem0 支持丰富的元数据过滤:
| 操作符 | 示例 | 说明 |
|---|---|---|
eq | {"age": {"eq": 25}} | 等于 |
ne | {"age": {"ne": 25}} | 不等于 |
in | {"role": {"in": ["admin"]}} | 在列表中 |
contains | {"name": {"contains": "john"}} | 包含 |
AND/OR/NOT | {"AND": [f1, f2]} | 逻辑组合 |
支持的 Provider
Mem0 是一个高度模块化的系统,支持丰富的后端选项:
| 类别 | 数量 | 代表提供商 |
|---|---|---|
| LLM | 24种 | OpenAI, Anthropic, AWS Bedrock, Azure OpenAI, Gemini, Groq, Ollama, DeepSeek… |
| 向量数据库 | 30种 | Qdrant, Pinecone, Chroma, Weaviate, Milvus, PostgreSQL, Redis, Elasticsearch… |
| 嵌入模型 | 15种 | OpenAI, Azure OpenAI, Gemini, HuggingFace, FastEmbed, Together… |
| 图数据库 | 4种 | Neo4j, Memgraph, Kuzu, Apache AGE |
对接策略
Mem0 采用混合对接策略:
1. 直接对接(主要方式)
对于主流 Provider,Mem0 直接使用官方 SDK 对接,性能更好、依赖更少:
| Provider | 使用的库 | 说明 |
|---|---|---|
| OpenAI | openai | OpenAI 官方 Python SDK |
| Groq | groq | Groq 官方 SDK |
| Ollama | ollama | Ollama Python 客户端 |
| Qdrant | qdrant-client | Qdrant 官方客户端 |
| Pinecone | pinecone | Pinecone 官方 SDK |
| Chroma | chromadb | Chroma 官方客户端 |
| Weaviate | weaviate-client | Weaviate 官方客户端 |
| Milvus | pymilvus | Milvus Python SDK |
| Redis | redis + redisvl | Redis 官方客户端 + RedisVL 向量扩展 |
| Elasticsearch | elasticsearch | Elasticsearch 官方客户端 |
2. 适配器模式
对于需要灵活性的用户,Mem0 提供了 LangChain 和 LiteLLM 适配器:
LangChain 适配器:
from langchain.chat_models import init_chat_model
from mem0 import Memory
# 使用 LangChain 支持的任何 LLM
langchain_model = init_chat_model("gpt-4o", model_provider="openai")
memory = Memory(config={
"llm": {
"provider": "langchain",
"config": {
"model": langchain_model # 传入 LangChain BaseChatModel 实例
}
}
})
LangChain 适配器支持:
LangchainLLM— 接受任何 LangChainBaseChatModelLangchainEmbedding— 接受任何 LangChainEmbeddingsLangchainVectorStore— 接受任何 LangChainVectorStore
LiteLLM 适配器:
from mem0 import Memory
# LiteLLM 统一调用多种 LLM
memory = Memory(config={
"llm": {
"provider": "litellm",
"config": {
"model": "anthropic/claude-3-opus-20240229" # LiteLLM 格式
}
}
})
LiteLLM 支持 100+ LLM Provider,包括:
- OpenAI, Azure OpenAI, Anthropic, Cohere, Replicate
- AWS Bedrock, Google Vertex AI, Azure AI
- Ollama, vLLM, LM Studio 等本地部署
3. 核心依赖 vs 可选依赖
Mem0 的依赖设计非常精简:
核心依赖(必须安装):
qdrant-client # 默认向量数据库
openai # 默认 LLM 和 Embedding
pydantic # 配置验证
sqlalchemy # SQLite 操作
可选依赖(按需安装):
# 安装向量数据库支持
pip install "mem0ai[vector_stores]"
# 安装 LLM 支持
pip install "mem0ai[llms]"
# 安装额外功能(LangChain、FastEmbed 等)
pip install "mem0ai[extras]"
这种设计让你可以根据需求灵活切换后端,无需修改业务代码,同时保持核心依赖最小化。
OpenClaw 插件详解
openclaw/ 目录实现了一个 OpenClaw 内存插件(@mem0/openclaw-mem0),为 OpenClaw Agent 提供长短期记忆能力。
项目概览
这是一个 Node.js / TypeScript 包,发布到 npm 作为 @mem0/openclaw-mem0(当前版本 1.0.11)。它是一个 OpenClaw 插件,通过 OpenClaw 的插件系统注册为 memory 类型的独占插槽(exclusive slot)。
技术栈:
| 维度 | 说明 |
|---|---|
| 语言 | TypeScript(ESM) |
| 构建工具 | tsup(输出 ESM 格式 + 类型声明 + sourcemap) |
| 测试框架 | vitest(带 Codecov coverage) |
| 包管理器 | pnpm |
| 核心依赖 | mem0ai 3.0.2(npm)、@sinclair/typebox 0.34.47 |
| 开发依赖 | typescript、tsup、vitest、@vitest/coverage-v8 |
| 兼容要求 | OpenClaw 插件 API >=2026.4.24、Gateway >=2026.4.24 |
构建产物:入口文件 index.ts 编译为 dist/index.js,与 openclaw.plugin.json 和 skills/ 目录一起发布。
目录结构与模块划分
openclaw/
├── index.ts # 插件主入口:definePluginEntry、生命周期钩子注册
├── openclaw.plugin.json # 插件清单:ID、工具契约、配置 Schema、UI 提示
├── config.ts # 配置解析:mem0ConfigSchema、默认指令/分类
├── providers.ts # Provider 创建:Platform / OSS 后端工厂
├── isolation.ts # 多代理隔离:userId 路由、子代理检测
├── filtering.ts # 消息过滤:噪声去除、消息选择
├── recall.ts # Skills 模式召回:查询重写、记忆注入
├── dream-gate.ts # Dream 门控:会话计数、锁机制
├── backend/ # 后端抽象层
│ ├── base.ts # Backend 接口定义 + 错误类
│ └── platform.ts # Mem0 云平台后端实现
├── tools/ # 8 个 Agent 工具(模块化)
│ ├── memory-search.ts # 语义搜索
│ ├── memory-add.ts # 存储事实
│ ├── memory-update.ts # 更新记忆
│ └── memory-delete.ts # 删除记忆
├── cli/ # CLI 命令实现
│ ├── commands.ts # 命令注册:init/login/search/add/list/dream/status
│ └── oss-wizard.ts # OSS 交互式配置向导
├── skills/ # Skills 模式协议文件
│ ├── memory-triage/SKILL.md # 记忆提取:四重决策门控、提取类别
│ └── memory-dream/SKILL.md # 记忆整理:四阶段流程
└── tests/ # 测试文件(vitest,12 个测试文件)
插件架构
插件通过 definePluginEntry() 定义,实现 OpenClaw 的 OpenClawPluginApi 接口:
const memoryPlugin = definePluginEntry({
id: "openclaw-mem0",
name: "Memory (Mem0)",
description: "Mem0 memory backend — Mem0 platform or self-hosted open-source",
register(api: OpenClawPluginApi) {
// 1. 初始化遥测
// 2. 读取配置(openclaw.json)
// 3. 创建 Provider(Platform 或 OSS)
// 4. 创建 Backend 实例
// 5. 注册 8 个工具
// 6. 注册 CLI 命令
// 7. 注册生命周期钩子(Skills 或 Legacy 模式)
// 8. 注册 Service(start/stop)
},
});
双模式架构
| 模式 | 后端 | 依赖 | 向量存储 | 特点 |
|---|---|---|---|---|
| Platform | Mem0 云端 API (api.mem0.ai) | MEM0_API_KEY | 云端托管 | 零配置,SaaS 服务 |
| Open-Source | 本地 mem0ai SDK | OpenAI/Anthropic/Ollama API Key | 本地 SQLite (~/.mem0/vector_store.db) 或 Qdrant/PGVector | 完全本地,数据自控 |
Platform 模式:
- 通过
PlatformBackend类实现,直接调用api.mem0.aiREST API - 支持事件列表和事件状态查询(异步处理)
- 配置最简:只需 API Key + User ID
Open-Source 模式:
- 通过
providerToBackend()适配器,将mem0aiSDK 的Memory实例包装为Backend接口 - 默认配置:
text-embedding-3-small嵌入(OpenAI)、gpt-5-miniLLM(OpenAI)、本地 SQLite 向量存储 - 支持 Ollama 完全本地部署(LLM + Embedding 均本地)
- 支持自定义 Provider:
oss配置块可指定 embedder、vectorStore、llm 的 provider 和 config
后端接口(Backend 抽象类):
interface Backend {
// 添加新记忆:content 为记忆文本,messages 为对话消息数组,opts 包含 userId/agentId 等选项
add(content?, messages?, opts?): Promise<Record<string, unknown>>;
// 语义搜索记忆:query 为搜索查询,opts 包含 userId/topK/threshold 等选项
search(query, opts?): Promise<Record<string, unknown>[]>;
// 按 ID 获取单条记忆
get(memoryId): Promise<Record<string, unknown>>;
// 列出所有记忆:opts 包含 userId/agentId/page/pageSize 等过滤选项
listMemories(opts?): Promise<Record<string, unknown>[]>;
// 更新记忆:memoryId 为记忆 ID,content 为新内容,metadata 为元数据
update(memoryId, content?, metadata?): Promise<Record<string, unknown>>;
// 删除记忆:memoryId 为记忆 ID,opts 包含 all/userId 等选项(all=true 删除全部)
delete(memoryId?, opts?): Promise<Record<string, unknown>>;
// 删除实体:opts 包含 userId/agentId 等选项,用于清理实体关联
deleteEntities(opts): Promise<Record<string, unknown>>;
// 获取状态:opts 包含 userId/agentId,返回记忆数量和后端状态信息
status(opts?): Promise<Record<string, unknown>>;
// 列出实体:entityType 为实体类型(如 PROPER/NOUN),返回匹配实体列表
entities(entityType): Promise<Record<string, unknown>[]>;
// 列出事件:仅 Platform 模式,返回后台处理事件列表(异步处理状态)
listEvents(): Promise<Record<string, unknown>[]>;
// 获取事件状态:仅 Platform 模式,eventId 为事件 ID,返回事件处理结果
getEvent(eventId): Promise<Record<string, unknown>>;
}
8 个核心工具
插件通过 OpenClaw skill system 注册 8 个工具,供 Agent 调用:
| 工具 | 功能 | 后端 |
|---|---|---|
memory_search | 语义搜索记忆,支持 scope(session/long-term/all)、categories、filters | 两者 |
memory_add | 存储事实,接受 text 或 facts 数组、category、importance、longTerm | 两者 |
memory_get | 按 ID 获取单条记忆 | 两者 |
memory_list | 列出所有记忆,可按 userId、agentId、scope 过滤 | 两者 |
memory_update | 原子更新记忆文本,保留编辑历史 | 两者 |
memory_delete | 按 ID、查询或全部删除 | 两者 |
memory_event_list | 查看后台处理事件 | 仅 Platform |
memory_event_status | 获取特定事件状态 | 仅 Platform |
每个工具独立实现于 tools/ 目录下,通过 registerAllTools() 批量注册。工具实现共享 ToolDeps 依赖对象(provider、backend、config、userId 解析等)。
Skills 模式(推荐)
通过 OpenClaw 的 Skill 系统,Agent 获得结构化的记忆协议。每个 Skill 是一个 SKILL.md 文件,定义了 Agent 应该遵循的协议、工具和规则。Skills 模式在 openclaw mem0 init 时默认启用。
架构原理:
Skills 模式使用 before_prompt_build 钩子(而非 before_agent_start):
prependSystemContext:静态记忆协议(provider 可缓存,无每轮成本)prependContext:动态回忆的记忆(每轮变化)
memory-triage:
记忆提取协议。Agent 在对话后评估是否有值得持久化的事实。核心问题:
“一个新 Agent(没有任何先验上下文)是否会受益于知道这个?”
四重决策门控:
- FUTURE UTILITY:这个事实几天或几周后还重要吗?(过滤工具输出、一次性命令、闲聊)
- NOVELTY:检查已回忆的记忆——是新的还是已知的?(已知不变→跳过,已知但有实质变化→更新,全新→继续)
- FACTUAL:是具体、可操作的事实吗?(过滤模糊印象、问题、通用助手回复)
- SAFE:是否包含凭证、密钥、令牌?(任何匹配→绝不存储值,只记录”已配置”)
提取类别(按优先级):
| 类别 | 重要性 | 保留期 | 示例 |
|---|---|---|---|
| Configuration & System State | 0.95 | 永久 | 工具/服务配置、模型分配、cron 计划 |
| Standing Rules & Policies | 0.90 | 永久 | 用户行为指令、安全约束 |
| Identity & Demographics | 0.95 | 永久 | 姓名、位置、职业、语言偏好 |
| Preferences & Opinions | 0.85 | 永久 | 通信风格、工具偏好(保留原始措辞) |
| Goals, Projects & Milestones | 0.75 | 90 天 | 活跃项目、里程碑、截止日期 |
| Technical Context | 0.80 | 永久 | 技术栈、开发环境、Agent 生态 |
| Relationships & People | 0.75 | 永久 | 人名、角色、团队结构 |
| Decisions & Lessons | 0.80 | 永久 | 重要决策及理由、经验教训 |
记忆存储原则:
- 实体分组:同一实体的所有信息合并为一条记忆(如 TechForward 事件的预算、WiFi、混合能力合并为一条)
- 自包含:每条记忆必须独立可理解,不使用代词
- 15-50 词:每条事实 1-2 句,蒸馏而非追加
- 时间锚定:时效性事实必须包含 “As of YYYY-MM-DD”
- 保留原话:用户表达感受/意见时保留原始措辞
- 第三人称:“User prefers…” 而非 “I prefer…”
- 按类别批量:同类别事实一次调用,不同类别分别调用
memory-recall(召回协议):
- 在
before_prompt_build钩子中执行 - 查询净化:
sanitizeQuery()将用户消息重写为搜索查询 - 召回策略:
"always"— 长期 + 会话记忆搜索(每轮 2 次 API 调用)"smart"— 仅长期记忆搜索(默认,每轮 1 次 API 调用)"manual"— 无自动召回,Agent 通过memory_search工具控制
- 子代理支持:子代理读取父级长期记忆,但不写入(避免孤立命名空间)
memory-dream:
记忆整理协议。审查所有存储的记忆,合并重复项,清理噪声和凭证,重写不清晰条目,执行 TTL 过期。
四阶段流程:
- Orient:
memory_list加载所有记忆,识别问题(重复、过短、缺少时间锚) - Gather Targets:搜索最近添加的记忆,分类为 DELETE / MERGE / REWRITE
- Consolidate:
- 3a. 删除危险/过期条目(凭证、纯时间戳、原始工具输出、心跳记录、7天+操作记忆、90天+项目记忆)
- 3b. 合并重复(选最完整版本为基准,
memory_update合并细节,memory_delete冗余条目) - 3c. 重写不清晰条目(第一人称→第三人称、补充时间锚、压缩过长条目)
- Report:输出操作摘要(审查数、删除数、合并组数、重写数、最终计数)
自动 Dream 门控:
Dream 不会每轮触发,而是通过门控机制:
- 时间门控:距离上次 Dream 至少 N 小时(默认配置)
- 会话门控:至少 N 次交互式会话
- 记忆门控:记忆数量达到阈值
- 锁机制:
acquireDreamLock()/releaseDreamLock()防止并发 Dream - 写操作验证:只有 Agent 实际执行了
memory_add、memory_update、memory_delete才标记为完成
Quality Targets:
- 零凭证/密钥记忆
- 零重复记忆
- 所有项目/操作记忆有时间锚
- 所有记忆使用第三人称
- 所有记忆正确分类
- 每条记忆 15-50 词,自包含,原子化
Legacy 模式
不使用 Skills 时的自动管线。当 skills 配置未设置时自动启用。
1. Auto-Recall(自动回忆):
在 Agent 响应前通过 before_prompt_build 钩子搜索并注入相关记忆:
- 触发条件:
cfg.autoRecall !== false(默认 true) - 超时保护:8 秒超时,超时后跳过注入
- 会话检测:检测新会话(不同
sessionKey),首次短查询触发宽泛搜索 - 动态阈值:过滤得分低于最高分 50% 的记忆
- 子代理支持:子代理读取父级命名空间的长期记忆
- 隐私保护:自动过滤 OpenClaw 元数据(
Sender (untrusted metadata)前缀)
召回结果注入格式:
<relevant-memories>
The following are stored memories for user "alice". Use them to personalize your response:
- User prefers TypeScript over JavaScript [technical]
- User works at TechForward as senior engineer [identity]
</relevant-memories>
2. Auto-Capture(自动捕获):
在 Agent 响应后通过 agent_end 钩子过滤噪声并存储新事实:
- 触发条件:
cfg.autoCapture !== false(默认 true) - 跳过场景:
- 非交互式触发器(cron、心跳、自动化)
- 子代理会话(主代理捕获整合结果)
- Agent 已使用记忆工具(避免重复)
- 用户内容过短(< 50 字符)
- 消息选择:最近 20 条消息 + 任意位置的摘要消息
- 噪声过滤:通过
filterMessagesForExtraction()管线 - 时间戳注入:系统消息注入当前日期,用于时间锚定
- 异步执行:fire-and-forget,不阻塞 Agent 响应
关键特性
| 特性 | 实现方式 |
|---|---|
| 多代理隔离 | 通过 sessionKey 自动路由到不同 userId 命名空间(agent:<name>:<uuid> → userId:agent:<name>) |
| 子代理支持 | 子代理读取父级长期记忆,但不写入(避免孤立命名空间) |
| 隐私保护 | 自动过滤 OpenClaw 元数据、时间戳锚定、凭证过滤规则 |
| CLI 命令 | openclaw mem0 init / login / search / add / list / dream / status / config / event |
| 遥测 | PostHog 匿名遥测,记录 recall/capture/dream 事件 |
| 配置持久化 | ~/.openclaw/openclaw.json,支持 SecretRef 敏感配置 |
| 记忆作用域 | Session(短期,run_id 隔离)+ User(长期,跨会话) |
| Skills 加载 | skill-loader.ts 读取 skills/ 目录下的 SKILL.md 文件 |
| Dream 门控 | dream-gate.ts 实现会话计数、锁机制、完成记录 |
| 安全文件系统 | fs-safe.ts 提供安全的文件读写操作,避免路径遍历攻击 |
| OSS 配置向导 | oss-wizard.ts 实现交互式 4 步向导(LLM → Embedder → Vector Store → User ID) |
| 测试覆盖 | 12 个测试文件,覆盖 backend、CLI、config、tools、telemetry 等 |
配置 Schema
插件通过 openclaw.plugin.json 定义完整的配置 Schema,支持 20+ 配置项:
核心配置:
| 键 | 类型 | 默认值 | 说明 |
|---|---|---|---|
mode | "platform" / "open-source" | "platform" | 后端模式 |
userId | string | OS 用户名 | 用户标识符 |
autoRecall | boolean | true | 自动注入记忆(Skills 模式下忽略) |
autoCapture | boolean | true | 自动存储事实(Skills 模式下忽略) |
topK | number | 5 | 最大召回数量 |
searchThreshold | number | 0.1 | 最低相似度阈值 |
Skills 模式配置:
| 键 | 类型 | 默认值 | 说明 |
|---|---|---|---|
skills.triage.enabled | boolean | true | 启用事实提取 |
skills.recall.enabled | boolean | true | 启用记忆回忆 |
skills.recall.strategy | "always" / "smart" / "manual" | "smart" | 召回策略 |
skills.recall.tokenBudget | number | 1500 | 最大 token 预算 |
skills.dream.enabled | boolean | true | 启用定期整理 |
skills.domain | string | "companion" | 领域覆盖 |
Open-Source 模式配置:
| 键 | 类型 | 默认值 | 说明 |
|---|---|---|---|
oss.embedder.provider | string | "openai" | 嵌入提供商 |
oss.vectorStore.provider | string | "memory" | 向量存储提供商 |
oss.llm.provider | string | "openai" | LLM 提供商 |
oss.historyDbPath | string | — | SQLite 路径 |
安全与隐私
数据流:
| 模式 | 数据去向 | 凭证需求 |
|---|---|---|
| Platform | 对话发送到 api.mem0.ai 进行记忆提取 | MEM0_API_KEY |
| Open-Source (OpenAI) | LLM/embedding 调用 OpenAI API;向量本地存储 | OPENAI_API_KEY |
| Open-Source (Ollama) | 完全本地 — LLM、embedding、向量均在本地 | 无 |
凭证存储:
配置存储在 ~/.openclaw/openclaw.json,支持:
- 环境变量引用:
"apiKey": "${MEM0_API_KEY}" - SecretRef:
"apiKey": {"source": "env", "provider": "default", "id": "MEM0_API_KEY"}
持久化位置:
| 文件 | 用途 |
|---|---|
~/.openclaw/openclaw.json | 插件配置(API Key、User ID、设置) |
~/.mem0/vector_store.db | 本地向量存储(Open-Source 模式) |
~/.mem0/history.db | 记忆编辑历史(Open-Source 模式) |
<pluginStateDir>/dream-state.json | Dream 状态 |