Files
zhuangang/topics/ai/rag/02-hybrid-rerank.md
T

4.7 KiB
Raw Blame History

id, title, type, domain, stack, tags, level, prerequisites, est_hours, related, week, order
id title type domain stack tags level prerequisites est_hours related week order
ai.rag.hybrid-rerank 检索优化:向量 + 关键词混合检索、RRF 融合、重排、查询改写 lesson ai
rag
混合检索
BM25
RRF
重排
rerank
查询改写
intermediate
ai.rag.pipeline-chunking
3
ai.rag.generation-eval
ai.vector-db.ann-metrics
ai.rag.interview
6
2

检索优化:向量 + 关键词混合检索、RRF 融合、重排、查询改写

解释说明

向量检索和关键词检索各有短板

向量检索 关键词检索(BM25)
擅长 语义相近、换种说法(“带薪假” ↔ “年假”) 精确词:产品型号、错误码、人名、条款编号
不擅长 精确的编号、罕见专有名词 同义词、口语化的问题

所以生产环境常用混合检索:两路各取 top-N,再融合。

融合:RRF(Reciprocal Rank Fusion):score(d) = Σ 1 / (k + rank_i(d)),k 常取 60。只用排名、不用原始分数,不需要处理两种分数的量纲差异,简单稳健。也可以按权重加权归一化后的分数。

中文关键词检索要先分词(jieba 等),否则 BM25 按单字或整句计算,效果很差。Elasticsearch / OpenSearch 需要安装中文分词插件;PostgreSQL 可以用 pg_trgm 或中文分词扩展。

重排(rerank):第一阶段召回 2050 个候选,用交叉编码器(cross-encoder)重排模型对“问题 + 候选块”逐对打分,取前 35 个给大模型。重排更准但更慢,只对少量候选使用。重排模型可以用云服务或私有部署的开源模型,以对应官方文档为准。

查询改写

  • 多轮对话中,“那病假呢?”要结合历史改写成独立问题“病假怎么规定的?”再检索;
  • 把一个复杂问题拆成多个子问题分别检索;
  • 生成假设答案再用它去检索(HyDE)。 改写会多一次模型调用,增加延迟和成本,按需使用。

元数据过滤:按权限、文档类型、时间等过滤,在检索阶段完成(向量库的 filter 或预过滤),见权限场景。

案例

IT 知识库:用户问“报错 0x80070005 怎么办”,纯向量检索返回的都是“权限问题”的泛泛文章,没有命中包含这个错误码的那篇;加入 BM25 后,精确匹配错误码的文档排第一。反过来,“在家怎么连公司网络”在 BM25 下命中很差(文档里写的是“VPN 远程办公”),向量检索能找到。混合之后两类问题都能覆盖。

代码示例

RRF 融合(摘自 examples/mini-rag,示例中还包含向量检索和 BM25 检索的实现):

def rrf(result_lists: list[list[str]], k: int = 60, top: int = 5) -> list[tuple[str, float]]:
    """result_lists:每一路检索按相关度排好序的文档 ID 列表。"""
    scores: dict[str, float] = {}
    for results in result_lists:
        for rank, doc_id in enumerate(results, start=1):
            scores[doc_id] = scores.get(doc_id, 0.0) + 1.0 / (k + rank)
    return sorted(scores.items(), key=lambda kv: -kv[1])[:top]


vector_hits = ["vpn-guide", "permission-faq", "printer"]          # 语义检索结果
bm25_hits = ["error-0x80070005", "vpn-guide", "office-install"]   # 关键词检索结果
for doc_id, score in rrf([vector_hits, bm25_hits]):
    print(f"{doc_id:18s} {score:.4f}")
# vpn-guide 两路都排得靠前,融合后排第一;只出现在一路的文档按名次排在后面

中文 BM25(rank_bm25 + jieba):

import logging

import jieba
from rank_bm25 import BM25Okapi

jieba.setLogLevel(logging.WARNING)
docs = ["Windows 更新报错 0x80070005 的处理方法", "VPN 远程办公连接指南", "打印机共享设置"]
bm25 = BM25Okapi([jieba.lcut(d) for d in docs])
print(bm25.get_scores(jieba.lcut("报错 0x80070005 怎么办")).round(2))  # 第一篇得分最高

易错点

  • 只做向量检索:错误码、型号、人名这类精确查询效果差。
  • 中文 BM25 不分词:分数几乎没有意义。
  • 直接把两路的原始分数相加:余弦相似度在 01,BM25 可能是 020,量纲不同。用 RRF 或先归一化。
  • 重排所有候选:重排模型慢,只对召回的前几十个使用。
  • top-k 设得很大以为更保险:无关内容增多,模型更容易被干扰,成本也更高。
  • 多轮对话直接用最后一句检索:“那病假呢”检索不到东西,需要改写。

面试常问

见 RAG 面试题:为什么要混合检索、RRF 原理、重排的作用、多轮对话怎么检索。