AI Engineering Academy · 课时

两阶段检索为何有效

了解单阶段检索中的召回率与精确率权衡,以及快速的粗略检索器配合较慢但准确的重排器如何兼得两者优势。

第 1 / 4 课13 个步骤

两阶段检索为何有效 是 CoddyKit 上的免费 AI Engineering Academy 课时。 这是第 1 节课,共 4 节。 你可以在下方免费阅读本课时的完整内容 — 然后在浏览器中使用内置代码编辑器和全天候 AI 导师进行实践。 这是 AI Engineering Academy 学习路径的一部分,你的进度在网页和 CoddyKit 应用中同步。 AI Engineering Academy 课程共包含 4 节课。

检索中的召回率-精确率权衡

每个检索系统都面临一个基本权衡:召回率衡量您找到多少相关文档(是否漏掉了某些文档?),而精确率衡量前排结果的准确程度(检索到的文档中有多少确实相关?)。同时最大化这两个指标的计算成本很高。快速检索器会牺牲精确率来换取召回率;精确的排序器则会牺牲速度来换取准确性。

双编码器与交叉编码器:核心区别

两阶段检索所依赖的两类模型,区别在于它们如何理解查询和文档。双编码器分别对查询和每个文档进行编码,并测量它们向量之间的相似度——速度快,但受到独立编码的限制。交叉编码器将查询和文档拼接成一个输入来处理,使二者能够进行深度交互——准确度很高,但在候选集上的复杂度为 O(n)。

# Bi-encoder: compute query embedding ONCE, compare to all doc embeddings
# O(1) query encoding + O(n) dot products via ANN index = fast
query_vec = embed(query)  # done once
results = vector_index.search(query_vec, top_k=100)  # fast ANN search

# Cross-encoder: re-scores (query, doc) pairs jointly
# O(k) forward passes for k candidate documents = slow but accurate
for doc in results[:100]:
    score = cross_encoder.score(query, doc.text)  # joint scoring

阶段 1:快速粗检索

第一阶段是一个快速检索器——通常是配合近似最近邻索引或 BM25 索引的双编码器——它以较高的召回率和一般的精确率检索出较大的候选集(50-200 个文档)。目标不是做到完全准确,而是不要漏掉相关文档。我们会扩大检索范围,接受一些误报,因为第二阶段会对其进行清理。

from langchain_community.vectorstores import FAISS
from langchain_openai import OpenAIEmbeddings

# Stage 1: retrieve 100 candidates (high recall, modest precision)
vectorstore = FAISS.from_documents(documents, OpenAIEmbeddings())
coarse_retriever = vectorstore.as_retriever(
    search_kwargs={'k': 100}  # large candidate set
)

candidates = coarse_retriever.invoke(query)
print(f'Stage 1: retrieved {len(candidates)} candidate documents')

阶段 2:使用交叉编码器进行精确重排序

第二阶段接收阶段 1 的候选集,并使用同时读取查询和文档的交叉编码器,为每个(查询、文档)对重新评分。由于它只处理 50-200 个候选项,而不是整个语料库,因此可以承担代价高昂的联合编码。交叉编码器对拼接输入进行深度注意力处理,因此在估计真实相关性方面远比双编码器准确。

from sentence_transformers import CrossEncoder

reranker = CrossEncoder('cross-encoder/ms-marco-MiniLM-L-6-v2')

def rerank(query: str, candidates: list[str], top_k: int = 5) -> list[str]:
    # Score each (query, document) pair jointly
    pairs = [[query, doc] for doc in candidates]
    scores = reranker.predict(pairs)

    # Sort by score descending
    ranked = sorted(zip(candidates, scores), key=lambda x: x[1], reverse=True)
    return [doc for doc, _ in ranked[:top_k]]

candidate_texts = [doc.page_content for doc in candidates]
final_docs = rerank(query, candidate_texts, top_k=5)
print(f'Stage 2: selected top {len(final_docs)} documents after re-ranking')

这种组合为何有效

两阶段设计利用了一个关键的不对称性:第一阶段的快速 ANN 搜索可以在毫秒级处理数百万个文档,而第二阶段的精确交叉编码器只处理很小的候选池。这样,您既获得了近似搜索的可扩展性,又获得了精确联合评分的准确性。整个流程既快速又高度准确——单独使用任何一个阶段都无法同时做到这一点。

两阶段检索的延迟情况

在典型的两阶段流程中:阶段 1(在 100 万个文档上执行向量 ANN 搜索)耗时 5-20 毫秒;阶段 2(对 100 个候选项使用交叉编码器)耗时 100-500 毫秒,具体取决于文档长度和硬件。总延迟预算为 150-600 毫秒,对大多数应用来说都可以接受。在阶段 2 使用 GPU 加速,可以将短文档的重排序时间缩短到 30 毫秒以内,使该流程在延迟敏感型应用中的表现不逊于单阶段检索。

import time

def two_stage_search(query, coarse_retriever, reranker, top_k=5):
    t0 = time.perf_counter()

    candidates = coarse_retriever.invoke(query)        # stage 1
    t1 = time.perf_counter()

    candidate_texts = [c.page_content for c in candidates]
    final_docs = rerank(query, candidate_texts, top_k)  # stage 2
    t2 = time.perf_counter()

    print(f'Stage 1 (retrieval): {(t1-t0)*1000:.1f}ms')
    print(f'Stage 2 (re-ranking): {(t2-t1)*1000:.1f}ms')
    print(f'Total: {(t2-t0)*1000:.1f}ms')
    return final_docs

选择合适的候选集大小

第一阶段的候选集大小是一个关键超参数。候选集太小(例如 10 个)时,相关文档可能在重排序开始前就被漏掉;候选集太大(例如 500 个)时,阶段 2 的延迟会急剧上升。N 时的召回率曲线——即在不同 N 值下捕获多少相关文档——可以指导这一选择。典型的最佳范围是 50 到 150 个候选项,此时召回率已接近饱和,而延迟仍然可控。

def recall_at_n(coarse_retriever, test_queries, golden_relevant, n_values):
    for n in n_values:
        recalls = []
        for query, relevant in zip(test_queries, golden_relevant):
            # Temporarily set k to n
            coarse_retriever.search_kwargs['k'] = n
            results = coarse_retriever.invoke(query)
            retrieved_ids = {r.metadata.get('id') for r in results}
            relevant_found = len(set(relevant) & retrieved_ids)
            recalls.append(relevant_found / len(relevant))
        avg = sum(recalls) / len(recalls)
        print(f'N={n}: recall={avg:.3f}')

第一阶段混合检索 + 第二阶段交叉编码器

最强大的两阶段配置,是将混合检索器(稠密检索 + BM25)作为第一阶段,将交叉编码器作为第二阶段。混合检索结合语义匹配和关键词匹配,最大化第一阶段的召回率;交叉编码器随后从合并的候选池中准确选出最相关的文档。这种配置在基准测试中始终能达到业界领先的检索质量。

from langchain.retrievers import EnsembleRetriever

# Stage 1: hybrid retrieval for maximum recall
hybrid_retriever = EnsembleRetriever(
    retrievers=[bm25_retriever, vector_retriever],
    weights=[0.4, 0.6],
)

# Stage 2: cross-encoder re-ranking for high precision
from langchain.retrievers.document_compressors import CrossEncoderReranker
from langchain_community.cross_encoders import HuggingFaceCrossEncoder

cross_encoder_model = HuggingFaceCrossEncoder(model_name='cross-encoder/ms-marco-MiniLM-L-6-v2')
compressor = CrossEncoderReranker(model=cross_encoder_model, top_n=5)

from langchain.retrievers import ContextualCompressionRetriever
two_stage = ContextualCompressionRetriever(
    base_compressor=compressor,
    base_retriever=hybrid_retriever,
)

商业重排序 API

如果您希望获得交叉编码器的准确性,但不想管理自己的模型,Cohere Rerank 和 Jina AI Reranker 都提供云端重排序 API。您只需发送一个查询和一组文档文本,就能接收相关性分数。这些 API 使用大型交叉编码器模型(通常包含超过 5 亿个参数),其性能优于自行托管的小型交叉编码器,但代价是额外的 API 延迟(50-300 毫秒),并且需要按重排序的文档数量付费。

import cohere

co = cohere.Client('YOUR_API_KEY')

def cohere_rerank(query: str, documents: list[str], top_k: int = 5):
    response = co.rerank(
        model='rerank-english-v3.0',
        query=query,
        documents=documents,
        top_n=top_k,
    )
    return [
        {'text': documents[r.index], 'score': r.relevance_score}
        for r in response.results
    ]

final = cohere_rerank(query, candidate_texts, top_k=5)
for doc in final:
    print(f'Score {doc["score"]:.3f}: {doc["text"][:80]}')

何时两阶段检索属于过度设计

与单阶段检索相比,两阶段检索会增加复杂性和延迟。它并非总是必要。对于少于 10,000 个文档的小型语料库,在完整语料库上运行单个交叉编码器可能已经足够快。对于延迟必须低于 100 毫秒且准确性提升有限的应用,单阶段稠密检索可能更合适。当您拥有大型语料库、有较高的准确性要求,并且能够接受 200-500 毫秒的检索延迟时,请使用两阶段检索。

面向极大规模的三阶段检索

对于拥有数千万份文档的语料库,有时会采用三级流水线:第一级使用 ANN 检索 10,000 个候选项,第二级使用快速的小型交叉编码器将结果重排序为 100 个,第三级使用大型高性能交叉编码器将结果重排序为 5 个。每一级都会对更小的集合应用成本更高但更准确的模型。这种架构广泛用于大规模搜索引擎和文档问答系统。

快速检查

请检验您是否理解本课中双阶段检索之所以有效的原因。

课程回顾

在本课中,您学到了:双编码器速度快,但只能分别对查询和文档进行编码;交叉编码器通过联合编码实现更高的准确性,但速度太慢,无法用于完整语料库搜索;双阶段检索将两者结合起来:先通过快速的第一级获得较高的召回率,再通过准确的第二级获得较高的精确率。第一级会检索出远多于实际所需数量的候选项,以避免遗漏相关文档。接下来,我们将使用 Cohere 和 BGE 实现交叉编码器重排序。

免费开始

用 AI 导师学习 Python — 免费

在浏览器中编写并运行真实代码,获得全天候 AI 导师的即时帮助,并在网页或应用中继续学习。

课程
30
课程
120

常见问题解答

「两阶段检索为何有效」课时是免费的吗?

是的 — 「两阶段检索为何有效」的完整文本可在网页上免费阅读。要进行交互式练习(内置代码编辑器和全天候 AI 导师)并解锁 AI Engineering Academy 课程的其余内容,请升级到 CoddyKit PRO。 AI Engineering Academy 课程共包含 4 节课。

「两阶段检索为何有效」这节课中我会学到什么?

了解单阶段检索中的召回率与精确率权衡,以及快速的粗略检索器配合较慢但准确的重排器如何兼得两者优势。 你通过在浏览器中直接运行的动手代码来练习 AI Engineering Academy,全天候 AI 导师会在你学习这节课的过程中回答你的问题。

学习 AI Engineering Academy 需要有经验吗?

无需任何先前经验。CoddyKit 上的 AI Engineering Academy 课程适合初学者到高级学习者,你可以从这里开始或从头开始,按照自己的节奏学习。 这是第 1 节课,共 4 节。

「两阶段检索为何有效」课时需要多长时间?

大多数 CoddyKit 课程大约需要 5–10 分钟。每节课都很精短且互动,所以你能稳步进步,并在网页和应用中从离开的地方继续。

我能在这节 AI Engineering Academy 课中编写并运行代码吗?

能。每节 AI Engineering Academy 课都包含内置代码编辑器,你可以在浏览器中直接编写并运行真实代码,并获得即时 AI 反馈 — 无需本地设置。

此课程中的所有课时

  1. 两阶段检索为何有效
  2. 使用 Cohere 和 BGE 进行交叉编码器重排
  3. 上下文压缩与相关性过滤
  4. 衡量重排的影响
← 返回 AI Engineering Academy