mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-11-06 17:27:31 +00:00
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:
@ -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, "稠密向量路是**唯一召回路径**") {
|
||||
|
||||
Reference in New Issue
Block a user