b26fe21408
排查"向量路为什么是空的"花了半小时,因为空就是空,没有任何线索。这次把
整条检索链上的静默降级一次清掉。
真 bug(不只是可观测性):
- kb_search 与 Search() 都拿 rag.Ready() 当总闸,而 Ready() 只代表"向量路
可用"(embedding + Milvus)。全文(bleve)与图谱(Neo4j)根本不依赖它们,却
被一并毙掉 → "模型配置没下发"表现为"整个知识库什么都搜不到",还不报错。
改为逐路判定,任一路可用就仍有召回。
不再吞错:
- milvus.search 原先把 error 转成 nil,nil —— 检索失败与无召回彻底无法区分;
- bleve.search / graph.search 出错直接回 nil,连日志都没有;
- searchPaths 丢掉 embedding 的 error。
三处改为如实返回,错误统一打日志。
逐路诊断(RouteDiag):每路上报 ok/empty/disabled/error + 耗时 + 原因,经
kb_search 的 diag 参数(仅试验台传,生产调用返回值不变)→ gateway → 检索
试验台。界面上现在能直接看出"这一路没配置/报错了/确实没匹配",不必翻日志。
内存兜底索引也会在 note 里点明"重启即清零"。
测试:3 组,覆盖"无 embedding 时全文仍可召回"、三种空的区分、内存索引提示。
把总闸加回去验证过第一条确实会红——测试能抓到这个回归,不是摆设。
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
109 lines
4.0 KiB
Go
109 lines
4.0 KiB
Go
package rag
|
||
|
||
import (
|
||
"context"
|
||
"testing"
|
||
)
|
||
|
||
// 这组测试钉死「一路挂了不能拖垮全部」以及「三种空必须分得清」。
|
||
//
|
||
// 背景:embedding 未配置时,kb_search 与 Search() 都用 Ready() 当总闸直接返回空——
|
||
// 而全文(bleve)、图谱(Neo4j)两路根本不依赖 embedding/Milvus。结果是"模型配置没下发"
|
||
// 表现为"整个知识库什么都搜不到",且无任何错误信息,排查时毫无线索。
|
||
|
||
// newFulltextOnlyEngine 造一个只有全文路可用的引擎:无 embedding、无 Milvus、无 Neo4j,
|
||
// 正是 mcp-go 先于 gateway 启动(控制面配置尚未下发)时的真实状态。
|
||
func newFulltextOnlyEngine(t *testing.T) *Engine {
|
||
t.Helper()
|
||
t.Setenv("BLEVE_PATH", t.TempDir()+"/bleve")
|
||
e := &Engine{bleve: openBleve(), graph: &graphStore{}}
|
||
t.Cleanup(func() { e.bleve.close() })
|
||
if !e.bleve.ready() {
|
||
t.Skip("bleve 不可用")
|
||
}
|
||
if err := e.bleve.index("k1", "d1", []string{
|
||
"星云一号卫星于2023年由长征七号发射,项目总设计师是李明华。",
|
||
"北斗增强终端解决了星间链路的可靠性难题。",
|
||
}); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
return e
|
||
}
|
||
|
||
// 没有 embedding/Milvus 时,全文路仍须照常召回——不能被总闸一刀切。
|
||
func TestSearch_FulltextSurvivesWithoutEmbedding(t *testing.T) {
|
||
e := newFulltextOnlyEngine(t)
|
||
if e.Ready() {
|
||
t.Fatal("前提不成立:该引擎本应是 not ready(无 embedding/Milvus)")
|
||
}
|
||
|
||
hits, err := e.Search(context.Background(), "k1", "星间链路可靠性", 5)
|
||
if err != nil {
|
||
t.Fatalf("检索报错: %v", err)
|
||
}
|
||
if len(hits) == 0 {
|
||
t.Fatal("embedding 未配置时全文路仍应有召回;返回空说明总闸回归了")
|
||
}
|
||
}
|
||
|
||
// 诊断必须区分三种"空":没配置 / 报错 / 确实没匹配。
|
||
func TestSearchByModeDiag_DistinguishesEmptyKinds(t *testing.T) {
|
||
e := newFulltextOnlyEngine(t)
|
||
ctx := context.Background()
|
||
|
||
_, diags := e.SearchByModeDiag(ctx, "k1", "星间链路可靠性", 5, "hybrid")
|
||
byName := map[string]RouteDiag{}
|
||
for _, d := range diags {
|
||
byName[d.Name] = d
|
||
}
|
||
if len(byName) != 3 {
|
||
t.Fatalf("应有 vector/fulltext/graph 三路诊断,得 %v", diags)
|
||
}
|
||
|
||
// 向量路:没配置 → disabled(不是 empty,更不是静默消失)
|
||
if got := byName["vector"].Status; got != "disabled" {
|
||
t.Fatalf("无 embedding 时向量路应为 disabled,得 %q", got)
|
||
}
|
||
if byName["vector"].Note == "" {
|
||
t.Fatal("disabled 必须给出原因,否则界面上仍是一个无法解释的空")
|
||
}
|
||
// 图谱路:Neo4j 未连 → disabled
|
||
if got := byName["graph"].Status; got != "disabled" {
|
||
t.Fatalf("无 Neo4j 时图谱路应为 disabled,得 %q", got)
|
||
}
|
||
// 全文路:有命中 → ok
|
||
if got := byName["fulltext"].Status; got != "ok" {
|
||
t.Fatalf("全文路应为 ok,得 %q(hits=%d err=%q)", got, byName["fulltext"].Hits, byName["fulltext"].Error)
|
||
}
|
||
|
||
// 换一个库里没有的词:全文路应是 empty(确实没匹配),而不是 disabled/error
|
||
_, diags2 := e.SearchByModeDiag(ctx, "k1", "完全不相关的查询内容xyz", 5, "fulltext")
|
||
for _, d := range diags2 {
|
||
if d.Name == "fulltext" && d.Status != "empty" {
|
||
t.Fatalf("无匹配时全文路应为 empty,得 %q", d.Status)
|
||
}
|
||
}
|
||
}
|
||
|
||
// 内存兜底(索引不持久)必须在诊断里说出来:命中数正常但数据随时会没,
|
||
// 只看 hits 数量完全看不出来。
|
||
func TestDiag_ReportsNonPersistentIndex(t *testing.T) {
|
||
e := &Engine{bleve: memBleve(), graph: &graphStore{}}
|
||
defer e.bleve.close()
|
||
if err := e.bleve.index("k1", "d1", []string{"星间链路可靠性难题"}); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
_, diags := e.SearchByModeDiag(context.Background(), "k1", "星间链路", 5, "fulltext")
|
||
for _, d := range diags {
|
||
if d.Name != "fulltext" {
|
||
continue
|
||
}
|
||
if d.Status != "ok" {
|
||
t.Fatalf("内存索引也应能召回,得 %q", d.Status)
|
||
}
|
||
if d.Note == "" {
|
||
t.Fatal("内存兜底必须在 note 里点明(重启即清零),否则运维看不出这是降级态")
|
||
}
|
||
}
|
||
}
|