犀牛鸟 2026 研究笔记 estelledc.github.io

Ch24: 三路检索源码——BM25、向量与并行调度

Part 8: WeKnora 源码走读 前置:Ch23 EventManagerCh04 检索三路军 后续:Ch25 RRF 与 Rerank 源码


结论先行:BM25 守 精确词,向量守 语义,图谱守 关系(Ch26)。源码层重点看:索引怎么建、query 怎么编码、TopK 在哪截断、三路结果结构是否统一(供 RRF 消费)。

一、BM25 路

索引阶段(文档摄入后):

查询阶段

// 概念流程
terms := tokenize(query)
scores := invertedIndex.Score(terms)  // BM25 公式
top := selectTopK(scores, kRetrieve)
return toChunkHits(top)

大师调试:query 只含错误码时,BM25 应排第一;若向量压过 BM25,查融合权重与 RRF 是否过度偏语义路。

BM25 打分(复习):

\[score(D,Q) = \sum_{t \in Q} IDF(t) \cdot \frac{f(t,D)(k_1+1)}{f(t,D)+k_1(1-b+b|D|/avgdl)}\]

二、向量路

索引:chunk → Embed(model) → 向量库 Upsert(id, vector, metadata)

查询

qv := embedder.Embed(query)
hits := vectorStore.AnnSearch(qv, topK, filter{kb_id})

metadata 常见字段kb_id, doc_id, chunk_index, source_url——Rerank 的 source_credibility 依赖这些。

大师调试

Ch30 向量库适配器(待读)。


三、两路输出统一格式(RRF 的前置条件)

RRF 需要每路返回 有序 doc/chunk id 列表,而非不可比的分数量纲:

type RetrievalHit struct {
    ChunkID string
    Rank    int     // 1-based,在本路内的排名
    Score   float64 // 可选,RRF 可不用
    Source  string  // "bm25" | "vector" | "graph"
}

读 fusion 包前,确认 retriever 返回结构是否一致——不一致是常见 bug 源


四、并行与超时

三路应并行;任一路超时策略:

策略 效果
丢弃慢路 延迟↓ 召回可能↓
降级为双路 图谱常先被弃
等待最慢路 延迟=最慢路

WeKnora 倾向 尽量等齐再 RRF——读 config 里 graph_timeout 等项。


五、与 Dify/FastGPT 对比(源码思维)

项目 路数 融合
WeKnora 3 RRF
Dify 2 加权
AnythingLLM 1

改 WeKnora 的 BM25 分词 = 改 中文精确召回;竞品多数在 Python 层用现成库,WeKnora 在 Go 层自管索引——竞赛差异化空间


六、动手:检索-only 实验

不写 LLM,只调内部 search API 或写临时 main:

  1. 同一 KB,query = 精确术语 → 看 BM25 路 top1
  2. 同一 query,改写成口语 → 看向量路 top1
  3. 记录两路 top5 重叠率——低重叠说明 三路融合必要

章末 · 大师自检


延伸阅读