fix(knowledge): 改成词法路为主,RRF 方案实测无效

## 上一版(RRF)在生产上零收益

73bc3f9 用「稠密 1.0 / 词法 0.5 的 RRF」重启部署后,生产上 7 条查询
结果基本没变,目标条目一次都没进前三。原因是我在**本地退化路径**上验证,
而生产走稠密路径——两个分支的融合逻辑根本不同。

在真实 196 条库 + 真实 chineseclip 上测出两路的名次,问题一目了然:

    查询                        稠密名次   词法名次
    PruneOldBackups                103         1
    strip_path                     164         1
    minSceneEvidence               130         1
    coldcontext_query              149         1
    上下文裁剪的保护条数受什么约束    157         1
    冷上下文取回为什么不能先删后灌    158         1

词法路全中,稠密路全在 100 名外。稠密权重 1.0 时它的 `1.0/61` 永远压过
词法的 `0.5/61`,所以词法路精确命中也没用。

## 为什么连 RRF 本身也不行

试过把权重翻成词法 1.0 / 稠密 0.5,仍然错。因为 RRF **丢掉分数差距**:
`strip_path` 的 top1 是 0.056、top2 是 0.055 —— 名次差 1,RRF 当它们等强,
稠密对 top2 的加分一叠就反超。

## 现在的做法:fuseLexPrimary

- 词法路给主序,**保留分数差距**(这是关键词)
- 稠密路用于:同分裁决、补位、以及词法无信号时接管
  (`minLexSignal = 0.02`,低于此视为 jieba 泛滥词的微弱命中,让稠密接)
- 稠密结果仍然被使用,不是死代码

## 实测(真实库 + 真实模型)

    top1 = 10/13   top3 = 11/13

改前生产表现:7/7 全错。剩下的 3 条未命中(仓颉关键字 rank2、鸿蒙、更新日志)
是既有的弱点,与本次改动无关。

## 判据

- `TestDensePathFeedsRanking`:钉「稠密结果必须喂进排序」(改了形状,意图不变)
- `TestSearchDocReflectsFusion`:钉文档写明 fuseLexPrimary + 词法为主
- 文档注释里记下两次翻转(2026-10-08 → 10-31)与 RRF 被否的实测原因,
  避免下一个人再翻回去
This commit is contained in:
JianFeeeee
2026-10-31 16:57:29 +08:00
parent 73bc3f94b7
commit 0365b08dc9
3 changed files with 124 additions and 86 deletions

View File

@ -162,18 +162,13 @@ func TestDensePathFeedsRanking(t *testing.T) {
// 这个反模式实测踩过:把结果赋给 _ 再走别的路径,排序完全落到另一路上,
// 而表面上「稠密路接了」看起来是好的。
//
// 2026-10-31 起稠密路不再是唯一路径(改成与词法路 RRF 融合),
// 但「结果必须喂进排序」这条不变,只是喂给 fuseRRF 而不是直接返回。
if !regexp.MustCompile(`(\w+)\s*:=\s*s\.denseHits\(qv\)`).MatchString(body) {
t.Fatal("稠密路的结果必须被接住(x := s.denseHits(qv))")
// 2026-10-31 起的合并策略是 fuseLexPrimary(词法为主,稠密做同分裁决与补位),
// 但「稠密结果必须喂进排序」这条不变。
if !regexp.MustCompile(`dense\s*:=\s*s\.denseHits\(qv\)`).MatchString(body) {
t.Fatal("必须写成 dense := s.denseHits(qv)(稠密路结果要被接住)")
}
if !regexp.MustCompile(`fuseRRF\(\s*(\w+)`).MatchString(body) {
t.Error("稠密路与词法路必须经 fuseRRF 融合;稠密结果不能只接不用")
}
// 稠密路产物必须真的作为融合输入之一(而不是另外造一份空的)。
if !regexp.MustCompile(`dense\s*:=\s*s\.denseHits\(qv\)`).MatchString(body) ||
!regexp.MustCompile(`fuseRRF\(\s*dense\s*,`).MatchString(body) {
t.Error("必须写成 dense := s.denseHits(qv) 然后 fuseRRF(dense, ...)")
if !regexp.MustCompile(`fuseLexPrimary\(\s*lex\s*,\s*dense\s*\)`).MatchString(body) {
t.Error("两路必须经 fuseLexPrimary(lex, dense) 合并;稠密结果不能只接不用")
}
if !regexp.MustCompile(`if k, ok := s\.items\[h\.id\]`).MatchString(body) {
t.Error("融合结果必须参与条目组装(s.items[h.id]),不能是摆设")
@ -229,11 +224,10 @@ func TestIdentifierTokenFeedsLex(t *testing.T) {
}
}
// TestSearchDocReflectsRRF 防止文档与实现漂移。
// TestSearchDocReflectsFusion 防止文档与实现漂移。
//
// 2026-10-31 起语义变为「稠密 + 词法 RRF 融合」,本判据随之更新:
// 文档必须声明 RRF,且不得再声称「稠密路是唯一召回路径」。
func TestSearchDocReflectsRRF(t *testing.T) {
// 2026-10-31 起语义变为「词法为主、稠密为辅」,本判据随之更新。
func TestSearchDocReflectsFusion(t *testing.T) {
src := readSourceFile(t, "knowledge.go")
i := strings.Index(src, "// Search ")
if i < 0 {
@ -241,9 +235,12 @@ func TestSearchDocReflectsRRF(t *testing.T) {
}
doc := src[i:min(len(src), i+2500)]
// ① 必须声明 RRF 融合(这是现行接口语义)。
if !strings.Contains(doc, "RRF") {
t.Error("Search 文档必须写明「稠密路 + 词法路按 RRF 融合」")
// ① 必须声明当前的合并策略(这是接口语义)。
if !strings.Contains(doc, "fuseLexPrimary") {
t.Error("Search 文档必须写明合并入口 fuseLexPrimary(词法为主、稠密为辅)")
}
if !strings.Contains(doc, "词法路为主") {
t.Error("Search 文档必须写明「词法路为主」——否则读代码的人会以为稠密路是主线")
}
// ①' 不得再声称稠密路是唯一路径(已与实现不符)。
if strings.Contains(doc, "稠密向量路是**唯一召回路径**") {