关键词已经够快,也够准
单看一个词,命中就是索引里真的有它,不是算出来的相似度;多词按「或」召回,宁可多给几条,再让模型筛。
单次查询 0.02–1.1 ms
这一页不介绍功能,只回答一个问题:凭什么信。 下面每一条数字都来自一次真实的对照读数;每一条被推翻的设计,都曾经看起来很有道理。
这是前身项目留下的负结果台账,也是新项目的立项依据。写在这里的每一条,都曾经是「显然正确」的设计。
12 条按三种疑问分:4 条在问「打分够不够」,2 条在问「能不能自动合并」,6 条在问「这个能力是不是真的在跑」。
| # | 原设计 | 实测读数 | 改成了什么 |
|---|---|---|---|
| 用分数代替判断(4 条) | |||
| 01 | 模型自报值 192/285 挤在 ≥0.9,全库只有 8 个不同取值 | 等于没查;按分数过滤也筛不出什么 | |
| 02 | 五个向量模型都分不开:对的和错的在分布上重叠 | 判定不用它;召回也先用关键词(见下) | |
| 03 | 能挡掉 6/6 条不该要的,但把 3/4 条本该要的也挡了 | 怎么调都分不开好坏;放弃这套打分 | |
| 04 | 6 条错的里,有 3 条被它判成了对 | 改到「实体」这一层再做印证 | |
| 能不能自动合并(2 条) | |||
| 05 | 覆盖率 8.7%(9/103);再放宽就是错并 | 改成「重复不写」+ 先粗筛候选,不自动合并 | |
| 06 | 被 >1 个来源写过的实体 0 条 | 只记冲突、不自动覆盖;猜错就把原话盖掉了 | |
| 这个能力是不是真的在跑(6 条) | |||
| 07 | 10 条真实查询,本该命中 22 次,结果 0 次,还不报错 | 改成逐词归一 + 影子列;10 条里 9 条查得到 | |
| 08 | 事务嵌套后每条写都悄悄失败,进度照常往前走 | 改成允许重投、写入幂等 | |
| 09 | 三张空间表在库里根本不存在;located_at 出现 0 次 | 只存在于纸上,砍掉 | |
| 10 | 每轮能扫的窗口正好被置顶和归档占满,实际处理 0 条 | 等于装饰,砍掉 | |
| 11 | attention 表没有任何面向人的读者,只到「已落库」为止 | 没人看得到,就算没交付 | |
| 12 | MCP 没有推送通道,系统只能等 agent 来问 | 「主动递来历史」从来没做到 | |
第 02 条讲的是「判定」;这里讲「召回」。三条理由按分量排。
单看一个词,命中就是索引里真的有它,不是算出来的相似度;多词按「或」召回,宁可多给几条,再让模型筛。
常驻的向量化模型要占内存和 CPU;云端的 embedding 服务我们也还没测过,不把没测过的东西放进主路径。
多一份索引、同步、失效与重建;现在的场景关键词基本够用。真需要时,做成可选接口加独立表,不动核心。
实测口径:本机中心 memory.db 只读打开,rc_shadow 1,045 行,2026-11-09; SQLite FTS5 MATCH,不含网络与结果组装。多词是「或」不是「且」——召回宁可宽一点,由模型和闸收窄。 另外,没有 LLM 时入库后的关键词检索照常可用;缺的是抽取,不是查。
负结果的反面就是约束。它们不是愿景,而是合并代码前要过的一关。
旧写法只断言 ok == true;新写法断言真正写了几行。 差别不在风格,而在于——前者对「什么都没写」永远为真。
// 返回了 Ok,但一行都没写 let report = cycle_once(&db)?; assert!(report.ok); // 事务不可重入 + 读走独立连接池 // ⇒ 每条写静默失败,水位照常推进 // ⇒ 这个测试连绿三个版本
// 断言真实行数与水位差 let before = count_rows(&db)?; cycle_once(&db)?; let after = count_rows(&db)?; assert_eq!(after - before, 3); assert!(watermark(&db)? > before_wm);
把「跑过什么」和「还没跑什么」分开写,才不会把设计稿读成线上真实数据。