登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  科技周边 >  人工智能

RAG 混合检索融合关键词与向量结果

来源:17golang原创

时间:2026-10-10 13:01:02 373浏览 收藏

RAG 同时接入关键词检索和向量检索后,真正棘手的通常不是“查两次”,而是如何把两份结果合成一份可信的候选列表。关键词检索擅长命中产品名、错误码和专有术语,向量检索更容易找到同义改写与语义相近内容。一个实用起点是让两路检索各自排序,再用 Reciprocal Rank Fusion(RRF)按名次融合,最后把统一的 Top-K 文档交给大模型。

先划清这个小项目的边界

本文只实现融合层:输入是两份已经排好序的候选,输出是去重后的统一列表。BM25、嵌入生成、向量索引和大模型调用都留在融合器外面。这样做的好处是可以独立检查融合逻辑,也便于以后替换 Elasticsearch、OpenSearch、Milvus、pgvector 或其他检索后端。

Elastic 的混合搜索文档把全文检索和向量检索组合为一份排名结果,并推荐用 RRF 合并两类排名。RRF 不直接比较不同检索器的原始分数,而是根据文档在每份结果中的名次贡献分数,因此很适合处理 BM25 分数与向量相似度不在同一量纲的问题。

为什么不能直接相加原始分数

假设关键词检索返回 12.8、9.4、7.1,向量检索返回 0.92、0.86、0.81。数值范围不同,相加后谁占主导取决于评分模型,而不是文档是否真的更相关。即使先做归一化,也要决定采用 min-max、z-score 还是按查询动态校准,线上数据分布一变,规则就可能漂移。

RRF 只看名次。对于文档 d,它在每份排名中的贡献为 1 / (k + rank),其中 rank 从 1 开始,k 是平滑常数。文档同时出现在两路前排时会获得两次贡献,只在一路出现也不会被直接丢弃。

同一查询进入关键词检索和向量检索并形成两份独立候选排名的原创结构图
图1:RAG 混合检索的两路候选结构,图片为原创说明图。

准备统一的候选数据结构

两路检索必须共享稳定的 doc_id。标题或正文不适合作为去重键,因为内容更新、分段策略或空格差异都会制造重复项。融合层还应保留来源、原始分数和元数据,便于日志分析,但排序只使用名次。

from dataclasses import dataclass
from typing import Iterable

@dataclass(frozen=True)
class Hit:
    # doc_id 必须能跨检索器唯一标识同一文档
    doc_id: str
    title: str
    source: str
    raw_score: float


# 下面两组数据代表检索器已返回的有序结果
keyword_ranked = [
    Hit("d1", "RRF 融合算法", "keyword", 12.8),
    Hit("d2", "RAG 检索调优", "keyword", 9.4),
    Hit("d4", "BM25 字段权重", "keyword", 7.1),
]

vector_ranked = [
    Hit("d2", "RAG 检索调优", "vector", 0.92),
    Hit("d3", "语义召回实践", "vector", 0.86),
    Hit("d1", "RRF 融合算法", "vector", 0.81),
]

把公式落成一个最小 Python 融合器

实现时遍历每份排名,以 doc_id 为键累计贡献。为了让相同分数的结果稳定,示例额外记录文档出现过的最好名次,并用 doc_id 作为最终兜底排序键。真实项目还可以把更新时间、权威级别或业务优先级放进明确的二级排序规则。

def rrf_fuse(
    ranked_lists: Iterable[list[Hit]],
    *,
    rank_constant: int = 60,
    top_k: int = 5,
) -> list[dict]:
    if rank_constant 

按照这组示例输入,d1 和 d2 都在两路结果中出现,会获得两次贡献;d3 与 d4 只获得一路贡献。这里不需要比较 12.8 与 0.92 谁更大,融合器只关心它们各自在所属列表里的位置。

关键词排名和向量排名按文档编号去重并累计RRF分数的原创数据结构图
图2:RRF 融合器的数据结构与输出关系,图片为原创说明图。

接入真实 RAG 链路时放在哪里

一次完整请求可以拆成六个动作:先接收用户查询,再并行执行关键词检索和向量检索;把两路结果映射成统一 Hit;调用融合器;按融合后的 doc_id 读取正文片段;最后控制上下文长度并交给生成模型。融合前的检索可以并行,融合后的正文读取则应严格按照新排名进行。

过滤条件要保持一致。例如租户、文档状态、语言和权限最好在两路检索前都应用;如果只对一路过滤,融合结果可能出现另一条路径带回的越权文档。融合之后仍应做一次权限兜底,但不能把它当作唯一的访问控制。

三个参数分别控制什么

  • 候选深度:每路取多少条参与融合。窗口越大,召回机会越多,但查询、传输和去重成本也越高。
  • rank_constant:控制头部名次与后部名次贡献的差距。数值越大,靠后候选之间的差异越平缓。Elastic 官方 RRF 文档当前给出的默认值是 60。
  • top_k:融合后最终保留多少条。它应与分段长度和模型上下文预算一起确定,而不是简单复制候选深度。

不要一开始就为两个检索器添加大量权重。先用等权 RRF 建立可解释基线,再根据带标注的查询集观察精确术语、同义改写、长查询和冷门主题的表现。需要偏置时,可以把每路贡献改为 weight / (k + rank),但权重应来自评测,而不是凭感觉。

本地验收时检查这五点

  1. 输入包含错误码或产品全名时,关键词路线的高位结果能够进入最终列表。
  2. 输入改写为同义表达时,向量路线仍能补回语义相关文档。
  3. 同一个 doc_id 同时出现于两路时,最终只保留一条且分数累加。
  4. 原始分数范围发生变化时,融合排序不会因为量纲不同而直接失真。
  5. 相同输入重复融合时,二级排序规则保证输出顺序稳定。

从最小实现走向生产环境

生产系统还需要补上超时、空列表、日志和评测。某一路超时时,可以降级为另一份排名;两路都为空时,不应让模型凭空回答。日志至少记录查询标识、两路候选 doc_id、各自名次、融合分数和最终入选项,方便定位“召回不到”还是“融合丢失”。

如果搜索引擎已经原生支持 RRF,可以把融合放到引擎端,减少应用层传输和重复代码;如果两路来自不同系统,本文这个小型融合器更容易统一接入。无论采用哪种方式,核心原则都相同:先保留每种检索信号的独立排序,再用可解释的融合规则生成最终上下文。

参考资料:Elastic Hybrid search、Elastic Reciprocal rank fusion。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>