内存API返回扁平列表无法传递「两条记录相互矛盾」「某条已失效」等关系语义,即使存储完美也难逃信息损失。
向记忆系统询问生产环境用的是哪个数据库,它能返回两条互相矛盾的记录,每条都带着一个自信的相似分,此外什么都没有。Ken Alger 在他的文章开头正是描述了这种情形:PostgreSQL 0.94 分、MongoDB 0.91 分,以及四条多月前的迁移记录——两个分数对此一无所知。
他是从接口层面来写这个问题的。站在存储层面的角度,这是同一个问题,而令人不安的是:存储本身明明已经持有了看清冲突所需的全部信息——两条记录和两个时间戳——却仍然在输出时把它们压平了。先交代利益相关:我工作在 Mnemoverse,一款面向 AI Agent 的记忆引擎,所以文中关于我们自身失败的描述,是我最有把握的部分。
因为回复根本没有地方放置它。
记忆 API 返回的是一组带分数的条目列表。这个结构能表达"这里是五条记录,按匹配程度排序"。它无法表达"这两条互相冲突"、"这条已被那条取代"或者"这条仍然为真但不再起主导作用"。这些都是记录之间的关系,而一个扁平列表根本没有字段来存放关系。
所以即使一个存储完美地追踪了冲突,也会在输出时把它压平。Agent 看到的是两条普通的结果,取第一条,于是 0.94 击败 0.91 就悄悄地完成了冲突解决——而这个数字从来没有被要求裁决任何事。
这不是谁的排序器有 bug。这是一个类型问题。要修复它,意味着回复携带的是边,而不仅仅是条目——这是一个比加一列大得多的改动。
这个分解是 Ken 的,来自产生这两篇文章的那次对话,也是我们两人写过的最犀利的东西:
取代(Supersession):这件事为真,现在另一件事为真。世界变了。
更正(Correction):这件事从来就不为真。我们的记录是错的,而且在我们相信它期间,它支撑着某些事情的发生。
宣告无效(Invalidation):这件事被接受为真,但不再起主导作用。根本不是关于世界的断言。
在存储层,这三种操作看起来完全相同,因为最终它们都写了一个值。它们在有人做审计时才会分叉。取代让过去仍然可以解释。更正则说过去建立在某种虚假的东西之上,这是一个不同且更昂贵的Fact。而宣告无效根本不是真理声明:一条记录可以完全准确,却仍然不再是决定性的东西。
大多数存储对三者只有同一个动词,或者一个都没有。我们自己的存储直到这周才有一个,而这个动词已经是慎重地弱化过的:REST 写入接受一个 supersedes 链接,旧记录保留并获得一个指向其替代物的指针,而读取侧还没有隐藏它,所以目前这个链接只是审计轨迹而不是更正。通过 MCP 仍然根本没有 update 动词,我们自己的变更日志说得清清楚楚:任何被承认的更正,最终都是作为第二条记录出现在目标旁边的。两个原子、两个时间戳,除非有人把它们链接起来,否则它们之间没有关系。这意味着存储真的无法区分"从未为真"和"不再为真"。
因为在界面问题底下还有一个引导问题。
要判断一条取代边是否值得携带,你必须已经持有着它所取代的那条记录。所以边的填充不能是在 top-k 截断之后应用的过滤器。它必须首先影响哪些候选者被考虑进来。关系必须在排序之前就可见,而不是之后。
这把一个排序问题变成了一个分配问题。一旦回复可以携带事实、关系、权威和先前的决定,就必须有东西来决定当它们不能同时装进上下文预算时,哪些被丢弃。正如 Ken 在讨论中说的,一条排名较低的权威边可能比下一条高度相关的事实更重要,丢弃一条取代关系可能改变幸存记录的含义。Top-k 对此一无所知。它只知道一个数字。
排序列表是贫乏的。它们也便宜,而任何取代它们的东西都继承了一个比它所解决的更难得问题。
如果昨天的决定在这个分歧再次出现时还要能被找到,这个分歧本身需要一个持久的身份。
显而易见的做法是用配对做键:A 与 B 冲突。这会崩溃。一旦第三条记录到来,A-与-B-冲突就不再是同一个对象,而昨天的决定指向的是一个以那种形状已经不再存在的冲突。改用记录所争论的主题做键,身份就能在任意多条记录堆积在它下面时仍然存活。参与者会变,被争论的东西不变。
大多数存储用原子做键。我们还没有发现一个用原子们在争论的问题做键的。
产生本文的那条讨论最终收敛成了一个我们谁都没有写下来的规范,而这属于说出它的人。
Max Quimby 命名了提升边界:一次经历不应该静默地升格为策略,因为策略按设计就是昂贵的(改变它代价高,创造它也应该代价高)。Reid Marlow 命名了另一端:每一条被提升的条目都需要一个撤销者,否则失效的策略会凭借听起来正式而继续存活。joinwell52 在中间加入了接受步骤,并把范围和取代分开。Suny Choudhary 从安全侧闭合了这个循环:来自不可信文档的十条一致记录不应该压过一条经过审查的决定,否则记忆投毒就变成了一种提升攻击而不是检索攻击。anasbuilds997 命名了为什么 Agent 不能充当自己的守门人:两条互相印证的工具输出不是权威,而自我提升是幻觉固化为永久真理的途径。
注意这五个人描述的都是什么。不是更好的排序。是一条有治理的写入路径,以及一条诚实到足以说出哪条记录在起主导作用、由谁的权威主导的读取路径。
对于正在构建这些东西的人来说,有一点值得明确:权威不是一个分数。将信任、最近性、有效性和有用性压缩成一个数字,就会产生没有人能解释的系统,因为一旦它们变成一个数字,你就再也无法追问是其中的哪一个做出了决定。
Mike Czerwinski 提出了一个重新定位了我观点的看法:把矛盾交回给 Agent 只在 Agent 随后的选择也被记录下来时才能闭合这个缺口。用他的话说,否则"静默解决只是把问题从存储移到了推理步骤,同一个 bug 在不同的地址上,而且更难发现,因为存储看起来是诚实的。"
Tae Kim 之前已经在处理贸易数据时遇到了这个问题,同一家公司在并购前后会解析到不同的节点。他们在关系上盖上时间范围,并开始把两个版本都返回给推理层,这有帮助。仍然坏掉的部分是 Agent 在两者之间的选择消失得无影无踪。他们开始把这些选择作为事件写回去,而诚实的理由不是架构品味:一个客户问到了一个奇怪的输出,但没有任何东西可以指给他们看。
这就是让一个字段变成真实的东西的压力。不是优雅,是审计。
关于这条记录存在的位置,有一个警告,讨论本身为它提供了证据。Giulio D'Erme 在读完前一篇之后去数了他自己的语料库:152 条备忘录中有零条、59 份文档中有零条携带了有效窗口或取代边。引擎可以读取那些字段。没有任何东西在写它们。可用性不是使用率。
所以一条放在记忆旁边、位于检索路径之外的决策日志,最终会处于完全相同的状态:可读、未读、技术上已经交付。如果明天的 Agent 不能在同一个冲突再次出现时拿回昨天的决定,那这个日志就是——正如 Ken 在讨论中说的——供人类查阅的法医证据,而不是系统操作上可用的记忆。
不是列表。更像是:这里是记录,这里是它们所属的冲突,这里是哪个在起主导作用、由谁的权威主导,以及上一次同样的冲突出现时做了什么决定。
我们没有交付那个。在个人领域,我们今天交付的东西用对最近记忆的新颖性检查来做准入判断,这不是信任也不是重要性,而且有一个我们公开文档化的失败模式:一条更正几乎是用和它要更正的内容完全一样的措辞写成的,所以它被算作一个几乎重复的内容,因此是最可能被拒绝的输入。那条过时的记录随后作为该主题上唯一的记录存活下来,而且看起来比任何人试图修复它之前更有权威。
我宁愿把这些写下来,而不是描述一个比我们现有的更漂亮的引擎。我会对任何记忆供应商(包括我们自己)运行的三个检查仍然是那些廉价的:存储一个事实、矛盾它、列出系统关于那个主题持有的所有内容;提交一条措辞像原始记录一样的更正,看它是否能存活;以及问回复除了分数还能表达什么。
Ken 的文章从接口端处理了同样的问题,值得与本文对照阅读。我们两人加起来,论证大致完整了:存储知道的可以比回复能够表达的多,而它不知道的部分,才是有人对此做了什么。
利益相关声明:我在 Mnemoverse 工作,一款通过 MCP 连接的 AI Agent 记忆引擎。这里描述的作为我们自身失败的案例都记录在我们自己的变更日志中,而不是为本文才发现的。上面归功于他人的每一个论点都来自前一篇的评论区,这比我能在这里说的任何话都更好地证明了如实写出自身缺陷的价值。