跳到正文

13|记忆底座实证:mem0 部署与 API 验证(#21 续) ​

一句话:按 12 篇方案,完成 Qdrant 起栈 + mem0 服务化(FastAPI)→ 事实记忆 实证链路(异步提取/轮询/语义召回/更新/删除/单字段过滤)→ 用户画像(模板/字段级提取/回填/查询)→ 实证审计与任务台账 的实机验证,沉淀工程坑位与生产化差距。 状态:⚠️ 本地实证完成(2026-10-26);身份绑定、跨租户隔离、删除传播、投毒防护、HA 均未验证,不是生产方案。 证据:D:\build\mem-service(server.py / mem_chain_test.py / profile_test.py / verify_api.py)、docker-qdrant-1、mem_service 库 生产架构:Agent → Higress → AuthZ/Credential Broker → Memory Service → mem0/Qdrant,隔离与删除契约以 12 篇与 05-AI基础设施架构审核与生产收敛.md 为准。


一、部署实证 ​

1.1 组件栈 ​

组件选型端口/标识证据
向量库qdrant/qdrant:latest(镜像走 5 mirror 生效)6333/6334docker ps:docker-qdrant-1 Up
记忆引擎mem0ai 2.2.1(Apache-2.0)venv D:\build\mem-service\.venvimport mem0; __version__ = 2.2.1
embedding复用 11 篇本地 TEI(bge-m3,1024 维):9005GET /v1/models → BAAI/bge-m3
LLM(提取/画像)tokenplan qwen3.8-max(OpenAI 兼容)compatible-mode/v108 专栏 .env BUILDER_MODEL_API_KEY
元数据MySQL(复用 docker-mysql-1):3306,库 mem_service5 张表
服务FastAPI + uvicorn:90069 端点 openapi.json

1.2 关键接线(mem0 config) ​

python
mem_config = {
    "vector_store": {"provider": "qdrant", "config": {
        "host": "localhost", "port": 6333,
        "collection_name": "mem0_emp_kb", "embedding_model_dims": 1024}},
    "embedder": {"provider": "openai", "config": {
        "model": "BAAI/bge-m3", "api_key": "local",
        "openai_base_url": "http://127.0.0.1:9005/v1",   # ← 本地 TEI,无 SSRF 问题
        "embedding_dims": 1024}},
    "llm": {"provider": "openai", "config": {
        "model": "qwen3.8-max", "api_key": "<TOKENPLAN_KEY>",
        "openai_base_url": "https://token-plan.maas.qianwenaiapi.com/compatible-mode/v1"}},
    "history_db_path": "D:/build/mem-service/mem0_history.db",
}
memory = Memory.from_config(mem_config)

与 RAGFlow 的差异:mem0 的 OpenAI embedder 直接接受 openai_base_url 指向本地,不经过 RAGFlow 的 SSRF 防护。这里能直连的前提是 mem0 服务本身是受信工作负载;生产仍必须用 NetworkPolicy 限制其只访问允许的 embedding 服务,不能把“客户端可配 URL”暴露给租户。


二、事实记忆链路实证(对标百炼 Memory API) ​

2.1 异步提取(对标 add-async + GetEvent) ​

POST /api/v2/memory/add-async
{"user_id":"emp_030","messages":[{"role":"user","content":"我是赵工,数据组组长,负责模型评估,偏好用英文写技术文档,每月最后一天需要做月度报告。"},...],"meta_data":{"dept":"data"}}
→ {"event_id":"evt_fdb3ccfeb8a547b6","status":"PENDING"}

GET /api/v2/memory/events/evt_fdb3ccfeb8a547b6   (轮询 2s × N)
→ {"status":"SUCCEEDED","result":{"results":[
     {"event":"ADD","memory":"User's name is 赵工, and they serve as the leader of the data group (数据组组长), with responsibility for model evaluation."},
     {"event":"ADD","memory":"User prefers to write technical documentation in English as their preferred language for work-related documents."},
     {"event":"ADD","memory":"User is required to prepare a monthly report on the last day of every month."}]}}

机制对齐确认:写入不存对话原文,抽取改写为 observation 节点("As of October 25, 2026, User is required...")——与百炼行为一致;异步返回 event_id 后轮询终态。

第二组证据(API 面 curl 实测,2026-10-26):emp_012(陈曦/数据平台算法工程师/周四评审偏好)走 HTTP 全链:

POST /api/v2/memory/add-async   → {"event_id":"evt_e935e94614e84e47","status":"PENDING"}
GET  /api/v2/memory/events/evt_e935e94614e84e47   (2s × 17 次轮询)→ SUCCEEDED,3 节点
     "User attends an algorithm review meeting every Thursday afternoon..."
     "User prefers to communicate in Chinese."
     "User's name is 陈曦 (Chen Xi), and they work as an algorithm engineer..."

幂等语义实测:同一 user_id 重复提交相同对话,add 返回空 results(mem0 history 去重),memory_task 仍记 SUCCEEDED、result_ref={"results": []}——生产幂等键须由业务层生成(run_id 等),不能用"重复对话"代替。

2.2 语义召回(对标 SearchMemory:messages + top_k + min_score) ​

POST /api/v2/memory/memory_nodes/search
{"user_id":"emp_030","messages":[{"role":"user","content":"帮我写这个月的月报"}],"top_k":3,"min_score":0.3}
→ results:
   0.6487  User is required to prepare a monthly report on the last day of every month.
   0.4451  As of October 25, 2026, User's name is 赵工 ...
   0.3829  User prefers to write technical documentation in English ...

阈值行为实测:换无关主题("订去上海的机票")召回分数仍落在 0.41~0.51(语义检索固有行为,min_score=0.3 拦不住)——印证百炼 FAQ 建议"相似度阈值建议 0.5~0.7";生产上按场景配 0.5~0.7 或接 reranker(mem0 支持 rerank 参数,见第九章)。

2.3 管理面(对标 List/Update/DeleteMemory) ​

GET  /api/v2/memory/memory_nodes?user_id=emp_030 → 3 节点
PATCH /api/v2/memory/memory_nodes/{id} {"user_id":"emp_030","text":"用户偏好英文技术文档,每月最后一天提交月度报告(已更新)"}
DELETE /api/v2/memory/memory_nodes/{id} → 3→2 节点

审计快照修复(2026-10-26):update/delete 的审计旧值最初用占位查询(get_all(filters={"user_id":""}))被 mem0 以 Invalid user_id: cannot be empty 拒绝 → 500。已改为 _find_node_old():先全量 get_all 按 node_id 定位旧快照(user_id + memory 文本),再执行更新/删除,审计写 old_content 原文。api_probe 实测:UPDATE 后审计行 old=原文、new=新文本,DELETE 后审计行 old=原文、new=NULL,全程留痕。

2.4 存储层跨用户过滤(对标 user_id 记忆实体) ​

实证证据:emp_004 查"咖啡提醒",只召回自己的记忆(对账提醒/张会计/英文报表),emp_003 的记忆零泄漏。隔离由 Qdrant payload user_id + mem0 filters 实现:

m.search("咖啡提醒", filters={"user_id":"emp_004"}, ...)
→ 全部为 emp_004 的记忆
assert 返回节点 user_id == 查询 user_id  (过滤断言通过)

必须说清边界:该测试证明“给定正确的 filters 时,Qdrant 过滤有效”,不证明客户端无法伪造 user_id。当前 实证的 API 直接接受请求体 user_id,若前置于网关时不做签名上下文绑定,攻击者改一个字段即可读他人记忆。生产必须由 Gateway/AuthZ 派生 tenant_id/owner_subject_id,请求体 user_id 仅作断言;并补跨租户、跨用户负向测试。


三、用户画像补强实证(对标 CreateProfileSchema / GetUserProfile) ​

3.1 建模板(字段级定义,引导模型提取) ​

POST /api/v2/memory/profile_schemas
{"name":"员工基础画像","description":"员工基础属性","fields":[
  {"name":"attr_name","description":"员工姓名/称呼","init_value":"unknown"},
  {"name":"attr_position","description":"职位/岗位","init_value":"unknown"},
  {"name":"attr_lang","description":"常用语言","init_value":"zh"},
  {"name":"attr_work_style","description":"工作偏好与习惯","init_value":"unknown"}]}
→ schema_id: schema_emp_basic

该 实证 schema 没有 tenant_id/owner_subject_id/visibility,只能用于单租户复现;生产表结构以 12 篇 §5.1 为准,并补跨租户负向测试。

3.2 对话提取画像(LLM 按字段定义结构化输出) ​

输入对话:"我是王敏,产品设计组的设计总监,大家叫我Mina。平时习惯用中文沟通,周三之前会集中评审本周的设计稿,喜欢先看交互稿再看视觉稿。"

LLM 提取结果:

json
{"attr_name": "王敏(Mina)", "attr_position": "产品设计组的设计总监",
 "attr_lang": "中文", "attr_work_style": "周三之前会集中评审本周的设计稿,喜欢先看交互稿再看视觉稿"}

字段级描述 + 初始值引导模型:未提及字段落初始值,无法判断落 null → 回填时转初始值。与百炼"画像字段=字段名/描述/初始值"机制同构;但 实证的 user_profile 仅按 schema_id + user_id + attr_name 存储,缺租户/主体类型,不能直接生产复用。

3.3 查询(对标 GetUserProfile) ​

GET /api/v2/memory/profile_schemas/schema_emp_basic/user_profile?user_id=emp_010
→ {"profile":{"attr_lang":"中文","attr_name":"王敏(Mina)","attr_position":"产品设计组的设计总监","attr_work_style":"周三之前..."}}

四、审计与任务台账(对标记忆观测/审计) ​

三张台账全部落库实测:

台账表实测内容
任务台账memory_taskevt_fdb3ccfeb8a547b6(emp_030,3 节点)+ evt_e935e94614e84e47(emp_012,3 节点)均 SUCCEEDED;重复对话提交记 SUCCEEDED 但 result_ref={"results":[]}(幂等去重);状态机 PENDING→SUCCEEDED/FAILED + finished_at
审计台账memory_auditemp_012:ADD×3(new_content 全文)→ UPDATE(old/new 快照)→ DELETE(old 原文、new=NULL);emp_030 同链路,全部留痕
画像台账user_profileemp_010 4 字段值 + emp_012 画像(add-async 携带 profile_schema 自动回填)+ 幂等更新(ON DUPLICATE KEY UPDATE)

实证限制:下表是实际复现用的最小表结构,old_content/new_content 明文存储且只有 user_id。生产必须按 12 篇 §5.4 改为 tenant_id + owner_subject_id + actor/service + trace_id + 前后哈希 + 只追加/保留策略,并做 PII 脱敏;该实证表不能直接上线。

sql
-- 审计核心表(前后快照)
CREATE TABLE memory_audit (
  id BIGINT AUTO_INCREMENT PRIMARY KEY,
  user_id VARCHAR(64), memory_node_id VARCHAR(64), event VARCHAR(8),
  old_content TEXT, new_content TEXT, source VARCHAR(64),
  created_at DATETIME DEFAULT CURRENT_TIMESTAMP);

五、机制对标(实证后判定更新) ​

#百炼机制落点判定(实证后)
M-1事实记忆自动提取(改写为 observation)mem0 默认提取器✅ 实证(2.1 节 3 节点)
M-2语义检索 min_score 阈值mem0 search threshold✅ 实证(2.2 节 0.6487/0.4451/0.3829)
M-3记忆 CRUD APImem0 add/search/get_all/update/delete✅ 实证(2.3 节)
M-4user_id 记忆实体隔离filters + Qdrant payload⚠️ 存储过滤有效;未证明身份不可伪造
M-5多项目并行(project_id)payload project_id + filters(未实证,P1)⏳ 待补
M-6过期时间(7/30/180 天)mem0 expiration_date 参数位就绪(未实证)⏳ 待补(回收器)
M-7用户画像(模板+字段提取)自建 profile_schema 三表 + LLM 提取器✅ 实证(3 节)
M-8技能记忆(轨迹归纳)自建(LangMem procedural)❌ 未开始(P1)
M-9记忆变量(键值对权重高)Qdrant payload / 独立表⏳ 未开始
M-10异步提取 + GetEventmemory_task 表 + 线程池✅ 实证(P0 级线程版;NATS 生产化见九章)
M-11观测/审计(前后快照)memory_audit 表⚠️ 实证留痕;缺租户/trace/不可篡改/PII 策略
M-12白盒规则可视化门户皮管理面⏳ P2
M-13Pro/Lite(Rerank)mem0 rerank 参数位 + 可选 reranker⏳ 未开始

判定更新:实证完成了服务化、事实记忆链路、画像与基础审计;但 M-4 只是存储过滤、M-11 只是明文留痕。身份绑定、跨租户隔离、删除传播、投毒防护、HA 均未完成,不能据此宣布 P0 生产就绪。


六、关键坑位与工程教训(必读) ​

#坑位现象根因对策
K-1pymysql 2.x with 上下文不提交服务端 INSERT 返回 200 但表里无行、GetEvent 404pymysql 2.2.8 Connection.__exit__ 只 close(),不 commit 也不 rollback(新版行为变更)连接统一 autocommit=True;写操作显式 commit;文档中留注释
K-2mem0 2.2.1 API 形态变化m.search("...", user_id=...) 抛 Top-level entity parameters not supportedsearch/get_all 顶层不支持 user_id,须 filters={"user_id":...}统一走 filters(恰与百炼隔离语义一致)
K-3mem0 返回结构嵌套m.search()/m.get_all() 返回 {"results":[...]} 非裸列表2.2.1 结果信封API 层统一取 ["results"]
K-4mem0 history 幂等去重相同对话二次 add 返回 0 条history.db 按内容去重(v1.1 版本行为)实证用新 user_id;生产靠 run_id/业务幂等键
K-5min_score 拦不住语义近邻无关主题召回 0.41~0.51bge-m3 语义空间近邻生产阈值 0.5~0.7 或接 reranker(百炼 FAQ 同口径)
K-6mem0 遥测 PostHog 告警启动打印 PostHog 多客户端警告遥测库无害;可 MEM0_TELEMETRY=false

七、API 面清单(生产对齐百炼 14 接口) ​

text
POST   /api/v2/memory/add                    # 同步添加
POST   /api/v2/memory/add-async              # 异步提交(推荐)✅ 实证
GET    /api/v2/memory/events/{event_id}      # 事件轮询          ✅ 实证
GET    /api/v2/memory/memory_nodes           # ListMemory       ✅ 实证
POST   /api/v2/memory/memory_nodes/search    # SearchMemory     ✅ 实证
PATCH  /api/v2/memory/memory_nodes/{id}      # UpdateMemory     ✅ 实证
DELETE /api/v2/memory/memory_nodes/{id}      # DeleteMemory     ✅ 实证
GET    /api/v2/memory/skill/export/{id}      # GetSkillExport   ⏳ P1
POST   /api/v2/memory/profile_schemas        # CreateProfileSchema ✅ 实证
GET    /api/v2/memory/profile_schemas        # ListProfileSchemas  ✅ 实证
GET    /api/v2/memory/profile_schemas/{id}   # GetProfileSchema    ✅ 实证
PATCH  /api/v2/memory/profile_schemas/{id}   # UpdateProfileSchema ⏳(PATCH 逻辑同 update)
GET    /api/v2/memory/profile_schemas/{id}/user_profile  # GetUserProfile ✅ 实证

八、数据飞轮最小环 ​

最小环验证:同一员工新会话可语义召回历史偏好(2.2 节);画像字段随对话更新(3.2 节);审计可追溯(4 节)。这不等于生产闭环。


九、生产化差距(这不是生产方案) ​

项实证现状生产形态优先级
身份与凭证直接接受请求体 user_idHigress + AuthZ/Credential Broker;从签名上下文派生 tenant_id/owner_subject_id;请求体只作断言P0
异步队列进程内线程池(重启丢任务,实证遗留 1 条 PENDING)NATS JetStream + 消费组 + outbox + DLQ;幂等键去重P0
DB 写路径autocommit=True生产 MySQL 主从 + 事务边界显式化 + 备份P0
隔离user_id 单维度,且可被请求体指定tenant_id > owner_subject_id > project_id > memory_type > visibility 强制过滤;跨租户/跨用户负向测试P0
删除仅删节点覆盖 Qdrant/MySQL/缓存/派生摘要/导出副本;可验证删除状态;legal hold 分离P0
审计明文 old/new + user_id租户/trace/actor/service;只追加;前后哈希;PII 脱敏;保留策略P0
投毒防护无来源可信度、冲突检测、Prompt Injection 检测、技能审核P0
阈值min_score 0.3(近邻噪声)按场景 0.5~0.7 + 可接 reranker;黄金集评估P1
过期无expiration_date + 定时回收器(M-6)P1
技能记忆未开始tool_calls 轨迹归纳 + 默认 private + 发布审核(M-8)P1
观测审计 UImemory_audit 表可查门户皮管理面(会话↔记忆变更对照,M-12)P2
水平扩展单服务无状态 API 多副本 + Qdrant 集群 + worker 消费组P2
HA/灾备单机Qdrant/MySQL/NATS 高可用、备份恢复演练、RPO/RTOP2

十、实证台账 ​

能力状态证据
Qdrant 镜像拉取(mirror 生效)✅ 实机docker pull qdrant/qdrant
Qdrant 起栈(6333/6334)✅ 实机docker ps docker-qdrant-1 Up
mem0 2.2.1 + 本地 TEI embedding + tokenplan LLM 接线✅ 实机mem_chain_test.py [1/7]
add-async 异步提取(对话→3 节点)✅ 实机evt_fdb3ccfeb8a547b6(emp_030)+ evt_e935e94614e84e47(emp_012,HTTP 全链)SUCCEEDED
GetEvent 轮询终态✅ 实机2s×17/18 次轮询
SearchMemory 语义召回(score 排序)✅ 实机0.6487 / 0.4451 / 0.3829;emp_012 0.6179 / 0.4450 / 0.4173
无关主题召回噪声(阈值实证)✅ 实机0.41~0.51(印证百炼 FAQ 0.5~0.7 建议)
存储层跨用户过滤(emp_004 查不到 emp_003)⚠️ 实证给定 filters 时零泄漏;未验证客户端伪造 user_id
UpdateMemory / DeleteMemory(含审计快照)✅ 实机3→2 节点;_find_node_old 修复后 old/new 快照完整
CreateProfileSchema / 字段级提取 / GetUserProfile✅ 实机schema_emp_basic + emp_010 4 字段;emp_012 经 add-async 自动回填
审计台账(ADD/UPDATE/DELETE)⚠️ 实证memory_audit 10+ 行(emp_030/emp_012);明文、无租户/trace/不可篡改
任务台账(PENDING→SUCCEEDED)✅ 实机memory_task 5 行(含幂等空 results 行)
记忆服务 9006 启动与存活✅ 实机Start-Process 后台启动;/docs HTTP 200;端口监听 0.0.0.0:9006
API 面 HTTP 全链(add-async→events→search→list→update→delete→schemas→user_profile)✅ 实机api_probe.py exit 0,全 8 步通过
pymysql 2.2.8 with 不 commit 坑位✅ 定位inspect __exit__ 源码
mem0 2.2.1 API 形态(filters/results)✅ 定位源码 + 实测
技能记忆(M-8)/ 过期回收(M-6)/ Rerank(M-13)⏳ P1方案 12 篇第五章
身份绑定与跨租户负向测试❌ 未执行需 Gateway/AuthZ 派生身份,伪造字段应 403
删除传播(缓存/派生摘要/导出)❌ 未执行当前仅删节点
投毒 / Prompt Injection 防护❌ 未执行需来源可信度、冲突检测、技能审核
HA / 备份恢复 / 容量❌ 未执行当前单机 Qdrant + 单服务

边界:本实证只覆盖实证链路(服务化/事实记忆/画像/基础审计)。身份绑定、租户隔离、删除传播、投毒防护、异步可靠性、HA 与容量均未完成;技能记忆、过期回收、NATS 生产化、门户皮管理面为 P1/P2。不得表述为“生产方案”或“全链生产可用”。

把 AI 工程讲到既懂也会用