如何为你的 RAG 流水线加重排序
改善平庸 RAG 答案的办法,很少是换更大的 LLM —— 通常是更好的检索排序。reranker 位于向量搜索和 LLM 调用之间,对候选重新打分,确保只有最相关的段落进入 prompt。
RAG 中的排序问题
典型的 RAG 系统会嵌入你的文档并存储向量。查询时它取出与查询嵌入最近的 k 个向量,把这些片段塞进 LLM 的 prompt。问题在于:独立嵌入之间的余弦相似度是个粗糙的相关性信号。正确的片段可能在前 20 个结果里,却排在第 14 位 —— 落在你真正送进模型的那 5 个之外。
这是“lost in the middle(迷失在中间)”问题的反面:正确答案一开始就没排在最前。重排序通过对检索器已经找到的短名单施加一个更昂贵、更准确的相关性模型来修复它。
检索—重排序—生成 范式
检索器负责规模(毫秒级处理数百万文档)。reranker 负责质量(对几十个候选做精确排序)。LLM 负责综合。每个阶段只做自己擅长的事。
代码讲解
下面是一个使用本地 bge-reranker 的自包含 Python 示例。在生产中,如果你更喜欢托管 API,可以把 rank_documents 换成对 Cohere、Jina 或 Voyage 的调用。
使用本地 cross-encoder(sentence-transformers)
from sentence_transformers import CrossEncoder
# Load once at startup — reuse across requests
reranker = CrossEncoder("BAAI/bge-reranker-base", max_length=512)
def rerank(query: str, candidates: list[str], top_n: int = 5) -> list[str]:
"""Return top_n candidates reranked by relevance to query."""
pairs = [(query, doc) for doc in candidates]
scores = reranker.predict(pairs)
ranked = sorted(zip(scores, candidates), reverse=True)
return [doc for _, doc in ranked[:top_n]]
# --- In your RAG pipeline ---
raw_chunks = vector_db.search(query, top_k=50) # retrieve wide
best_chunks = rerank(query, raw_chunks, top_n=5) # rerank tight
answer = llm.complete(build_prompt(query, best_chunks)) # generate
使用 Cohere 托管 API
import cohere
co = cohere.Client("YOUR_API_KEY")
def rerank(query: str, candidates: list[str], top_n: int = 5) -> list[str]:
result = co.rerank(
model="rerank-v3.5",
query=query,
documents=candidates,
top_n=top_n,
)
return [candidates[r.index] for r in result.results]
使用 Jina Reranker API
import requests
def rerank(query: str, candidates: list[str], top_n: int = 5) -> list[str]:
resp = requests.post(
"https://api.jina.ai/v1/rerank",
headers={"Authorization": "Bearer YOUR_KEY"},
json={
"model": "jina-reranker-v2-base-multilingual",
"query": query,
"documents": candidates,
"top_n": top_n,
},
).json()
indices = [r["index"] for r in resp["results"]]
return [candidates[i] for i in indices]
在框架内(LangChain · LlamaIndex · Haystack)
如果你已经在用某个 RAG 框架,重排序通常就是一个即插即用的节点,把你现有的检索器包起来。模式完全一样 —— 宽召回、重排序、保留 top-n —— 只是换成了框架的术语来表达。
# LangChain — ContextualCompressionRetriever wraps any base retriever
from langchain.retrievers import ContextualCompressionRetriever
from langchain.retrievers.document_compressors import CrossEncoderReranker
from langchain_community.cross_encoders import HuggingFaceCrossEncoder
model = HuggingFaceCrossEncoder(model_name="BAAI/bge-reranker-base")
compressor = CrossEncoderReranker(model=model, top_n=5)
retriever = ContextualCompressionRetriever(
base_compressor=compressor,
base_retriever=vectorstore.as_retriever(search_kwargs={"k": 50}),
)
docs = retriever.invoke(query) # retrieve 50 → rerank → top 5
# LlamaIndex — a node postprocessor on the query engine
from llama_index.core.postprocessor import SentenceTransformerRerank
reranker = SentenceTransformerRerank(model="BAAI/bge-reranker-base", top_n=5)
query_engine = index.as_query_engine(
similarity_top_k=50, # retrieve wide
node_postprocessors=[reranker], # rerank to top 5
)
response = query_engine.query(query)
# Haystack 2.x — a Ranker component in the pipeline
from haystack import Pipeline
from haystack.components.rankers import TransformersSimilarityRanker
ranker = TransformersSimilarityRanker(model="BAAI/bge-reranker-base", top_k=5)
pipe = Pipeline()
pipe.add_component("retriever", retriever) # returns ~50 docs
pipe.add_component("ranker", ranker)
pipe.connect("retriever.documents", "ranker.documents")
这三者也都能通过各自的集成包接入托管 reranker(Cohere、Jina、Voyage)—— 换掉组件,流水线照旧。
如何选 top-k 取值
你有两个 k 值要调:召回多少个,以及重排序后保留多少个。
| 参数 | 常见范围 | 说明 |
|---|---|---|
retrieval_k | 20–100 | 越多 = 召回越好、reranker 越慢。50 是常见默认值。不要低于 20,否则可能彻底错过正确片段。 |
rerank_top_n | 3–10 | 越少 = prompt 越便宜,但漏掉有用片段的风险越高。从 5 开始;根据你的上下文窗口和答案质量来调。 |
经验法则:召回的数量至少是你打算保留数量的 5 倍。想要最终 5 个片段,就至少召回 25–50 个。reranker 只能修正顺序,无法凭空变出根本没被召回的片段。
延迟取舍
重排序会给你的流水线增加一次模型调用。成本取决于具体方案:
| 方案 | P50 延迟(50 篇文档) | 成本 |
|---|---|---|
| Cohere / Jina / Voyage API | 80–200 ms | 按次计费(约 $0.0002–0.002 / 千片段) |
| bge-reranker on CPU (small) | 200–600 ms | 你的基础设施成本;按次免费 |
| bge-reranker on GPU | 15–60 ms | GPU 成本;按次免费 |
| Local tiny model (e.g. jina-tiny) | 30–120 ms CPU | 免费 |
对大多数 RAG 应用来说,整条流水线 100–300 ms 的延迟完全可以接受,而质量提升是值得的。如果你的 SLA 非常紧,可以部署到 GPU、改用微型模型,或把 retrieval_k 从 50 降到 20–30。
积极使用缓存:如果同一查询反复出现(例如客服机器人),就按 (query, 语料版本) 的哈希缓存重排序结果。对重复查询而言,reranker 实际上变成了免费。
如何衡量它是否真的有帮助
不要凭感觉就加 reranker —— 要量化它。构建一个小型评测集,把查询与应当被检索到的片段配对,然后在同一集合上对比「仅检索」与「检索 + 重排序」。三个指标几乎覆盖所有场景:
| Metric | 它衡量什么 | 适用场景 |
|---|---|---|
| Recall@k | 正确片段是否出现在前 k 个里? | 用来确定 retrieval_k —— reranker 无法找回检索器从未返回的片段。 |
| MRR (Mean Reciprocal Rank) | 平均而言,第一个相关片段排得有多高? | 单一答案的查找(FAQ、客服、「找出某条款」)。 |
| nDCG@k | 相关片段是否排得靠前(按位置加权)? | 多段落答案:多个片段都重要,且顺序也要紧。 |
MRR 最直观:正确片段排第 1 得 1.0,排第 2 得 0.5,排第 4 得 0.25。在整个查询集上取平均,就得到一个数字,刻画「答案离顶部有多近?」—— 这正是 reranker 应当改善的东西。
# Minimal MRR + Recall@k over a labelled eval set.
# Each example: a query, the candidate texts, and the indices that are relevant.
def reciprocal_rank(ranked_ids, relevant_ids):
for i, doc_id in enumerate(ranked_ids, start=1):
if doc_id in relevant_ids:
return 1.0 / i
return 0.0
def evaluate(examples, rank_fn, k=5):
mrr = recall = 0.0
for ex in examples:
ranked = rank_fn(ex["query"], ex["candidates"]) # returns ids, best first
mrr += reciprocal_rank(ranked, ex["relevant"])
recall += 1.0 if set(ranked[:k]) & set(ex["relevant"]) else 0.0
n = len(examples)
return {"MRR": mrr / n, f"Recall@{k}": recall / n}
base = evaluate(eval_set, retrieve_only) # baseline
reranked = evaluate(eval_set, retrieve_then_rerank) # with the reranker
print(base, reranked) # expect MRR and nDCG to rise after reranking
哪怕只有 30–50 条标注查询,也足以看出信号。从日志里捞出真实查询,标注每条由哪个片段回答,你就有了一套回归测试 —— 每次改动检索器、分块或 reranker 都能重跑。若不想自己手写,ir-measures、ranx 以及 BEIR 的评测器都能替你算出这些指标。
常见坑:召回的候选太少(reranker 只能对拿到的东西重新排序);max_length 留得太短,长片段在打分前就被截断;把排序写成升序而非降序;以及以为不同模型的原始分数可以互相比较 —— 它们并未校准,重要的是排名顺序,而非绝对数值。
该选哪个 reranker
简短版:
- 想自建、仅英文、免费:bge-reranker-v2-m3 —— 强大且广泛部署。
- 想要托管 API、最佳多语言质量:Cohere Rerank v3.5。
- 想要开源权重 + 托管 API + 可在浏览器运行的微型模型:Jina Reranker v2。
- 专注检索专项质量:Voyage Rerank 2。
- Want Apache 2.0 open weights, browser-runnable xsmall: mxbai-rerank.
查看完整模型对比 →