大模型 RAG 中 RRF(Reciprocal Rank Fusion倒数排序融合)是什么

4 阅读5分钟

大模型 RAG 中 RRF(Reciprocal Rank Fusion倒数排序融合)是什么

Reciprocal Rank Fusion (RRF) 是一种将多个搜索结果列表(比如 ES 的全文检索列表和向量搜索列表)合并成一个统一排名列表的算法。它的核心思想非常朴素且强大:一个文档在不同列表中排名越靠前,它的最终得分就越高。

它的优势在于不需要对不同查询的得分(Score)进行归一化(比如全文检索分可能是 100,向量检索分是 0.9),直接根据“排名”来计算。


一、RRF是什么

1. RRF 的数学公式

对于每一个文档 dd,其 RRF 得分的计算公式为:

RRFscore(dD)=rR1k+r(d)RRFscore(d \in D) = \sum_{r \in R} \frac{1}{k + r(d)}

  • RR: 所有的排名列表集合(比如全文检索结果集和向量检索结果集)。

  • r(d)r(d): 文档 dd 在列表 rr 中的排名(从 1 开始)。

  • kk: 一个常数(平滑参数),通常默认为 60。它的作用是减轻低排名文档对总分的影响,防止排名太靠后的文档通过“凑数”挤到前面。


2. 具体计算示例

假设用户搜“入职流程”,我们得到两个列表:

  • 列表 A(全文检索): [文档1, 文档2, 文档3]

  • 列表 B(向量检索): [文档2, 文档1, 文档4]

k=60k = 60,计算得分:

  • 文档1: 160+1(A)+160+2(B)0.01639+0.01612=0.03251\frac{1}{60+1} (A) + \frac{1}{60+2} (B) \approx 0.01639 + 0.01612 = 0.03251

  • 文档2: 160+2(A)+160+1(B)0.01612+0.01639=0.03251\frac{1}{60+2} (A) + \frac{1}{60+1} (B) \approx 0.01612 + 0.01639 = 0.03251

  • 文档3: 160+3(A)+0(B)0.01587\frac{1}{60+3} (A) + 0 (B) \approx 0.01587

  • 文档4: 0(A)+160+3(B)0.015870 (A) + \frac{1}{60+3} (B) \approx 0.01587

最终,文档1和文档2会排在最前面,因为它们在两个维度上都表现优秀。


3. 在 Elasticsearch 中如何开启?

在 ES 8.8+ 版本中,官方已经原生内置了 RRF,你不需要自己写 Python 逻辑去算分,直接在查询请求中配置 rank 参数即可。

JSON

{
  "sub_searches": [
    {
      "query": { "match": { "content": "入职流程" } }  // 1. 全文检索
    },
    {
      "knn": {                                        // 2. 向量检索
        "field": "content_vector",
        "query_vector": [0.1, 0.2, ...],
        "k": 10,
        "num_candidates": 100
      }
    }
  ],
  "rank": {
    "rrf": {                                          // 3. 开启 RRF 合并
      "window_size": 50,                              // 参与排序的前 N 个文档
      "rank_constant": 60                             // 公式中的 k 值
    }
  }
}

4. 为什么要用 RRF?(对大模型处理的好处)

在 RAG 系统中,如果你只给大模型看前 3 条资料,这 3 条资料的质量决定了回答的上限。

  • 互补性:全文检索负责抓取“专有名词”(如:工号、政策编号),向量检索负责抓取“语义意图”(如:怎么请假)。

  • 稳定性:RRF 不需要像 Linear Combination(线性加权,如 0.7×text+0.3×vector0.7 \times \text{text} + 0.3 \times \text{vector})那样手动调权重,它在大多数业务场景下表现非常稳健。

5. 纯 Python 实现逻辑

如果你的 ES 版本较低,不支持原生的 rank 参数,你可以手动用 Python 处理:

Python

def rrf_merge(results_list, k=60):
    rrf_scores = {}
    for results in results_list:
        for rank, doc_id in enumerate(results, start=1):
            if doc_id not in rrf_scores:
                rrf_scores[doc_id] = 0
            rrf_scores[doc_id] += 1 / (k + rank)
    
    # 按得分从高到低排序
    sorted_docs = sorted(rrf_scores.items(), key=lambda x: x[1], reverse=True)
    return sorted_docs

二、以ES 为例怎么实现不同模式查询

1. 搜索“一句话”:使用 Full-text Queries

如果你直接把用户的一句提问(比如:“如何办理入职手续?”)传给 ES,你应该使用 match 查询。

  • 工作原理:ES 会先把这一句话进行“分词”(Tokenization)。比如把“如何办理入职手续”拆成“如何”、“办理”、“入职”、“手续”。

  • 召回逻辑:它会去索引里找包含这些词的文档。即便文档里写的是“入职流程办理”,只要关键词重合度高,它就能被检索出来。

  • 适用场景:标准的 RAG 场景,用户提问通常就是一句话。

// 示例:全文检索一句话
{
  "query": {
    "match": {
      "content": "如何办理入职手续?"
    }
  }
}

2. 搜索“关键词/精确值”:使用 Term-level Queries

如果你只想查某个特定的 ID、标签或者状态(比如:status: "active"),通常使用 term 查询。

  • 工作原理:它不会对你的输入进行分词,而是拿着你输入的字符串去匹配索引里一模一样的内容。

  • 局限性:如果你用 term 去搜一句话,大概率什么也搜不到,因为它在找一整句完全一致的索引项(而索引通常是按词拆分的)。

3. 如何让“一句话”搜得更准?

既然你是为了给大模型(LLM)提供上下文,以下三种进阶搜索方式更符合你的需求:

查询方式适合的输入特点
match一句话只要词对上了就行,顺序乱了也没关系。
match_phrase短语/整句不仅要求词对上,还要求顺序也得一致(比如搜“年假规定”,不会搜出“规定年假”)。
ELSER (语义搜索)模糊的提问ES 8.x 推出的模型,支持语义理解。即使你搜“怎么休假”,也能搜到包含“请假制度”的文档,即便字面上没有重复的词。

4. 给大模型处理时的建议

由于大模型对上下文的质量很敏感,建议你在 ES 检索时:

  1. 开启高亮或截断:不要直接把万字长文丢给模型,利用 ES 的 highlight 找到最相关的片段。

  2. 混合检索(Hybrid Search)

    • match 保证关键词没漏(比如具体的专有名词、工号)。

    • knn(向量搜索)保证语义理解没偏。

    • 通过 RRF(倒数排名融合)合并结果,给模型最精准的那几段话。


三、代码实现

在代码层面实现 ES 检索并对接大模型,通常有两种主流方式:一种是使用 Elasticsearch 官方 Python SDK(适合底层控制),另一种是使用 LangChain/LlamaIndex(适合快速搭建 RAG 流程)。

以下是几种核心查询方式的具体实现逻辑:

1. 使用官方 SDK 实现三种查询

在 Python 中,我们通常构建一个 query 字典发送给 ES 接口。

from elasticsearch import Elasticsearch

# 连接 ES 
es = Elasticsearch("http://localhost:9200")
index_name = "knowledge_base"

def search_es(user_input, query_type="match"):
    if query_type == "match":
        # 1. 全文检索:自动分词,匹配度越高排名越靠前
        query = {
            "match": { "content": user_input }
        }
    elif query_type == "match_phrase":
        # 2. 短语匹配:要求词序一致,适合搜具体的规定名称
        query = {
            "match_phrase": { "content": user_input }
        }
    elif query_type == "bool":
        # 3. 组合查询(最常用):既要满足关键词,又要满足过滤条件
        query = {
            "bool": {
                "must": [{"match": {"content": user_input}}],
                "filter": [{"term": {"status": "published"}}]
            }
        }
    
    response = es.search(index=index_name, query=query, size=3)
    # 提取查询结果给大模型做上下文
    return [hit['_source']['content'] for hit in response['hits']['hits']]

2. 向量检索(Vector Search)的实现

如果你想支持“语义相似”而不只是“字面匹配”,需要先将“一句话”转化成向量。

# 假设你已经有了向量模型模型(如 OpenAI 或 HuggingFace)
user_vector = embedding_model.encode("怎么申请带薪假?")

query = {
    "knn": {
        "field": "content_vector",      # 预先在 ES 中存好的向量字段
        "query_vector": user_vector,
        "k": 3,
        "num_candidates": 100
    }
}
res = es.search(index=index_name, knn=query)

3. 对接大模型(LLM)的完整闭环

这是 RAG 最关键的一步:将 ES 拿回来的数据“塞”进 Prompt。

def generate_answer(user_question):
    # 第一步:ES 检索
    context_list = search_es(user_question, query_type="match")
    context_text = "\n".join(context_list)
    
    # 第二步:构建 Prompt
    prompt = f"""
    你是一个助手,请根据以下参考资料回答用户的问题。
    如果资料中没有提到,请说不知道。
    
    参考资料:
    {context_text}
    
    用户问题:{user_question}
    """
    
    # 第三步:调用大模型(以 OpenAI 接口为例)
    # response = llm.chat(prompt) 
    return f"已根据 {len(context_list)} 条背景资料生成回答"

4. 为什么现在流行“混合检索” (Hybrid Search)?

在实际业务中,你会发现:

  • 全文检索搜“工号 12345”很准,但搜“心情不好想请假”很差。

  • 向量检索搜“情绪”很准,但搜“工号”经常乱匹配。