主题
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/6334 | docker ps:docker-qdrant-1 Up |
| 记忆引擎 | mem0ai 2.2.1(Apache-2.0) | venv D:\build\mem-service\.venv | import mem0; __version__ = 2.2.1 |
| embedding | 复用 11 篇本地 TEI(bge-m3,1024 维) | :9005 | GET /v1/models → BAAI/bge-m3 |
| LLM(提取/画像) | tokenplan qwen3.8-max(OpenAI 兼容) | compatible-mode/v1 | 08 专栏 .env BUILDER_MODEL_API_KEY |
| 元数据 | MySQL(复用 docker-mysql-1) | :3306,库 mem_service | 5 张表 |
| 服务 | FastAPI + uvicorn | :9006 | 9 端点 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_task | evt_fdb3ccfeb8a547b6(emp_030,3 节点)+ evt_e935e94614e84e47(emp_012,3 节点)均 SUCCEEDED;重复对话提交记 SUCCEEDED 但 result_ref={"results":[]}(幂等去重);状态机 PENDING→SUCCEEDED/FAILED + finished_at |
| 审计台账 | memory_audit | emp_012:ADD×3(new_content 全文)→ UPDATE(old/new 快照)→ DELETE(old 原文、new=NULL);emp_030 同链路,全部留痕 |
| 画像台账 | user_profile | emp_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 API | mem0 add/search/get_all/update/delete | ✅ 实证(2.3 节) |
| M-4 | user_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 | 异步提取 + GetEvent | memory_task 表 + 线程池 | ✅ 实证(P0 级线程版;NATS 生产化见九章) |
| M-11 | 观测/审计(前后快照) | memory_audit 表 | ⚠️ 实证留痕;缺租户/trace/不可篡改/PII 策略 |
| M-12 | 白盒规则可视化 | 门户皮管理面 | ⏳ P2 |
| M-13 | Pro/Lite(Rerank) | mem0 rerank 参数位 + 可选 reranker | ⏳ 未开始 |
判定更新:实证完成了服务化、事实记忆链路、画像与基础审计;但 M-4 只是存储过滤、M-11 只是明文留痕。身份绑定、跨租户隔离、删除传播、投毒防护、HA 均未完成,不能据此宣布 P0 生产就绪。
六、关键坑位与工程教训(必读)
| # | 坑位 | 现象 | 根因 | 对策 |
|---|---|---|---|---|
| K-1 | pymysql 2.x with 上下文不提交 | 服务端 INSERT 返回 200 但表里无行、GetEvent 404 | pymysql 2.2.8 Connection.__exit__ 只 close(),不 commit 也不 rollback(新版行为变更) | 连接统一 autocommit=True;写操作显式 commit;文档中留注释 |
| K-2 | mem0 2.2.1 API 形态变化 | m.search("...", user_id=...) 抛 Top-level entity parameters not supported | search/get_all 顶层不支持 user_id,须 filters={"user_id":...} | 统一走 filters(恰与百炼隔离语义一致) |
| K-3 | mem0 返回结构嵌套 | m.search()/m.get_all() 返回 {"results":[...]} 非裸列表 | 2.2.1 结果信封 | API 层统一取 ["results"] |
| K-4 | mem0 history 幂等去重 | 相同对话二次 add 返回 0 条 | history.db 按内容去重(v1.1 版本行为) | 实证用新 user_id;生产靠 run_id/业务幂等键 |
| K-5 | min_score 拦不住语义近邻 | 无关主题召回 0.41~0.51 | bge-m3 语义空间近邻 | 生产阈值 0.5~0.7 或接 reranker(百炼 FAQ 同口径) |
| K-6 | mem0 遥测 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_id | Higress + 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 |
| 观测审计 UI | memory_audit 表可查 | 门户皮管理面(会话↔记忆变更对照,M-12) | P2 |
| 水平扩展 | 单服务 | 无状态 API 多副本 + Qdrant 集群 + worker 消费组 | P2 |
| HA/灾备 | 单机 | Qdrant/MySQL/NATS 高可用、备份恢复演练、RPO/RTO | P2 |
十、实证台账
| 能力 | 状态 | 证据 |
|---|---|---|
| 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。不得表述为“生产方案”或“全链生产可用”。