为什么需要Rerank?

检索阶段的目标是高召回:宁可多召回一些无关内容,也不要漏掉相关内容(通常取Top-20或Top-50)。但直接把20个候选结果全部扔给LLM,会带来两个问题:

Rerank的职责是:在召回的20-50个候选中,精确判断哪些真正相关,筛选出最相关的Top-3或Top-5,再送入LLM。

Bi-encoder vs Cross-encoder

Bi-encoder(双编码器)— 用于初步检索

就是普通的Embedding模型。Query和Document分别独立编码,然后计算相似度。速度极快,可以预计算文档向量并存入向量库。缺点是query和document之间没有交互,精度有限。

Query  → Encoder → 向量A  ──┐
                              ├─ 相似度计算 → 分数
Document → Encoder → 向量B ──┘
(两路独立编码,无交互)
    

Cross-encoder(交叉编码器)— 用于Rerank

将Query和Document拼接后一起输入模型,让两者在Transformer的自注意力机制中充分交互,输出一个精确的相关性分数。精度比Bi-encoder高得多,但因为无法预计算,必须对每个候选实时推理,速度慢10-100倍。

[CLS] Query [SEP] Document [SEP]
        │
    Cross-Encoder
        │
    相关性分数 (0-1)
(query和document充分交互,精度高)
    

主流Rerank模型

模型 类型 中文支持 调用方式 速度
BGE-reranker-large Cross-encoder 优秀 本地 中等
BGE-reranker-v2-m3 Cross-encoder 优秀 本地 中等
Cohere Rerank 专有模型 良好 API 快(云端)
Jina Reranker v2 Cross-encoder 良好 API/本地 中等

Rerank的成本和延迟代价

Rerank是有代价的,需要权衡:

建议:延迟敏感场景可以用BGE-reranker本地部署(GPU推理,约100ms/次);成本敏感场景用Cohere;对延迟不敏感的后台处理场景任意选。

带Rerank的完整RAG链路代码

pip install sentence-transformers chromadb openai
from sentence_transformers import CrossEncoder
import chromadb
from openai import OpenAI

# ===== 初始化模型 =====
openai_client = OpenAI()
reranker = CrossEncoder("BAAI/bge-reranker-large")  # 本地下载约1.3GB

# ===== 向量库初始化 =====
chroma = chromadb.Client()
collection = chroma.create_collection("docs_with_rerank")

documents = [
    "BGE-M3最大支持8192个token输入",
    "RAG通过检索增强生成,减少幻觉",
    "Rerank使用Cross-encoder精确重排候选结果",
    "向量数据库用HNSW加速近似最近邻搜索",
    "BM25是基于词频的关键词检索算法",
    "混合检索融合向量搜索和关键词搜索",
    "Embedding模型将文本映射为高维向量",
    "chunk切分策略影响RAG系统的检索质量",
]

def get_embedding(text):
    resp = openai_client.embeddings.create(
        model="text-embedding-3-small", input=text
    )
    return resp.data[0].embedding

# 建立向量索引
for i, doc in enumerate(documents):
    collection.add(
        ids=[f"doc_{i}"],
        documents=[doc],
        embeddings=[get_embedding(doc)]
    )

# ===== 带Rerank的检索函数 =====
def rag_with_rerank(query: str, initial_k: int = 10, final_k: int = 3):
    """
    两阶段检索:
    1. 向量检索召回 initial_k 个候选
    2. Cross-encoder Rerank,保留 final_k 个
    """
    # 第一阶段:向量检索(宽召回)
    query_embedding = get_embedding(query)
    candidates = collection.query(
        query_embeddings=[query_embedding],
        n_results=initial_k,
        include=["documents"]
    )
    candidate_docs = candidates["documents"][0]
    print(f"[召回阶段] 召回 {len(candidate_docs)} 个候选")

    # 第二阶段:Rerank精排
    pairs = [(query, doc) for doc in candidate_docs]
    scores = reranker.predict(pairs)

    # 按Rerank得分排序
    ranked = sorted(
        zip(candidate_docs, scores),
        key=lambda x: x[1],
        reverse=True
    )
    top_docs = [doc for doc, score in ranked[:final_k]]

    print(f"[Rerank阶段] 精选 {final_k} 个相关文档")
    for doc, score in ranked[:final_k]:
        print(f"  [{score:.3f}] {doc}")

    # 第三阶段:拼接上下文,调用LLM
    context = "\n".join([f"- {doc}" for doc in top_docs])
    prompt = f"""请基于以下参考内容回答问题。

参考内容:
{context}

问题:{query}

请给出简洁准确的回答。"""

    response = openai_client.chat.completions.create(
        model="gpt-4o-mini",
        messages=[{"role": "user", "content": prompt}]
    )
    return response.choices[0].message.content

# ===== 使用示例 =====
answer = rag_with_rerank("如何提升RAG系统的检索精度?")
print("\n=== 最终答案 ===")
print(answer)

小结:RAG链路中的职责分工

检索阶段(Bi-encoder/BM25):高召回,宽网捞鱼,取Top-20~50
    ↓
Rerank阶段(Cross-encoder):精排,精选Top-3~5
    ↓
生成阶段(LLM):基于精选上下文生成回答