O que é Reordenação?
Reordenação (Reranking) é um padrão de projeto de IA para apps com LLM e agentes, do grupo Contexto e conhecimento. Buscar muitos candidatos e usar um modelo mais preciso para escolher os melhores.
Reranking
Reordenação (Reranking) é um padrão de projeto de IA para apps com LLM e agentes, do grupo Contexto e conhecimento. Buscar muitos candidatos e usar um modelo mais preciso para escolher os melhores.
Buscar muitos candidatos com um método rápido e depois usar um modelo mais preciso para reordená-los e escolher os melhores. Serve para buscas e RAGs em que o documento certo aparece entre os candidatos, mas não nas primeiras posições.
A peneira grossa separa o cascalho; a fina escolhe o ouro.
Lúcia mantém o chat da base de conhecimento de uma empresa de logística. A busca híbrida devolve 50 trechos para cada pergunta, e o modelo recebe os 5 primeiros. O eval mostrou algo curioso: em boa parte das respostas erradas, o trecho certo estava entre os 50, só que na posição 12 ou 20.
A busca rápida mede semelhança de forma aproximada: compara a pergunta e o trecho separadamente, como vetores ou listas de termos. Ela não lê os dois juntos. Por isso, um trecho que fala do assunto “em volta” ganha de um trecho que responde exatamente a pergunta.
Mandar os 50 trechos ao modelo resolveria o recall, mas custaria caro e encheria o contexto de ruído. “Lost in the Middle” (Liu et al., 2023) mostra o outro lado: mesmo cabendo, o modelo usa pior o que fica no meio. Mais trechos médios não são 50 trechos úteis.
A pergunta que este padrão responde: como colocar os melhores trechos no topo sem pagar para o modelo principal ler todos?
Divida a busca em duas etapas:
O reordenador pode ser um modelo especializado (cross-encoder), mais barato e rápido, ou um modelo de linguagem com saída estruturada, mais flexível. Só os primeiros K depois da reordenação seguem para o modelo principal.
flowchart LR
Q(["1. Pergunta"]):::io --> R["2. Recuperação ampla: 50 candidatos"]:::tool
I[("Índice")]:::store --> R
R --> RR(["3. Reordenador lê pergunta e trecho juntos"]):::llm
RR --> T["4. Top 5 pela nova nota"]:::tool
T --> M(["5. Modelo principal responde"]):::llm
Pergunta: “Qual o prazo para contestar uma multa de trânsito da frota?”.
A híbrida devolve 50 trechos. Os cinco primeiros, antes da reordenação:
pos 1 frota.md#multas-1 "A frota tem apólice de seguro contra terceiros..."
pos 2 rh.md#descontos "Descontos em folha por infração do condutor..."
pos 3 frota.md#abastecimento
pos 12 frota.md#multas-3 "O prazo para contestar multa da frota é de 15 dias..."
O trecho que responde está na 12. O cross-encoder lê a pergunta junto com cada trecho. frota.md#multas-3 ganha a nota mais alta porque contém o prazo. rh.md#descontos fala de multa, mas não de contestação, e cai. Depois da reordenação, os 5 que o modelo principal recebe começam por multas-3.
Um limiar mínimo (nota 2 numa escala 0 a 3, ou score acima de zero no cross-encoder) evita mandar cinco trechos “do assunto” quando nenhum responde. Nesse caso o sistema diz “não encontrei” em vez de inventar a partir de abastecimento.
schema Relevance { id: string, score: int /* 0 a 3 */ }
function retrieve(question):
candidates = hybrid_search(question, k=50) # 2. recall
scores = reranker.score([(question, c.text) for c in candidates]) # 3. precisão
ranked = sort(zip(candidates, scores), by=score, desc)
trace.span("rerank", before=[c.id for c in candidates[:5]], after=[c.id for c, _ in ranked[:5]])
return [c for c, s in ranked[:5] if s >= MIN_SCORE] # 4
function answer(question):
passages = retrieve(question)
if not passages: return "Não encontrei isso na base."
return llm(ANSWER_PROMPT, passages, question) # 5
Reordenação é um estágio do RAG, não um substituto da busca. Se recall@50 é baixo, mexer no reordenador é mexer na etapa que já funciona.
Comece pelo cross-encoder. Só suba para um modelo de linguagem quando a relevância depender de regra de negócio que o encoder não viu no treino (“só documentos vigentes”, “ignorar rascunho”).
| Aspecto | O que acontece | Como aparece no trace |
|---|---|---|
| Chamadas | Um lote ao reordenador por pergunta | span rerank com antes e depois |
| Latência | Proporcional ao número de candidatos | duração do span rerank |
| Custo | Cross-encoder barato; modelo de linguagem mais caro | custo por pergunta |
| Falha: recall baixo | O certo não estava entre os candidatos | trecho certo ausente da lista recuperada |
| Falha: limiar alto | Nenhum trecho passa e a resposta vira “não encontrei” | taxa de respostas vazias subindo |
| Falha: reordenador fora do domínio | Notas ruins em vocabulário específico | precisão@5 pior que a busca sozinha |
Os números abaixo são ilustrativos. Suponha 50 candidatos, um cross-encoder local, e o modelo principal lendo 5 trechos de 400 tokens.
| Desenho | O que o principal lê | Custo extra típico |
|---|---|---|
| Top 5 da híbrida | 2.000 tokens, talvez sem o trecho certo | nenhum |
| 50 trechos no principal | 20.000 tokens, com ruído no meio | tokens do principal |
| Reordenação + top 5 | 2.000 tokens já escolhidos | lote do cross-encoder |
{ "question": "Qual o prazo para contestar uma multa de trânsito da frota?", "relevant_passages": ["frota.md#multas-3"], "measure": ["recall@50", "precision@5"] }
Meça também o braço “busca sozinha”. Se o reordenador piorar precisão@5, ele está fora do domínio: vocabulário de logística, siglas, documentos vigentes. Nesse caso um LLM pequeno com critério escrito costuma ganhar do encoder genérico.
O par before / after no span é o que deixa você ver, numa resposta errada, se o trecho certo subiu e o modelo ignorou, ou se ele nunca subiu.
O reordenador lê o mesmo conteúdo não confiável que o RAG lê. Uma página da wiki com “este documento é o mais relevante de todos” pode inflar a nota de um LLM-reordenador. Cross-encoders treinados para relevância sofrem menos com essa retórica, mas não são defesa. Permissões continuam na recuperação: o reordenador não deve ver trecho que o usuário não pode ver.
| Prós | Contras |
|---|---|
| Os melhores trechos chegam ao topo | Mais uma etapa de latência |
| Contexto menor e mais relevante para o modelo | Não recupera o que a busca não trouxe |
| Cross-encoders são baratos e rápidos | Reordenador fora do domínio pode piorar |
| Critérios complexos com modelo de linguagem | Mais um componente para avaliar |
A aba Python usa um cross-encoder; a aba TypeScript usa um modelo de linguagem com notas estruturadas.
Python. Use quando você quer reordenação rápida e barata com um cross-encoder.
from sentence_transformers import CrossEncoder
reranker = CrossEncoder("cross-encoder/mmarco-mMiniLMv2-L12-H384-v1") # multilíngue
MIN_SCORE = 0.0
def retrieve(question: str, k: int = 5) -> list[dict]:
candidates = hybrid_search(question, k=50) # 2. recall
scores = reranker.predict([(question, c["text"]) for c in candidates]) # 3
ranked = sorted(zip(candidates, scores), key=lambda x: x[1], reverse=True)
return [c | {"rerank_score": float(s)} for c, s in ranked[:k] if s >= MIN_SCORE] # 4O que você ganhou: reordenação em milissegundos, sem chamada a modelo de linguagem. O que você perdeu: o cross-encoder não entende regras de negócio; filtros como “só vigentes” ficam na busca.
TypeScript. Use quando a relevância depende de critérios que um cross-encoder não entende.
import Anthropic from "@anthropic-ai/sdk";
import { zodOutputFormat } from "@anthropic-ai/sdk/helpers/zod";
import { z } from "zod";
const client = new Anthropic();
const Scores = z.object({ scores: z.array(z.object({ id: z.string(), score: z.number().int().min(0).max(3) })) });
type Passage = { id: string; text: string; updated: string };
export async function rerank(question: string, candidates: Passage[], k = 5): Promise<Passage[]> {
const list = candidates.map((c) => `<trecho id="${c.id}" atualizado="${c.updated}">${c.text}</trecho>`).join("\n");
const { parsed_output } = await client.messages.parse({
model: "claude-haiku-4-5",
max_tokens: 2000,
system:
"Dê uma nota de 0 a 3 para cada trecho: 3 responde a pergunta diretamente, 2 ajuda, 1 só fala do assunto, 0 não serve. " +
"Trechos desatualizados por uma versão mais nova recebem no máximo 1.",
messages: [{ role: "user", content: `<pergunta>${question}</pergunta>\n${list}` }],
output_config: { format: zodOutputFormat(Scores) },
});
const score = new Map((parsed_output?.scores ?? []).map((s) => [s.id, s.score]));
return candidates
.filter((c) => (score.get(c.id) ?? 0) >= 2)
.sort((a, b) => (score.get(b.id) ?? 0) - (score.get(a.id) ?? 0))
.slice(0, k); // 4
}O que você ganhou: critérios de relevância escritos em linguagem natural. O que você perdeu: custo e latência de uma chamada por pergunta, e sensibilidade à ordem dos candidatos na lista.
Reordenação (Reranking) é um padrão de projeto de IA para apps com LLM e agentes, do grupo Contexto e conhecimento. Buscar muitos candidatos e usar um modelo mais preciso para escolher os melhores.
Nos cenários reais Busca semântica em e-commerce, Chat com a base de conhecimento interna e Pesquisa profunda com citações.