Por que a recuperação em duas etapas funciona
Entenda o compromisso entre revocação e precisão na recuperação em uma única etapa e como um recuperador rápido e preliminar, seguido por um reclassificador lento porém preciso, combina o melhor dos dois mundos.
Por que a recuperação em duas etapas funciona é uma aula grátis de AI Engineering Academy no CoddyKit. Esta é a aula 1 de 4. Você pode ler a aula completa abaixo gratuitamente — depois pratica ao vivo no navegador com um editor de código integrado e um tutor de IA 24/7. Faz parte do caminho de aprendizado de AI Engineering Academy, e seu progresso é sincronizado entre a web e o app CoddyKit. O curso de AI Engineering Academy inclui 4 aulas no total.
O compromisso entre cobertura e precisão na recuperação
Todo sistema de recuperação enfrenta um compromisso fundamental: a cobertura mede quantos documentos relevantes você encontra (algum ficou de fora?), enquanto a precisão mede o quanto os principais resultados são corretos (quantos dos documentos recuperados são realmente relevantes?). Maximizar as duas ao mesmo tempo é computacionalmente caro. Recuperadores rápidos sacrificam precisão em favor da cobertura; classificadores precisos sacrificam velocidade em favor da exatidão.
Bi-Encoder versus Cross-Encoder: a distinção central
Os dois tipos de modelo no centro da recuperação em duas etapas diferem em como interpretam a consulta e o documento. Um bi-encoder codifica a consulta e cada documento de forma independente e mede a similaridade entre seus vetores: é rápido, mas limitado pela codificação independente. Um cross-encoder vê a consulta e o documento concatenados como uma única entrada, permitindo uma interação profunda entre eles: é altamente preciso, mas tem complexidade O(n) sobre o conjunto de candidatos.
# 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 scoringEtapa 1: recuperação aproximada rápida
A primeira etapa é um recuperador rápido — normalmente um bi-encoder com um índice aproximado de vizinhos mais próximos ou um índice BM25 — que recupera um conjunto grande de candidatos (50 a 200 documentos) com alta cobertura, mas precisão moderada. O objetivo não é ser preciso; é não deixar documentos relevantes de fora. Fazemos uma busca ampla e aceitamos alguns falsos positivos, sabendo que a segunda etapa os eliminará.
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')Etapa 2: reordenação precisa com Cross-Encoder
A segunda etapa recebe o conjunto de candidatos da etapa 1 e recalcula a pontuação de cada par (consulta, documento) usando um cross-encoder que lê ambos em conjunto. Como processa apenas 50 a 200 candidatos (e não todo o corpus), pode arcar com o custo da codificação conjunta. A atenção profunda do cross-encoder sobre a entrada concatenada o torna muito mais preciso para estimar a relevância real do que um bi-encoder.
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')Por que essa combinação funciona
O design em duas etapas explora uma assimetria importante: a busca ANN rápida da primeira etapa é escalável para milhões de documentos em milissegundos, enquanto o cross-encoder preciso da segunda etapa opera apenas sobre o pequeno conjunto de candidatos. Você obtém a escalabilidade da busca aproximada com a precisão da pontuação conjunta exata. O pipeline geral é rápido e altamente preciso — algo que nenhuma das etapas consegue alcançar sozinha.
Perfil de latência da recuperação em duas etapas
Em um pipeline típico de duas etapas: a etapa 1 (busca ANN vetorial em 1 milhão de documentos) leva de 5 a 20 ms; a etapa 2 (cross-encoder em 100 candidatos) leva de 100 a 500 ms, dependendo do tamanho dos documentos e do hardware. O orçamento total de latência é de 150 a 600 ms, aceitável para a maioria das aplicações. A aceleração por GPU na etapa 2 pode reduzir a reordenação para menos de 30 ms em documentos curtos, tornando o pipeline competitivo em latência com a recuperação de uma única etapa em aplicações sensíveis ao tempo de resposta.
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_docsEscolhendo o tamanho certo do conjunto de candidatos
O tamanho do conjunto de candidatos da primeira etapa é um hiperparâmetro crítico. Se for pequeno demais (por exemplo, 10), documentos relevantes podem ser perdidos antes mesmo do início da reordenação. Se for grande demais (por exemplo, 500), a latência da etapa 2 aumenta excessivamente. A curva de cobertura em N — quantos documentos relevantes são capturados em diferentes valores de N — orienta essa escolha. Os pontos ideais típicos ficam entre 50 e 150 candidatos, quando a cobertura está próxima da saturação, mas a latência continua administrável.
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}')Primeira etapa híbrida + segunda etapa com Cross-Encoder
A configuração mais poderosa em duas etapas combina um recuperador híbrido (denso + BM25) na primeira etapa com um cross-encoder na segunda. A recuperação híbrida maximiza a cobertura da primeira etapa ao combinar correspondência semântica e por palavras-chave, e o cross-encoder então seleciona com precisão os documentos mais relevantes do conjunto combinado de candidatos. Essa configuração alcança consistentemente qualidade de recuperação de última geração em avaliações de referência.
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,
)APIs comerciais de reordenação
Se quiser a precisão de um cross-encoder sem gerenciar seu próprio modelo, tanto o Cohere Rerank quanto o Jina AI Reranker oferecem APIs de reordenação hospedadas na nuvem. Você envia uma consulta e uma lista de textos de documentos e recebe as pontuações de relevância. Essas APIs usam modelos cross-encoder grandes (frequentemente com mais de 500 milhões de parâmetros), que superam cross-encoders pequenos hospedados por você, ao custo de uma latência adicional de API (50 a 300 ms) e de uma cobrança por documento reordenado.
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]}')Quando duas etapas são exagero
A recuperação em duas etapas adiciona complexidade e latência em comparação com a de uma única etapa. Ela nem sempre é necessária. Para corpora pequenos, com menos de 10.000 documentos, um único cross-encoder sobre todo o conjunto pode ser rápido o suficiente. Em aplicações nas quais uma latência inferior a 100 ms é essencial e os ganhos de precisão são modestos, a recuperação densa de uma única etapa pode ser preferível. Use duas etapas quando tiver um corpus grande, requisitos elevados de precisão e puder aceitar uma latência de recuperação de 200 a 500 ms.
Recuperação em três etapas para escala extrema
Para corpora com dezenas de milhões de documentos, às vezes é usada uma pipeline de três etapas: na primeira, 10.000 candidatos são recuperados com ANN; na segunda, eles são reordenados até chegar a 100 usando um codificador cruzado pequeno e rápido; e, na terceira, são reordenados até chegar a 5 usando um codificador cruzado grande e potente. Cada etapa aplica um modelo mais caro e preciso a um conjunto menor. Essa arquitetura é usada por mecanismos de busca e sistemas de perguntas e respostas sobre documentos em grande escala.
Verificação rápida
Teste sua compreensão sobre por que a recuperação em duas etapas funciona, com base nesta lição.
Resumo da lição
Nesta lição, você aprendeu que bi-codificadores são rápidos, mas limitados à codificação independente da consulta e do documento; codificadores cruzados são precisos graças à codificação conjunta, mas lentos demais para pesquisar um corpus inteiro; e a recuperação em duas etapas combina os dois: uma primeira etapa rápida, com alta revocação, seguida por uma segunda etapa precisa, com alta precisão. A primeira etapa recupera muito mais candidatos do que o necessário para evitar deixar de encontrar documentos relevantes. A seguir, implementaremos a reordenação com codificador cruzado usando Cohere e BGE.
Perguntas Frequentes
A aula “Por que a recuperação em duas etapas funciona” é grátis?
Sim — o texto completo de “Por que a recuperação em duas etapas funciona” é grátis para ler aqui na web. Para praticá-la interativamente (um editor de código integrado e um tutor de IA 24/7) e desbloquear o restante do curso de AI Engineering Academy, atualize para CoddyKit PRO. O curso de AI Engineering Academy inclui 4 aulas no total.
O que vou aprender em “Por que a recuperação em duas etapas funciona”?
Entenda o compromisso entre revocação e precisão na recuperação em uma única etapa e como um recuperador rápido e preliminar, seguido por um reclassificador lento porém preciso, combina o melhor dos… Você pratica AI Engineering Academy com código prático que executa diretamente no navegador, e um tutor de IA 24/7 responde suas dúvidas enquanto trabalha na aula.
Preciso ter experiência prévia para começar AI Engineering Academy?
Nenhuma experiência prévia é necessária. AI Engineering Academy no CoddyKit é estruturado para alunos iniciantes até avançados, então você pode começar aqui ou desde o início e aprender no seu ritmo. Esta é a aula 1 de 4.
Quanto tempo leva a aula “Por que a recuperação em duas etapas funciona”?
A maioria das aulas CoddyKit leva cerca de 5–10 minutos. Cada uma é compacta e interativa, então você faz progresso constante e retoma exatamente de onde parou entre web e app.
Posso escrever e executar código nesta aula de AI Engineering Academy?
Sim. Cada aula de AI Engineering Academy inclui um editor de código integrado, então você escreve e executa código real direto no navegador e recebe feedback de IA instantaneamente — nenhuma configuração local necessária.
Todas as aulas deste curso
- Por que a recuperação em duas etapas funciona
- Reclassificação com codificador cruzado usando Cohere e BGE
- Compressão contextual e filtragem por relevância
- Medindo o impacto da reclassificação