混合检索是什么?为什么要用混合检索而不是纯向量检索?

#RAG#Agent#面试八股

混合检索是什么?为什么要用混合检索而不是纯向量检索?

结论:混合检索(Hybrid Search)就是稠密向量召回和稀疏词项召回各跑一遍,再把两路结果融合。 纯向量检索擅长"意思相近",但对错误码、型号、人名、内部缩写这类精确串不敏感;BM25 恰好相反。 两路的漏召回集合并不重合,所以并起来能显著抬高召回上限——这是它唯一的、也是充分的理由。

1. 两种召回各自会在哪里翻车

用户查询纯向量召回BM25 召回
"ORA-01555 怎么处理"召回一堆"回滚段原理"泛文,具体错误码可能进不了前 10精确命中含该错误码的片段
"电脑一直转圈打不开网页"正确理解成 DNS/网络问题原文没有"转圈""打不开"这些词,命中 0
"P0 故障升级流程"尚可,但"P0"这种短代码容易被语义稀释依赖分词,"P0" 可能被当停用词切掉
"怎么开发票" vs 原文"发票申请入口"命中,语义对齐词面不同,可能漏

向量模型在训练时没见过的内部术语,会被塞到一个"差不多"的位置;BM25 不懂同义改写,但只要字面出现就一定算分。

2. 融合方式:RRF 比加权求和更稳

最简单的融合是加权:score = α·cos + (1-α)·bm25。问题是两个分数分布完全不同——余弦在 0~1,BM25 是无上界的正数, 每次查询的分布还在变,归一化做不好就会一路碾压另一路。

倒数排名融合(Reciprocal Rank Fusion,RRF)只用名次,不看分数:

score(d) = Σ_i  w_i / (k + rank_i(d))        k 常取 60

文档   向量排名   BM25 排名   RRF 得分(k=60, w=1)
D1        1          9       1/61 + 1/69 = 0.0309
D2        6          1       1/66 + 1/61 = 0.0315   ← 双路都在前列者胜出
D3       20          -       1/80        = 0.0125

k 的作用是压平"头几名"的差距:k 越大,第 1 名和第 5 名的得分越接近,融合越依赖两路的共识而不是单路的第一名。

3. 整体链路

query
 ├─▶ Embedding ─▶ ANN top-50 ─┐
 │                            ├─▶ RRF 融合 top-20 ─▶ Rerank top-5 ─▶ LLM
 └─▶ 分词 ─▶ BM25 top-50 ─────┘

两路召回各取 30~50 条,融合后 20 条交给重排——混合检索解决"有没有",重排解决"准不准",别指望混完就完事。

4. 代码:本地 BM25 + 向量 + RRF

import jieba
import numpy as np
from rank_bm25 import BM25Okapi

def bm25_rank(query: str, corpus: list[str], top: int = 50) -> list[int]:
    # 中文必须先分词,整句当 token 的 BM25 等于没做
    tokens = [list(jieba.cut(doc)) for doc in corpus]
    scores = BM25Okapi(tokens, k1=1.5, b=0.75).get_scores(list(jieba.cut(query)))
    return list(np.argsort(scores)[::-1][:top])

def vector_rank(qvec: np.ndarray, mat: np.ndarray, top: int = 50) -> list[int]:
    scores = mat @ qvec                      # 向量已归一化,点积即余弦
    return list(np.argsort(scores)[::-1][:top])

def rrf(rankings: list[list[int]], k: int = 60, weights: list[float] | None = None) -> list[int]:
    weights = weights or [1.0] * len(rankings)
    fused: dict[int, float] = {}
    for w, ranking in zip(weights, rankings):
        for rank, doc_id in enumerate(ranking, start=1):
            fused[doc_id] = fused.get(doc_id, 0.0) + w / (k + rank)
    return sorted(fused, key=fused.get, reverse=True)

# 短查询里型号/错误码权重上调;长口语问句里向量权重上调
final_ids = rrf([vector_rank(qv, mat), bm25_rank(q, corpus)], k=60, weights=[0.7, 1.3])[:20]

生产环境通常把 BM25 那一路交给 Elasticsearch 或 OpenSearch(它们同时支持 knn 字段),省掉自己维护分词与倒排。

5. 什么时候可以不混

场景判断
语料是自然语言问答,查询也全是口语纯向量通常够
领域内有大量编号、型号、错误码必须加 BM25
多语言混合语料建议加,BM25 对小语种分词要求高,可用字符 n-gram
延迟预算 < 50 ms慎用,两路召回加热融合会翻倍

6. 常见追问

Q:两路权重怎么定? 按查询类型动态调:含大写代码、数字、引号的查询提高 BM25 权重;长自然语言问句提高向量权重。固定 0.5/0.5 也能用,但不如按查询特征分层。

Q:混合检索能替代重排吗? 不能。融合只是把两路粗排结果并起来,真正判"这段能不能回答问题"要靠交叉编码器逐对打分。

Q:中文分词用什么? 通用场景 jieba,ES 里用 IK 或 smartcn;分词粒度不一致会让同一术语被切成不同 token,务必在索引与查询两侧用同一套分析器。

小结

混合检索的本质是用两种互补的偏差互相纠错。先看评估集里漏召回的是"语义类"还是"字面类",再决定要不要加第二路,别默认上。

笔记加载中…