§13会话搜索:把不该用LLM的活要回来

Session Search: Taking Back the Work LLMs Should Not Do

第一版书之后这套记忆系统最大的改动,不是检索变快了,而是有人把一颗本不该在这里的LLM拔了出去。结果是同一件事,免费、即时、还不会胡编。

先看一句官方的话

v0.15.0这版的release notes里有一行,我读到的时候停了一下。它说session_search重写了,no LLM、no cost、4500×faster。

四千五百倍。这种数字一出现,我的第一反应是怀疑。AI圈里「快了多少倍」的说法,大半是营销话术,换个benchmark就原形毕露。所以我去翻了源码,想看看这4500×到底是怎么来的。

翻完之后我得承认,这次是真的。但真相比「快了4500倍」有意思得多。

旧版到底慢在哪、贵在哪

旧版的会话搜索是这么干的。你想找「上周那个聊数据库迁移的会话」,它先用SQLite的FTS5全文索引去召回几个候选会话,这一步本来很快。然后它做了一件多余的事:再喊一个辅助LLM,把命中的几个会话「摘要」一遍给你看。

问题就出在这颗辅助LLM身上。每召回三个会话摘要一次,大约30秒、大约花掉0.3美元。更糟的是,当FTS5根本没召回到那个对的会话时,这个LLM不会说「没找到」,它会一本正经地编一段你从没说过的内容给你。

你品一下这个设计的拧巴:检索这件事,FTS5已经给出了准确答案,哪几条消息命中了,清清楚楚躺在数据库里。结果非要再花一次钱、等30秒,让一个会幻觉的模型把这些真实消息「转述」一遍。明明手里有原文,偏要找个人复述给你听,复述的人还可能记错。

新版做的事:把那颗LLM删了

新版的解法朴素到近乎无趣——直接返回数据库里的真实消息,全程不碰LLM。

不推荐推荐
旧版:FTS5召回→喊辅助LLM摘要三个会话→约30新版:FTS5召回→直接从DB取出命中的真实消息→
秒、约0.3美元、可能编造→返回「转述版」约20ms、零成本、不可能编→返回原文

源码里这句话写得很硬气。session_search的工具文件开头那段注释明明白白:所有模式都走SQLite的FTS5索引,「No LLM calls anywhere」,每种返回都是数据库里的真实消息。

discovery这种带搜索的查询,从原来约90秒压到约20毫秒;纯翻页(scroll)更快,约1毫秒。所谓4500×,就是90秒除以20毫秒算出来的,是端到端这一整套工具的延迟对比,不是FTS5引擎本身变快了4500倍。

这点我要替读者多说一句,免得被数字带偏。检索引擎从头到尾都是FTS5,一行没换。变的只是删掉了最慢、最贵、最会胡编的那一环。本质上,这是把一件本不该用LLM干的活,从LLM手里要了回来。

一个反直觉的判断:很多人下意识觉得「上了LLM=更智能=更好」。但检索是个有标准答案的任务:命中就是命中,没命中就是没命中。给一个确定性任务套一层概率性的模型,换来的不是智能,是成本、延迟和幻觉。工程上最克制的进步,有时候是做减法。

不用LLM摘要,凭什么够用

这是我最初的疑虑:LLM摘要好歹给你「这个会话讲了啥」的概览,纯返回原文,不会一堆消息糊你一脸吗?

新版的巧思在discovery这个形态里。它一次调用,就同时给你三样东西:会话开头的头几条消息(让你知道这事当初要干嘛)、命中点上下几条的窗口(让你看到关键那一段)、会话结尾的几条消息(让你知道最后聊出了什么结论)。

开头加结尾的这种「书挡」式截取,加上命中点的上下文窗口,主模型自己就能拼出「这个会话讲了什么」。

LLM摘要本来想给你的那个概览,用结构化的头、尾、命中窗就能让主模型重建出来,不用再单独花一次LLM的钱。目标、命中、结论,一次拿齐。

四种检索姿势,全靠参数推断

release notes说有三种模式(discovery/scroll/browse),但我翻源码发现其实是四种。而且没有所谓的mode参数让你选。传了哪些参数,它自己推断你想干什么。

用LLM
形态你传什么它给你什么
吗
DISCOVERY关键词 queryFTS5命中按会话去重,每条带摘录片段、命中点上下窗、头尾否
搜书挡
SCROLL 翻会话id + 锚点消息以锚点为中心上下若干条(默认5,最多20),不搜不书挡否
id
READ 读只传会话id整段会话(太长就取头20+尾10)否
BROWSE 逛什么都不传最近若干会话的标题、预览、时间戳否

这种「没有模式开关,看参数办事」的设计,我挺欣赏。一个工具承担四件事,对调用它的Agent来说认知负担最小,不用先想清楚自己处在哪个mode,传它手头有的东西就行。READ这个形态还多干一件实事:你在聊天里贴一个会话链接进来,它能跨profile只读地把那段会话翻出来给你看,这是「跨身份记忆互通」的入口。

给中文用户的彩蛋:双FTS5表

这一点旧书完全没提,但对中文读者来说很值钱。

FTS5默认的分词器(unicode61)有个老毛病:碰到中文会逐字切开。你搜「大别山项目」,它内部变成「大AND 别 AND 山 AND 项 AND 目」,既容易误报,又匹配不到精确短语。日文、韩文、泰文也是同一个坑。

新版建了两张FTS5虚拟表来解决。一张是默认的unicode61,伺候英文;另一张专门给CJK,用的是trigram(三字符重叠)分词,让任意文字的子串匹配天然可用。

非中文查询

主表 unicode61

中文查询CJK双字表(可用时)

否则 trigram表 / LIKE兜底

6月写上一版时的路由是:英文走主表,BM25排序加高亮;中文且每个词够三个汉字,走trigram表;词太短(像「桂林 OR 漓江」这种两字词),trigram匹配不上,就退回LIKE按时间倒序全表扫。

v0.20之后这里又进了一步。社区贡献者Soju06提交了一个只有两百多行的FTS5分词器,叫cjk_unicode61 ,把连续的中日韩文字切成两两重叠的双字(「项目管理」切成「项目」「目管」「管理」),这样一两个字的中文词也能走索引,不用再全表扫。源码注释里记了旧办法的代价:在几个G的数据库上,一次LIKE扫描要吃三到六秒CPU,是中文用户session_search最主要的开销。有了CJK表之后,trigram表就退居后备。

不过这里有个条件:CJK表只有在那个分词器扩展成功加载时才会建。我在测试安装里翻了state.db,只看到了trigram表,没看到CJK表,大概是我那种从源码直接装的方式没把扩展编进去。官方安装脚本装出来是什么情况,我没验证,你可以用sqlite3 ~/.hermes/state.db ".tables"看一眼有没有messages_fts_cjk 。不管哪种,这一切都在本地、零成本,这就是「为什么Hermes的会话搜索对中文也好用」的答案。

最后这个彩蛋,是给所有人的方法论

去LLM化是真删了代码。但我在翻的时候,发现周边的文档和配置没全跟上,留下两处自相矛盾。

一处在配置示例文件里,旧版那套给「会话摘要」用的LLM配置块还原封不动留着,provider、model、timeout、并发数全在。可新代码根本不读这些。这是一段已经死掉的配置。

另一处更直接。README的特性表里还写着「FTS5 session search with LLM summarization」(我10月20日又看了一眼,v0.22.2的README和文档首页还是这句),而源码注释白纸黑字写着「No LLM calls anywhere」。一个文件说带LLM,另一个文件说全程没LLM,就在同一个仓库里打架。

注意

README说「带LLM摘要」,源码说「全程零LLM」,信哪个?信源码。代码是会运行的事实,文档是可能过期的说法。这个矛盾本身,就是「别信二手解读,去读源码」最好的教学案例。

我把这个彩蛋单拎出来,是因为它指向一件比session_search本身更重要的事。AI工具迭代太快,文档永远滞后于代码。你看到的教程、别人转述的「它有什么功能」,可能描述的是三个版本之前的样子。真要搞清楚一个工具现在到底怎么干活,唯一可靠的来源是它正在跑的那份源码。

这一节我愿意当成全书「工程克制」的范本来读。它没加任何新东西,反而拿走了一颗看似高级的LLM。换来的是免费、即时、不幻觉,外带对中文的原生友好。下一节我们离开记忆,去看Hermes怎么把一身本事拆成可加载、可共享的Skill。