有时候,Agent 的问题不是忘了,而是明明存了,却搜不到。
NousResearch/hermes-agent issue #16751 讲的就是这种很烦的记忆检索盲区:session_search 只索引了 messages.content,却没有覆盖 tool_calls 和 tool_name。
结果是:
- 用户明明在前面会话里调用过某个工具;
- 工具名或参数里明明出现过关键字;
- 数据库里那条消息也确实存在;
- 但
session_search搜不到。
这会让长任务恢复和上下文追查变得特别难。
因为你以为“它没发生过”,其实只是“没进搜索索引”。
现象:正文能搜到,工具字段却完全失明
issue 的复现很有代表性。
作者构造了几类 marker:
BUCKETMARKER_CONTENT:出现在 message contentBUCKETMARKER_TOOLCALL:出现在 tool_calls JSON argsFUNCNAMEMARKER:出现在 tool function nameTOOLNAMEMARKER:出现在 tool_name
然后比较两种查询:
通过 search_messages
结果只有 content 中的 token 能命中:
BUCKETMARKER_CONTENT: 1 hit
BUCKETMARKER_TOOLCALL: 0 hit
FUNCNAMEMARKER: 0 hit
TOOLNAMEMARKER: 0 hit
直接查 messages 表
所有字段其实都在:
BUCKETMARKER_CONTENT: 1 hit
BUCKETMARKER_TOOLCALL: 1 hit
FUNCNAMEMARKER: 1 hit
TOOLNAMEMARKER: 1 hit
这说明不是数据没存,而是搜索层没索引到。
根因:FTS5 只盯着 content 字段
issue 里把 schema 说得非常清楚。
当前 messages_fts 是一个外部内容 FTS5 表:
CREATE VIRTUAL TABLE IF NOT EXISTS messages_fts USING fts5(
content,
content=messages,
content_rowid=id
);
对应 triggers 也只读取:
new.content / old.content
也就是说,FTS 只看 messages.content。
但 Hermes 的消息表里,很多关键上下文其实藏在:
tool_callstool_name- 未来可能还有更多结构化字段
这些字段对于工具轨迹、任务恢复、错误分析非常关键。
如果只索引正文,就会出现“工具调用痕迹消失”的问题。
这不是 CJK tokenizer 问题
issue 特别强调,这个不是之前那个 CJK tokenization 问题。
这里用的是 ASCII marker:
BUCKETMARKER_CONTENT
BUCKETMARKER_TOOLCALL
FUNCNAMEMARKER
TOOLNAMEMARKER
所以不是分词器不认识中文,也不是 trigram 失灵。
而是索引层本身就没覆盖工具列。
这点很重要,因为它决定了修复方向完全不同。
为什么这对长任务尤其致命?
长任务恢复时,最需要查的往往不是“对话正文”,而是:
- 哪个工具刚刚调用过;
- 工具名是什么;
- 参数里带了什么路径、ID、URL;
- 有没有执行过某个副作用动作;
- 是否已经发布、删除、移动、重启过。
这些信息在 Agent 场景里经常比正文更重要。
比如:
- 文章发布流程里,工具名可能比正文更能定位到“是否发布过”;
- debug 时,
tool_calls.arguments里的参数比正文更能定位问题; - 多步 agent 任务中,工具轨迹就是事实链。
如果 session_search 搜不到这些字段,用户会以为 Hermes 没记住,但实际上只是记忆不可检索。
可能的修复方向
issue 里列了三种靠谱方向。
1. 给 FTS5 增加更多列
把 tool_calls 和 tool_name 也作为 FTS 列:
content
tool_calls
tool_name
优点:
- 可以列级查询;
- external-content 语义更完整;
- 工具轨迹能被搜索。
2. 新增 messages.search_text
把正文和结构化字段拼成一个可搜索的派生列:
content + tool_calls + tool_name
然后让 FTS 只镜像这个派生列。
优点:
- 逻辑清晰;
- 对现有查询改动相对可控。
缺点:
- 需要 backfill;
- 需要重建 FTS;
- 字段同步要严谨。
3. 改成内部内容或 contentless FTS5
让触发器直接维护索引文本,而不是绑定单一 content 列。
优点:
- 灵活;
- 索引内容可完全由 Hermes 控制。
缺点:
- 需要重新设计 snippet/highlight 体验。
为什么 naive 拼接不可取?
把 tool_calls 和 tool_name 直接拼进 content,看起来最简单。
但 issue 提醒,这会破坏外部内容表的一致性:
content=messages
这个绑定意味着 FTS 列和原始 source column 应该一致。
如果你把别的字段塞进去,后面 rebuild、snippet()、highlight() 的语义就容易错位。
所以这不是“加个字符串”就能糊过去的事。
用户怎么判断自己是否踩到了这个坑?
如果你有这种体验,就很像 #16751:
- session 里明明出现过某个 tool name;
- 工具参数里明明有某个 token;
- 你在正文里搜不到;
session_search返回空;- 但你直接看数据库或消息原文却能找到。
典型症状就是:
它明明发生过,但搜索说没发生过。
这在长任务 recovery 里很要命。
和 DeepAI API 中转站的关系
这个问题和 DeepAI API 中转站没有直接关系。
不管你用的是:
- DeepAI;
- OpenAI-compatible provider;
- 本地模型;
- GitHub Copilot;
- 其他 provider;
只要 Hermes 的 session search 只索引 messages.content,工具轨迹就会丢。
DeepAI 负责模型 API 入口,不负责 Hermes 内部 session DB 的 FTS schema。
所以这不是换 provider 能解决的问题,而是 Hermes 记忆检索层的 schema 覆盖范围问题。
FAQ
为什么正文能搜到,工具字段搜不到?
因为 FTS 只索引了 messages.content,没有覆盖 tool_calls 和 tool_name。
这和中文分词问题一样吗?
不一样。issue 明确说明这不是 CJK tokenizer 问题,而是 schema / trigger 层的问题。
会影响哪些场景?
长任务恢复、工具轨迹追查、发布记录检索、debug、会话复盘。
修复最稳的方式是什么?
给 FTS 增加工具字段,或者引入专门的 search_text 派生列,再重建索引。
DeepAI 能修这个吗?
不能。DeepAI 不负责 Hermes 的 session DB indexing。
总结
#16751 的教训很直接:
存了,不等于能搜到。
Hermes 的 session_search 只看正文,不看 tool_calls / tool_name,会让很多关键工具轨迹在搜索里“消失”。
对长任务来说,这不是小瑕疵,而是恢复链路的断点。
如果你做的是 Agent 记忆系统,正文索引只是底线,工具轨迹才是半条命。