自托管 AI 记忆:数据主权、部署形态与合规实践
把用户记忆放在别人的服务器上,是一个需要论证的决定,不是一个默认选项。本文讲清自托管真正解决的问题、三种部署形态怎么选、备份与恢复怎么做才不会在关键时刻失效,以及合规审查时对方会追问的那几个点。所有容量与延迟数字为实测。
先明确边界:自托管不等于自动安全。它把「数据在哪里」这个问题的答案交给你,同时也把「谁负责保护它」的责任交给你。本文按这个前提写,不做「自托管一定更安全」的笼统断言。
一、自托管真正解决的问题
常见说法是「自托管更安全」。这句话太粗,掩盖了真正有价值的三个具体点。
1. 删除的彻底性可以被验证
用户说「把我的记忆删掉」,托管服务会告诉你「已删除」。但你能验证吗?对方的备份里可能还有、灾备副本里可能还有、日志里可能还有。你只能相信,不能检查。
自托管下,整个记忆库就是你磁盘上的一个目录。删掉它、确认目录不存在,删除就完成了——没有副本要追。这是可验证的,不是承诺。
2. 数据不出你的网络边界
对受监管行业(医疗、金融、政务),数据出境或离开受控网络本身就是合规事件。自托管让「数据在哪里」变成一个物理问题而不是合同问题。
3. 成本模型可预测
托管按订阅或按调用计费,用量增长时成本线性上升。自托管的边际成本接近零(已有硬件),只在你扩容时才产生支出。对高频写入场景,这个差异会随时间放大。
二、三种部署形态
| 形态 | 适合 | 数据位置 | 多用户 | 运维成本 |
|---|---|---|---|---|
| 单机进程 | 个人使用、本地开发 | 本机磁盘 | 不需要 | 几乎为零 |
| 容器 + 持久卷 | 团队共享、内网服务 | 云盘或 NAS | 支持 | 中(需管卷与备份) |
| Serverless | 低频、突发流量 | 取决于挂载 | 支持 | 低,但冷启动有影响 |
形态 A:单机进程
最简单的形态。引擎作为库嵌入你的程序,或作为本地进程运行。
pip install mnemosyne-os
# 作为库使用
python -c "
from mnemosyne import Memory
from mnemosyne.api.config import MemoryConfig
m = Memory(MemoryConfig(brain_dir='./mem', namespace='default'))
m.add('Alice 偏好深色模式', user_id='alice')
print(m.search('alice', filters={'user_id':'alice'}, top_k=3))
"
记忆库就是 ./mem 这个目录。备份 = 复制目录。
形态 B:容器 + 持久卷
# 关键点:记忆库必须挂在持久卷上
docker run -d -p 8788:8788 -v /srv/mnemosyne-data:/data -e MNEMOSYNE_MCP_TOKEN="a-long-random-secret" -e MNEMOSYNE_DIR=/data your-image
最容易犯的错:记忆库落在容器根文件系统。表现是「每次重新部署,所有记忆消失」。这不是 bug,是卷没挂。部署前务必确认挂载点,并用 df -h /data 之类的方式验证它确实是独立卷。
形态 C:Serverless
适合调用稀疏的场景。注意两点:冷启动会增加首次调用延迟;本地文件系统通常是临时的,必须挂载持久存储,否则记忆不持久。
三、备份与恢复:别在关键时刻才发现失效
记忆库是单文件存储,备份很简单,但「简单」不等于「一定有效」。以下三条来自实际踩坑。
1. 备份要能恢复,不只是能复制
# 备份(SQLite 需要一致性快照,不能直接 cp 正在写的库)
sqlite3 /data/memory.db ".backup '/backup/memory-$(date +%F).db'"
# 或者停进程后整目录打包(更简单,代价是短暂停机)
tar czf /backup/mem-$(date +%F).tar.gz -C /data .
# 恢复
tar xzf /backup/mem-2026-10-27.tar.gz -C /data
必须定期演练恢复。一个从没恢复过的备份,等于没有备份。建议每月在测试环境恢复一次并跑完整性校验。
2. 完整性校验是备份的验收标准
# 校验哈希链,检测篡改并定位到具体记录
python -c "
from mnemosyne import Memory
from mnemosyne.api.config import MemoryConfig
m = Memory(MemoryConfig(brain_dir='/data', namespace='default'))
print(m.verify_integrity())
"
# 实测输出(120 条):{'default': {'valid': True, 'total': 120, 'first_broken_at': None}}
实测 120 条记忆的全链校验耗时 4.5 ms,完全可以放进每日定时任务。
3. 备份要加密,且密钥不要和数据放一起
记忆库里是用户的个人事实——偏好、住址、健康状况、工作内容。备份文件如果明文躺在对象存储里,等于把记忆库公开了。加密后再上传,密钥单独管理。
四、多用户隔离:最容易出事的环节
自托管常被用于团队或产品后端,这时隔离就是核心风险点。
| 做法 | 隔离强度 | 失败模式 |
|---|---|---|
同一张表 + WHERE user_id = ? | 弱 | 任一处漏写条件 = 跨用户泄露 |
| 每个用户一个命名空间(独立文件) | 强 | 查错库需显式传错参数,由文件系统兜底 |
| 每个用户一个数据库实例 | 最强 | 运维成本高,备份碎片化 |
为什么推荐物理隔离:逻辑隔离的安全性依赖「每一处查询都记得加条件」,这是靠人保证的。物理隔离的安全性依赖「参数指向了哪个文件」,这是靠代码路径保证的。后者出错概率低一个量级。
必须做的一条:命名空间由服务端决定
如果 API 接受客户端传 namespace 并直接采信,那么任何持有有效 token 的人都能读别人的库。正确做法是:服务端从 token 反查归属,强制覆盖客户端传来的值。
# 错误做法
ns = request.json.get("namespace") # 客户端说了算 → 越权
# 正确做法
ns = key_namespace(token_owner) # 服务端从 token 推导
payload["namespace"] = ns # 强制覆盖
五、日志:最隐蔽的泄露面
记忆系统的日志是最容易被忽视的隐私缺口。因为记忆内容会经过日志——请求体、错误堆栈、调试输出都可能带正文。
| 该记 | 不该记 |
|---|---|
| 请求方法、路径、状态码 | 请求体(含记忆正文) |
工具名(retain / recall) | 工具参数里的 content |
| 命名空间标识 | 用户邮箱、手机号 |
| 耗时、错误类型 | 完整异常堆栈里的业务数据 |
本项目网关的日志实现刻意只打 method、工具名、作用域,不打正文——原因写在源码注释里:记忆内容属用户隐私,日志会进可观测平台,不该出现用户数据。
自托管时特别注意:如果你用的是第三方日志服务或 APM,它们可能默认采集请求体。部署前检查采集配置,必要时在采集侧加字段过滤,而不是只靠应用层不打日志。
七、安全加固清单:自托管必须自己做的事
托管服务替你做了这些。自托管下,漏掉任何一条都是真实的暴露面。
| 项 | 做什么 | 漏掉的后果 |
|---|---|---|
| 磁盘加密 | 整盘或目录级加密(LUKS / BitLocker / 云盘加密) | 磁盘被拆走或快照泄露 = 记忆库明文外泄 |
| 访问控制 | 只对内网开放;管理端点不暴露公网 | 端口被扫到即被尝试 |
| TLS | 对外一律 HTTPS,禁止明文 | token 与记忆正文在链路上可被截获 |
| 令牌强度 | 长随机串,不用 change-me 这类默认值 | 可被字典攻击 |
| 令牌轮换 | 定期更换,离职/泄露时立即换 | 旧令牌永久有效 |
| 日志脱敏 | 不记请求体;第三方日志服务加字段过滤 | 记忆明文落盘到日志系统 |
| 备份加密 | 加密后上传,密钥单独管理 | 备份成为最薄弱的泄露点 |
| 依赖更新 | 定期升级引擎与系统包 | 已知漏洞长期存在 |
| 完整性监控 | 定期 verify_integrity() | 篡改无法被发现 |
一个具体的配置示例
# 只监听本机,前面放反向代理做 TLS 与鉴权
python -m mnemosyne.webui.mcp_server --transport http --host 127.0.0.1 --port 8788 --brain-dir /data --namespace default
# nginx 侧:强制 HTTPS + 限制来源
# server {
# listen 443 ssl http2;
# server_name mem.internal.example.com;
# location /mcp {
# allow 10.0.0.0/8; # 只允许内网
# deny all;
# proxy_pass http://127.0.0.1:8788;
# }
# }
八、故障排查:自托管特有的坑
| 现象 | 常见原因 | 排查命令 |
|---|---|---|
| 重启后记忆全丢 | 记忆库在容器根文件系统 | docker inspect 看 Mounts;df -h /data |
| 写入极慢 | 记忆库在网络卷 / NFS / overlay 层 | SQLite 对网络文件系统不友好,换本地卷 |
| 数据库被锁 | 多进程同时写同一库 | SQLite 单写者模型;改用单进程或队列 |
| 磁盘满 | 未清理旧备份 / 记忆增长超预期 | du -sh /data /backup |
| 校验失败 | 文件被截断或人为修改 | verify_integrity() 返回 first_broken_at 定位 |
| 跨用户看到别人数据 | 作用域未强制、或客户端可传 namespace | 检查服务端是否强制覆盖 namespace |
九、容量与性能实测
本机实测(Windows / Python 3.13.12 / 8.1.0.post1 / 默认零配置):
| 指标 | 实测值 |
|---|---|
| 引擎初始化 | 0.24 ms |
| 写入一条(中位) | 11.91 ms |
| 召回一次(中位) | 3.924 ms |
| 召回一次(P95) | 5.955 ms |
| 完整性校验(120 条) | 4.5 ms |
| 120 条记忆占盘 | 5.5 MB |
容量估算:按实测比例,万条记忆约占数百 MB 量级(含向量与索引)。这个量级下单机 SQLite 完全够用。真正的瓶颈在记忆量到十万级时的向量检索延迟——那时需要引入 numpy 加速,或外接专用向量库。
十、合规审查会问什么
| 问题 | 自托管的回答 | 能否举证 |
|---|---|---|
| 数据存放在哪里? | 你自己的服务器 / 内网,可指到具体机房与磁盘 | 能(物理位置可查) |
| 是否出境? | 不出境(除非你主动同步) | 能(网络流量可审计) |
| 谁能访问? | 由你的访问控制决定 | 能(访问日志) |
| 删除如何执行? | 删文件,可当场验证目录不存在 | 能(当场验证) |
| 是否有第三方处理者? | 核心零第三方依赖,不向外部发送数据 | 能(依赖清单 + 网络监控) |
| 如何证明未被篡改? | 哈希链校验,可定位到被改的具体记录 | 能(随时自跑) |
最后两条是自托管相对托管服务的实质优势:托管服务也可以做哈希链,但校验发生在对方服务器上,你拿到的仍是「对方告诉你的结果」。自托管下你可以自己在任意时间点跑校验,并且能当场演示删除。
十一、常见问题
自托管会不会很麻烦?
单机形态几乎零成本——装一个包、跑一行代码。麻烦的是团队形态:卷管理、备份、监控、升级。如果你的使用场景是个人或小团队,麻烦程度远低于想象。
没有 GPU 能跑吗?
能。默认配置用内置 128 维确定性嵌入,不需要 GPU,也不需要下载模型。要更高召回质量时才需要配置嵌入模型,那时可以选远端 API(不占本地算力)或本地模型(需内存/显存)。
能不能部分自托管?
可以。常见组合是:记忆存储自托管(数据在自己手里),嵌入模型用远端 API(省算力)。这时要评估的是「送出去的只有待嵌入的文本片段」,与「整个记忆库在别人服务器」是两个不同量级的暴露面。
怎么从托管迁回自托管?
导出为 JSONL(Memory Exchange Protocol)后导入本地库。建议先核对条数,再切换流量。