多路召回怎么配置才不漏结果:两路检索与融合参数实操
多路召回的核心是同时运行多个检索器,再按统一规则合并结果。
配置时先确定各路检索方式和数量,再设置融合参数。
以向量检索加关键词检索两路为例,每路召回20条,用RRF融合取前10条,是比较稳妥的起点。
先理清召回链路和前提条件
多路召回通常包含三个环节:多个检索器并行查询、结果格式统一、融合排序。
向量检索负责语义相似,关键词检索负责精确匹配,两者互补能减少漏召回。
开始配置前需要确认:
- 已经有一个可用的向量库(如Milvus、Qdrant、FAISS),且文档已完成嵌入。
- 有一个支持全文检索的组件(如Elasticsearch、OpenSearch),或能执行关键词匹配的数据库。
- 两路检索返回的结果都包含文档ID和分数,便于后续融合。
如果只有一路检索器,多路召回配置就无从谈起,应先补全第二路。
配置两路检索器的关键参数
以Python伪代码为例,两路检索器分别设置召回数量:
vector_results = vector_store.search(query, top_k=20)
keyword_results = es_client.search(index="docs", body={"query": {"match": {"content": query}}, "size": 20})
每路的top_k或size建议设为最终需要条数的2到3倍。
例如最终要10条,每路取20条。
太少会导致融合后候选不足,太多会增加计算开销。
向量检索侧注意嵌入模型必须与建库时一致,查询向量维度不能错。
关键词检索侧注意分词器和字段映射,中文环境建议使用ik分词器,否则匹配效果会打折扣。
用RRF融合结果并去重
RRF(Reciprocal Rank Fusion)按排名融合,不依赖分数绝对值,适合不同检索器的结果合并。
公式为score = sum(1 / (k + rank)),其中k常取60。
配置步骤:
- 将两路结果按各自分数降序排列,得到排名。
- 对每个文档ID计算RRF分数:每路中该文档的
1/(60+排名)相加,未出现的路不计。 - 按RRF分数降序排序,取前N条。
- 按文档ID去重,保留最高分那条。
如果使用LangChain,可以直接用EnsembleRetriever,传入两个retriever并设置weights和c参数(对应k值)。
融合后建议再截断到最终条数,避免下游处理过多候选。
避坑:分数尺度不一致和重复文档
不同检索器的分数范围可能完全不同,向量相似度常在0到1之间,而BM25分数可能从0到几十。
直接加权平均会导致某一路主导结果。
使用RRF可以避开这个问题,如果坚持用分数加权,务必先做归一化。
另一个常见问题是同一文档在两路中重复出现,融合后需要按ID去重。
去重时保留RRF分数高的那条,不要简单保留先出现的。
还有一点:如果关键词检索返回空结果,检查查询词是否被分词器拆散,或字段是否设置了analyzed。
验证召回效果是否达标
配置完成后,用一组已知答案的查询做验证:
- 准备10到20个查询,每个查询标记1到2个相关文档。
- 分别运行单路召回和融合召回,计算召回率(相关文档是否出现在前N条中)。
- 对比融合前后召回率变化,如果融合后反而下降,检查融合权重或去重逻辑。
也可以用日志观察每路返回条数和耗时,确保两路都正常执行。
如果某一路经常返回0条,优先排查该路检索器本身。
多路召回配置不是一次调完就结束,上线后应根据查询日志定期复查各路召回率和融合效果,再微调每路数量和融合参数。