记忆插件悄悄被截掉一半:Hermes 把「砍上下文」改成可选,还修了两处坑


你给 Hermes 接了一个外部记忆服务——Hindsight、Mem0、Honcho 都行。用了两周,它确实“记得”你,直到某天你提到上周定下的接口约定,它却像没听过一样。你怀疑是模型变笨了,或者记忆服务没存上。真实原因更尴尬:那段召回内容确实取回来了,只是在送进模型之前,被 Hermes 自己换成了“头一段 + 尾一段 + 一个文件路径”。

这个行为现在改了。10 月 9 日合并的三处修复一起解决外部记忆插件的三个“静默故障”:召回被截断、插件在 hermes memory status 里凭空消失、以及每次启动都重装一遍的安装循环。这篇按症状讲机制与用法。

症状一:召回内容被截断,而且没有任何提示

Hermes 有一个通用的“溢出(spill)“机制:当某个 hook 的输出超过阈值时,不把整段塞进上下文,而是换成头尾预览加一个文件路径。这对普通插件 hook 是合理的默认——不让一个话多的插件吃掉整个上下文窗口。

问题是这个机制也套在了外部记忆的预取召回上。于是一个已经按自己的预算裁剪好召回内容的记忆服务,返回的东西又会被 Hermes 再砍一次:最有用的中间部分不再直接可见,模型只看到开头和结尾。

修复方式是把这件事变成显式选择(PR #135157):

# ~/.hermes/config.yaml
memory:
  prefetch_spill_enabled: false   # 默认值:外部召回保持完整

行为细节值得记清楚:

  • 默认 false:外部记忆的召回原样进入上下文。安全感由一个新的上限提供——超过 hooks.output_spill.max_chars 的 10 倍(且至少 10 万字符)时仍然会溢出,避免一个失控的 provider 撑爆上下文窗口。
  • 设为 true:回到“超阈值就换成头尾预览 + 文件路径”的旧行为。它复用 hooks.output_spill 的阈值、预览长度和目录;而 hooks.output_spill.enabled: false 依然拥有最终否决权。
  • 普通插件 hook 的溢出行为不变,仍然默认开启,和记忆的这个开关互相独立。
  • 设置在 provider 注册时被快照,所以改完配置要重启 Hermes 才生效,而且正在进行的会话行为不会被中途改变。
  • 官方文档的建议很直白:如果 provider 自带 recall 预算,优先用 provider 的——它比“截断”更懂什么该留。

一个实际取舍:保留完整召回会让 prompt 变大。如果你跑的是按 token 计费的模型、又用了话很多的记忆服务,把 prefetch_spill_enabled: true 打开反而是省钱选项——前提是你接受中间段落可能不那么显眼。

症状二:你的记忆插件在 hermes memory status 里不见了

第二个修复(PR #135524)只有一行代码加一行文档,但影响很直接。

Hermes 判断“这个插件目录是不是记忆 provider”时,看的是它的 __init__.py 里有没有 register_memory_provider 或 MemoryProvider。这是个不导入模块的廉价扫描——但只看文件的前 8 KB。一个模块文档字符串写得比较长的 provider 就会漏掉:hermes memory status 里没有它,hermes memory setup <名字> 还说“not found”。

插件目录里的 excel_line 就撞上了这个坑,当时的解法很无奈——把文档字符串缩短,好让注册代码落进前 8 KB(#135442 / #135473)。现在这个 workaround 不需要了:快路径(8 KB 文本扫描)保持不变,扫不到时才回退到已有的 _defines_memory_provider——它会解析包里的顶层模块,不执行导入。已经命中的 provider 一分钱额外开销都不付,只有漏判的那些才多一次 AST 解析。

如果你写的插件“装上了但没被识别”,别再删文档——先升级。

症状三:记忆插件每次启动都重装一遍

第三个修复(PR #135529)对付的是一个滚雪球式的重试循环。

记忆 provider 的自动安装在 agent 启动时执行(hermes chat、cron、gateway agent、Desktop 重启都算一次启动)。修复前,一次失败的安装会在每一次启动时被重试,而每次重试都记成一次失败。看 PR 里的遥测数据就明白了(14 天,按天去重):

插件 成功 失败 有失败的安装日 平均每个失败日的失败数
hindsight 130 1048 204 5.1
mnemosyne 47 288 83 3.5

hindsight 的日失败数从 09-29 到 10-02 一路是 8 → 47 → 170 → 204,其中大部分是同一个失败被反复计数的结果,而真正的原因是少数网络类错误(PM 的 uv lock 失败、git clone 失败)。结论很清楚:这是重试循环,不是 1048 个不同的故障。

现在语义变干净了:hermes_cli/memory_provider_migration.py 里的 STARTUP_RETRY_SECONDS = 3600.0——启动时自动安装失败的 provider,一小时内不再被任何进程重试。再触发时会告诉你该怎么手动处理:

Its automatic install failed less than an hour ago: run `hermes plugins install hindsight`

同一个 PR 还顺手修了一个容易让人误判的地方:当目录里已经有一个指向别处的软链接 plugins/<name> 时,目录安装会说明“这个槽位已被占用”,而不是甩锅给插件的 manifest 格式。

怎么自查

hermes memory status     # 当前激活的是哪个外部 provider,装没装上
hermes memory setup      # 选择/配置 provider(也会提示缺失的安装)
hermes memory off        # 关掉外部 provider
hermes plugins list      # 插件是否在位

排查外部记忆类问题的顺序建议是:先看 hermes memory status 确认 provider 在不在;再看 config.yaml 里 memory.provider 与 prefetch_spill_enabled 的值;然后判断“记不住”到底是模型没收到召回内容(配置问题),还是 provider 那边本来就没存(服务问题)。想了解不同记忆后端的取舍,可以看这份语义记忆实践和记忆整理(dreaming)机制;命令面的完整说明在 hermes memory 参考。

发布状态

三处修复都合并于 2026-10-09,晚于 v0.21.6 的发布(10 月 8 日),因此目前只在 origin/main 上,等下一个版本或 hermes update。插件目录本身的评审流程背景可以看插件目录指南。

小结

这三处都属于“静默故障”:没有报错,没有日志,只是效果悄悄变差——召回被截断、插件被漏判、失败被重复计数。它们能藏这么久,是因为每一个都发生在“用户看不见的中间层”。如果你在用外部记忆服务,今天最值得做的一件事是打开 config.yaml 看一眼 memory.prefetch_spill_enabled 的值,然后决定你是想要完整的召回,还是想要更小的 prompt。