记忆也能改口:一条事实的版本史、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,升级不用做任何事;如果还没配,它现在是目录插件,装一条命令就够了。