记忆也能改口:一条事实的版本史、as_of 查询和真忘记


上个月客户把订单库从 Postgres 换成了 MariaDB,你当时就跟 agent 说了一遍,它也记下了。这个月你顺口问「我们订单库用的是哪个库来着」,它不假思索地答 Postgres——你翻出那条记忆一看,内容是新的那次覆盖写进去的,可旧的那句也没消失,只是躺在别处,谁也不知道哪个才算数。记忆系统真正难的部分从来不是「存不下」,而是改口:世界变了要改、当时说错了要改、不再成立的要结束,而「忘掉」也不该等于「假装没写过,但文本还在文件里」。

本地事实库 holographic 这几周补的正是这一半。它原本以内置 provider 的形式住在 Hermes 核心树里,10 月 11 日搬去维护者自己的仓库、以目录插件形式发布,随后在 10 月 13 日升到 0.13.0。这次升级带来的不只是搬家,还有一条事实的完整生命周期。

从「覆盖」到「改口」:五种动作

过去你对一条记忆能做的操作基本只有两种:加一条,或者删掉重加。后者的问题是它会悄悄改变事实的编号,于是任何以编号指代这件事的地方都会断掉。现在一条事实有了自己的历史,动作分成五种:

动作 什么时候用 意味着什么
change 世界变了(同一个东西,新的答案) 同一条事实新增一个版本,编号不变
correct 当时就写错了 这是修正,被修正的说法不再出现在结果里
end 这个说法不再成立,但也没错 结束它,保留历史
revert 刚才那次改动想撤销 回到上一个版本
history 你想看看这条事实经历过什么 列出全部版本

兼容性上不用担心:旧的调用写法仍然有效,add supersedes 会按 change 处理(而且编号不再变化),update content 按 correct,remove 按 forget。也就是说你以前的脚本不用重写。

问「当时是什么」:as_of 查询

历史留下来了,新的问题就出现了:你想问的到底是「现在是什么」,还是「9 月 10 日那天我们以为是什么」?这两个问题的答案可能不一样,而排查事故时往往需要后者。

搜索和列表因此接受一个 as_of 参数,形如 as_of: 2026-09-10:它只从那天成立的版本里回答,另外单独给出一份 undetermined(无法判定当时状态)的清单。不知道开始时间就如实说不知道,绝不替你编一个——这条设计比功能本身更值得注意。默认搜索仍然只看当前有效的事实,历史文本存在自己的表和索引里,不会污染日常结果。

每个版本还会记下它是在哪写的:平台、会话(chat / thread)编号、session 编号,以及发话者的 id 和名字。聊天标题从不保存,所以这份出处不会顺手把你的群名写进数据库。

真的忘记:forget 会擦掉所有版本

「删除」在很多系统里只是把一个标记位翻过来,文本还躺在文件和备份里。这里的 forget 做得更彻底:事实和它的每个版本一起删除,两个全文索引重建,写入使用 secure_delete,随后 VACUUM 并截断 WAL——目的是让文本从文件里消失,而不只是查不到。

删完之后留下一个墓碑:只保留编号和一个带密钥的哈希值,不保留文本。作用是防止它被自动抽取又「复活」,同时让别的工具能按编号清理自己那份副本。代价也要说清楚:这个动作不可撤销。

一个 bot 服务很多不相关的群:scope: chat 与 scope: thread

如果你的机器人同时待在几个客户群里,所有事实挤在一个库是隐患。scope: chat 让每个会话有自己的数据库,scope: thread 更细一层,按论坛主题分开。用的是核心本来就会传给记忆 provider 的会话编号,不需要改核心,也不需要自己维护路由。

三条安全线:凭证、图片、不可信文本

记忆库有个容易被忽略的风险面:它会把内容原样注入到后续每一轮对话里。所以写得进来还不够,出去的时候也要过一遍。

  • 带值的凭证不存:password hunter2、API key sk-… 这种「凭证词 + 紧邻的值」会被拒绝写入,并给出原因;只出现词(「我用密码管理器」)仍然是正常事实。0.13.0 还把这个规则放宽到三词距离,同时收紧误报——the token is 0.125 USD 是价格,不是密码。
  • 图片不是事实:Markdown 或 HTML 的图片写法直接不当作事实,因为渲染它的客户端会自己去请求那个地址,数据可以藏在路径或查询串里。
  • 注入前再查一遍:0.13.0 让召回在下发前重新检查一遍:带聊天角色标记、工具调用标记、命令式话术、隐形字符或图片的内容会被排除在召回之外,每个进程只记一次日志。注意它不删除这些内容——search、list、history 仍然查得到,因为是你主动问的,你也有权改掉或忘掉它。

想少踩一个坑:cron 里的写入

如果你用定时任务做记忆维护(比如每晚整理一次),这里有个曾经安静很久的 bug:从 0.2.0 起,定时任务里的记忆写入根本不生效——核心会把 agent_context: "cron" 传下来,而插件当时把所有这类上下文都当成只读,于是你的维护任务一直「成功但什么也没存」。定时整理那条路上的插件尤其容易遇到。

现在这种方式被区分开了:定时任务里显式的 fact_store / fact_feedback 调用恢复写入(cron_writes,默认打开),而自动写入(自动抽取、镜像、召回计数)在 cron 里仍然保持关闭。手动整理的脚本能干活了,但不会在无人值守时后台攒一堆事实。

关于这次搬家:你现在什么都不用做

holographic 原来住在核心树里(plugins/memory/holographic/),10 月 11 日起成为目录里由 itpartypattaya 维护的插件。好消息是只有代码换了住址:

  • provider 名字不变,配置里仍然是 memory.provider: holographic;
  • 配置段不变,还是 plugins.hermes-memory-store;
  • 数据库不变,还是 $HERMES_HOME/memory_store.db(或你自己设的 db_path);
  • 工具不变。

升级路径是自动的:hermes update 会给每个声明了该 provider 的 profile 装上目录插件;如果你通过桌面版升级,agent 第一次启动时会补装并告诉你。装不上的情况也不会默默跑在没有记忆的状态下——如果 security.allow_lazy_installs 设成 false、或者离线、或者你拒绝了安装,警告里会带上该 profile 该执行的那条 hermes plugins install holographic 命令。想确认状态,两条命令就够:

hermes memory status
hermes plugins list

安装和切换 provider 的完整流程:

hermes plugins install holographic   # 从插件目录安装
hermes memory setup                  # 交互式选 provider
# 或者直接写配置:
hermes config set memory.provider holographic

细节可以对照我们之前写的记忆 provider 搬出核心之后那篇,搬家的机制是一样的;这次的差别是,搬完之后它自己又长出了新能力。(如果你当初装的是 Nous Research 那份过渡副本,hermes plugins update holographic 会把你挪到目录版本上,数据原地不动。)

对照一下:它和其他记忆方案的区别

方案 存什么 适合谁
内置 MEMORY.md / USER.md 你希望它永远记得的偏好与事实 所有人(一直生效)
holographic(本次) 本地 SQLite 事实库,带版本史与信任分 想完全本地、要能改口和真忘记
语义记忆(LanceDB) 向量检索的长期记忆 更看重语义相似召回
定时整理(dreaming) 夜里核对候选事实、提议保留 已有一套事实库、想让它自动收敛

多语言检索这几周也补了不少:中文简繁互认、日文写法变体、德语复合词、俄语同根词、英西法葡的词形变化都进了词典(数据来自 Open English Wordnet、jieba、SudachiDict、Wikidata Lexemes、DerivBase.Ru 等,许可分别是 CC BY 4.0 / MIT / Apache-2.0 / CC0)。作者用盲测集给出的数字是:中文异体字提问的召回从 33% 提到 100%,俄语同根词提问从 83% 提到 92%——这类数字属于维护者自测,参考即可。想少一点噪音的话,recall_indicator(默认开)和 auto_extract(默认关)是两个可以直接调的开关,信任分的加减是 +0.05 / −0.10。

小结

  • 世界会变,说错也会发生。能改口的记忆才有资格叫记忆:change / correct / end / revert 保留版本,as_of 让你回到某一天去看当时的样子。
  • 真忘记是一次破坏性操作:所有版本、索引、文件里的残留一起清掉,只留一个不带文本的墓碑,而且不可撤销。
  • 注入前重新检查一遍内容,是这个库值得信任的关键——凭证、图片、命令式话术都不会被悄悄送进下一轮对话。
  • 如果你配了 holographic,升级不用做任何事;如果还没配,它现在是目录插件,装一条命令就够了。