RAG 系列(十一):Rerank——让检索结果按重要性排队

向量检索的排序问题

上一篇我们用混合检索解决了"召回率"问题------BM25 加向量,能更全面地找到相关文档。

但召回只是第一步。找到文档之后,还有一个问题:这些文档按什么顺序喂给 LLM?

向量检索给出的排序,依据是 query embedding 和 doc embedding 的余弦相似度。这个相似度可以快速计算,但精度有限------它是两段文字的"粗略相似度",并不是真正意义上的"这篇文档有多适合回答这个问题"。

一个常见现象:检索 top-4,第 1 篇是泛泛的背景介绍,第 3 篇才是真正回答问题的文档。LLM 拿到的上下文开头是废话,关键信息埋在后面,生成质量自然下降。

Rerank 的思路很简单:先多召回,再精排。

向量检索召回 top-10(宁可多要,别漏掉),然后用一个更精准的模型对这 10 篇重新打分、重新排序,最终只保留 top-4 给 LLM。


Bi-Encoder vs Cross-Encoder

理解 Rerank,先要理解两种编码架构的区别。

Bi-Encoder(向量检索用的)

arduino 复制代码
query  → Encoder → query vector
doc    → Encoder → doc vector
相似度 = cosine(query_vec, doc_vec)

query 和 doc 分别独立编码,相似度通过向量计算得出。优点是极快------doc 向量可以离线预计算,检索时只需算一次 query embedding,然后做向量比较。缺点是 query 和 doc 从不"见面",编码时各自不知道对方的存在,相关性判断较粗。

Cross-Encoder(Reranker 用的)

csharp 复制代码
[query, doc] → Encoder → relevance score

query 和 doc 拼接在一起,同时输入模型,模型直接输出一个相关性分数。这样 query 的每个词都能和 doc 的每个词做注意力交互,相关性判断精准得多。缺点是慢------每对 (query, doc) 都要跑一次完整推理,无法预计算。

所以两者的正确用法是串联,而不是替代:

sql 复制代码
向量检索(Bi-Encoder,快速召回)→ Reranker(Cross-Encoder,精准排序)

核心指标:context_precision

在 RAGAS 的 4 个指标里,context_precision 专门衡量排序质量:

ini 复制代码
context_precision = 有多少个"对的"文档排在了"错的"文档前面
  • context_precision = 1.0:相关文档全部排在不相关文档前面
  • context_precision = 0.5:相关文档的排序很随机
  • context_precision = 0.0:相关文档全部沉底

这正是 Reranker 要优化的指标。召回率(context_recall)关心"找没找到",精准率(context_precision)关心"排没排对"。


实验设计

复用之前的知识库(8 篇 RAG 技术文档)和测试集(8 条问题),对比两种策略:

策略 检索方式 最终返回
基准(Baseline) 向量检索直接 top-4 4 篇
Rerank 向量检索 top-10 → Cross-Encoder 精排 → top-4 4 篇

两种策略最终都给 LLM 提供 4 篇文档,唯一区别是排序质量。

Reranker 使用 BAAI/bge-reranker-v2-m3,通过 SiliconFlow API 调用。


实现:自定义 SiliconFlowReranker

LangChain 没有内置 SiliconFlow 的 Reranker 封装,需要自己实现。继承 BaseDocumentCompressor,实现 compress_documents 方法:

python 复制代码
import requests
from langchain_core.documents import Document
from langchain_core.documents.compressor import BaseDocumentCompressor
from typing import Sequence

class SiliconFlowReranker(BaseDocumentCompressor):
    model: str = "BAAI/bge-reranker-v2-m3"
    api_key: str = ""
    api_base: str = "https://api.siliconflow.cn/v1"
    top_n: int = 4

    def compress_documents(
        self,
        documents: Sequence[Document],
        query: str,
        callbacks=None,
    ) -> Sequence[Document]:
        if not documents:
            return []

        doc_texts = [d.page_content for d in documents]

        resp = requests.post(
            f"{self.api_base}/rerank",
            headers={"Authorization": f"Bearer {self.api_key}"},
            json={
                "model": self.model,
                "query": query,
                "documents": doc_texts,
                "top_n": self.top_n,
                "return_documents": True,
            },
            timeout=30,
        )
        resp.raise_for_status()

        reranked = []
        for item in resp.json().get("results", []):
            doc = documents[item["index"]]
            doc.metadata["rerank_score"] = item["relevance_score"]
            reranked.append(doc)

        return reranked

接口说明:

SiliconFlow 的 /v1/rerank 接口格式:

json 复制代码
// 请求
{
  "model": "BAAI/bge-reranker-v2-m3",
  "query": "中文场景用哪个 Embedding 模型",
  "documents": ["文档1内容", "文档2内容", ...],
  "top_n": 4,
  "return_documents": true
}

// 响应
{
  "results": [
    {"index": 2, "relevance_score": 0.952, "document": {"text": "..."}},
    {"index": 0, "relevance_score": 0.834, "document": {"text": "..."}},
    ...
  ]
}

index 是原始文档列表的下标,relevance_score 是 Cross-Encoder 给出的相关性分数(越高越好)。


串联:ContextualCompressionRetriever

有了 Reranker,用 ContextualCompressionRetriever 把它接在向量检索器后面:

python 复制代码
from langchain_classic.retrievers import ContextualCompressionRetriever
from langchain_chroma import Chroma
from langchain_openai import OpenAIEmbeddings

embeddings = OpenAIEmbeddings(
    model="BAAI/bge-large-zh-v1.5",
    api_key=os.getenv("EMBEDDING_API_KEY"),
    base_url="https://api.siliconflow.cn/v1",
)
vectorstore = Chroma.from_documents(docs, embedding=embeddings)

# 召回阶段:多要一些
recall_retriever = vectorstore.as_retriever(search_kwargs={"k": 10})

# 精排阶段
reranker = SiliconFlowReranker(
    api_key=os.getenv("EMBEDDING_API_KEY"),
    top_n=4,
)

# 串联
rerank_retriever = ContextualCompressionRetriever(
    base_compressor=reranker,
    base_retriever=recall_retriever,
)

调用时和普通 retriever 完全一样:

python 复制代码
docs = rerank_retriever.invoke("中文场景用哪个 Embedding 模型?")
# 内部自动:向量检索 top-10 → reranker 精排 → 返回 top-4

实验结果

markdown 复制代码
======================================================================
  RAGAS 指标对比(向量检索 vs 向量检索 + Rerank)
======================================================================

  指标                    基准 (top-4)   Rerank (10→4)     变化
  ──────────────────────────────────────────────────────────────────
  context_precision          0.552          0.792    ↑+0.240  ◀ 核心指标
  context_recall             0.500          0.688    ↑+0.188
  faithfulness               0.688          0.854    ↑+0.167
  answer_relevancy           0.429          0.381    ↓-0.049
======================================================================

  结论:
  ✓ Rerank 将 context_precision 从 0.552 提升到 0.792
    → 更相关的文档排在前面,LLM 能看到更高质量的上下文

数字解读:

  • context_precision +0.240:最显著的提升。基准检索时,0.552 意味着不少相关文档被排在了不相关文档后面;Rerank 后 0.792,排序质量大幅改善。
  • context_recall +0.188:预期之外的收获。召回从 top-4 扩大到 top-10 后,本来担心精排会丢掉相关文档------实际上 context_recall 也提升了,说明放大召回本身就帮助找回了被向量检索漏掉的相关文档。
  • faithfulness +0.167:排序质量改善后,LLM 获得了更高质量的上下文,幻觉率也随之下降。三个核心指标联动提升,这是 Rerank 带来的连锁反应。
  • answer_relevancy -0.049:轻微下降,变化在误差范围内(LLM 评分本身有随机性)。整体来看不影响结论。

Reranker 的实际工作原理

用一个具体例子感受 Reranker 在做什么。

假设 query = "中文场景应该选哪个 Embedding 模型",向量检索返回了这 4 篇(按余弦相似度排序):

arduino 复制代码
向量检索排序(余弦相似度):
1. doc-001  "RAG 技术介绍" ------ 泛泛介绍,提到 embedding 一词
2. doc-002  "向量数据库选型" ------ 相关,但重点是数据库
3. doc-003  "Embedding 模型推荐" ------ 直接回答问题 ✓
4. doc-005  "RAG 评估方法" ------ 无关

Reranker 重新打分后:

ini 复制代码
Reranker 排序(Cross-Encoder 相关性分数):
1. doc-003  score=0.952  "Embedding 模型推荐" ✓
2. doc-002  score=0.621  "向量数据库选型"
3. doc-001  score=0.234  "RAG 技术介绍"
4. doc-005  score=0.089  "RAG 评估方法"

原来排第 3 的正确文档被提到了第 1 位。LLM 读到的上下文第一段就是核心答案,生成质量自然更高。


何时使用 Rerank

建议使用 Rerank 的场景:

  • 知识库文档数量较多(> 100 篇),向量检索排序容易出错
  • 查询涉及精确术语或特定概念,相关性判断要求高
  • 对答案质量要求高,能接受略高的 API 成本和延迟
  • 已经在用混合检索,想在排序阶段继续优化

可以跳过 Rerank 的场景:

  • 知识库很小(< 20 篇),向量检索排序已足够准确
  • 对延迟极敏感(Rerank 每次需要额外 API 调用)
  • 查询都是宽泛的概念性问题,排序质量影响不大

成本估算: SiliconFlow 的 bge-reranker-v2-m3 按 token 计费,对 10 篇文档重排序,大约是向量检索成本的 3-5 倍。通常用在高价值查询或对质量要求高的场景。


完整代码

代码已开源:

github.com/chendongqi/...

核心文件:

  • rerank.py --- 基准检索 vs Rerank 的完整对比实验

运行方式:

bash 复制代码
git clone https://github.com/chendongqi/llm-in-action
cd 11-rerank
cp .env.example .env   # 填入 Embedding API Key 和 LLM API Key
pip install -r requirements.txt
python rerank.py

小结

本文通过代码实验展示了 Rerank 的价值:

  1. Bi-Encoder(向量检索):快速召回,但相关性判断是"独立视角",排序不够精准
  2. Cross-Encoder(Reranker):慢但精准,同时看 query 和 doc,直接输出相关性分数
  3. 两者串联(召回 + 精排):在本实验中,context_precision 从 0.552 提升到 0.792,同时 context_recall 和 faithfulness 也连带改善

在生产级 RAG 系统里,Rerank 通常是性价比最高的优化之一------它不改变任何数据、不调整任何 Prompt,只是让文档按正确顺序排列,LLM 就能看到更好的上下文。


参考资料

相关推荐
handsomestWei1 小时前
OpenAI 与 Anthropic 接口协议差异简述
大模型·llm·openai·模型接口
冬奇Lab1 小时前
一天一个开源项目(第96篇):OpenHarness - 轻量级 AI 代理基础设施框架
人工智能·开源·资讯
lulu12165440782 小时前
JetBrains IDE 终极AI编程方案:CC GUI插件让Claude Code和Codex丝滑运行
java·ide·人工智能·python·ai编程
TENSORTEC腾视科技2 小时前
腾视科技重磅推出AI NAS,重塑数据管理方式,开启智能高效新时代
人工智能·ai·七牛云存储·nas·企业存储·ainas·家庭存储
tanis_20772 小时前
MinerU2.5-Pro 中文 PDF 识别准确率全解:OmniDocBench v1.6 权威基准数据
人工智能·python·pdf
我是发哥哈2 小时前
跨AI模型生成视频的五大维度对比:选型避坑指南
大数据·人工智能·学习·机器学习·chatgpt·音视频
如去2 小时前
第四篇《AI+教育:个性化学习的实现路径与教育公平的再平衡》
人工智能
Elastic 中国社区官方博客2 小时前
Elastic 9.4:Workflows 正式发布、Agent Builder 更新,以及 Prometheus / PromQL 支持
运维·数据库·人工智能·elasticsearch·搜索引擎·信息可视化·prometheus
机器视觉_Explorer3 小时前
【halcon】编程技巧:鼠标擦除
图像处理·人工智能·深度学习·算法·视觉检测