Reranking: el segundo filtro
El gap entre retrieval y relevancia
Sección titulada «El gap entre retrieval y relevancia»Tu retriever — dense o hybrid — está optimizado para recall: traer todos los chunks relevantes en una lista corta. No está optimizado para precisión: ordenarlos perfectamente por relevancia.
Si pedís top-4, los 4 chunks que devuelve incluyen los relevantes pero también algo de ruido. Si pedís top-20, casi seguro están todos los relevantes — pero ahora el problema es que 16 son ruido.
Reranking resuelve esto en dos pasos:
- Retrieve wide: traer top-20 candidatos rápido (dense + hybrid).
- Rerank: usar un modelo más fino (y más caro) para reordenar los 20 y quedarte con los top-4.
Tradeoff: latencia ×2, calidad típicamente +10–20%. Es la mejor relación esfuerzo/ganancia que vas a encontrar en Nivel 3.
Las dos opciones de reranker
Sección titulada «Las dos opciones de reranker»Cross-encoder (lo correcto en producción)
Sección titulada «Cross-encoder (lo correcto en producción)»Un modelo cross-encoder (BGE-reranker, mxbai-rerank, Cohere Rerank) toma (query, chunk) como input y devuelve un score de relevancia. Mucho más preciso que comparar embeddings independientes porque el modelo ve query y chunk juntos y puede cruzar señales.
Tradeoff: rapidísimo (~50ms por par) pero requiere instalar @xenova/transformers + onnxruntime-node y descargar un modelo (~270 MB). En el curso lo dejo como avanzado: el patrón está descrito al final, son ~30 líneas extras a tu pipeline.
LLM-as-reranker (lo del curso)
Sección titulada «LLM-as-reranker (lo del curso)»Le mandás los N candidatos al LLM con un prompt que pide ordenarlos. Cero setup nuevo — usa el mismo llama3.2:3b que ya tenés. Más lento (~150-300ms) y menos preciso que un cross-encoder, pero funciona y enseña el patrón.
El archivo
Sección titulada «El archivo»Vive en packages/01-vercel-ai-sdk/src/retrievers/rerank.ts. La idea es simple: numerar los candidatos, pasárselos al LLM, parsear el JSON.
const prompt = `Sos un evaluador de relevancia. Para la PREGUNTA y los PASAJES de abajo, asignale a cada pasaje un score entre 0 y 1 (1 = totalmente relevante, 0 = irrelevante).
Respondé EXACTAMENTE con este JSON, sin texto adicional, sin bloques de código:[{"id": 1, "score": 0.9}, {"id": 2, "score": 0.4}, ...]
PREGUNTA:${query}
PASAJES:${numbered}
JSON:`;Tres detalles:
- Score continuo 0-1, no labels. Más expresivo: el LLM puede decir “este es claramente más relevante que este otro pero ambos son OK”.
- JSON cerrado: simplifica el parser. El regex captura el primer
[...]. - Temperatura 0: queremos consistencia, no creatividad.
Defensiva con el parser
Sección titulada «Defensiva con el parser»Los LLMs chicos (3B params) a veces devuelven JSON malformado, prosa antes/después, o se olvidan de un id. El parser hace lo razonable:
const match = data.response.match(/\[\s*\{[\s\S]*\}\s*\]/);if (!match) { // Fallback: keep original order, score 0.5 across the board return candidates.slice(0, topK).map((c) => ({ chunk: c.chunk, score: 0.5 }));}
let parsed: Array<{ id: number; score: number }>;try { parsed = JSON.parse(match[0]);} catch { return candidates.slice(0, topK).map((c) => ({ chunk: c.chunk, score: 0.5 }));}
const seen = new Set<number>();const ranked: RerankHit[] = [];for (const item of parsed) { if (typeof item.id !== 'number' || /* ... */ seen.has(item.id)) continue; // ... map to chunk, push}Si el LLM falla feo, el binario sigue funcionando con el ranking original (top-K del retriever). No mata la query — solo no aporta el lift del rerank.
El comando
Sección titulada «El comando»pnpm vercel:query:adv "<pregunta>" --rerankInternamente:
- Retrieve dense (o hybrid si pasás
--hybridtambién) conRETRIEVE_K = 20en vez de los 4 default. - Pasa los 20 candidatos al LLM con el prompt de arriba.
- Filtra al top-4 por score del rerank.
- Genera la respuesta con esos 4.
Combinable con todas las otras flags del nivel.
La métrica clave: nDCG@K
Sección titulada «La métrica clave: nDCG@K»Cuando midas el impacto del rerank, retrieval@k no alcanza: rerank no cambia QUÉ chunks tenés (siguen siendo los del top-20 del retriever) — cambia el ORDEN dentro del top-K.
Para medir reordenamiento querés nDCG@K (Normalized Discounted Cumulative Gain): puntúa más alto cuando los chunks correctos están ARRIBA, no solo cuando aparecen.
El harness del Nivel 2 (06-evals) por ahora solo computa retrieval@k. Si querés agregar nDCG, está documentado al final de este capítulo cómo. Para uso típico, faithfulness y answer-relevance van a moverse cuando rerank funciona — tu RAG pasa a tener chunks más enfocados arriba → menos ruido → menos alucinación.
Lab: medir el lift
Sección titulada «Lab: medir el lift»Workflow recomendado: corré la misma pregunta del eval set con y sin --rerank, compará contexts y faithfulness. O — mejor — corré el harness completo:
pnpm --filter @rag-lab/06-evals run eval -- --only vercel-ai-sdk --top-k 4# (no rerank — baseline)
# Para comparar con rerank, por ahora hay que invocar query.adv.ts# directamente con cada pregunta del golden set. La integración del# rerank en el adapter del harness es un ejercicio que queda pendiente# (requiere modificar query.fn.ts para aceptar opciones avanzadas).Apéndice: cómo cambiar a cross-encoder real
Sección titulada «Apéndice: cómo cambiar a cross-encoder real»Si querés reemplazar LLM-as-reranker con un cross-encoder real:
pnpm add @xenova/transformers onnxruntime-nodeenpackages/01-vercel-ai-sdk.- Crear
src/retrievers/rerank-ce.tscon la misma signature(query, candidates, topK) → RerankHit[]. - Cargar el modelo:
await AutoModel.from_pretrained('Xenova/bge-reranker-v2-m3')yAutoTokenizer.from_pretrained(...). - Para cada candidato: tokenizar
(query, candidate.preview), pasar al modelo, leer el logit como score. - Sort por score, slice top-K.
Patrón completo: ~30 líneas. La primera vez baja el modelo (~270 MB) — después cachea local. Latencia: 30-80ms por par contra 200-400ms del LLM-as-reranker.
Cuándo NO usar rerank
Sección titulada «Cuándo NO usar rerank»- Latencia crítica (< 500ms total). Rerank duplica latencia mínimo.
- Top-K = 1. Si solo querés el chunk más probable, retrieve te lo da. Rerank empieza a aportar desde top-3 en adelante.
- Corpus muy chico (< 100 chunks). El retriever ya está cerca del techo; rerank es overkill.
Lo que viene
Sección titulada «Lo que viene»Tenés un retriever fino (dense + hybrid) y un filtro fino (rerank). Pero hay otra dimensión: la pregunta misma. Si tu pregunta tiene varios temas, una sola búsqueda no alcanza. El siguiente capítulo: multi-query.