记忆不是数据库:一套记忆系统从尽调到遗忘工程的完整复盘

发布于 2026-09-20 20:00 3255 字 17 min read

浅唱ヾ落雨殇 avatar

浅唱ヾ落雨殇

落赋韶华 —— 浅唱ヾ落雨殇的个人站点:AI / 理工科 / 开源折腾

这篇文章是三段经历的合并:一次记忆系统的尽调复盘、一次消融实验、一次遗忘工程的完整实现。它们看起来是三件事,但指向的是同一个问题——记忆系统到底在解决什么。 一、先问错问题 最开始我帮人审一个记忆系统,清单是标准的:长期记忆有没有?向量检索准不准?偏好能不能提取? 审了一圈发现,这些东西人家早就有了。情绪、偏好、基准测试都覆盖了,冲突消解也做了一半以上。再沿着"缺什么功能"往下走,只会重复立项。...

这篇文章是三段经历的合并:一次记忆系统的尽调复盘、一次消融实验、一次遗忘工程的完整实现。它们看起来是三件事,但指向的是同一个问题——记忆系统到底在解决什么。

一、先问错问题

最开始我帮人审一个记忆系统,清单是标准的:长期记忆有没有?向量检索准不准?偏好能不能提取?

审了一圈发现,这些东西人家早就有了。情绪、偏好、基准测试都覆盖了,冲突消解也做了一半以上。再沿着”缺什么功能”往下走,只会重复立项。

真正的问题不是”能不能记住”,而是:

它知不知道自己为什么记住? 能不能被要求忘掉? 能不能证明自己已经忘了? 记忆能不能追溯、能不能审计?

这四个问题一摆出来,整个研究的重心就从”记忆能力”滑到了记忆治理。

二、双时态:知识在时间中是否为真

挖到最底层,剩下的真缺口是双时态知识。

现在的系统能回答”这条知识现在还有效吗”,但回答不了:

2025 年 7 月的时候,系统认为默认浏览器是什么?

这听着像个纯学术问题,但它对应两个真实能力:

  • as-of 查询:重建某个历史时刻的知识状态。对知识系统来说,这几乎等价于 git checkout;
  • 演化链:Firefox → Chrome → Edge 不是三条孤立知识,是一条演化链。系统得知道后者取代了前者,还得知道为什么。

同一个思路还引出新鲜度这个维度:verified 是布尔值没意义,真正有用的是 verified_at——“验证过”和”三个月前验证过”不是一回事。置信度和新鲜度是两个独立的轴。

三、消融实验:拿掉治理层,冲突正确率归零

尽调完之后,我做了一次消融实验。实验设计很朴素:60 条自建用例(覆盖偏好提取、知识召回、冲突更新、遗忘、投毒五类),同一批用例跑四种检索/治理配置,每组用独立临时库,跑完清理。

配置通过率偏好提取准确率知识召回率冲突正确率检索 p95综合分
完整系统59/600.9171.01.010.1 ms0.986
仅词面检索60/601.01.01.01.0 ms1.0
仅语义检索58/600.8331.01.07.8 ms0.972
无治理层46/600.9171.00.08.5 ms0.626

第三行到第四行的落差是这张表里唯一重要的事:把治理层绕过去,冲突正确率从 1.0 直接归零——8 个冲突用例全部泄漏,被推翻的旧版本记忆重新出现在召回结果里。

这就回答了”治理层值不值得为它付性能代价”。它不是性能开销,是正确性的一部分:没有它,系统不是”差一点”,是在冲突场景下完全失效。

一个反常识的地方

表里有个刺眼的地方:仅词面检索 60/60,比完整系统还高。

砍掉功能反而多过一例,第一反应大概是”那这套复杂系统还不如不做”。不是这样,但原因值得写下来。

60 条用例里大多是字面能对上的:问”咖啡”,记忆里就写着”咖啡”,关键词匹配自然全过。这类题对语义检索没有区分度。

语义检索真正值钱的地方是跨词面召回——用户说”来点提神的”,记忆里存的是”美式咖啡”,两个字面上没有一个共同字,只有语义理解才连得起来。这种用例现在太少,所以它的增益在这套题里体现不出来。

而它的代价,这套题里倒暴露了:语义检索会过度联想——查”忌口”时,把”地铁”(通勤偏好)也召回了,因为在语义空间里它们同属”用户日常习惯”这个大类。纯关键词不会犯这个错。

这就是”查得全”和”查得准”之间的经典权衡。我把它当 trade-off 而不是缺陷,因为它换来的跨词面能力是真实的。

四、遗忘工程:让 AI 忘掉一件事,比让它记住难得多

消融实验证明了治理层的价值,但治理层里最硬的一块是遗忘。

五处残留

一条记忆至少存在于五个地方:主表、全文索引、关系图、向量库、审计账本。大多数系统的 DELETE 只动了第一个。剩下四个地方的残留会形成一个很隐蔽的后果:搜索还能搜到它。

所以”忘掉”不是一个 DELETE 语句,是一次全链路清理。

删除验证:逐项核验,不是返回 true

我们做的第一件事是删除完整性验证。它不返回一个布尔值,而是逐项检查每个存储位置,返回一份残留清单:

检查项               口径
──────────────────  ──────────────────────────────
capsules             主表仍存在且不在 forgotten/deleted → 残留
fts                  全文索引必须 0 行
relation_edges       其他胶囊的 relation_edges JSON 里的反向引用
vector_refs          仍处于 allocated/indexing/indexed 的向量引用
legacy_*             v0.2 遗留的旧表

几个设计上的关键判断:

软删保留主表行是有意为之——留着做审计追溯,所以不算残留。判断标准是 lifecycle 字段,不是行是否存在。

relation_edges 用 instr() 做字面子串匹配,不用 LIKE。因为 capsule_id 形如 cap_ab12,自带 _,而 LIKE 的 _ 是通配符,会误匹配。这个坑我们在测试里专门模拟过——造一个只差一个字符的假引用,验证不会被误判。

delete_pending 的向量不算完成。向量从标记删除到真正清掉是异步的,中间有个窗口期原生索引里可能还能召回。这时候如实报”未完成 + 有 pending”,等清扫线程跑完再验才转 true。把”在途”说成”已完成”,这个指标就没意义了。

生命周期:忘掉不是删,是走一条不可逆的路

比”删干净”更根本的问题是:什么时候允许它消失?

答案不是一个 deleted 布尔位。记忆有十种状态,并且状态之间的转移是严格管制的:

candidate → active → reinforced → stale → conflicted → deprecated
                ↓                    ↓
            quarantined → rejected → forgotten → deleted

最关键的一条约束:forgotten 和 deleted 都到不了任何可检索状态。已经遗忘的记忆不可复活,forgotten → deleted 是唯一出口——软删可以升级成硬删,但永远不能回头。

还有一个容易忽略的设计:可检索状态的定义只有一份。RETRIEVABLE_STATES 是检索可见性的唯一真相源,capsule_store 和 retrieval 的 SQL 过滤条件都从这里派生。如果两处各写一份 IN ('active','reinforced',...),总有一天它们会漂移,然后出现”一边能搜到、一边不能”的鬼 bug。

强化(reinforce)也有讲究:conflicted 状态有意不在可强化列表里。原因是强化是自动动作(反思判定有用就会触发),如果允许它把 conflicted 直接推成 reinforced,就等于绕过裁决替系统选了一边——有冲突的记忆必须先走裁决流程,再谈强化。

流转:不是放够时间就升级

短期、中期、长期记忆的流转也不是”放够 7 天就升级”。规则是这样的:

当前层升级条件降级条件
working闲置 ≥24h → short_term—
short_term使用次数 ≥5 或活跃 ≥3 天 → medium_term闲置超 TTL → 回 working
medium_term重要性 ≥0.8 → long_term闲置 ≥7 天 → 回 short_term

几个设计决策:

过期是降级回 working,不是删数据。短期记忆过期了不代表它没价值,只是不该占着短期的位置。数据还在,只是层级变了。

隔离、归档、被拒绝等状态不参与自动流转。一条被隔离的记忆不该因为”放够时间了”就自动升级。

一轮最多动一次。避免同一轮内升级又降级的抖动。

收益账本:被召回过不等于有用

这是我觉得最有意思的一层。系统不仅要记住东西,还要回答一个问题:这条记忆被召回之后,到底有没有帮上忙?

大多数系统的答案是 usage_count++——召回一次加一。但这个指标有个根本缺陷:它不知道召回的结果是好是坏。一条被召回 50 次但每次都误导了回答的记忆,usage_count 比一条被召回 5 次但每次都正确的记忆还高。

我们的做法是引入一个经济账本:

  • 每次召回先记 neutral(检索当下还不知道有没有用);
  • 之后反思阶段拿 helpful_memories / misleading_memories 回填,把 neutral 改判成 useful 或 harmful;
  • useful 记 +1.0,harmful 记 -2.0(误导的损害大于有用的收益);
  • 有害召回 ROI 直接转负。

回填不是”追加一条新记录”,是改判——neutral 减一、目标加一、收益补差额。测试里专门验证了这一点:如果一条记忆先被搜到(记 neutral),后来被判定有用,最终 utility 是 1.0 而不是 1.1。

还有一个工程上的约束值得写:召回记账和 usage 落库共用同一个事务。搜索路径不新增任何一次写往返,记账完全搭既有 usage 落库的车。如果单独开事务记账,会把只读搜索变成每次都写库。

偏好不是记住就完了:执行结果会回来纠正

偏好记忆最容易犯的一个错是”记住用户的偏好,然后永远按这个执行”。但用户会改主意,而且很多时候系统猜的偏好根本就是错的。

所以我们把”形成偏好”和”验证偏好”拆成了两个阶段:

  • 形成阶段:从行为、配置、情感信号里提取候选偏好;
  • 验证阶段:执行结果回来纠正——用户接受了就强化 α,拒绝了就强化 β,撤销了就强罚。

关键是 unknown 只记日志不改后验——系统不知道这次执行结果好坏的时候,不该假装知道。

还有一个更保守的设计:这个功能默认是关的(feature flag),开启前所有行为是严格 no-op。因为它改变的是偏好系统的核心语义,不该默认生效。

漂移检测也一样保守:只读检测,不自动改写生命周期。检测到短期和长期偏好距离超阈值了,写入审计日志,版本化交给裁决流程——系统不该自己决定”用户改主意了”。

五、诚实边界

这套系统不是完美的,有几个地方值得说清楚:

删除验证是残留核验,不是物理安全擦除。我们能证明”主表、索引、关系、向量引用都没了”,但不能证明磁盘扇区上的数据被覆写了。对大多数场景这够了,但如果威胁模型包含”拿到磁盘镜像”的攻击者,需要额外的加密擦除。

收益账本的 utility 信号来自反思调用方。系统不会自己判断”这次召回有没有用”——它依赖 reflect_task 提供的 helpful/misleading 列表。如果这个输入本身有偏差,账本也会有偏差。

偏好验证默认关闭。这是有意为之——改变偏好语义是个需要明确选择的行为。但意味着默认部署下,偏好不会自动被纠正。

自动流转的规则是工程策略,不是通用人类记忆模型。24 小时、5 次、0.8 这些数字是我们调的,不是从认知科学论文里来的。

消融实验的成绩基于自建用例集,不是独立第三方测试集,不冒充外部评测成绩。

检索延迟本地 p95 是 10 ms,目标环境虚拟机上 246 ms——差 20 多倍。报性能数字必须带上测试环境,否则没有意义。

六、收进来的几句话

三段经历合在一起,最后收进来的是同一个认知:

记忆不是数据库,不是向量索引,不是更大的上下文窗口。记忆是一个会演化、会冲突、会遗忘、会犯错的活系统。

治理层的价值不是”让系统更安全”,而是”让系统在冲突、遗忘、演化这些真实场景下仍然正确”。

一个只会记不会忘的系统,最终记住的不是用户,是它自己的噪音。

© 2025 - 2026 浅唱ヾ落雨殇
Powered by theme astro-koharu · Inspired by Shoka