第 1 篇:「架构鸟瞰与 Memory 初始化」------ 从 pip install 到三个工厂
系列 :Mem0 深度分析(00 篇为总路线图)
对应官方文档 :Open Source Overview、Open Source Configuration
代码基线 :main 分支 2026-09-04 快照,
mem0ai 2.0.20(pyproject.toml:7)核心源码 :
mem0/memory/main.py:487-580、mem0/configs/base.py:28-62、mem0/utils/factory.py阅读本文你将了解: Mem0 三条产品线(OSS
Memory/AsyncMemory/ PlatformMemoryClient)的边界;Memory()一行构造背后发生了什么(8 个初始化步骤);工厂注册表 + 反射的插件机制;默认配置链里那些"和你想的不一样"的值;换 LLM/嵌入/向量库的最小改动面。
1. 先分清三条产品线:同一个心智模型,三个类
官方文档反复强调一个路由规则(llms.txt 原文):有 API key 走 MemoryClient(托管 Platform),自托管走 Memory(OSS)。三者共享同一套 CRUD 语义(add / search / get / get_all / update / delete / delete_all),这正是"drop-in"的表层含义:
python
# 产品线 A:OSS 自托管(本系列主战场)
from mem0 import Memory, AsyncMemory # mem0/__init__.py:6-7
m = Memory() # 需要 OPENAI_API_KEY
# 产品线 B:Platform 托管版
from mem0 import MemoryClient # mem0/client/main.py
client = MemoryClient(api_key="...") # 一切发生在 mem0 云端
# 产品线 C:Node SDK 对应物
# import { Memory } from "mem0ai/oss" (mem0-ts/,243 个文件,本系列不展开)
mem0/__init__.py:5-6 只导出四个名字,包的公共 API 面小到可以一眼看完------这是成熟库的克制。本系列聚焦产品线 A:所有记忆管线代码都在你自己的进程里跑,LLM 调用、向量检索、SQLite 写入全部可审计。
选型判断(带证据) :OSS 版能力边界在 MemoryConfig 的字段里写死了------没有 enable_graph、没有平台版 Dream/webhooks/custom categories(对照官方文档 platform/features/* 页面,均为 [Platform] 标签)。但 OSS 版有 Platform 没有的东西:custom_instructions 直接进提取提示词(mem0/configs/base.py:56-61)、reranker 可插拔(mem0/configs/base.py:46-49)、以及全链路源码可改。
2. Memory() 一行代码的 8 个初始化步骤
Memory.__init__(mem0/memory/main.py:488-552)是理解全库的枢纽。逐行拆开:
python
class Memory(MemoryBase):
def __init__(self, config: MemoryConfig = MemoryConfig()):
self.config = config
self.embedding_model = EmbedderFactory.create( # 步骤1:嵌入模型
self.config.embedder.provider,
self.config.embedder.config,
self.config.vector_store.config, # 注意:向量库配置也会传给嵌入器
)
self.vector_store = VectorStoreFactory.create( # 步骤2:向量库
self.config.vector_store.provider, self.config.vector_store.config
)
self.llm = LlmFactory.create(self.config.llm.provider, self.config.llm.config) # 步骤3:LLM
self.db = SQLiteManager(self.config.history_db_path) # 步骤4:SQLite 历史/消息库
self.collection_name = self.config.vector_store.config.collection_name
self.api_version = self.config.version # 默认 "v1.1"
self.custom_instructions = self.config.custom_instructions
self.reranker = None # 步骤5:重排序器(可选)
if config.reranker:
self.reranker = RerankerFactory.create(
config.reranker.provider, config.reranker.config
)
self._entity_store = None # 步骤6:实体库【惰性】,见 05 篇
if MEM0_TELEMETRY: # 步骤7:遥测(可关)
... # 复用同一 provider 建遥测专用 collection "mem0migrations"
# mem0/memory/main.py:529
if getattr(type(self.vector_store), "keyword_search", None) is VectorStoreBase.keyword_search:
logger.warning( # 步骤8:混合检索能力检查
"The '%s' vector store does not support keyword search. "
"Hybrid (BM25) scoring will be disabled ...",
self.config.vector_store.provider,
) # mem0/memory/main.py:543-550
capture_event("mem0.init", self, {"sync_type": "sync"})
四个设计点值得停下来想:
(a)嵌入器拿到了向量库的配置 。EmbedderFactory.create 的第三个参数是 self.config.vector_store.config(mem0/memory/main.py:494)------这是为了让向量库维度约束(如 embedding_model_dims)能反向校验嵌入器输出维度,避免"嵌入 384 维、向量库建表 1536 维"这类启动期不可见的运行时错配。
(b)实体库是惰性的 (mem0/memory/main.py:513-514, 559-580)。_entity_store = None 之后第一次访问 self.entity_store property 才创建:克隆向量库配置、换独立 collection(_entity_collection_name),如果是 Qdrant 嵌入式模式(path=...)则共享同一个 client 实例 ------注释明说是为了避免 RocksDB 锁竞争(mem0/memory/main.py:569-571)。这是嵌入式向量库并发写的一个真实坑,官方用"共享连接"而不是"延迟加锁"来解。
(c)能力检查前置到初始化 。mem0/memory/main.py:543-550 用 getattr(type(...), "keyword_search", None) is VectorStoreBase.keyword_search 判断你的向量库是否实现了 keyword_search(基类默认抛 NotImplementedError,mem0/vector_stores/base.py:68)。没实现就在 init 时打警告:混合 BM25 评分自动降级为纯语义。这个警告只出现一次,但影响每一次 search ------04 篇会展开降级后的评分分母变化。26 家向量库里目前实现了 keyword_search 的以 qdrant 为代表(mem0/vector_stores/qdrant.py:454)。
(d)遥测是独立 collection 而非独立库 。MEM0_TELEMETRY 开启时复用同一向量库 provider 建 mem0migrations collection(mem0/memory/main.py:529);FAISS/Qdrant 嵌入式场景会重定向到 migrations_<provider> 本地路径(mem0/memory/main.py:533-536)。介意遥测的可以 MEM0_TELEMETRY=False。
3. 默认配置链:那些"和你想的不一样"的值
MemoryConfig(mem0/configs/base.py:28-62)全字段如下,逐个标注默认值与出处:
| 字段 | 默认值 | 出处 | 值得注意 |
|---|---|---|---|
vector_store.provider |
qdrant(嵌入路径) | mem0/vector_stores/configs.py:7 |
默认 path="/tmp/qdrant"、collection "mem0"、dims 1536(mem0/configs/vector_stores/qdrant.py:11-16) |
llm.provider |
"openai" |
mem0/llms/configs.py:7 |
config 走 field_validator 白名单校验(18 家,mem0/llms/configs.py:12-35) |
| LLM 默认模型 | gpt-5-mini |
mem0/llms/openai.py:40 |
不是旧资料里的 gpt-4o-mini;temperature 默认 0.1、max_tokens 2000(mem0/configs/llms/openai.py:15-18) |
embedder.provider |
"openai" |
mem0/embeddings/configs.py:7 |
模型 text-embedding-3-small、dims 1536(mem0/embeddings/openai.py:15,19) |
history_db_path |
~/.mem0/history.db |
mem0/configs/base.py:40-43 |
可用 MEM0_DIR 环境变量整体重定向(mem0/configs/base.py:7-8) |
reranker |
None |
mem0/configs/base.py:46-49 |
不配就没有重排,search 的 rerank=True 形同虚设 |
version |
"v1.1" |
mem0/configs/base.py:50-53 |
决定返回结构为 {"results": [...]}(mem0/memory/main.py:1426) |
custom_instructions |
None |
mem0/configs/base.py:56-61 |
02 篇讲它如何拼进提取提示词 |
最意外的默认值是 LLM 模型 :OpenAILLM.__init__ 里 if not self.config.model: self.config.model = "gpt-5-mini"(mem0/llms/openai.py:39-40)。网上大量旧教程写的是 gpt-4o-mini,照抄会导致成本预估失真------提取提示词 1000+ 行 token 级别的输入(mem0/configs/prompts.py:468-946),模型单价直接影响记忆写入成本。
4. 工厂机制:注册表 + 反射,而不是 if-else
三个工厂(Llm / Embedder / VectorStore / Reranker)共用一套骨架,以 LlmFactory 为例(mem0/utils/factory.py:44-63):
python
class LlmFactory:
provider_to_class = {
"ollama": ("mem0.llms.ollama.OllamaLLM", OllamaConfig),
"openai": ("mem0.llms.openai.OpenAILLM", OpenAIConfig),
...
"deepseek": ("mem0.llms.deepseek.DeepSeekLLM", DeepSeekConfig),
"vllm": ("mem0.llms.vllm.VllmLLM", VllmConfig),
"langchain": ("mem0.llms.langchain.LangchainLLM", BaseLlmConfig),
}
@classmethod
def create(cls, provider_name, config=None, **kwargs):
if provider_name not in cls.provider_to_class:
raise ValueError(f"Unsupported Llm provider: {provider_name}")
class_type, config_class = cls.provider_to_class[provider_name]
llm_class = load_class(class_type) # mem0/utils/factory.py:29
# rsplit(".", 1) + importlib.import_module
...
create 的四分支配置归一化(mem0/utils/factory.py:64-127)是真正的看点:None → 默认构造;dict → 合并 kwargs 构造;BaseLlmConfig 实例 → 反射 provider 专属 config 的签名 ,只转发对方接受的字段(mem0/utils/factory.py:100-115,用 inspect.signature 探测 **kwargs 或显式参数名,reasoning_effort/is_reasoning_model 只在对方收货时才传)。这解决了一个真实的演进痛点:基类字段膨胀后,旧 provider 的构造函数会被不认识的新字段炸掉。
扩展机制也在这层:register_provider(mem0/utils/factory.py:128)允许运行时注册自定义 provider 的 module.path.Class 字符串------不需要改 mem0 源码就能接入私有 LLM。
工程评价 :注册表 + 字符串反射的代价是 IDE 跳转失效和拼写错误延迟到运行时("opnesai" 会在 create 抛 ValueError 而不是静态检查期),收益是核心包零依赖------26 家向量库的 import 全部推迟到真正用到那一刻(load_class,mem0/utils/factory.py:29-32)。装 mem0 不会连带装 milvus/pinecone,这是插件式设计对包体积的直接回报。
4.5 配置类的白名单校验:provider 合法性在哪一层拦截
三个配置入口都有独立的 provider 校验,但机制不同------这是排错时容易混淆的点:
LlmConfig:pydanticfield_validator白名单,18 家硬编码在mem0/llms/configs.py:12-35。不在这份列表里的 provider(哪怕工厂注册表里有sarvam,配置类却在 18 家列表里没收录的话)会在构造 MemoryConfig 时 就抛ValueError: Unsupported LLM provider。注意这份白名单比LlmFactory.provider_to_class(mem0/utils/factory.py:44)略短------两份清单是手工同步的,加新 provider 要改两处,这是设计上的一个小破绽。EmbedderConfig/VectorStoreConfig:同样是 field_validator,但VectorStoreConfig更进一步做配置类动态解析 ------mem0/vector_stores/configs.py:43-53按 provider 名 importmem0.configs.vector_stores.<provider>模块、取对应 Config 类、把用户 dict 转换成强类型配置,还检查path等字段是否在白名单 annotation 里(mem0/vector_stores/configs.py:63-64)。也就是说向量库配置在 pydantic 校验期就完成了"dict → 类型化配置"的升级,错误暴露得比 LLM 配置更早。- 工厂层兜底 :
LlmFactory.create入口的if provider_name not in cls.provider_to_class抛错(mem0/utils/factory.py:75-76)只对绕过 MemoryConfig 直接调工厂的调用方生效。
结论:LLM provider 拼错在 Memory() 构造期就炸;向量库 provider 拼错在配置校验期炸且带完整可用清单。排错时看异常栈的层就能定位是哪一环的清单没同步。
4.6 四个工厂签名对比:一个参数之差
| 工厂 | create 签名 |
特殊之处 | 出处 |
|---|---|---|---|
LlmFactory |
(provider, config=None, **kwargs) |
BaseLlmConfig → 专属 config 的反射式字段转发 | mem0/utils/factory.py:64 |
EmbedderFactory |
(provider, config, vector_config) |
第三参收向量库配置 ,用于维度一致性防线;有单例 reset(reset,mem0/utils/factory.py:221) |
mem0/utils/factory.py:168 |
VectorStoreFactory |
(provider, config) |
26 家注册,全部延迟 import | mem0/utils/factory.py:210 附近 |
RerankerFactory |
(provider, config=None, **kwargs) |
5 家注册;None 时 Memory 直接跳过创建 | mem0/utils/factory.py:245 |
EmbedderFactory 是唯一带"跨域参数"的工厂------它的第三参 vector_config 正是 §2(a) 所说维度防线的实现入口。而 reset 类方法(mem0/utils/factory.py:221)暴露了一个事实:某些 embedder 实现内部有缓存单例,测试隔离时需要显式 reset,写单元测试时会踩到。
4.7 from_config 与配置对象的两条路
Memory 提供两个构造入口:直接 Memory(MemoryConfig(...)) 传强类型配置,或 Memory.from_config(config_dict)(mem0/memory/main.py:731-737)传 dict。后者内部就是 cls(MemoryConfig(**config_dict)) 一行,但它是官方文档所有示例的标准姿势------因为 dict 形式对 JSON/YAML 配置文件友好,且 pydantic 会把 dict 自动升级为嵌套的强类型配置(VectorStoreConfig 的动态解析在此生效)。自托管部署推荐永远走 from_config:配置文件可以直接落盘,重启加载逻辑不用写两遍。
5. SQLiteManager:被低估的第三存储
self.db = SQLiteManager(self.config.history_db_path)(mem0/memory/main.py:500)看似只是"审计日志",实际承担两个职责:history 表 (每次 ADD/UPDATE/DELETE 的新旧记忆快照,m.history(memory_id) 的数据源)和 messages 表 (保存原始对话消息,v3 新增------02 篇会看到 add() Phase 0 的 get_last_messages 直接依赖它做提取上下文,mem0/memory/main.py:920)。
构造函数里有三个防御性设计(mem0/memory/storage.py:13-80):
check_same_thread=False+ 自持threading.Lock(mem0/memory/storage.py:16-18)------SQLite 连接跨线程复用,所有写操作走锁。AsyncMemory 高并发写入时这是全局串行点,07 篇会讨论它对吞吐的影响。_migrate_history_table()先于建表执行(mem0/memory/storage.py:20-79)------旧 schema(含 group-chat 会话列)与新 schema(is_deleted/actor_id/role列)之间自动迁移:rename → 建新表 → 拷贝交集列 → drop 旧表,还处理了"上次迁移失败留下的history_old残表"(mem0/memory/storage.py:44)。这是把 schema 演进做进单文件存储的务实做法。- 默认
":memory:"路径(mem0/memory/storage.py:14)------不配history_db_path就意味着历史表随进程消失。测试场景合理,生产自托管必须显式指定,否则history()接口形同虚设。
这里的消息持久化是 v3 管线的关键依赖:
add()不再只依赖调用方传入的 messages,而是从 SQLite 里回捞同一会话最近 10 条消息做提取上下文(mem0/memory/main.py:919-921)。换句话说,SQLite 在 v3 已经从可选审计组件升级为提取管线的状态存储------把它当可丢弃组件的人会默默损失提取质量。
6. 类图:Memory 的组合关系

图解 :这张类图对应 Memory.__init__ 的八步初始化(mem0/memory/main.py:488-552)。四个外部依赖全部走工厂(组合箭头指向 Factory),只有 SQLiteManager 是直接实例化(mem0/memory/main.py:500)------因为历史库没有多实现需求,抽象就是过度设计。VectorStore 接口上的 keyword_search 是可选能力:基类默认实现抛 NotImplementedError(mem0/vector_stores/base.py:68),Qdrant 等厂商覆写它(mem0/vector_stores/qdrant.py:454),init 末尾的能力检查(mem0/memory/main.py:543)据此决定混合检索是否降级。Memory 与 MemoryConfig 是强组合,而 _entity_store 用虚线般的惰性持有(05 篇展开)。
7. 实战:换 provider 的最小改动面
python
from mem0 import Memory
config = {
"llm": {
"provider": "deepseek", # mem0/utils/factory.py:55
"config": {"model": "deepseek-chat", "api_key": "...", "temperature": 0.1},
},
"embedder": {
"provider": "ollama",
"config": {"model": "nomic-embed-text", "embedding_dims": 768},
},
"vector_store": {
"provider": "qdrant",
"config": {"path": "./mem0_qdrant", "embedding_model_dims": 768},
},
"history_db_path": "./mem0_history.db",
}
m = Memory.from_config(config) # mem0/memory/main.py:731,类方法包一层 MemoryConfig(**config)
三个对齐陷阱:嵌入维度、向量库 embedding_model_dims、以及(Qdrant 场景)建 collection 时的 dims 三者必须一致------嵌入器与向量库配置是独立传的,EmbedderFactory 拿到向量库配置做校验(mem0/memory/main.py:494)是唯一的一致性防线;Qdrant 嵌入式路径换成 Windows 本地目录时注意 /tmp/qdrant 默认值在 Windows 下的实际落点;DeepSeek 等 provider 的 config dict 键名要匹配各自 XxxConfig 构造签名(mem0/configs/llms/deepseek.py),传错字段会被 pydantic 拒绝。
第二个常用配方:全本地栈(Ollama),零外部 API、可审计:
python
config = {
"llm": {
"provider": "ollama",
"config": {"model": "qwen2.5", "temperature": 0.1, "ollama_base_url": "http://127.0.0.1" + ":11434"},
},
"embedder": {
"provider": "ollama",
"config": {"model": "nomic-embed-text", "embedding_dims": 768},
},
"vector_store": {"provider": "qdrant", "config": {"path": "./mem0_qdrant", "embedding_model_dims": 768}},
"history_db_path": "./mem0_history.db",
}
这个配方有一个必须知道的取舍:提取质量对 LLM 能力极其敏感------v3 的 ADDITIVE_EXTRACTION_PROMPT(mem0/configs/prompts.py:468-946,02 篇解剖)要求模型输出严格 JSON 并做日期推理,小参数模型常见症状是 JSON 解析失败。代码对此有两级容错:先 json.loads 失败后走 extract_json 抢救(mem0/memory/main.py:977-981),抢救失败则提取结果静默为空(mem0/memory/main.py:982-984)------注意这与 LLM 调用本身失败(抛 LLMError,mem0/memory/main.py:963-969)不同:模型太弱导致的输出劣化是"成功调用 + 空提取",没有任何报错,只能通过 history 表增长停滞间接发现。本地小模型跑记忆提取时,务必先人工 review 几轮 add 的提取结果再放量。
7.1 AsyncMemory:复制出的镜像类,还是真异步?
mem0/__init__.py:5-6 同时导出 AsyncMemory,它位于 mem0/memory/main.py:2172 起(3868 行文件的后半段)。打开看一眼就能确认:AsyncMemory.add 是 async def(mem0/memory/main.py:2434),内部 await 真正的异步 SDK 调用,不是 asyncio.to_thread 包同步 Memory 的壳。代价是同步与异步两个类几乎完全镜像 (方法一一对应,mem0/memory/main.py:2172-3868 约 1700 行是前半段的异步复写),任何管线改动都要双写------这是本仓库维护成本最高的技术债,也是 02/04 篇每个机制都"同一处出现两次"的原因。选型建议:FastAPI 服务化场景(07 篇)用 AsyncMemory 避免线程池开销;普通脚本、notebook 用同步 Memory 调试更直观。SQLite 层两者共享同一把 threading.Lock 语义(mem0/memory/storage.py:16-18),异步并发写入时历史表仍是串行点。
7.2 自托管前自查:五个高频坑
/tmp/qdrant落点 (mem0/configs/vector_stores/qdrant.py:16):Windows 下path未显式指定时行为依赖 qdrant-client 的路径归一,自托管一律显式写绝对路径。MEM0_DIR与~/.mem0(mem0/configs/base.py:7-8):history.db 与迁移文件都写这里,多实例并行时要么各自指定history_db_path,要么共享但接受 SQLite 锁竞争。rerank=True无 reranker :search()里if rerank and self.reranker(mem0/memory/main.py:1500)------没配置 reranker 时这个参数静默无效,不报错。想要重排必须在 MemoryConfig 里配reranker(mem0/configs/base.py:46-49)。- 遥测体积 :
mem0migrationscollection 会持续累积事件(mem0/memory/main.py:529),FAISS/Qdrant 嵌入式场景落在MEM0_DIR/migrations_<provider>(mem0/memory/main.py:533-536),磁盘敏感场景MEM0_TELEMETRY=False关闭。 - 18 家白名单 vs 工厂注册表不同步 (§4.5):新 provider 出现后,照抄网上
LlmConfig写法可能在 pydantic 校验期被拒------升级 mem0 版本时这类校验清单也会变,报错时先核对mem0/llms/configs.py:12-35。
8. 验证检查点
-
Memory()初始化后m.api_version == "v1.1"(mem0/memory/main.py:502) - 默认 LLM 打印
m.llm.config.model == "gpt-5-mini"(mem0/llms/openai.py:40) - 用 FAISS(无 keyword_search)初始化能看到 BM25 降级警告(
mem0/memory/main.py:543-550) -
m.entity_store第一次访问才创建 collection(mem0/memory/main.py:559-580,可在 Qdrant collection 列表里对照验证) -
m.db.connection指向~/.mem0/history.db,含history与messages两张表(mem0/memory/storage.py)
下一篇 :02 篇进入
add()的 V3 八阶段批量管线------从 UUID 映射防幻觉到 MD5 批量去重,一次看懂 v1→v3 的提取架构迁移。