feat(rag): 检索质量评测 —— 单路检索能力 + recall@k/MRR 评测台

- 引擎抽 searchPaths(三路召回) + SearchByMode(vector/fulltext/graph/hybrid,
  纯检索不 rerank,公平对比);kb_search 加 mode 参数(空=生产含rerank),
  gateway KbSearch 透传 mode
- scripts/rageval.py:标注语料+查询 → 四模式 recall@k/MRR 对比表(可复用)
- live 量化:纯语义改写让全文0.88/图谱0.75 漏召回,混合 1.00 兜回,
  混合=各路上界的稳健组合

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Blizzard
2026-06-30 13:56:39 +08:00
parent 4f52b30f95
commit a17e25b6ba
4 changed files with 157 additions and 11 deletions
+9 -1
View File
@@ -341,7 +341,15 @@ func (g *Gateway) kbSearch(ctx context.Context, call *contract.ToolCall) *contra
if !g.rag.Ready() {
return &contract.ToolResult{OK: true, Content: "[]"}
}
hits, err := g.rag.Search(ctx, kb, q, topK)
// mode 空=生产混合检索(含 rerank);显式 vector/fulltext/graph/hybrid=评测用单路/纯融合(不 rerank)。
mode, _ := call.Args["mode"].(string)
var hits []rag.Hit
var err error
if mode == "" {
hits, err = g.rag.Search(ctx, kb, q, topK)
} else {
hits = g.rag.SearchByMode(ctx, kb, q, topK, mode)
}
if err != nil {
return &contract.ToolResult{OK: false, Error: "kb_search: " + err.Error()}
}
+38 -10
View File
@@ -233,16 +233,7 @@ func (e *Engine) Search(ctx context.Context, kb, query string, topK int) ([]Hit,
}
fanout := topK * 3
// 向量路
vecs, err := e.embed().Embed(ctx, []string{query})
if err != nil || len(vecs) == 0 {
return nil, err
}
vecHits, _ := e.mv.search(ctx, kb, vecs[0], fanout)
// 全文路
ftHits := e.bleve.search(kb, query, fanout)
// 图谱路(GraphRAG:查询提到的实体的相连三元组)
graphHits := e.graph.search(ctx, kb, query, fanout)
vecHits, ftHits, graphHits := e.searchPaths(ctx, kb, query, fanout)
// RRF 融合(三路,按文本去重)
cand := rrf([][]Hit{vecHits, ftHits, graphHits}, fanout)
log.Printf("[rag] hybrid: 向量=%d 全文=%d 图谱=%d → 融合=%d", len(vecHits), len(ftHits), len(graphHits), len(cand))
@@ -277,6 +268,43 @@ func (e *Engine) DeleteDoc(ctx context.Context, kb, fileID string) error {
return nil
}
// searchPaths 跑三路召回,返回各路命中(供混合融合与离线评测按单路对比)。
func (e *Engine) searchPaths(ctx context.Context, kb, query string, fanout int) (vec, ft, graph []Hit) {
if vecs, err := e.embed().Embed(ctx, []string{query}); err == nil && len(vecs) > 0 {
vec, _ = e.mv.search(ctx, kb, vecs[0], fanout)
}
ft = e.bleve.search(kb, query, fanout)
graph = e.graph.search(ctx, kb, query, fanout)
return
}
// SearchByMode 按指定模式返回 topK(评测用,纯检索不 rerank,便于公平对比)。
// mode: vector|fulltext|graph|hybrid(RRF)。
func (e *Engine) SearchByMode(ctx context.Context, kb, query string, topK int, mode string) []Hit {
if !e.Ready() || topK <= 0 {
if topK <= 0 {
topK = 5
}
}
fanout := topK * 3
vec, ft, graph := e.searchPaths(ctx, kb, query, fanout)
var hits []Hit
switch mode {
case "vector":
hits = vec
case "fulltext":
hits = ft
case "graph":
hits = graph
default: // hybrid
hits = rrf([][]Hit{vec, ft, graph}, fanout)
}
if len(hits) > topK {
hits = hits[:topK]
}
return hits
}
func (e *Engine) Close() {
if e.mv != nil {
e.mv.close()