01怎么读这一页
- 预注册。标准在跑之前写好。一个在“激发它的那批查询”上测出来的档位是假设,不是结果,会被标为探索性。
- 配对检验。每一条 A 对 B 的结论都在同一批查询上配对,带 bootstrap 置信区间和对不一致对的 McNemar 检验。胜和负分开报 —— “0 变化得到的净零”和“40 胜 40 负得到的净零”是两件事。
- 不重开。查询集一旦冻结、结论一旦记录,就算数。新问题要新查询集。
- pp 是百分点。CI95 是 95% bootstrap 区间。
这一页上每一套查询集都是工具作者自己写的。bootstrap 修的是抽样噪声,对“作者知道工具擅长什么”这件事一点办法都没有。这个项目最需要的贡献,是一套别人写的查询集。
02W1 · 四面融合输给了单一个面
默认已撤 479 条查询 · 一个真实的 1,700 条书签库 · 预注册
整个项目的前提就是「四个面融合比任何单一个都强」。三条标准在跑之前就写好了。三条全没达到。
| 档 | 面 | Recall@5 | Recall@1 | MRR@10 | p50 |
|---|---|---|---|---|---|
| A | 只用内容向量 | 0.643 | 0.505 | 0.564 | 148 ms |
| B | +两个词面 | 0.589 | — | — | 189 ms |
| C | 四面全上 | 0.635 | — | — | 526 ms |
| D | +上下文+图 | 0.639 | — | — | 523 ms |
融合付出了 5.4pp 的 Recall@5,并且慢了 3.5 倍。A 档分类型看:内容型 0.959、模糊型 0.706、情景型 0.279。
它为什么输
平权重的 RRF 没有任何表达「有多确定」的手段。两个弱面碰巧一致,得 0.0279;一个强面非常确定,得 0.0164。碰巧赢了。这不是调参问题,这就是公式本身的行为。
同一轮里活下来的
| 幸存者 | 效果 | 胜 / 负 | p | 代价 |
|---|---|---|---|---|
| 图扩展作为单独一组 | +2.09pp Recall@5 | 10 / 0 | 0.0019 | 9 ms |
| 重排,对 Recall@1 | +4.80pp CI95 [+1.46, +8.35] | 45 / 22 | 0.0067 | — |
两个都发了出去。注意图扩展只在作为补充时成立 —— 单独成组返回,不合进排名。
03W2/W3 · 情景门先发了,后来输了
发出去之后回滚了默认
情景门识别「和 X 差不多时候存的那个」,然后把检索限制在那个保存窗口内。在它自己的 616 条 holdout 上它赢得很干净,所以发了出去。
| 查询集 | 对比 | ΔRecall@5 | CI95 | 胜 / 负 | p |
|---|---|---|---|---|---|
| 616 条 holdout | A → A_gatedctx | +3.09pp | [1.79, 4.55] | 19 / 0 | 3.8e−6 |
| 361 条精度探针 | A → A_gatedctx | −18.83pp | [−23.27, −14.68] | 3 / 71 | — |
第二行是同一个功能,只是换了一套事后才建的查询集去问另一个问题:它在不该触发的查询上触发了,会怎样?Recall@5 从 0.9058 掉到 0.7175,Recall@1 从 0.801 掉到 0.363。
分层一看就全明白了
| 分层 | n | ΔRecall@5 |
|---|---|---|
| 保存窗口里确实有目标 | 57 | +0.00pp —— 恰好是零 |
| 保存窗口里没有目标 | 304 | −22.37pp |
门对的时候,它什么都不加。门错的时候,它把答案扔了。结论 gate_precision_unqualified,默认回滚到不加门。
一个更窄的门在原来那 616 条上拿到 +1.79pp,在精度探针上是 −10.52pp。明知道第二个数字存在还拿第一个发货,等于挑了一套能给出我们想要的答案的查询集。没发。
04门的另一面 · 无结论
仅描述 · 低于预注册的样本下限
精度探针问的是「它触发了、但不该触发」。这一轮问反面:它多少次该触发却没触发?协议在跑之前就预注册好了,镜像精度那份。
| 指标 | 值 |
|---|---|
| 可用探针 | 冻结的 v3 holdout 里的 16 条 q_save_action |
| 门触发了几次 | 16 条里 0 条 |
| 漏检率 | 100.0%,Wilson CI95 [80.64, 100.00] |
| ΔRecall@5(A_gatedctx − A) | +0.00pp,CI95 [0.00, 0.00] |
| McNemar | 0 得 0 失,p = 1.0,0 个不一致对 |
| 协议自检 | 通过 —— 未触发子集必须刚好动 0.00pp,它做到了 |
| 结论 | 无。16 低于预注册的 25 条下限 |
门一次都没触发,所以两边跑的是完全相同的代码,逐查询名次一模一样。一个恰好为零、且不一致对也为零的 Δ,不是「门无害」的证据 —— 它是「什么都没测到」的证据。可检测最小效应在这里无定义,因为公式要除以不一致对的个数,而它是 0。
这 16 条全部是在说「我收起来的那个」 —— 之前收起来的那个、the link I set aside、我塞进清单里的那篇。没有一条包含门的触发词表里的词 —— 那张表目前看的是 保存、收藏、saved、bookmark 等十几个。
那个看上去最自然的动作 —— 把这 16 条加进词表 —— 恰恰是协议禁止的,因为用衡量它的探针去选词表是循环论证。要拿到结论,得用新种子、在冻结参数下新生成至少 25 条探针,然后同时过漏检率这关和 361 条精度那关。在那之前,词表不动。
05五个候选修法,五个结论
W1 杀掉融合之后,五个看上去很显然的修法,逐个拿去测了,而不是拿去辩论了。
| 候选 | 测到什么 | 结论 |
|---|---|---|
| 干脆把词面删了 | 内容型里 80.1%、模糊型里 46.3% 根本不需要向量 —— 但有 6.05%(479 条里 29 条)只有词面能找到,超过预注册的 5% 线。 | 保留 |
| 给面加权重而不是平权 RRF | 两个弱面碰巧一致得 0.0279;一个强面确定得 0.0164。 | 解释了为什么输 |
| 修好中文上的三元组面 | 原本 211 条中文查询只命中 25 条(11.85%)。修后 202/211(95.73%)。整体 Recall@5:没变。 | 修好了,没收益 |
| 把 boost 上限括大 | MAX_BOOST = 1.60 在 A 档能跨越分数区间的 79.7%,在 C/D 只有 20.9%。要有同等位移能力得 6.03。而且 66.3% 的候选拿到的就是 1.0。 | 测了,没发 |
| 把意图面打开 | 50 条生成意图里只有 19 条(38%)站得住,低于预注册的 50% 线。信息词根本不在页面上的比例整体 34.0%,正文贫乏的页面上 62.4%。 | 关 |
第三行是最有意思的。一个真的 bug 被找到并修好了 —— 三元组面从在中文上没用变成能用 —— 而端到端的召回没动。一个确实是修复、但不改变结果的修复,是一个正常结果;把它报出来,是另外四行还能信的唯一理由。
06karakeep 往返 · 不忠实
roundtrip_unfaithful 2,376 条书签 · 616 条 holdout 查询 · 协议先冻结
问题:把一个库推进 karakeep 桥再读回来,它还是同一个库吗?三条标准先写好。
| 标准 | 线 | 实测 | 判定 |
|---|---|---|---|
| 指标忠实度 | |ΔRecall@5| ≤ 3pp 且 CI95 落在 ±5pp 内 | −0.81pp,CI95 [−2.44, +0.81] | 过 |
| 名次忠实度 | overlap@5 中位数 ≥ 4 且 top-1 一致率 ≥ 80% | 中位数 4.0,top-1 79.06% | 差 0.94pp 没过 |
| 读路径等价 | HTTP 与原生在 616×2 上完全一致 | 0 处不一致 | 过 |
原因完全归因清楚
- 正文逐字节相同:1,876 / 1,876。
- 摘要存活:2,375 / 2,375,100%。
- 主题匹配率 0%,实体 1.18% —— 因为 karakeep 的 tag 就是浏览器的文件夹名,不是主题。
- 关键词从 19,016 个不同词塌到 13 个;平均每页从 10.32 跌到 0.76;最常见的标签是
未分类,出现在 1,124 页上。 - 向量的中位余弦位移是 0.9846 —— 很小,但足够把一个 top-5 洗一遍。
把源富化植回去之后,2,376 / 2,376 的嵌入文本逐字节相同,残差为零 —— 归因闭环。重跑一次 facetmark index 就修好了:0 个 karakeep 正文需要重抓,2,376 行全部重新富化,图除了 212 条语义边之外完全一致(26,485 对 26,697)。
指标层面的结论能迁移到一个经 karakeep 富化的库上。名次层面的不能,除非你重建索引。跑了桥,就再跑一次 facetmark index。
07衰减层测了两次 —— 第二次推翻了第一次
衰减层把看起来陈旧的页面往后排。第一轮测完,什么也没测到:
| 第一轮 | 值 |
|---|---|
| ΔRecall@5 | 0.0000pp,CI95 [0.00, 0.00] |
| 冷页面 | 2,376 中的 8 个 |
| 230 个目标里的冷页面 | 0 |
health 表里是零行,2,376 页的 open_count 全是 0。那一层根本无法触发,因为它没东西可读。一个协议执行得很正确的、干净的零 —— 测的是虚空。
第二轮用同一份字节,先跑了一次本地健康检查。
| 第二轮 | 出厂值(0.02) | 可达值(0.0) |
|---|---|---|
| Recall@5 | 0.5860 | 0.5714 |
| Recall@1 | 0.4237 | 0.4188 |
| 救援阀打开 | 616 中的 417 | 616 中的 0 |
| health 行数 | 2,376(原为 0) | 2,376 |
| 冷页面 | 73 —— 3.07%(原为 8,0.34%) | 73 |
| 冷 ∩ 230 个目标 | 8,涉及 19 条查询 | 8 |
ΔRecall@5 从第一轮的 +0.0000pp 变成第二轮的 −1.4610pp,CI95 [−2.5974, −0.4870]。机制是可以数出来的:37 处名次变化里,12 处直接掉出了前 20 —— 其中 10 个原本在前 5,5 个原本是第 1。另有 24 处上升,21 处只升了一名,恰好 1 处进了前 5。净 −10 + 1 = −9,而 −9/616 = −1.4610pp。
为什么阈值至今没改
两个 bug 在互相抵消,而且这个抵消是承重的。
- bug 一:冷层把「URL 死了」当成「存下来的副本没用了」。但 facetmark 存了正文。URL 死了恰恰是本地快照最值钱的时候,
drifted更糟 —— 那时快照是唯一幸存的记录。 - bug 二:
rrf_k = 60下,单个单位权重的面封顶是1/61 = 0.016393,低于救援阈值0.02。所以在出厂的单面 profile 下,救援阀总是开着,那个降权从来没有真正执行过。 - 单拆掉任一个,结果都会实测变差。两者都被
tests/test_decay_reach.py钉住,防止被当成「顺手清理一下」删掉。
真正改的是仪表盘。cold_census() 现在分开报三个条件,facetmark stats 和 facetmark health --check 会把 never_opened_selects_everything 和 health_never_checked 直接叫出来。
还有一个值得留着的细节:8 个受损目标里有 4 个 char_count = 0,却仍然被正确检索出来,靠的是标题和词面。正文丢了不等于检索丢了。
08一个真实库,从头到尾
合成语料会掩盖集成层面的失败。这是一份真实的浏览器导出,用出厂代码路径导入并建索引。
| 阶段 | 结果 |
|---|---|
| 文件 | favorites_2026_8_4.html,1.7 MB,96 个文件夹,四层嵌套 |
| 导入 | 解析 1,710 → 写入 1,701,合并 9 条重复,1 条不可索引 |
| 建索引(不抓页面) | 322 个保存会话、9,132 条边、1,386 个域名、1,775 个向量 |
| 查询延迟中位数 | 2,265 ms |
延迟那个数字诚实而不好看:它是一个未抓取、冷启的索引在笔记本上的数字,也正是发布博文里会被您默默略掉的那一个。
09这些都没测到什么
- 别人的查询是不是长这样。每一套查询集都是工具作者写的。这是这一页上每个数字最大的威胁,而且 bootstrap 一点都碰不到它。
- 衰减层到底有没有用,因为在出厂 profile 下它根本触发不了。
- 意图面在别的库上会不会有用。它只在这个库上、用一个模型生成、然后输了。
- karakeep 桥对真实实例能不能跑。协议钉住了、回放了;一个真在跑的实例从来没测过。
- 真的 cross-encoder 重排有没有用。离线发出去的是词重叠。在那个重排器下跑的消融测的是台架,不是想法,不能引用为「重排有用」的证据。
- 长期行为。每一次测量都是快照。没有人把它跑一年,看一个不断长大的库会把会话聚类搞成什么样。
对着你自己的库写 100 条查询,每条带上目标 URL,存成 JSONL。跑 facetmark eval --no-build --queries yours.jsonl --rungs A,C,full。把 JSON 贴出来。这一件事比任何功能请求都有价值,而且它恰好是作者在结构上做不了的那件事。