大模型 RAG 中 RRF(Reciprocal Rank Fusion倒数排序融合)是什么
Reciprocal Rank Fusion (RRF) 是一种将多个搜索结果列表(比如 ES 的全文检索列表和向量搜索列表)合并成一个统一排名列表的算法。它的核心思想非常朴素且强大:一个文档在不同列表中排名越靠前,它的最终得分就越高。
它的优势在于不需要对不同查询的得分(Score)进行归一化(比如全文检索分可能是 100,向量检索分是 0.9),直接根据“排名”来计算。
一、RRF是什么
1. RRF 的数学公式
对于每一个文档 ,其 RRF 得分的计算公式为:
-
: 所有的排名列表集合(比如全文检索结果集和向量检索结果集)。
-
: 文档 在列表 中的排名(从 1 开始)。
-
: 一个常数(平滑参数),通常默认为 60。它的作用是减轻低排名文档对总分的影响,防止排名太靠后的文档通过“凑数”挤到前面。
2. 具体计算示例
假设用户搜“入职流程”,我们得到两个列表:
-
列表 A(全文检索): [文档1, 文档2, 文档3]
-
列表 B(向量检索): [文档2, 文档1, 文档4]
取 ,计算得分:
-
文档1:
-
文档2:
-
文档3:
-
文档4:
最终,文档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(线性加权,如 )那样手动调权重,它在大多数业务场景下表现非常稳健。
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 检索时:
-
开启高亮或截断:不要直接把万字长文丢给模型,利用 ES 的
highlight找到最相关的片段。 -
混合检索(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”很准,但搜“心情不好想请假”很差。
-
向量检索搜“情绪”很准,但搜“工号”经常乱匹配。