主题
第 8 章 部署形态:本地优先的真实边界
一个三人团队把记忆服务接进客服工单系统。 第一版跑在开发者笔记本上,pip install 之后就能用,全程没有外部账号。 三周后要上生产,运维问的第一个问题不是性能。 「要不要连数据库、要不要配 key、要不要开容器」——三个答案都是否。 于是它被塞进了现有的 Python 应用进程。
半年后应用迁到 K8s,问题才出现。 ./mem 这个相对路径跟着工作目录漂移。 滚动更新后新 Pod 看不到旧 Pod 的记忆。 团队一直以为「本地优先」等于「数据在服务器上」。
这类失效不是配置疏漏,而是对「本地优先」的误读。 本地优先(local-first)通常指三件事同时成立:
- 数据落在自己控制的磁盘上
- 核心功能不依赖外部网络
- 安装过程不引入运维组件
最容易做到的是第三件,最难说清的是第一件。 「自己控制的磁盘」在笔记本上是持久路径。 在容器编排里,它是可能随 Pod 消失的临时层。
本章讨论的不是哪种形态更好。 而是每种形态把哪些复杂度转移给了谁。 部署形态向下决定三件事,这是它排在运行篇最前面的原因:
- 能不能直接复制文件做备份——第 10 章的隔离验收依赖它
- 写后读能不能靠单进程内存换一致性——第 9 章的
flush依赖它 - 跨作用域聚合要不要多次读取——第 11 章的成本结构依赖它
这一章不是运维细节,而是后三章的物理前提。
8.1 零依赖的工程边界
8.1.1 install_requires 为空意味着什么
mnemosyne-os 8.1.0 的 install_requires 是一个空列表。 这个事实本身没有价值,有价值的是它为什么能成立。
一个记忆系统至少要碰四类外部能力。 调模型抽取、调嵌入模型向量化、把向量存进索引、把文本存进数据库。 行业惯例是每一类引一个 SDK。 于是装一个记忆库等于装四个 SDK 及其传递依赖。
Mnemosyne 把这四类收敛到「可选适配器 + 内置等价实现」两层。 适配器分三档传输,这个分档是理解零依赖的关键:
| 传输档 | 实现方式 | 需要的第三方包 | 典型 provider |
|---|---|---|---|
| stdlib HTTP | 标准库 urllib 直接说 HTTP | 无 | openai anthropic gemini ollama deepseek qdrant |
| sdk | 延迟导入厂商 SDK,缺包时报出缺哪个 | 对应 SDK | langchain aws_bedrock chroma pgvector |
| builtin | 引擎内置实现 | 无 | rules 提取器、128 维嵌入器、hashing 嵌入器、SQLite 向量与图 |
表 8-1 组件适配器的三档传输
72 个适配器里绝大多数落在前两档之一:stdlib HTTP 与 builtin。 只有 langchain、aws_bedrock 这类没有稳定 HTTP 契约的,才落在 sdk 档。 能用 HTTP 说清楚的接口,没有理由引一个把依赖树放大十倍的包。
代价是适配器要自己处理重试、超时、认证头差异。 providers/transport.py 因此承担 stdlib HTTP 与重试、凭据脱敏的职责。 复杂度没有消失,只是从依赖管理转移到了引擎内部。 它同时也成为引擎自己必须测试覆盖的部分。
8.1.2 延迟导入与「明确报缺哪个」
需要 SDK 的适配器不参与安装期依赖。 它在真正被构建时才导入。 缺包时它不抛笼统的 ImportError,而是明确说出缺的是哪个包。 这条设计决定升级路径的顺滑程度。 从「零配置可用」走到「接上商用模型」,每一步只多装一个包。 而不是一次性接受一个依赖闭包。
判断这条边界是否成立有个简单方法。 在无网络机器上装好包,然后执行 Memory() 与 add()。 能跑通说明内置路径完整。 跑不通说明某处把可选依赖写成了硬依赖。
8.2 零依赖的四个后果与代价
零依赖是架构决策的结果,不是营销点。 它的价值只体现在四个具体后果上。 每个后果都对应一项明确代价。
第一,离线可用。 内置 rules 确定性提取器让 add() 在没配模型时也能工作。 内置 128 维嵌入器让 search() 在没配嵌入模型时也能返回结果。 断网环境、没有 key 的开发机、被禁止外联的内网,都能把链路跑起来。
代价在召回质量上。 128 维确定性嵌入器不理解语义,它靠字符与词法层面的相似性。 对「我更喜欢深色模式」和「夜间界面看着舒服」这类同义改写,它几乎无能为力。 商用嵌入模型(例如 text-embedding-3-small)在这类改写上明显更好。 内置嵌入器保证的是「能开始用」,不是「用得够好」。 这是起点,不是终点。
第二,供应链安全。 没有第三方包,就没有第三方包的漏洞要跟踪。 也没有传递依赖的版本冲突要调和。 对一个长期驻留用户磁盘的组件,这条价值随时间增长。 代价是引擎自己承担底层细节的正确性。 HTTP 重试语义、SQLite 并发模式选择、字符编码边界,都得自己测。 这些原本由成熟库兜住的地方,现在需要引擎自己的测试覆盖。
第三,部署简单。 一条 pip install 之后,不需要 Docker、不需要独立数据库进程、不需要 key、不需要下载模型权重。 这对「先接进现有应用试一周」影响最大。 它把试用成本压到接近零。 代价是运维能力上限被压低。 单进程单文件要水平扩展,不是改个配置,而是换后端或加网关。
第四,升级可控。 没有依赖冲突意味着升级不会因另一个包的版本约束失败。 代价是引擎无法借力生态演进。 某个 provider 的 API 不兼容变更时,适配器要跟着改。 而这个改动不会像 SDK 那样由厂商维护。
四项代价里第一项最容易被低估。 降级不能静默,这是这条边界的诚实性所在:
python
from mnemosyne import Memory
m = Memory.from_config({"llm": {"provider": "openai", "config": {"model": "gpt-4o-mini"}}})
print(m.describe()["degraded"])没有配置 key 的机器上,输出是一个四字段字典。 它分别回答「要的是什么」「实际用的是什么」「为什么」「怎么办」。 降级必须可被程序读取,而不只是写在日志里。 否则一个跑在容器里的服务会在半年后才发现自己一直在用规则提取器。
8.3 SQLite 作为记忆引擎的合理性
8.3.1 四个被用到的特性
选择 SQLite 不是因为「轻量」。 而是因为记忆系统的读写形状恰好落在它的强项区间。 四个特性各有明确用途:
- ACID 事务:一次写入涉及正文、结构化事实、图三元组、审计账本四张表,必须原子提交。审计账本是 SHA-256 链,半条链写入等于链断裂。
- WAL 并发读:写不阻塞读。负载是写频中等、读频高,WAL 让正在写入的会话不影响其他会话的召回。
- FTS5 全文索引:中文 Bigram 分词后直接落到 FTS5,BM25 关键词一路信号因此不需要额外检索引擎。
- JSON1 扩展:结构化事实、内容原子、过滤器谓词都以 JSON 存进单列,避免为每种分型开一张表。
叠起来的直接结果是:一个 SQLite 文件同时承担五种角色。 正文库、向量索引、图存储、全文索引、审计账本。 builtin 向量库的说明就是「一个 SQLite 文件同时持有记忆与向量」。
8.3.2 写频中等、读频高、单机
记忆系统的负载形状和典型 Web 应用不同。 它不是高并发写入,也不是低延迟强一致读。 而是「会话结束时写一批、回合开始时读几次」。 按这个形状估算,单机上万个作用域、每个作用域数千条记忆,SQLite 够用。
物理隔离让这个估算更容易成立。 每个作用域组合派生独立 namespace,对应独立目录与独立 SQLite 文件。 而不是共享一张表加 WHERE 过滤。 单文件的好处在这里被放大。 备份是复制文件,迁移是拷贝目录,删除租户是删目录。 这三件事在共享表方案里都需要专门逻辑。
代价有三个,且都不小:
- 跨作用域聚合读不是一次查询,而是 N 次打开文件。要统计「全平台最常被召回的记忆」,在物理隔离下变成目录遍历。
- 文件数量随作用域组合增长。十万个租户就是十万个文件,外加等量 WAL 与 SHM 附属文件,文件系统本身成为瓶颈。
- 单文件没有跨机复制能力。多副本部署必须靠外部机制,而 SQLite 在网络文件系统上的语义并不可靠。
8.4 四种部署形态
同一个引擎有四种使用形态。 差别不在功能,而在「谁承担进程生命周期」。 下表按接入成本从低到高排列。
| 形态 | 接入方式 | 适用场景 | 主要代价 |
|---|---|---|---|
| Python 库 | from mnemosyne import Memory | 已有 Python 应用,记忆是内部能力 | 与应用共享进程,崩溃即同时失效 |
| CLI | mnemosyne add/search/list | 脚本、批处理、人工排查 | 每次调用冷启动,无连接复用 |
| 单进程 REST + 控制台 | mnemosyne-web --port 9090 | 多语言客户端、需要人工查看 | 单点,鉴权与 TLS 需自建 |
| MCP stdio 服务器 | python -m mnemosyne.webui.mcp_server | 接入支持 MCP 的 Agent 客户端 | 与客户端进程绑定,客户端退出即停 |
表 8-2 四种部署形态的接入方式与代价
Python 库形态的边界最清晰。 记忆是应用的内部模块,生命周期由应用控制。 代价是与应用同生共死。 一次 OOM 会连带丢掉热路径缓冲里尚未落盘的写入。
CLI 形态的价值常被低估。 mnemosyne --agent search ... 返回 JSON 信封,任何语言的工具循环都能直接调用。 它也是排查时唯一可靠的手段。 REST 层可疑时,用 CLI 对同一个 --brain-dir 执行同样查询。 能立刻区分存储层问题与传输层问题。 代价是每次调用都要重新初始化引擎,批量操作必须走 retain_batch。
单进程 REST + 控制台是唯一同时提供人和机器两种界面的形态。 控制台与 REST 共用一个监听端口。 它接受 X-API-Key、Bearer、Token 三种认证头。 代价是服务端该有的东西全留给使用者:反向代理、TLS、限流、多副本。
MCP stdio 服务器是 Agent 生态里最常见的接法。 它由客户端进程拉起,通过 stdio 上的 JSON-RPC 通信。 tools/list 返回 55 条,44 原生加 11 客户端兼容,去重后 52 个能力。 代价是生命周期绑在客户端上。 客户端退出服务端随之消失,依赖「服务常驻」的设计都不成立。
有一个细节值得单独指出。 快速上手示例里,mnemosyne-web 用 9090,随后的 curl 示例写的是 8788。 部署脚本里的端口必须来自单一配置源。 否则控制台起来了、健康检查打到另一个端口。 故障表现是「服务正常但不可达」。
8.5 自托管与 SaaS 的边界
同一份引擎,两种交付。 开源版是 MIT 许可、不限量、无功能付费墙。 SaaS 是托管形态,提供控制台、API key 管理、订阅计费。 这条边界的准确定义是:SaaS 卖的是运维便利,不是功能权限。
定价结构本身可以验证这句话。 截至 2026 年 10 月,四个层级如下:
| 层级 | 美元 | 人民币 | 内容 |
|---|---|---|---|
| Free | $0 | ¥0 | 全部 52 个工具 · 冷层召回窗口 30 天 · 1 个命名空间 · 审计窗口 7 天 |
| Pro | $6.9/月 · $69/年 | ¥9.9/月 · ¥99/年 | 冷层永久 · 跨命名空间召回 · 审计历史不限 · 完整导出 |
| Team | $19.9/席/月 · $199/席/年 | ¥29.9/席/月 · ¥299/席/年 | Pro 全部 · 多用户命名空间 · 团队策略 · 审计报告导出 |
| Compliance Add-on | $9.9/月 | ¥19.9/月 | 仅月付、仅加购,叠加在 Pro 或 Team 上:定期 verify_integrity 报告、防删除导出、数据驻留配置 |
表 8-3 截至 2026 年 10 月的 SaaS 定价层级
看这张表的方式不是看价格。 而是看每一档收窄了什么。 Free 层的限制全部落在读取窗口上:冷层 30 天、审计 7 天、1 个命名空间。 写入通道在每一档都是开放的。 这是理解定价逻辑的钥匙。 收窄读取窗口不会让用户丢数据,只会让旧数据需要升级才能被召回。
年付按「付 10 个月用 12 个月」计价,约 16.7% 折扣。 新账号含 30 天 Team 试用,无需绑卡。 美元经 PayPal Subscriptions 扣款,人民币经微信扫码由运营方确认。
门控机制也支持这个判断。 MNEMOSYNE_TIER_GATE 不设即为关闭,自部署与 PyPI 用户不受任何限制。 门控只在出口做结果投影过滤。 不改检索参数、不改 SQL、不改存储。 付费与否不改变引擎行为,只改变返回结果的可见范围。 代价是:它无法阻止付费用户在本地跑开源版获得全部能力。 所以这套定价只能服务「不愿意自己运维」的人群。
横向看,另一条路线把治理能力放在付费层里。 截至 2026 年 10 月,Mem0 公开定价为 Free 与 Custom 两档。 图记忆、用户画像、Dream 巩固、SSO 与审计日志都归 Custom,需单独签订协议。 两种分层的差别不在价格高低。 而在「能力是否随付费变化」。 选型的实际含义是:如果图遍历是硬需求,走后者就必须接受数据出域。
8.6 部署决策判据
部署形态的选择可以压缩成一个两问的判定树。 两个问题都不涉及性能。
第一问:数据主权是不是硬约束。 如果记忆内容受行业监管约束、不能离开特定地域,自托管是唯一选择。 且功能不受任何损失,这是 MIT 许可加零付费墙的直接结果。 反过来,如果数据主权只是「希望如此」,SaaS 是合理的默认选项。
第二问:有没有人愿意承担运维。 自托管的真实成本不在安装,而在三件长期事务:
- 备份策略:复制文件容易,验证可恢复不容易
- 升级节奏:零依赖降低了升级风险,但没消除
- 容量监控:第 11 章的三类指标需要有地方跑
如果没人愿意长期承担这三件事,SaaS 的定价就是为这件事付费。 而不是为功能付费。
两个问题都指向自托管时,还有一层选择:库形态还是服务形态。 判据是记忆是否被多个进程消费。 单进程消费用库形态,多进程或多语言消费用 REST 或 MCP。
两个问题都指向 SaaS 时,需要确认冷层窗口。 30 天窗口对「最近三个月的事」这类召回需求是不足的。 实际使用中往往第一个月就会撞上。
混合形态常被当成折中方案,而实际更复杂。 本地跑引擎做写入与热数据召回,定期把冷数据导出到托管侧归档。 代价是双写一致性没有保证。 本地写入成功、导出失败时,两侧记忆集合会分叉。 分叉在召回层面表现为「同一件事有时记得有时不记得」。 除非有明确的跨侧对账机制,混合形态不如在两端里选一端。
8.7 本章小结
本章确立三条判断。
第一,零依赖是架构决策而非营销属性。 它的四个后果——离线可用、供应链安全、部署简单、升级可控——各自对应一项代价。 其中召回质量下降最容易被低估。 describe()["degraded"] 是让这个代价可见的机制。 这条判断成立的条件是「内置实现足以支撑起步阶段的使用」。 一旦召回质量成为核心指标,内置嵌入器必须被替换。 此时零依赖的价值只剩供应链与部署两项。
第二,SQLite 的合理性来自负载形状而非数据量。 写频中等、读频高、单机部署。 物理隔离让备份、迁移、删除都变成文件操作。 代价是跨作用域聚合退化为多次读取,且文件数量本身会成为瓶颈。 这条判断在两种条件下失效。 需要多副本水平扩展,或跨租户聚合查询是常规操作。
第三,自托管与 SaaS 的边界是运维而非功能。 引擎完全一致,开源版没有功能付费墙。 SaaS 的每一档只收窄读取窗口,写入通道永远开放。 技术后果是定价无法阻止本地部署获得全部能力。 因此它只能服务不愿运维的人群。 部署决策因此归结为两个非性能问题:数据主权、运维意愿。
一个本章没有回答的问题是:零依赖带来的「单进程单文件」假设,在进程随时可能被替换的环境里如何保持状态连续。 这需要先弄清写入时序,以及异步写入引入了什么样的可见性缺口。 第 9 章从这里继续。