mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-10-15 12:15:16 +00:00
feat(knowledge)!: 检索改为纯稠密向量路,去掉三路融合
按「完全改用向量方案」的要求,SearchIn 现在只走多模态稠密路,
不再与稀疏语义路 / 词法路融合。
## 为什么必须去掉融合(实测,不是判断)
生产知识库(192 条)上,10 条带标注查询的命中率:
改前(三路融合) top1= 1/10 top3= 2/10
改后(纯稠密路) top1= 6/10 top3= 7/10
改前连条目**自己的全名**都搜不到:
`plugin_dev_sdk` 搜不到 `plugin_dev_sdk`、`homeagent_identity` 搜不到
`homeagent_identity`,用户看到的是「查什么都返回同样的几个无关条目」。
而稠密路单独打分时**全库排名 1/192**:
the_jet_engine → the_jet_engine_rolls-royce 0.769
plugin_dev_sdk → plugin_dev_sdk 0.852
homeagent_identity → homeagent_identity 0.823
### 根因:逐路归一化把噪声抬到与信号同权
`addPath` 把每路都按**查询内最大值**归一化到 1.0 再加权:
scores[h.id] += weight * h.score / max
于是词法路原始分只有 **0.027** 的弱信号归一化后也变成 1.0,与稠密路的
1.0 等权相加。叠加两个因素:
- jieba 把英文标识符切碎:`the_jet_engine_rolls-royce` →
`[t h e _ j n g i r o l s - y c]`,两条**无关**文档 token 重叠 50%
- `SetMinScore(0)` 导致 186/192 条都进候选,几乎全库打分
余弦分数**本身就可比**(同空间同维度),根本不需要逐路归一化 ——
那一步当初是为「量级不同的三路」服务的,现在只剩一路,它反而成了
破坏排序的东西。
## 历史结论为何在此翻转
旧注释引的实测(仅稠密路 top-1 15% / MRR 0.271;融合后 21% / 0.376)
建立在这两个**已不成立的前提**上:
- 33 条的小规模 KB(现 192 条,含大量英文标识符)
- 稀疏 fastText 词向量(现 chineseclip 512 维多模态空间)
那段结论我没删,而是在 Search 的注释里写明「前提已变、结论翻转」,
免得后来者看到旧数字又把它改回去。
## 退化路径有意保留
未注入多模态空间时(provider 留空)仍走稀疏两路。不保留的话,
那些部署的知识库会直接变成**完全不可用** —— 比排序差更严重。
该路径行为与接入稠密路之前逐字一致(TestNoDenseSpaceKeepsLegacyBehavior)。
## 判据(都可杀变异,逐个实测)
新增 `dense_recall_quality_test.go`:
- `TestDenseRecallQuality`——带标注 10 条查询,门槛 top1≥4 且 top3≥7。
把 denseEnabled 改成恒假 → FAIL「top1 仅 1/10」(正是退化态)
- `TestDensePathIsSoleRecall`——钉形状:稠密路必须在稀疏路之前且
**结果真的参与组装**。第一版只查「有没有调 denseHits」,把结果赋给 `_`
仍能骗过它;补上 `s.items[h.id]` 断言后,变异(结果不参与组装)才被杀
- `TestSearchDocReflectsDenseOnly`——防文档漂移。第一版简单搜「三路」
会误伤(新文档解释「为何不再融合」时正当地提到它),改为检查只在
*声称当前如此* 时才会出现的句子
## 顺带修的
- `TestDenseCrossModalRecall` 的桩把**所有未知文本**映射到同一向量,
导致 `other` 与 `cat-photo` 正文同向、「猫」查询下 `other` 以余弦 1.0
压过 `cat-photo` 的 0.707。那是**桩的假象不是产品缺陷**。给 other
一个真正正交的方向(且 key 必须是 denseFor 实际传入的
`Name + " " + Content`,写成纯 Content 会静默回落到默认向量)。
- 清掉随融合退场的死代码 `denseSpaceWeight` / `hasAnyDense`,
并把 `vec`/`lex` 字段注释改准(它们现在只服务退化路径)。
- `rankdiag_test.go` 现在测的是**退化路径**(它不注入多模态空间),
注释与门槛说明据此改准:实测 top-1 24% / MRR 0.358,
别拿它当知识库的检索水平。
全仓 `go test ./...` → 48 包 ok / 0 FAIL。
This commit is contained in:
@ -62,13 +62,27 @@ func TestDenseCrossModalRecall(t *testing.T) {
|
||||
}
|
||||
defer s.Stop()
|
||||
|
||||
// 两个正交方向:eax 表示“猫的图”,eby 表示“别的”
|
||||
// 两个正交方向:eax 表示“猫的图”,eby 表示“别的”。
|
||||
//
|
||||
// ★ 桩的 key 必须与 denseFor 实际传入的文本**逐字一致**:
|
||||
// denseFor 算的是 `k.Name + " " + k.Content`,不是纯 Content。
|
||||
// 写成纯 Content 会静默回落到 __default,两条条目就拿到同一向量。
|
||||
//
|
||||
// ★ ecy(第三个正交方向)给「别的条目」的正文:若 other 与 cat-photo
|
||||
// 的正文同向量,纯稠密路下“猫”查询会让 other(余弦 1.0)排在
|
||||
// cat-photo(文本⊕图融合后 0.707)之前——那不是产品缺陷,是
|
||||
// **桩把所有未知文本映射到同一向量**造成的假象。
|
||||
eax := []float64{1, 0, 0, 0}
|
||||
eby := []float64{0, 1, 0, 0}
|
||||
ecy := []float64{0, 0, 1, 0}
|
||||
mm := &fakeMM{
|
||||
dim: 4, fp: "fake-v1", loaded: true,
|
||||
text: map[string][]float64{"__default": eby, "猫 图片 说明": eby},
|
||||
img: map[string][]float64{"__default": eby, "d-cat": eax},
|
||||
text: map[string][]float64{
|
||||
"__default": eby,
|
||||
"cat-photo 猫 图片 说明": eby,
|
||||
"other 完全无关的正文": ecy,
|
||||
},
|
||||
img: map[string][]float64{"__default": eby, "d-cat": eax},
|
||||
}
|
||||
s.SetDenseSpace(mm)
|
||||
s.SetMediaGetter(fakeMedia{byDigest: map[string][]byte{"d-cat": []byte("d-cat")}})
|
||||
@ -96,7 +110,10 @@ func TestDenseCrossModalRecall(t *testing.T) {
|
||||
if !(hits[0].score > 0.5) {
|
||||
t.Errorf("cat-photo 与查询向量应显著同向,实为 %f", hits[0].score)
|
||||
}
|
||||
// 正文里的“猫”字查询也应召回它(文本路 + 稠密路共同作用)
|
||||
// 正文里的“猫”字查询也应召回它。
|
||||
// ★ 纯稠密路下这里的分数是 1.0(查询向量与 cat-photo 正文向量同向),
|
||||
// 而 other 是正交的 0 —— 前提是桩给 other 一个**真正无关**的方向
|
||||
// (见上面 ecy 的说明)。
|
||||
if got := s.Search("猫", 3); len(got) == 0 || got[0].Name != "cat-photo" {
|
||||
t.Errorf("按正文查询应召回 cat-photo,实为 %v", namesOf(got))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user