第 5 章 · 02 8 种记忆 provider 与 FTS5 会话搜索


第 5 章 · 02 8 种记忆 provider 与 FTS5 会话搜索

本节摘要:有界策展解决"记少而精",大规模记忆交给可插拔 provider:plugins/memory/ 下并列 8 种后端——honcho(用户建模)/mem0(LLM 事实抽取)/hindsight(知识图谱)/holographic(本地 SQLite+FTS5+HRR 向量)/retaindb(云混合检索)/byterover(brv CLI 知识树)/supermemory(语义长记忆)/openviking(火山引擎上下文库)——同一套 MemoryProvider ABC(is_available/initialize/prefetch/sync_turn/get_tool_schemas...),同时只激活一个。跨会话的另一条记忆通路是会话历史搜索:hermes_state.py 用 SQLite FTS5 虚拟表对全部历史消息建全文索引(session_search 工具),支持 BM25/短语/布尔/前缀语法;针对 CJK,原生扩展 native/fts5_cjk/fts5_cjk.c(252 行 C)包装 unicode61 分词器,把 CJK 连续段重切为重叠二元组(Lucene CJKAnalyzer 语义),使两字中文/韩文词也能走索引而非 3-6 秒全表 LIKE 扫描。

内容来源:原项目源码 agent/memory_provider.py(ABC)、plugins/memory/*/README.md 与各 provider 实现、hermes_state.py(FTS_CJK_TABLE_SQL/load_fts5_cjk_extension)、hermes_state_search.py(查询清洗/CJK 路由)、native/fts5_cjk/fts5_cjk.cnative/fts5_cjk/build.sh

⚠️ 注意:8 种 provider 是互斥选择而非叠加:MemoryManager 只接受一个外部 provider,hermes config set memory.provider <name> 切换。FTS5 的 CJK 大表 messages_fts_cjk 仅当 libfts5_cjk.so 可加载时才存在;加载失败的进程会自愈式删掉触发器继续写消息(索引变陈旧,等下次 hermes sessions optimize-storage 在有能力的机器上重建)。

学习目标

阅读完本节,你应当能够:

  1. 横向对比 8 种 provider 的机制、依赖与适用场景。
  2. 说出 MemoryProvider ABC 的核心生命周期钩子与"一 provider"约束。
  3. 讲清 FTS5 会话搜索链路:查询清洗→MATCH→BM25/snippet。
  4. 解释 cjk_unicode61 分词器为什么用 bigram:unicode61 的整段切分缺陷与 trigram 的 3 字符门槛。
  5. 描述 CJK 扩展的注册方式与 external-content 表+触发器门控的存储纪律。

一、8 种 provider 横向对比

provider 机制 依赖 适用
honcho AI 原生跨会话用户建模,多轮辩证推理+会话摘要+双向 peer 工具 honcho-ai 包+云账号/自托管 深度理解"用户是谁"
mem0 服务端 LLM 事实抽取+语义搜索+多信号混合检索(Platform v3 API,亦可 OSS 本地) mem0ai 包+API key 自动化事实记忆
hindsight 知识图谱+实体消解+多策略检索;云/本地内嵌/本地外置三模式 云 key 或本地 LLM 关系型知识
holographic 本地 SQLite 事实库:FTS5+信任分+实体消解+HRR 组合式向量检索 无(SQLite 自带,NumPy 可选) 零依赖本地方案
retaindb 云记忆 API:向量+BM25+重排混合检索,7 种记忆类型 付费账号 云端全托管
byterover brv CLI 层级知识树:模糊文本→LLM 驱动的分层检索 byterover CLI CLI 生态用户
supermemory 语义长记忆+画像召回+显式记忆工具+整会话摄取 supermemory 包(托管/自托管) 语义召回
openviking 火山引擎(字节)上下文库:文件系统式知识层级+分级检索+自动抽取 openviking-server 中文/字节生态

八者覆盖了记忆后端的光谱:云端 SaaS(honcho/mem0/retaindb/supermemory)↔ 纯本地(holographic);抽取式(mem0/openviking)↔ 图谱式(hindsight)↔ 层级式(byterover);其中 holographic 连 FTS5 都复用 SQLite 自带,与下一节的会话搜索同源。安装统一走 hermes memory setup 交互向导(自动装依赖、写 ~/.hermes/.env 密钥),或手工 hermes config set memory.provider <name>;mem0 还支持 mode: oss 切自管进程内模式,hindsight 有云/本地内嵌/本地外置三形态,honcho 提供 OAuth/设备码/API key 三种接入(SSH 无头机用设备码)。检索语义也各有侧重:holographic 的信任分(default_trust: 0.5)给事实加权;retaindb 的 7 种记忆类型区分事实/偏好/事件等;plugins/memory/query_rewrite.py 则在查询侧做统一的改写预处理。

二、provider 插件接口:MemoryProvider ABC

agent/memory_provider.py:104 定义抽象基类,核心钩子分四组:

104 class MemoryProvider(ABC): 107 @property 109 def name(self) -> str: ... # 'honcho'/'hindsight'/... 115 def is_available(self) -> bool: ... # 只查配置与依赖,不发网络请求 123 def initialize(self, session_id, **kwargs) -> None: ... # kwargs 恒有 hermes_home/platform;可含 agent_context(primary/subagent/cron/flush # — 非 primary 上下文应跳过写入)、agent_identity、user_id 等 157 def system_prompt_block(self) -> str: ... # 静态信息进系统提示 166 def prefetch(self, query, *, session_id="") -> str: ... # 轮前召回:要求快,后台线程做真活,这里返回缓存结果 180 def queue_prefetch(self, query, *, session_id="") -> None: ... # 轮后排队,为下一轮 prefetch 备料 201 def sync_turn(self, user_content, assistant_content, ...) -> ... # 轮后同步:把本轮对话喂给后端 220 def get_tool_schemas() -> ... / handle_tool_call(...) # 暴露自己的工具

设计上有两个易漏的细节:initializeagent_context 参数要求 provider 在 cron/subagent/flush 上下文跳过写入——"cron system prompts would corrupt user representations"(定时任务的系统提示会把用户画像带偏);prefetch 的契约是"快",重活必须在 queue_prefetch 的后台线程里预做。可选钩子还有一排:on_session_end(真会话边界触发,可全量摄取)、on_pre_compress(压缩前交出 provider 记忆防丢)、on_delegation(子 agent 任务沉淀)、recall_status(给 UI 一行"recalled N memories"状态)、get_config_schema/save_config(向导可配置项)、backup_paths(备份纳入)。第 5-01 节的 MemoryManager 负责把这些钩子接到 run_agent.py 的四个时机(构造/组 prompt/轮前/轮后),并通过 normalize_tool_schema 兜住两种 schema 形状——包错一层就会让 DeepSeek 以 HTTP 400 拒掉整个请求(#47707);memory_provider_tools_exposed 还确保 provider 的系统提示块与其工具同进同退,不让提示广告不存在的工具(#81014)。

三、FTS5 会话全文搜索

会话历史的每一行消息都进 state.db 的 FTS5 虚拟表,session_search 工具走 hermes_state_search.py:1722_search_messages_impl:

1724 """Full-text search across session messages using FTS5. 1726 Supports FTS5 query syntax: 1727 - Simple keywords: "docker deployment" 1728 - Phrases: '"exact phrase"' 1729 - Boolean: "docker OR kubernetes", "python NOT java" 1730 - Prefix: "deploy*"

用户输入先过 _sanitize_fts5_query(1180 行):剥 FTS5 特殊字符(+{}():"^@/#&|~[]<>,;!?$=)、给带连字符/点号的词加引号(it'sgateway/run.pya,b 这类词裸放会炸语法)、截到 MAX_FTS5_QUERY_CHARS。命中结果按 BM25 rank 排序,或按时间序(newest/oldest 以时间戳为主键、rank 决胜),每条带 snippet() 摘要与前后文。长查询还有一层防哑火(1327 行注释):FTS5 词间隐式 AND,一个罕见短词就能让整条查询零命中——查询改写层会把多词查询组织成 OR 组合的宽松形态再试。一个体贴的语义细节:被用户"回退"(active=0, compacted=0)的行默认排除——"the user took those back";而**压缩归档行**(compacted=1)默认仍可搜——被摘要掉的原始转写仍是会话记录的一部分(#38763)。索引不健康时有 LIKE 全表兜底路径(_search_messages_like_fallback),维护则靠后台的 bounded merge pass 与 hermes sessions optimize-storage(84 行起:"Run one bounded FTS5 merge pass without failing the completed write")。

四、native/fts5_cjk:为中文做的 C 扩展

问题陈述写在 fts5_cjk.c 头注释里:unicode61 把一整段 CJK 当一个 token("웅기가말했다" 索引成单个 6 字符词,2 字查询永远匹配不进去);自带的 trigram 分词器能做子串但要每个查询词 ≥3 字符——两字词(일본、구글、以及大量中文双字词)退化为全表 LIKE,在 6.8GB 消息表上实测 3-6 秒,是 session_search 延迟的头号来源。解法(12-18 行):"wrap unicode61. Every token it emits is re-examined; maximal CJK runs inside the token are re-emitted as overlapping character BIGRAMS (Lucene CJKAnalyzer semantics) ... Because FTS5 turns consecutive tokens emitted from one query term into a phrase, a query word like 캘린더 → [캘린][린더] gets exact substring semantics with index-speed lookups, down to 2-char terms."。核心实现 cjk_emit(96 行起):

102 /* Fast path: no CJK anywhere → pass through untouched. */ ... 131 /* CJK run: collect char byte-boundaries, emit bigrams. */ ... 142 while (i < nToken) { ... 147 rc = p->xOuterToken(p->pOuterCtx, tflags, 148 pToken + bounds[0], bounds[2] - bounds[0], ...); 151 bounds[0] = bounds[1]; /* 滑窗前移一格 */ 152 bounds[1] = bounds[2]; ... 155 if (rc == SQLITE_OK && nChars == 1) { 156 /* lone CJK char: emit as unigram */

CJK 判定(cjk_is_cjk,34 行)用码点区间覆盖谚文音节/Jamo、中日统一表意文字(含扩展 A-F)、平假名、片假名;UTF-8 解码器(52 行)手写逐码点推进,非法字节按原样吐出保证分段可终止;非 CJK 段原样直通,英文行为零变化;每个子 token 的偏移量都 clamp 回原区间(highlight 高亮不越界,匹配不受影响)。注册入口 sqlite3_ftscjk_init(231 行)通过 SELECT fts5(?) 绑定 fts5_api_ptr 拿 fts5_api 指针,注册名为 cjk_unicode61 的分词器——额外参数透传给 unicode61(如 remove_diacritics 2);Windows 下入口函数 __declspec(dllexport) 导出,另留 sqlite3_fts5_cjk_init 下划线别名。编译与加载:

# native/fts5_cjk/build.sh(核心一步) gcc -shared -fPIC -O2 fts5_cjk.c -o libfts5_cjk.so cp libfts5_cjk.so ~/.hermes/lib/
# hermes_state.py:3487(节选) 3487 def load_fts5_cjk_extension(conn: sqlite3.Connection) -> bool: 3497 path = fts5_cjk_so_path() # ~/.hermes/lib/libfts5_cjk.so 3501 conn.enable_load_extension(True) 3503 conn.load_extension(str(path)) 3505 conn.enable_load_extension(False)

效果对照(以"漓江"两字词查 6.8GB 消息表):

分词器 建索引行为 两字 CJK 查询
unicode61(默认) 整段 CJK 作单 token 永不命中,退 LIKE 3-6s
trigram 三字符滑窗 <3 字词零命中,退 LIKE
cjk_unicode61 CJK 段切重叠 bigram 索引速度短语精确匹配

这段扩展由社区贡献(PR #65544,Soju06)。

五、CJK 索引的存储纪律与查询路由

建表 DDL(hermes_state.pyFTS_CJK_TABLE_SQL)用 external-content 模式:虚表挂在排除 tool 行的视图上(WHERE role <> 'tool'——工具输出约占消息字节九成且是机器噪声),零内联文本副本。触发器用专属水位标记对(fts_cjk_rebuild_high_water/fts_cjk_rebuild_progress)门控:trigram→cjk 的升级回填不会卡住主索引的触发器;索引陈旧时触发器必须保持 DROP——"an external-content 'delete' op for a rowid the index never held is the canonical FTS5 index-corruption hazard"(对索引里不存在的 rowid 发 delete 是 FTS5 索引损坏的经典路径)。查询侧(hermes_state_search.py:1878 起):只要大表可用且不含孤立单字 CJK,一切 CJK 查询形状统一走 bigram 路由——每个词加引号变成短语(FTS5 会把一个词切出的连续 bigram 当短语匹配,即精确子串语义),沿用主路径的 BM25 排序与 snippet;例外只剩 role='tool' 过滤(大表没索引 tool 行,退 LIKE)与孤立单字(LIKE 子串语义更宽)。

💡 循环要点:本章两节合成记忆器官的完整解剖:小而硬的本地有界记忆(MEMORY.md/USER.md)+大而软的外部语义 provider(8 选 1)+全量可溯的会话全文检索(FTS5+CJK bigram)。三层各司其职——注入靠小,召回靠检索,理解靠 provider。而 fts5_cjk 用 252 行 C 代码换来中文两字词从"3-6 秒全表扫描"到"索引速度"的跨越,是整个项目里中文生态诚意最硬核的注脚。

本节要点回顾

  1. 8 provider 光谱:云端(honcho/mem0/retaindb/supermemory)↔本地(holographic 无依赖)↔图谱(hindsight)↔层级(byterover)↔字节生态(openviking);同时只激活一个。
  2. ABC 四组钩子:可用性/初始化(agent_context 区分 primary/cron/subagent,非 primary 不写)、prompt 块、轮前 prefetch(要快)+轮后 queue_prefetch(真活)、轮后 sync_turn;可自带工具 schema。
  3. FTS5 搜索:查询清洗(剥特殊字符/引号包裹连字词)→MATCH→BM25 或时间排序;回退行默认排除、压缩归档行默认可搜。
  4. cjk_unicode61=unicode61 包装器:CJK 连续段切重叠 bigram,单字出 unigram,非 CJK 直通;两字词获得索引级精确子串语义。
  5. 存储:external-content 挂无 tool 行视图、水位标记门控触发器、陈旧索引保持触发器 DROP 防腐坏;加载失败自愈降级。
  6. 查询路由:大表可用即统一 bigram 短语路由,例外仅 tool 行过滤与孤立单字。

记忆与技能都会注入 prompt——下一章研究注入的纪律:prompt caching 神圣不可侵犯的铁律、三层组装流水线,以及上下文压缩与 micro-compaction 如何在省钱与保缓存之间走钢丝。


作者与出处
原作者: 灏天文库
整理: 灏天文库整理
本站整理收录,版权归原作者/开源协议所有;欢迎通过原文链接访问源仓库。
发布者: 作者: 灏天文库 转发
评论区 (0)
U