主题
第 10 章 多用户与多 Agent 的作用域治理
一家 SaaS 公司把记忆服务接进面向企业的客服平台,客户是三家银行。 上线两周后,其中一家银行的合规部门在做数据盘点时问:你们的记忆存储里,能不能证明 A 银行的数据不会出现在 B 银行的召回结果里。
工程团队的答案是一段代码:查询语句里带了 WHERE user_id = ?。 合规部门追问的是另一个问题:如果某天有人漏写这个条件,你们怎么知道。 这个问题没有代码层面的答案,因为它的答案取决于隔离是结构性的还是纪律性的。
作用域治理讨论的就是这件事。 它不是一个鉴权问题,也不是一个查询写法问题,而是「边界由什么保证」的问题。 由 WHERE 保证的边界,其强度等于「每个写查询的人都不犯错」这个假设的强度。 由文件系统保证的边界,其强度等于「路径拼错了会找不到文件」这个事实的强度。
这一层之所以归到运行篇而不是治理篇,是因为它的正确性依赖部署形态。 物理隔离只有在「能直接操作文件系统」的部署里才成立——SaaS 多租户共享存储时,你无法给每个租户一个独立文件。 第 8 章的形态选择,在这里第一次产生安全后果。
本章按「作用域语义 → 隔离方式 → 派生规则陷阱 → 多 Agent 共享 → 团队与席位 → 验收」展开。 多 Agent 共享记忆这一节会明确写出「这是未解问题」,而不是给一个看起来完整的方案。
10.1 四维作用域的语义
作用域有四个维度:user_id、agent_id、run_id、app_id。 它们不是四个可选的过滤字段,而是四个正交的身份维度。
| 维度 | 语义 | 典型用途 | 生命周期 |
|---|---|---|---|
user_id | 记忆归属的自然人 | 个人偏好、历史决策、健康记录 | 长期,跨会话 |
agent_id | 产生记忆的智能体身份 | 工作助手与个人助理各自的观察 | 与 Agent 部署同寿 |
run_id | 一次运行/一次任务 | 任务内临时上下文、可丢弃的中间状态 | 短,任务结束即失效 |
app_id | 接入的应用实例 | 同一 Agent 在多产品线的不同上下文 | 与应用版本相关 |
表 10-1 四维作用域的语义与生命周期
四个维度组合起来派生一个 namespace。 这不是「四个字段一起做 WHERE」,而是「四个字段决定数据落在哪个目录」。 源码里 _namespace_for 负责派生,_build_brain 为每个作用域组合构建独立的 MemoryBrainContext。
理解这张表的关键是生命周期那一列。 把短生命周期的维度用在长生命周期的数据上,会产生大量几乎为空的孤立作用域。 把长生命周期的数据挂在 run_id 下,任务一结束数据就再也召不回来。 最常见的错误是把 run_id 当成「会话 id」用,结果每次会话都开一个新的记忆库。
10.2 物理隔离与逻辑隔离
两种隔离方式的差别,在正常路径上看不出来,只在出错时显现。
逻辑隔离是共享表加过滤条件。 所有租户的数据在同一批表里,每条记录带 tenant_id,查询时带上条件。 优点是实现简单、跨租户聚合容易、备份只需一次。 风险是单点:一个遗漏的条件就是一次跨租户泄露,而遗漏在代码审查里极难被发现——多一个 WHERE 和不写 WHERE 在 diff 里只差一行。
物理隔离是每个作用域一个独立 SQLite 文件。 越权读取不是「查到了别人的数据」,而是「打开了一个不存在的文件」。 这个差别在事故定性上很大:前者是数据泄露,后者是 404。
Mnemosyne 选的是物理隔离。 代价在第 8 章已经列过,这里补上治理视角的那一条:跨作用域聚合查询要多次读取。 一个「统计全平台最常被召回的记忆」的需求,在物理隔离下是遍历目录加 N 次查询,且结果不是事务性快照(第 9 章已述)。 对运营报表可以接受,对实时计费不行。
判据是数据主权要求的强度。 如果合规要求能证明「A 的数据物理上不在 B 的文件里」,物理隔离是唯一能给出这个证明的方式。 如果只是「逻辑上不该串」,逻辑隔离加严格的查询层封装也能成立——但它需要额外的机制来保证「没有遗漏」,而这个机制通常就是测试覆盖率加代码审查,强度有限。
10.3 namespace 派生规则与「同一个库」的陷阱
物理隔离的前提是「派生规则真的派生出了不同的 namespace」。 如果配置被写死,这个前提就不成立。
最常见的形态是把 namespace 配成一个固定字符串:
python
# 错误:所有作用域都落到同一个库
brain = MemoryBrain("./mem", namespace="default")
brain.retain("A 银行的客户偏好", user_id="bank_a")
brain.retain("B 银行的客户偏好", user_id="bank_b")如果 namespace 参数覆盖了派生逻辑,两条记忆会落在同一个 SQLite 文件里。 此时 user_id 只是一个普通字段,隔离退化成逻辑隔离,而且是没有 WHERE 保证的那种。 更糟的是它不会报错:写入成功、召回成功,只是召回时可能把两家的数据混在一起。
检测方法很直接:看 retain_batch 的返回值。 它返回的 namespace 字段应当与你预期的派生结果一致。 如果两个不同 user_id 的写入返回了同一个 namespace,隔离没有生效。 这个检查应该做成启动自检,而不是靠人去比对。
真实事故的形态通常是「上线时是好的,某次重构之后坏了」。 一个把 namespace 参数抽成配置项的重构,把「按作用域派生」改成了「读配置」。 测试全绿,因为测试用的就是那个固定值。 问题在三个月后由客户的合规审计发现,而这段时间里所有跨租户的召回都是污染的。
10.4 多 Agent 共享记忆:一个未解问题
同一用户的三个 Agent——工作助手、个人助理、健康顾问——在什么条件下应该看到彼此的记忆。 这个问题在 2026 年没有公认答案。
具体要回答的是四个问题,每一个都缺标准做法:
- 谁有权写:健康顾问写的「用户有睡眠障碍」,工作助手能不能读到
- 谁有权删:用户对个人助理说「忘掉这件事」,是否等同于对所有 Agent 生效
- 冲突如何仲裁:两个 Agent 对同一事实给出不同观察,以谁为准
- 越权如何发现:一个 Agent 读了不该读的作用域,日志在哪里
Mnemosyne 目前的答案是「不解决,但支持不共享」。 物理隔离让「每个 Agent 一个独立作用域」成为默认且可靠的形态。 跨作用域共享需要上层自建权限模型:谁在什么条件下可以读哪个 namespace,由应用自己判断。 引擎提供的是隔离原语,不是治理策略。
这一点必须说清楚,因为「支持多 Agent」和「解决了多 Agent 的共享治理」是两件不同的事。 前者是能力,后者是策略,而策略依赖组织、法规与产品语义,不是存储层能决定的。
横向看,「共享记忆」这个词在 2026 年已经换了含义。 截至 2026 年 10 月,Letta 的 shared memory 指「组织拥有的共享 Git 仓库」,且仅云托管 agent 可用;本地 agent 各用各自的 MemFS。 也就是说,它共享的是记忆的载体(一个仓库),而不是记忆的治理规则。 这个观察对选型的意义是:如果需求是「多个 Agent 看到同一份事实」,共享存储就够了;如果需求是「多个 Agent 对同一事实的读写权限不同」,共享存储反而会让问题更难——因为它把权限判断从「打不打得开文件」变成了「查不查这一行」。
代价是显式的:不解决共享治理,意味着多 Agent 场景下用户会重复被问同样的问题。 工作助手知道的偏好,个人助理不知道,用户要再说一遍。 这是当前阶段的真实体验,不应该被描述成「设计选择」而模糊掉。
10.5 团队与席位
Team 层解决的是「多用户命名空间」与「团队策略」,不是「多 Agent 权限」。 两者容易混淆:前者是「同一个组织下多个人的记忆如何组织」,后者是「同一个人的多个 Agent 如何共享」。
Team 层的能力包括多用户命名空间、团队策略、审计报告导出,定价为 $19.9/席/月 或 $199/席/年(人民币 ¥29.9/席/月 或 ¥299/席/年)。 截至 2026 年 10 月,新账号含 30 天 Team 试用,无需绑卡。
门控默认关闭这件事在治理语境下比在定价语境下更重要。 MNEMOSYNE_TIER_GATE 不设即为关闭,自部署与 PyPI 用户不受任何限制。 门控只在出口做结果投影过滤,不改检索参数、不改 SQL、不改存储。
这三句话的含义是:付费层级不是安全边界。 它收窄的是读取窗口(Free 层冷层 30 天),不是访问权限。 把「升级到 Team」当成「获得更严格的隔离」是误读——隔离强度由部署形态与 namespace 派生决定,与付费层级无关。
代价是这套设计把「谁能看到什么」的责任完全交给了部署方。 SaaS 形态下平台负责运行,但不负责你的租户划分是否正确。 自托管形态下连运行都是你自己的。 两种情况下,隔离验收都不能省。
10.6 隔离的验收方法
隔离必须能被测试,否则它只是配置意图。 三项测试覆盖三个不同的失效面。
- 跨租户读取:用 A 的凭据写入一条唯一标记的记忆,再用 B 的凭据召回同一关键词。结果必须为空。若召回命中,说明 namespace 派生失效或查询层有越权路径。
- 删除爆炸半径:对 A 执行
delete_all_memories或memory_cascade_delete,然后检查 B 的记忆条数。数字必须不变。若 B 少了,说明删除逻辑跨了作用域。 - 备份恢复后隔离保持:备份整个目录,恢复到一个新路径,重跑前两项测试。若恢复后出现跨租户可见,说明备份把多个作用域打平了。
第三项最容易被跳过,也最容易出问题。 物理隔离的备份是复制目录,看起来万无一失。 但如果备份脚本用了「导出所有 namespace 到一个文件」的实现,恢复时就会把所有作用域写回同一个库。 这类问题在恢复演练之前完全不可见。
补充一项成本很低的检查:在 CI 里对 retain_batch 的返回值断言 namespace。 它是唯一能在写入路径上直接暴露派生失效的信号,且不需要构造跨租户查询。
10.7 本章小结
本章确立三条判断。
第一,四维作用域是四个正交身份维度,不是四个过滤字段。 选错维度的后果不是查不到,而是数据落在错误生命周期的作用域里,任务结束即失联。 这条判断成立的条件是「作用域在写入时就确定」;如果业务需要事后重新划分归属,物理隔离会让迁移变成文件搬移,成本高于逻辑隔离。
第二,物理隔离的强度来自「越权读取打不开文件」,逻辑隔离的强度来自「每个查询都不漏条件」。 前者代价是跨作用域聚合退化为多次读取且非事务性,后者代价是把正确性押在纪律上。 这条判断在「必须提供物理不在场证明」的合规场景下没有替代品;在纯运营分析场景下,物理隔离的代价可能大于收益。
第三,多 Agent 共享记忆是未解问题,当前的可靠能力是「支持不共享」。 跨作用域共享需要上层自建权限模型,引擎只提供隔离原语。 把它描述成「已支持多 Agent 协作」会让使用者低估自己需要承担的策略工作。
一个本章没有回答的问题是:物理隔离在规模化之后会变成什么。 十万个租户就是十万个文件,跨租户统计变成目录遍历,备份变成文件系统快照管理。 这些问题不再是正确性问题,而是成本与延迟问题。 第 11 章从这里继续。