实测到了什么

九个结果。其中四个杀死了它们自己要证明的那个功能,一个推翻了同一个项目里早先的结论,还有一个根本没有结论 —— 因为样本太小。它们都在这里,理由相同:一条没有协议的检索结论,只是一种偏好。

01怎么读这一页

  • 预注册。标准在跑之前写好。一个在“激发它的那批查询”上测出来的档位是假设,不是结果,会被标为探索性。
  • 配对检验。每一条 A 对 B 的结论都在同一批查询上配对,带 bootstrap 置信区间和对不一致对的 McNemar 检验。胜和负分开报 —— “0 变化得到的净零”和“40 胜 40 负得到的净零”是两件事。
  • 不重开。查询集一旦冻结、结论一旦记录,就算数。新问题要新查询集。
  • pp 是百分点。CI95 是 95% bootstrap 区间。
最大的一条保留,放在最前面说一次

这一页上每一套查询集都是工具作者自己写的。bootstrap 修的是抽样噪声,对“作者知道工具擅长什么”这件事一点办法都没有。这个项目最需要的贡献,是一套别人写的查询集。

02W1 · 四面融合输给了单一个面

默认已撤 479 条查询 · 一个真实的 1,700 条书签库 · 预注册

整个项目的前提就是「四个面融合比任何单一个都强」。三条标准在跑之前就写好了。三条全没达到。

Recall@5Recall@1MRR@10p50
A只用内容向量0.6430.5050.564148 ms
B+两个词面0.589189 ms
C四面全上0.635526 ms
D+上下文+图0.639523 ms

融合付出了 5.4pp 的 Recall@5,并且慢了 3.5 倍。A 档分类型看:内容型 0.959、模糊型 0.706、情景型 0.279

它为什么输

平权重的 RRF 没有任何表达「有多确定」的手段。两个弱面碰巧一致,得 0.0279;一个强面非常确定,得 0.0164。碰巧赢了。这不是调参问题,这就是公式本身的行为。

同一轮里活下来的

幸存者效果胜 / 负p代价
图扩展作为单独一组+2.09pp Recall@510 / 00.00199 ms
重排,对 Recall@1+4.80pp CI95 [+1.46, +8.35]45 / 220.0067

两个都发了出去。注意图扩展只在作为补充时成立 —— 单独成组返回,不合进排名。

03W2/W3 · 情景门先发了,后来输了

发出去之后回滚了默认

情景门识别「和 X 差不多时候存的那个」,然后把检索限制在那个保存窗口内。在它自己的 616 条 holdout 上它赢得很干净,所以发了出去。

查询集对比ΔRecall@5CI95胜 / 负p
616 条 holdoutA → A_gatedctx+3.09pp[1.79, 4.55]19 / 03.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,默认回滚到不加门。

gate_v2 写好了,被毙了

一个更窄的门在原来那 616 条上拿到 +1.79pp,在精度探针上是 −10.52pp。明知道第二个数字存在还拿第一个发货,等于挑了一套能给出我们想要的答案的查询集。没发。

04门的另一面 · 无结论

仅描述 · 低于预注册的样本下限

精度探针问的是「它触发了、但不该触发」。这一轮问反面:它多少次该触发却没触发?协议在跑之前就预注册好了,镜像精度那份。

指标
可用探针冻结的 v3 holdout 里的 16q_save_action
门触发了几次16 条里 0 条
漏检率100.0%,Wilson CI95 [80.64, 100.00]
ΔRecall@5(A_gatedctx − A)+0.00pp,CI95 [0.00, 0.00]
McNemar0 得 0 失,p = 1.0,0 个不一致对
协议自检通过 —— 未触发子集必须刚好动 0.00pp,它做到了
结论无。16 低于预注册的 25 条下限
那个零是结构性的,不是让人安心的

门一次都没触发,所以两边跑的是完全相同的代码,逐查询名次一模一样。一个恰好为零、且不一致对也为零的 Δ,不是「门无害」的证据 —— 它是「什么都没测到」的证据。可检测最小效应在这里无定义,因为公式要除以不一致对的个数,而它是 0。

这 16 条全部是在说「我收起来的那个」 —— 之前收起来的那个the link I set aside我塞进清单里的那篇。没有一条包含门的触发词表里的词 —— 那张表目前看的是 保存收藏savedbookmark 等十几个。

那个看上去最自然的动作 —— 把这 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@50.0000pp,CI95 [0.00, 0.00]
冷页面2,376 中的 8 个
230 个目标里的冷页面0
第一轮测的是一个根本没开的仪器

health 表里是零行,2,376 页的 open_count 全是 0。那一层根本无法触发,因为它没东西可读。一个协议执行得很正确的、干净的零 —— 测的是虚空。

第二轮用同一份字节,先跑了一次本地健康检查。

第二轮出厂值(0.02)可达值(0.0)
Recall@50.58600.5714
Recall@10.42370.4188
救援阀打开616 中的 417616 中的 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 statsfacetmark health --check 会把 never_opened_selects_everythinghealth_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 贴出来。这一件事比任何功能请求都有价值,而且它恰好是作者在结构上做不了的那件事。