主题
第 2 章 记忆系统参考架构
一个团队在三个月里换了三套记忆方案:先用向量库自己拼,发现召回不稳;再换托管记忆服务,发现删除不彻底;最后回到自建,发现审计又成了新问题。三次切换的成本不在代码量,而在于每一次都重新定义了一遍「什么算记忆正确」——第一套只要求「搜得到」,第二套只要求「删得掉」,第三套才第一次问「谁能证明它没被改过」。
这个过程的浪费是可以避免的。它之所以发生,是因为架构在动手之前没有被拆成责任域:谁负责抽取、谁负责存储、谁负责裁决新旧冲突、谁负责证明完整性。缺少这份拆解时,每个新需求都会推动一次整体替换,而不是一次局部演进。
本章给出的参考架构不绑定任何具体产品,它回答三个问题:一套记忆系统由哪些责任域组成、每个责任域的保证强度是什么、哪些能力适合自建而哪些适合交给共享平台。本章先给架构全景与三个视图,再逐层拆解构建面、运行面与治理面,然后说明生命周期如何把这些层串成闭环,最后用一张责任边界表收束。
2.1 架构全景
2.1.1 用三个视图理解同一套系统
同一套记忆系统需要被三种人用三种方式看。
组件视图回答「有哪些零件」:抽取器、嵌入器、向量索引、图存储、审计账本、作用域管理器。它服务于实现者,作用是避免能力遗漏——每缺一个零件,就有一类失效无法被观测。
责任视图回答「谁保证什么」:存储层保证持久性,检索层保证相关性下限,审计层保证不可否认性。它服务于架构师,作用是避免责任混乱——当事故发生时能定位到「哪一层的保证被打破了」。
生命周期视图回答「什么时候做什么」:写入时检查什么、召回时过滤什么、删除时级联什么、定期校验什么。它服务于运维与合规,作用是避免把一次性配置误当成持续机制。
三个视图不冲突,它们是同一系统的三个切面。常见错误是只画组件图,然后在事故复盘时发现没有责任归属;或者只画生命周期图,然后在扩容时发现零件之间没有清晰接口。
2.1.2 五个能力责任域
把记忆系统拆成五个责任域,每个域有明确的输入、输出与保证强度。
| 责任域 | 输入 | 输出 | 保证强度 | 失效后果 |
|---|---|---|---|---|
| 抽取域 | 对话、文档、行为日志 | 候选事实(含类型、来源、时间) | 尽力而为 | 噪声进入库中 |
| 存储域 | 候选事实 | 持久记录 + 索引 + 图边 | 强(ACID) | 数据丢失或错位 |
| 裁决域 | 新事实 + 已有事实 | ADD / NOOP / UPDATE / 保留双方 | 尽力而为 | 旧事实压制新事实 |
| 检索域 | 查询 + 预算 | 排序后的记忆条目 | 相关性下限 | 注入噪声或漏召回 |
| 治理域 | 全库 + 策略 | 审计证据、删除结果、脱敏结果 | 强(可验证) | 无法自证清白 |
表 2-1 记忆系统的五个能力责任域
这张表最需要注意的是「保证强度」这一列。抽取与裁决是尽力而为——它们依赖模型判断,永远存在错误率;存储与治理是强保证——它们依赖确定性的数据结构与算法,错了就是 bug。把这两类混在一起设计,最常见的后果是用「模型会判断」去替代「数据结构保证」,于是审计和删除这两件事永远做不实。
2.1.3 企业级设计的四条原则
原则一:状态外置。 记忆不能依附于某个执行实例。实例可替换,记忆必须连续。这条原则的直接推论是:任何「进程内缓存即记忆」的实现,在多副本场景下都会失效。
原则二:保证强度分层。 不同责任域承诺不同强度,并在文档与接口中显式声明。把「尽力而为」包装成「强保证」是记忆系统最常见的失信方式。
原则三:可验证优于可配置。 一项能力如果不能被脚本验证,它的存在就只能被相信。审计、删除、隔离这三项尤其如此——它们恰好是合规审查最先问的三件事。
原则四:按任务风险匹配最低充分架构。 不是所有场景都需要哈希链与级联删除。个人项目用单文件 SQLite 加内置提取器就够了;金融与医疗场景则需要完整证据链。架构的目的是让每一项被授予的能力都有对等的约束,而不是把系统做大。
2.2 构建面:抽取、存储、裁决、检索
2.2.1 抽取:一次调用能拿到什么
抽取域负责把非结构化输入变成候选事实。2026 年的主流做法是单遍抽取:一次模型调用完成事实识别、类型判定、时间解析与来源标注,不做多轮迭代。
单遍的收益是成本确定性与可预测延迟;代价是抽取质量受限于单次判断,复杂多跳表述容易漏。补偿手段通常有两个:一是用确定性规则兜底(无模型时仍能抽取简单陈述),二是允许调用方显式指定 observation_date 等元数据,把「何时发生」从模型推断改为调用方声明。
Mnemosyne 的选择是内置 rules 确定性提取器作为缺省,因此无 API key 也能完成写入。代价必须说清:内置提取器对复杂多轮对话的召回质量明显低于配置 LLM 的情形,它是零配置的起点,不是生产级的终点。describe()["degraded"] 会报告当前哪个组件回退到了内置实现及原因,避免静默降级。
2.2.2 存储:物理隔离与分层
存储域的核心决策不是「用哪个数据库」,而是隔离怎么做。
逻辑隔离(共享表 + WHERE user_id = ?)实现简单,风险集中在一个点上:任何一处遗漏过滤条件就是跨租户泄露,而遗漏在代码审查中极难发现,因为它看起来和正确代码一模一样。
物理隔离(每个作用域一个文件或一个独立 namespace)把风险从「写对条件」转移到「派生对路径」。前者是概率问题,后者是确定性问题——路径错了会立刻报错或读空,不会静默返回别人的数据。
Mnemosyne 采用物理隔离:四维作用域(user_id / agent_id / run_id / app_id)的每种组合派生一个 namespace,对应独立目录与独立 SQLite 文件。代价是跨作用域聚合查询需要多次读取并在应用层合并;对记忆场景而言这个代价通常可接受,因为跨作用域召回是例外而非常态。
分层是存储域的第二个决策。热/温/冷三层的目的不是省钱,而是让「遗忘」变成可逆的降级而非不可逆的删除:低价值记忆被降级并压缩,仍可被 expand 恢复。这比直接删除更安全,代价是存储不会真正缩小。
2.2.3 裁决:新旧冲突由谁决定
裁决域是最容易被低估的一环。它的输入是新事实与已有事实,输出是一个动作:追加、忽略、更新、或保留双方。
Mnemosyne 的 conflict_resolve 给出六类冲突判定与四种操作。六类是时间更新、偏好变更、作用域差异、精度差异、逻辑矛盾、来源冲突;四操作是 ADD、NOOP、UPDATE、KEEP_BOTH。默认 dry_run=true,只给计划不改数据;scope=audit 则扫描全库,只报「共享实体且共享谓词」的真冲突。
这个设计的要点是把裁决从写入路径上移开。写入永远只追加,裁决在需要时按需进行。好处是写入快且不会摧毁事实;代价是库里会暂时存在互相矛盾的条目,检索时必须依赖时间与来源信号来消解,而不是依赖「库中只有一条正确记录」这个假设。
2.2.4 检索:多信号融合的必然性
检索域要在「相关性」与「预算」之间做取舍。单靠向量检索在长周期记忆上会失效,原因是语义相似不等于决策等价:「我对花生过敏」与「昨天吃了花生没事」在嵌入空间里距离很近,但一条是约束,一条是反例。
Mnemosyne 用五路信号融合:语义、BM25 关键词、实体图、时间、标签,经校准的相关性下限过滤后返回,并保留词法回退——嵌入服务不可用时 BM25 仍能提供基础召回。代价是融合本身带来计算开销,且各路权重需要按场景调,默认权重在专有名词密集的场景下可能偏语义。
预算控制是检索域的第二条约束。成熟做法是两步披露:先返回目录(标题 + 一行摘要 + 展开成本),调用方判断后再展开 1–2 条。注入成本从 O(全部记忆) 降到 O(目录 + 少量展开)。代价是多一次往返,对延迟敏感的场景不划算。
2.3 运行面:部署、状态与规模
2.3.1 从能跑到能持续跑
构建面解决「功能对不对」,运行面解决「能不能一直对」。两者的分界线是:一旦系统需要跨实例、跨时间、跨协作者,构建面的假设就会逐个失效。
三个假设最先失效:进程内状态不再可信、单副本锁不再互斥、内存缓存不再一致。运行面的任务就是为这三件事提供替代机制。
2.3.2 零依赖的工程含义
「零依赖」常被当作营销词,但它有确切的工程含义:核心包的 install_requires 为空,所有外部服务的适配器用标准库直接发起 HTTP,需要第三方 SDK 的适配器延迟导入并在缺包时明确报错。
Mnemosyne 的核心包符合这个定义。它带来四个后果:离线可用、供应链攻击面小、部署简单(一条 pip install,无 Docker 无数据库无 API key)、升级不涉及依赖冲突。代价同样明确:内置嵌入器为 128 维确定性实现,召回质量低于商用嵌入模型;需要更强召回时必须显式接入外部 provider。
2.3.3 部署形态的四种选择
| 形态 | 适用场景 | 主要代价 |
|---|---|---|
| 库嵌入 | 单进程应用、CLI 工具、脚本 | 无并发隔离,多进程需自行处理文件锁 |
| 单进程服务 | 控制台 + REST 共用端口,团队内部使用 | 单点,需自行做进程级高可用 |
| MCP 服务器 | 接入 Claude Code、Cursor 等客户端 | stdio 传输下每客户端一个进程 |
| 托管 SaaS | 无运维能力、可接受云端存储 | 数据出域,依赖服务商可用性 |
表 2-2 四种部署形态的适用场景与代价
选择的判据不是「哪个先进」,而是数据主权是否硬约束。若是,自托管是唯一选择,且成熟产品的开源版功能不应因自托管而受损;若不是,托管降低运营负担。
2.3.4 规模化的三个瓶颈
记忆系统的扩容瓶颈很少是「存不下」,通常是三处。
写入串行化:哈希链审计要求每条记录链接前一条,天然串行。写入吞吐上限由账本追加速度决定,而不是数据库。缓解方式是批量提交与分区账本,代价是校验时需要跨分区合并。
检索融合计算:五路信号各自有成本,重排器成本最高。缓解方式是缓存高频查询与按场景裁剪信号路数,代价是缓存一致性需要额外机制。
跨作用域聚合:物理隔离使跨作用域查询退化为 N 次读取。缓解方式是预聚合视图,代价是视图与源数据的一致性问题。
2.4 治理面:审计、删除、安全
2.4.1 治理为什么是横切能力
治理不是生命周期的最后一个阶段,而是贯穿每个阶段的约束:权限模型在架构设计时确定,工具策略在构建时实现,沙箱与网关在运行时执行,可观测与安全分析在治理阶段检测,评估在调优阶段验证变更。
一个可用的自检标准是:如果某项治理要求只能在上线后通过流程和人工补齐,而不能在架构与构建阶段落到策略、接口或数据结构上,那么它在规模化之后很可能会失效。
2.4.2 三种保证的强度差异
治理域内部也有强度差异,混淆它们会导致承诺过度。
| 机制 | 能证明什么 | 不能证明什么 |
|---|---|---|
| 哈希链审计 | 记录未被篡改,且能定位到具体哪一条 | 记录已不存在 |
| 级联删除 | 源记录、派生结论、缓存、依赖摘要均被处理 | 备份与导出副本已被清除 |
| 作用域隔离 | 默认路径下数据不交叉 | 配置错误时仍然不交叉 |
表 2-3 三种治理机制的证明边界
这张表应当被直接放进对客户的说明里。它的作用是提前划定期望:审计不等于删除证明,删除不等于备份清除,隔离不等于配置免疫。
2.4.3 安全必须发生在写入前
记忆系统与一次性 prompt 的根本差异在于:被污染的记忆会在后续所有对话中持续生效。因此安全检查的位置很关键。
在检索时过滤,意味着污染内容已经进入存储,任何一次过滤遗漏都会让它重新生效;在写入前检查,意味着污染内容从未落地。Mnemosyne 的公证(notary)选择后者:凭据模式匹配后字段级脱敏、不可见 Unicode 检测、HTML 注入检测,全部在写入前完成。
代价是写入延迟增加,且规则需要持续维护——新的凭据格式、新的注入手法都需要跟进。
2.5 生命周期:从架构到调优的闭环
2.5.1 五个阶段的产物
| 阶段 | 回答什么 | 关键产物 | 出口条件 |
|---|---|---|---|
| 架构设计 | 形态、自主性、责任域、权限边界 | 责任边界表、保证强度声明 | 边界可被复核 |
| 构建 | 如何实现为可复现版本 | 抽取器、作用域规则、检索策略、评估基线 | 离线评估通过 |
| 运行 | 如何可靠、隔离、可恢复 | 事件、检查点、产物、Trace、成本 | 故障演练通过 |
| 治理 | 如何看见、约束、追责 | 审计证据、删除记录、隔离验证 | 合规检查通过 |
| 调优 | 如何复现问题、验证改进 | 数据集、评估器、实验记录 | 回归通过 |
表 2-4 记忆系统生命周期的五个阶段
架构设计被列为第一阶段而不是隐含在构建里,原因是形态选择与权限边界一旦确定,后续成本区间基本锁定。把这些决策留给实现细节,等价于让代码反向定义架构,而这类问题通常只能通过重构解决。
2.5.2 调优的产物必须回流到构建
任何由模型、反馈或轨迹生成的记忆策略、提取规则、检索权重,都应被视为候选变更,需要经过可复现评估、安全检查、版本化、灰度与可回滚发布之后才能进入生产。
这条约束解释了为什么运行阶段必须产出结构化的 Trace 与成本事实。没有可复现证据,调优只能依赖主观印象;没有版本化发布单元,即使找到改进方向,也无法证明改进来自这次变更。
2.6 本章小结
本章确立了四条判断。
第一,记忆系统必须按责任域拆分,而不是按功能罗列。 五个责任域中,抽取与裁决是尽力而为,存储与治理是强保证。把两类混在一起设计,最常见的后果是用「模型会判断」替代「数据结构保证」,于是审计与删除永远做不实。
第二,隔离方式的选择比数据库选型更重要。 逻辑隔离把风险放在「过滤条件是否写对」,物理隔离把风险放在「路径是否派生对」。前者是概率问题,后者是确定性问题。代价是跨作用域聚合需要多次读取。
第三,治理机制的证明边界必须显式声明。 哈希链证明未被篡改但不能证明已不存在;级联删除处理四个层次但不含备份;作用域隔离防默认路径不防配置错误。这三句话应当出现在产品说明里,而不是出现在事故复盘里。
第四,生命周期闭环的关键在回流约束。 调优产出的任何变更都必须经构建阶段的门禁才能进入生产,否则「自进化」会退化为「无审批的自我修改」。
本章的架构是静态的。一个尚未回答的问题是:同一套架构,在不同成熟度的组织里应该用到什么程度?把所有责任域都建满,对十人团队是浪费;只建一半,对金融场景是风险。这是第 3 章要给出的分级判断。