首页
/ 深入 Mem0 Platform 记忆架构:从写入管线、混合检索到四维多租户作用域

深入 Mem0 Platform 记忆架构:从写入管线、混合检索到四维多租户作用域

2026-09-04 14:41:28作者:翟萌耘Ralph

Mem0 是一个托管式 AI 记忆层,它封装了记忆抽取、去重、冲突消解与语义检索的全部复杂性,让你的应用只需调用 search()add() 两个接口。本文基于 Mem0 Platform 架构参考文档,结合仓库中 Python 客户端源码类型定义,完整拆解记忆的处理管线、检索管线、生命周期、记忆对象结构、多租户作用域与性能特征,读完你可以准确理解 v3 版本的 ADD-only 模型、异步处理语义以及 user_id / agent_id / app_id / run_id 四维作用域的存储与查询规则。

核心概念:应用与记忆层之间的三步循环

Mem0 Platform 的定位是"受管记忆层"(managed memory layer),位于你的 AI 应用与用户之间。所有集成本质上都遵循同一个三步循环:

User Input → Retrieve relevant memories → Enrich LLM prompt → Generate response → Store new memories

即:检索相关记忆 → 用记忆增强 LLM 提示词 → 生成回复 → 把新事实存回记忆库。正如 SKILL.md 中总结的 "retrieve → generate → store" 模式,抽取、去重、冲突消解和语义检索的复杂性全部由 Mem0 承担。

底层存储由两部分构成:

  • 向量存储(Vector store):存放嵌入向量,支撑语义相似度检索;
  • 实体存储(Entity store):在 add() 时自动抽取实体并建立实体链接(entity linking),为"关系感知"的检索提供图谱侧信号。

从源码结构看,这一存储分工对应 v3 的两处演进:实体链接取代了 v2 的图记忆(graph memory),且无需任何配置、在 add() 期间自动完成——这也是 v2 配置中 enable_graphgraph_store 参数被移除的原因(详见 Platform v2 → v3 迁移指南)。

记忆处理管线:client.add() 内部发生了什么

当调用 client.add() 时,消息会依次经过三个阶段:

Messages In
    │
    ▼
┌─────────────────────┐
│  1. EXTRACTION       │  单次 LLM 调用抽取所有新的独立事实
│     (infer=True)     │  若 infer=False,则原文照存
└─────────┬───────────┘
          │
          ▼
┌─────────────────────┐
│  2. DEDUPLICATION    │  基于哈希的去重(MD5 拦截完全重复)
│                      │  无 UPDATE/DELETE —— v3 是 ADD-only
└─────────┬───────────┘
          │
          ▼
┌─────────────────────┐
│  3. STORAGE          │  批量嵌入 → 向量存储
│                      │  实体抽取 → 实体存储
└─────────┬───────────┘
          │
          ▼
    Memory Object

阶段一:抽取(Extraction)

抽取有 infer 参数控制两种模式:

推断模式(infer=True,默认):

  • LLM 从对话中抽取结构化事实;
  • 抽取阶段附带冲突消解:重复内容被去重,矛盾内容被消解;
  • 适用于:自然对话 → 记忆的典型场景。

原文模式(infer=False):

  • 文本原样存储,不经过任何 LLM 处理;
  • 跳过冲突消解——同一事实可能被存两次;
  • 只存储 user 角色的消息,assistant 消息被忽略;
  • 适用于:批量导入、预结构化数据、数据迁移。

警告: 不要对同一份数据混用 infer=Trueinfer=False,否则同一事实会被存两次。

这一点在 SDK 中有对应的类型定义:AddMemoryOptionsinfer 是一个可选布尔字段,此外还提供 custom_categories(自定义分类标签)、custom_instructions(定制事实抽取指令)、timestampexpiration_date(时间属性)、structured_data_schema(结构化数据抽取模式)等可选参数,用于在同一管线上进一步定制抽取行为。

阶段二:去重(Deduplication)

v3 的去重采用基于哈希(MD5)的精确重复拦截,且不产生 UPDATE/DELETE 操作——v3 抽取是单遍的 ADD-only 模型。这意味着记忆随时间累积(accumulate)而非被持续合并(consolidate);同一实体的认知演化通过新增记忆体现,由检索阶段的多信号打分决定哪条记忆更相关。

阶段三:存储(Storage)

通过去重后的记忆会被批量嵌入(batch embed)写入向量存储,同时完成实体抽取写入实体存储,最终落库为一条 Memory Object(结构见下文)。

v3 的异步处理模型

v3 默认异步处理 add()

  • API 立即返回 {"status": "PENDING", "event_id": "evt-..."}
  • 通过 GET /v1/event/{event_id}/ 轮询处理状态;
  • 也可以配置 Webhook 在处理完成时接收实时通知。

从源码看,MemoryClient.add()POST /v3/memories/add/ 发起请求(L217),并把响应原样返回——PENDING + event_id 正是这个响应的内容。这一模型的实际影响是:刚调用完 add() 立即 search() 可能查不到新记忆SKILL.md 建议等待 2~3 秒),这是异步管线的直接推论。

检索管线:client.search() 的三信号融合

当调用 client.search() 时,查询经过三个阶段:

Query In
    │
    ▼
┌─────────────────────┐
│  1. PREPROCESSING    │  关键词词形还原(lemmatize)、实体抽取
└─────────┬───────────┘
          │
          ▼
┌─────────────────────┐
│  2. PARALLEL SCORING │  语义检索(向量相似度)
│                      │  BM25 关键词检索(词项匹配)
│                      │  实体匹配(实体图加权)
└─────────┬───────────┘
          │
          ▼
┌─────────────────────┐
│  3. SCORE FUSION     │  多路信号融合为单一分数
│                      │  可选:rerank=True 深度重排
└─────────┬───────────┘
          │
          ▼
    Results (combined score per memory)

三路并行打分分别覆盖三种匹配形态:向量相似度捕捉语义等价("不吃肉" ≈ "素食主义者"),BM25 词项匹配捕捉精确关键词(型号、代号等),实体匹配则利用实体存储中自动建立的实体链接做关系加权。三路信号在融合阶段合成单一 score;开启 rerank=True 后还会经过一轮深度重排(代价是额外延迟,见性能章节)。

v3 检索默认值

参数 默认值 备注
top_k 20 v2 中为 100
threshold 0.1 v2 中为 None
rerank False v2 中为 True

这些默认值对应 SDK 中的可选参数定义:SearchMemoryOptions 暴露了 top_kthresholdrerankfilterscategorieskeyword_search(纯关键词检索)、show_expiredlatest_onlyreference_date(相对时间查询的参考日期)等字段,全部为可选——不传即使用上表默认值。

隐式空值作用域(Implicit null scoping)

这是 v3 检索中最容易踩坑的语义:当 filters 只带 user_id 时,Mem0 只返回 agent_idapp_idrun_id 全部为空(null)的记忆。这一设计从默认行为上防止了跨作用域泄漏(cross-scope leakage)。

若需要包含带非空作用域字段的记忆,必须改用显式 OR 过滤:

# 获取 alice 的所有记忆(无论 agent/app/run 是什么)
filters={"OR": [{"user_id": "alice"}]}

这一点在 SDK 层有硬约束佐证。mem0/client/main.py 中定义了:

ENTITY_PARAMS = frozenset({"user_id", "agent_id", "app_id", "run_id"})

search()get_all() 都会拦截并拒绝顶层传入这四个实体参数(L267-L273、L316-L322 抛出 ValueError),强制要求把它们放进 filters 字典——v3 API 不接受顶层实体参数。这与 types.py 模块文档的说明一致:Identity fields (user_id, agent_id, app_id, run_id) must be passed inside the filters dict。也就是说,作用域语义不仅在服务端生效,SDK 在客户端就先行校验,确保作用域信息以统一的 filters 结构下发。

记忆生命周期(v3)

v3 使用 ADD-only 抽取:记忆随时间累积而不被合并。完整的生命周期操作在 MemoryClient 中都有对应实现:

创建

client.add(messages, user_id="alice")

单遍抽取 → 去重 → 存储,返回 {"event_id": "...", "status": "PENDING"}

更新

client.update(memory_id, text="...")   # 替换文本
client.batch_update([...])             # 批量,每批最多 1000 条

从源码看,batch_update()PUT /v1/batch/L571-L595),batch_delete()DELETE /v1/batch/L598-L621),二者共用 batch 端点、以 HTTP 方法区分。

删除

client.delete(memory_id)                                  # 单条
client.batch_delete([...])                                # 批量
client.delete_all(filters={"user_id": "alice"})           # 按条件批量清除

delete_all() 对应 DELETE /v1/memories/L414-L442),而针对整个实体的清理(删除某 user/agent/app/run 下的一切)由 delete_users() 通过 DELETE /v2/entities/{type}/{name}/ 完成,reset() 则调用它实现全量清空。端点级细节可继续查阅 docs/api-reference/memory/ 下的 delete-memory.mdxbatch-update.mdxbatch-delete.mdx 等页面。

记忆对象(Memory Object)结构

每条记忆落库后的完整结构如下:

{
  "id": "uuid-string",
  "memory": "Extracted memory text",
  "user_id": "user-identifier",
  "agent_id": null,
  "app_id": null,
  "run_id": null,
  "metadata": { "source": "chat", "priority": "high" },
  "categories": ["health", "preferences"],
  "created_at": "2025-03-12T12:34:56Z",
  "updated_at": "2025-03-12T12:34:56Z",
  "structured_attributes": {
    "day": 12, "month": 3, "year": 2025,
    "hour": 12, "minute": 34,
    "day_of_week": "wednesday",
    "is_weekend": false,
    "quarter": 1, "week_of_year": 11
  },
  "score": 0.85
}
字段 类型 说明
id UUID 唯一标识符,用于 update/delete
memory string 抽取或存储的文本内容
user_id string 用户作用域(主实体)
agent_id string Agent 作用域
app_id string 应用作用域
run_id string 会话/运行作用域
metadata object 自定义键值对,可用于过滤
categories array 自动分配或自定义的分类标签
created_at datetime 创建时间戳
updated_at datetime 最后修改时间戳
structured_attributes object 时间维度拆解,支持基于时间的查询
score float 语义相似度(仅检索结果中出现,0–1)

值得注意的两个设计点:

  1. structured_attributes 是对 add() 时间参数的落库体现。AddMemoryOptions 中传入 timestamp(Unix 时间戳)或 expiration_date(YYYY-MM-DD)后,服务端会把时间戳拆解为天/月/年、时/分、星期、是否周末、季度、年度周次等字段——这是时间感知检索(如 reference_date 相对时间查询)的底层依据。
  2. categories 支持自动与自定义两条来源。 add() 时可通过 custom_categories 传入标签 schema 覆盖自动分类;检索时又可用 SearchMemoryOptions.categories 按标签过滤。

作用域与多租户(Scoping & Multi-Tenancy)

Mem0 沿四个维度隔离记忆,防止数据混用:

维度 字段 用途 示例
用户 user_id 持久化的人物画像或账户 "customer_6412"
Agent agent_id 独立的智能体或工具 "meal_planner"
应用 app_id 产品表面或部署 "ios_retail_app"
会话 run_id 短生命周期的流程或线程 "ticket-9241"

存储模型:每个实体组合独立成记录

每个实体组合(entity combination)都会生成独立的记录user_id="alice" 的记忆与 user_id="alice" + agent_id="bot" 的记忆是两条不同的存储记录。这与 SDK 中 ENTITY_PARAMS 常量(main.py L40)所约束的四个身份字段一一对应。

关键陷阱:跨实体查询

# 这样查不到任何结果 —— user 记忆与 agent 记忆是分开存的
filters={"AND": [{"user_id": "alice"}, {"agent_id": "bot"}]}

# 用 OR 查询多个作用域
filters={"OR": [{"user_id": "alice"}, {"agent_id": "bot"}]}

# 用通配符 "*" 匹配任意非空值
filters={"AND": [{"user_id": "*"}]}  # 所有用户(不含 null)

第一条查询返回空不是 bug 而是设计:AND 语义要求同一条记忆同时满足 user 和 agent 两个条件,而两者各自独立存储、从不落在同一条记录上。SKILL.md 的"常见边界情况"一节把这条列为高频误区。

推荐的作用域模式

# 用户级:持久化偏好
client.add(messages, user_id="alice")

# 会话级:临时上下文
client.add(messages, user_id="alice", run_id="session_123")
# 用完即清:client.delete_all(run_id="session_123")

# Agent 级:agent 专属知识
client.add(messages, agent_id="support_bot", app_id="helpdesk")

# 多租户:完全隔离
client.add(messages, user_id="alice", agent_id="bot",
          app_id="acme_corp", run_id="ticket_42")

记忆分层:从单轮到终身

Mem0 支持三层记忆,按生命周期从短到长排列:

对话记忆(Conversation memory)

  • 单个回合内的进行中消息;
  • 包含工具调用、思维链推理;
  • 生命周期: 单次响应——回合结束即消失;
  • 管理者: 你的应用,而不是 Mem0。

会话记忆(Session memory)

  • 面向当前任务或频道的短期事实;
  • 多步流程:入职引导、调试、客服工单;
  • 生命周期: 数分钟到数小时;
  • 管理者: Mem0,通过 run_id 参数;
  • 清理方式:client.delete_all(run_id="session_id")

用户记忆(User memory)

  • 与人或账户绑定的长期知识;
  • 个人偏好、账户状态、合规信息;
  • 生命周期: 数周到永久;
  • 管理者: Mem0,通过 user_id 参数;
  • 跨所有会话与交互持久存在。

分层在实际代码中的组合方式

def chat(user_input: str, user_id: str, session_id: str) -> str:
    # 1. 检索用户记忆(长期偏好)
    user_mems = mem0.search(user_input, filters={"user_id": user_id})

    # 2. 检索会话记忆(当前任务上下文)
    session_mems = mem0.search(user_input, filters={
        "AND": [{"user_id": user_id}, {"run_id": session_id}]
    })

    # 3. 两层记忆合并为 LLM 上下文
    context = format_memories(user_mems) + format_memories(session_mems)

    # 4. 生成响应
    response = llm.generate(context=context, input=user_input)

    # 5. 分别写入会话作用域(临时)与用户作用域(持久)
    messages = [{"role": "user", "content": user_input},
                {"role": "assistant", "content": response}]
    mem0.add(messages, user_id=user_id, run_id=session_id)

    return response

注意第 2 步用了 AND 过滤——这里之所以安全,是因为两个条件(user + run)指向的是同一批run_id 的记忆记录,而非跨记录组合;这与上文"跨实体查询"陷阱并不矛盾。

性能特征

延迟(文档给出的典型参考值)

操作 典型延迟
混合检索(v3 默认) ~100–150ms
+ rerank 额外 +150–200ms
Add(异步) 响应 < 50ms

add() 之所以能压到 50ms 以内响应,正因为 v3 把 LLM 抽取挪到了异步后台,API 只做受理并返回 event_id

处理模型

  • 异步(默认): 立即返回,后台处理;
  • 批量操作: batch_update / batch_delete 每批最多 1000 条记忆;
  • Webhooks: 异步处理完成时实时通知。

面向性能的作用域策略

  • 面向用户的所有查询都带上 user_id(最常见、最快);
  • 追加 run_id 做会话隔离(收窄检索空间);
  • 避免在大数据集上使用 "*" 通配过滤(会扫描所有非空记录);
  • 只需要少量记忆时,用 top_k 限制返回数量。

与其他方案的取舍对比

方案 优点 缺点
裸向量数据库 快、完全可控 无抽取、无去重、无冲突消解
内存中的聊天历史 零延迟 重启即丢失、无跨会话能力、无界增长
基于文档的 RAG 适合静态知识 无个性化、记忆不可更新
Mem0 Platform 托管式抽取 + 去重 + 实体链接 + 作用域 外部依赖、异步处理延迟

Mem0 Platform 的差异化在于把向量检索(语义召回)、LLM 抽取、冲突消解(去重)与结构化多租户作用域组合进同一个托管 API,以换取应用侧零基础设施成本。

小结与延伸阅读

回顾本文的核心结论:

  1. 写入侧是"单遍抽取 + 哈希去重 + 双存储落库"的 ADD-only 管线,add() 默认异步、以 event_id 受理(add() 实现);
  2. 检索侧是"语义 + BM25 + 实体"三信号并行打分再融合,默认 top_k=20 / threshold=0.1 / rerank=Falsesearch() 实现);
  3. 作用域侧user_id / agent_id / app_id / run_id 四维独立存储,隐式空值作用域防止跨域泄漏,SDK 用 ENTITY_PARAMS 强制 filters 传参(main.py L40);
  4. 分层记忆(对话/会话/用户)通过 run_iduser_id 的组合落地,配合 delete_all 完成会话级清理。

想继续深入,可以沿以下仓库内资料阅读:

登录后查看全文
热门项目推荐
相关项目推荐