Reranker y hybrid retrieval: el comité que decide los 5 chunks que el LLM va a leer de verdad

Este post desmonta la capa de retrieval dentro de la pieza RAG del pipeline LLMOps de seis etapas. Encaja directamente sobre el post de curación del corpus: si el bibliotecario decidía qué entra al índice, el comité de este post decide qué sale del índice a la cara del LLM. Es la etapa que más mueve la calidad real de un RAG en producción, y la que más equipos resuelven con “dense top-k y a correr”.

TL;DR

El antipattern más extendido en RAG real es: corpus curado, dense embeddings con bge-m3, query directo del usuario, top_k=5 de cosine similarity y al LLM. El sistema entrega respuestas que parecen buenas en el demo, mediocres con tráfico real y pésimas con queries de tres palabras, jerga técnica, abreviaturas internas o cualquier cosa fuera del dominio de fine-tuning del embedder. La causa raíz casi nunca es el modelo: es que una sola pasada de dense retrieval sobre el query crudo es estructuralmente insuficiente para los casos que un usuario real produce. El campo ha consolidado en 2026 el patrón de retrieval de tres capas: capa ancha que mezcla dense + sparse (BM25 / SPLADE) vía Reciprocal Rank Fusion para asegurar recall, capa fina con cross-encoder reranker (BAAI/bge-reranker-v2-m3, Cohere Rerank 3, mxbai-rerank-large) que reordena los 30-50 candidatos por relevancia verdadera, y opcionalmente capa de late interaction con ColBERT-v2 o un LLM reranker para los casos críticos. Encima de todo, un patrón de query rewriting que reescribe queries malformadas y HyDE que genera un documento hipotético para hacer el embedding de la respuesta esperada en vez del de la pregunta. Este post entra ficha a ficha: por qué cada capa existe, qué presupuesto de latencia consume, las matemáticas mínimas de RRF y del trade-off recall/precisión, el stack OSS dominante en 2026, el hardware on-premise para servirlo bajo soberanía de datos, y las siete trampas operativas que matan la etapa.

La analogía: el comité de oposición a la cátedra

Un departamento universitario tiene una plaza de catedrático y recibe 3.000 candidaturas. Nadie va a leer 3.000 CVs a fondo. El proceso real se organiza en rondas:

Ronda 1 — pre-screening masivo. Un sistema de filtros automáticos lee cada CV en segundos. Una vía mira las palabras clave del puesto (publicaciones en revistas concretas, idiomas, certificaciones); otra vía mira el perfil global (área de investigación, índice h, trayectoria). Las dos vías se ejecutan en paralelo. Cada una propone su lista corta de 30-50 candidatos. Se combinan los dos rankings y queda una lista de unos 50-100 candidatos sobre los que va a actuar la siguiente ronda. La métrica que importa aquí es recall: no perder al candidato bueno por error de filtro.

Ronda 2 — lectura cuidadosa. Un miembro del tribunal lee a fondo cada CV de la lista corta y lo puntúa contra el perfil real de la plaza. Es lento — 10-20 minutos por CV — pero discrimina mucho mejor que el filtro automático. Reordena los 100 candidatos en un ranking fino y se queda con los 5 finalistas. La métrica que importa aquí es precisión: que el top-5 sea efectivamente el top-5 real.

Ronda 3 (opcional) — entrevista personal. Para los 5 finalistas se hacen entrevistas largas. Caras a cara, preguntas adaptadas al perfil de cada candidato, contraste con la realidad del puesto. Es carísimo y muy lento, pero para las plazas críticas justifica el coste.

El retrieval de un RAG moderno funciona exactamente así. Cambia el vocabulario y conserva la estructura:

Query del usuario"cómo cancelo suscripción"+ rewriter / HyDECapa 1a · SparseBM25 / SPLADEtop-50 lexicalCapa 1b · Densebi-encoder (bge-m3)top-50 semanticCapa 2 · FusionReciprocal Rank Fusion→ top-30 combinadosCapa 3 · Rerankercross-encoder bge-rerank-v2→ top-5 reordenadosCapa 4 (opcional) · ColBERT / LLM rerankerlate interaction o juez LLMpara casos críticosContexto del LLM5 chunks listos para responder~2.000 tokens

recall ancho~50-100ms totalk=60 típicoprecisión fina~80-300ms

La equivalencia es exacta. La capa ancha garantiza que el chunk correcto esté en la lista corta (recall). La capa fina ordena bien dentro de la lista corta (precisión). Cada capa se justifica por una asimetría operativa: la ancha es barata y paraleliza, la fina es cara y secuencial. Resolverlo todo con la cara — un único cross-encoder sobre el corpus entero — es operativamente insostenible. Resolverlo todo con la barata — un único dense top-k — es funcionalmente insuficiente.

El problema: por qué dense solo no basta

El argumento contra el dense-only no es teórico. Es que las queries que produce un usuario real tienen patrones que el dense bi-encoder no captura bien por construcción:

Queries cortas. “fechas IRPF 2026” pasa por el embedder y produce un vector que está cerca de cualquier chunk que mencione fechas e IRPF — incluyendo chunks de 2023, chunks de calendarios genéricos, chunks de IRPF empresarial cuando preguntabas por el personal. BM25 desambigua mejor porque el peso de los términos infrecuentes (“2026”, “IRPF”) domina la puntuación.

Términos técnicos infrecuentes o internos. Códigos de producto (FBR-X42-PRO), abreviaturas de la organización (PYM-23-bis), nombres de variables internas. El embedder, entrenado con corpus público, los pasa al sub-token y los empuja a la zona “ruidosa” del espacio latente. Dos códigos completamente distintos pueden quedar a 0,02 de distancia coseno. BM25 los trata como tokens exactos y los puntúa correctamente.

Polisemia. “Java” como lenguaje vs Java como isla vs Java como café. El dense embedding promedia los significados; el ranking se vuelve mediocre. Aquí el dense puede ganar al BM25 si la query lleva contexto suficiente — pero ese “si” rara vez se cumple con queries de usuario real.

Sinónimos y reformulaciones. “Cómo cancelo mi suscripción” vs un chunk titulado “Procedimiento de baja del servicio”. Aquí el dense gana al BM25: el espacio semántico los acerca aunque no compartan tokens. Es exactamente el caso donde el dense brilla y el BM25 fracasa.

La conclusión operativa: ningún retriever solo cubre los cuatro casos. La combinación los cubre todos. Y para los casos en los que ambos retrievers fallan (queries muy ambiguas, contexto contradictorio entre chunks), entra la tercera capa: el reranker que lee el query y cada chunk a la vez con un cross-encoder, y produce un score de relevancia real mucho más discriminante que la similitud coseno.

Capa 1 — Hybrid retrieval: dense + sparse

Por qué BM25 sigue vivo en 2026

BM25 es de 1994. No es deep learning. No requiere GPU. Y sigue siendo competitivo con dense retrievers de cientos de millones de parámetros en muchos benchmarks de retrieval (BEIR, 2021-2024). Su fórmula es simple:

$$ \mathrm{BM25}(q, d) = \sum_{t \in q} \mathrm{IDF}(t) \cdot \frac{\mathrm{tf}(t,d) \cdot (k_1 + 1)}{\mathrm{tf}(t,d) + k_1 \cdot (1 - b + b \cdot \frac{|d|}{\overline{|d|}})} $$

Donde $\mathrm{tf}(t,d)$ es la frecuencia del término $t$ en el documento $d$, $|d|$ la longitud del documento, $\overline{|d|}$ la longitud media del corpus, e IDF la inversa de la frecuencia documental. $k_1$ y $b$ son hiperparámetros (típicamente $k_1 = 1.2$, $b = 0.75$). En la práctica usas pyserini, Tantivy, Elasticsearch o OpenSearch y no escribes la fórmula.

SPLADE (Formal et al., 2021) es la generación neuronal de BM25: aprende pesos sparse sobre el vocabulario BERT, expandiendo cada token a sus términos relacionados. Mantiene la interpretabilidad de BM25 (puedes ver qué términos del query matchean qué términos del documento) y supera a BM25 puro en BEIR. El precio es que necesitas inferencia GPU sobre el query (y sobre cada documento al indexar, una vez).

La regla del pulgar en 2026:

  • Corpus < 10M chunks, queries con vocabulario amplio: BM25 puro vía Elasticsearch / Tantivy. Indexación trivial, queries 1-5ms.
  • Corpus 10M-100M, dominio específico: SPLADE-v3 indexado en Elasticsearch o Vespa. Indexación lenta (necesitas GPU una vez), queries 5-20ms.
  • Corpus > 100M, latencia crítica: BM25 con expansión de query externa (synonyms, query2doc) es la opción operacional, con SPLADE reservado para los segmentos críticos.

Dense bi-encoder: el segundo carril

El bi-encoder produce un vector único por chunk y otro por query, y la relevancia es la similitud coseno (o producto escalar) entre ambos. El modelo dominante en 2026 para multilingüe es BAAI/bge-m3: 568M params, dimensiones 1024, soporta hasta 8.192 tokens de contexto por chunk, multivector (dense + sparse + multi-vec ColBERT-style en el mismo embedder), entrenado sobre 100+ idiomas. Alternativas:

EmbedderParamsDimMultilingüeComentario
BAAI/bge-m3568M1024el todo-terreno por defecto en 2026
intfloat/multilingual-e5-large-instruct560M1024competidor directo, instruct-style queries
nomic-ai/nomic-embed-text-v1.5137M768inglésrápido, dim variable Matryoshka
jinaai/jina-embeddings-v3570M1024con task-specific LoRAs por dominio
mixedbread-ai/mxbai-embed-large-v1335M1024ingléstop inglés en MTEB
Snowflake/arctic-embed-l-v2.0568M1024enterprise-oriented, licencia Apache

El criterio práctico de selección: multilingüe sí o sí si tu corpus o queries lo necesitan (bge-m3, multilingual-e5, jina-v3, arctic-l-v2), Matryoshka si quieres ahorrar memoria del vector store reduciendo dimensiones sin reembedear (nomic), y MTEB-leaderboard ranking sólo como tie-breaker, nunca como criterio único — los benchmarks de MTEB son contaminables y la diferencia de 1-2 puntos rara vez se traduce en mejora real en tu dominio.

La fusión: Reciprocal Rank Fusion (RRF)

Tienes dos rankings, uno de BM25 y uno de dense. ¿Cómo los combinas? La opción ingenua — sumar puntuaciones — falla porque los scores no son comparables (BM25 produce números 0-30, coseno 0-1, distancias L2 0-2…). La opción que se ha consolidado es Reciprocal Rank Fusion (Cormack, Clarke, Buettcher 2009), que ignora los scores absolutos y usa sólo los rankings:

$$ \mathrm{RRF}(d) = \sum_{r \in R} \frac{1}{k + \mathrm{rank}_r(d)} $$

Donde $R$ es el conjunto de retrievers y $\mathrm{rank}_r(d)$ es la posición del documento $d$ en el ranking del retriever $r$. La constante $k$ es típicamente $60$ y suaviza el peso de las primeras posiciones.

Ejemplo numérico. Tienes BM25 con top-5: [d_a, d_b, d_c, d_d, d_e] y dense con top-5: [d_b, d_f, d_a, d_g, d_h]. Calculas RRF de cada candidato:

Docrank BM25rank dense$\frac{1}{60+r_{\text{BM25}}}$$\frac{1}{60+r_{\text{dense}}}$RRF total
d_b210,016130,016390,03252
d_a130,016390,015870,03226
d_c30,0158700,01587
d_f200,016130,01613
d_d40,0156200,01562

d_b gana porque aparece bien rankeado en ambos. d_a lo sigue. Los que aparecen sólo en una lista quedan por debajo. Sin tunear nada, RRF tiende a empujar arriba los candidatos consensuados, que es exactamente lo que quieres como filtro grueso antes del reranker.

Variantes: weighted RRF asigna un multiplicador por retriever ($w_r \cdot \frac{1}{k + \mathrm{rank}_r(d)}$) cuando tienes razones para confiar más en uno; learned-to-rank entrena un modelo (LambdaMART o LightGBM ranker) sobre features de los dos retrievers — opción más potente pero exige labels de relevancia que la mayoría de equipos no tiene.

En Qdrant, Vespa, Weaviate y Elastic 8.11+ la fusión RRF está integrada como modo nativo: pides hybrid search con un query lexical y un vector y devuelven los top-k combinados sin tener que calcular tú la fusión.

Capa 2 — El reranker: bi-encoder vs cross-encoder

El bi-encoder mira query y documento por separado y compara los vectores resultantes. Es rapidísimo (un dot-product) y permite indexar los embeddings de documento una vez para siempre. El precio es que el modelo nunca ve query y documento juntos, y la representación comprimida pierde matices.

El cross-encoder mira query y documento concatenados: [CLS] query [SEP] document [SEP] pasa por el modelo y la salida es un score de relevancia. Discrimina muchísimo mejor — los matices de orden y de relación local entre términos sí se capturan — pero el coste se dispara: cada par (query, doc) requiere una inferencia completa del modelo. No puedes precomputar nada. Si quieres rerankear 50 candidatos, son 50 inferencias.

AspectoBi-encoderCross-encoder
Inferencia por chunk en indexadosí, 1 vezno precomputable
Inferencia en query1 (query) + dot-product por chunk1 por par (query, chunk)
Latencia típica para top-505-20 ms (CPU posible)80-300 ms (GPU recomendado)
Discriminaciónmediaalta
Uso típicorecall (capa 1)precisión (capa 2)

Esta asimetría es por qué el patrón canónico es embudo de dos pasos: el bi-encoder hace recall ancho barato y el cross-encoder hace precisión fina cara, sobre 30-50 candidatos en lugar de millones.

Stack de cross-encoder rerankers OSS 2026

RerankerParamsMultilingüeLatencia top-50 (A100)Comentario
BAAI/bge-reranker-v2-m3568Msí (100+ idiomas)~90 msel defaul de facto en 2026
BAAI/bge-reranker-v2-gemma2B~250 msmás calidad, más coste
mixedbread-ai/mxbai-rerank-large-v21.5Binglés~180 mstop inglés en BEIR rerank
jinaai/jina-reranker-v2-base-multilingual278M~50 msrápido y suficiente para muchos casos
Alibaba-NLP/gte-multilingual-reranker-base306M~55 mscompetidor directo de jina
Cohere Rerank 3(cerrado)sí (100+ idiomas)~120 ms (API)comercial, no on-prem
Voyage rerank-2(cerrado)~100 ms (API)comercial, no on-prem

Para despliegues bajo soberanía de datos la elección obvia es bge-reranker-v2-m3 como punto de partida, con jina-reranker-v2-base como alternativa ligera. Si el dominio es exclusivamente inglés y el presupuesto de hardware lo permite, mxbai-rerank-large-v2 saca uno o dos puntos más en BEIR. Cohere y Voyage son la opción cuando puedes salir a una API externa — descartado en escenarios ENS / NIS2 con datos sensibles.

Late interaction: ColBERT-v2 como compromiso

Hay una tercera opción que está ganando tracción: late interaction, materializada en ColBERT-v2 (Santhanam et al., 2022). La idea es que el documento se embedea no como un vector único, sino como una matriz de embeddings — uno por token —. Y en query time, el query también se embedea como matriz, y la similitud es la suma de los máximos por columna (MaxSim):

$$ S(q, d) = \sum_{i \in q} \max_{j \in d} \langle E_q[i], E_d[j] \rangle $$

El resultado es retrieval con calidad cercana a cross-encoder pero coste mucho menor (no hay que pasar el modelo entero por cada par). El precio es espacio: cada chunk son ~150-200 vectores en lugar de 1, y eso multiplica por ~100 el tamaño del índice. Para corpus pequeños-medianos (<10M chunks) es manejable; para corpus enormes la compresión PLAID y ColBERT-v2 quantization lo hace viable hasta 100M.

En 2026 las implementaciones de referencia son RAGatouille (wrapper Python), JaColBERT (japonés), Vespa.ai (motor de búsqueda con soporte nativo de ColBERT como first-class citizen), y la integración nativa en bge-m3 que produce los multivectors ColBERT-style como parte del mismo modelo dense.

Mi recomendación operativa: empieza con cross-encoder reranker sobre top-30 del hybrid retrieval. Sólo si los números de precisión no llegan, prueba late interaction como alternativa al cross-encoder (no como capa adicional). Late interaction como tercera capa rara vez compensa el coste operacional para la mayoría de casos.

Query rewriting y HyDE: corregir el query antes del retrieval

El query del usuario es la entrada peor controlada de todo el sistema. Los problemas más frecuentes:

Queries demasiado cortas: “RGPD multas” → recupera cualquier chunk sobre RGPD o sobre multas.

Queries con típos o jerga: “qiero saber como cancelar la suscripcion premium” → embedder ruidoso por los typos.

Queries multi-intención: “compara los planes premium y enterprise y dime cuál tiene mejor SLA” → el dense intentará embedear las tres preguntas en un solo vector.

Queries con referencias resueltas en contexto: “y para el mes que viene?” → sin el contexto previo, el dense no sabe de qué habla.

Patrones canónicos 2026 para mitigar:

Query rewriting con LLM ligero. Antes del retrieval, un modelo pequeño (Qwen2.5-7B, Llama-3.2-3B, Phi-4-mini) reescribe el query del usuario en una versión “canónica”: completa abreviaturas, corrige typos, descompone multi-intención en sub-queries, resuelve referencias usando el historial de la conversación. Coste: una inferencia barata (~50-100 ms). Beneficio: las cuatro patologías anteriores se reducen sensiblemente.

Multi-query: el LLM genera 3-5 reformulaciones del query y haces retrieval con cada una; luego fusionas con RRF. Aumenta recall a coste de tiempo y de tokens del LLM.

Step-back prompting (Zheng et al., 2023): el LLM genera una pregunta más general que el query original ("¿qué es una suscripción premium en este sistema?"), haces retrieval con ambas y combinas. Ayuda con queries muy específicas que necesitan contexto.

HyDE — Hypothetical Document Embeddings (Gao et al., 2022): en lugar de embedear la pregunta, le pides a un LLM ligero que genere una respuesta hipotética y embedeas eso. El razonamiento es que el espacio semántico de las respuestas está más cerca del de los chunks reales que el espacio de las preguntas. Funciona especialmente bien en dominios con vocabulario muy específico. Coste: una inferencia del LLM ligero (~100-200 ms). Beneficio: en muchos casos +5-15 puntos de Recall@10.

El trade-off operativo es la latencia añadida. Cada técnica suma 50-300 ms al pipeline. En interacciones síncronas con SLO de 2s aún hay margen; en interacciones agentic con bucles de tool-use, cada ms cuenta. La elección práctica de 2026: rewriting con LLM ligero como default, HyDE para casos específicos donde se ha medido que ayuda, multi-query y step-back sólo cuando el caso lo justifica con números.

Matemáticas mínimas: presupuesto de latencia y métricas

Presupuesto de latencia de un pipeline canónico

Un pipeline hybrid + rerank típico en producción 2026 sobre un corpus de 5M chunks con bge-m3 + bge-reranker-v2-m3 sobre H100 con TEI:

EtapaLatencia p50Latencia p95Comentario
Query rewrite (LLM 3B)60 ms150 mssólo si activado
HyDE (LLM 3B)90 ms220 mssólo si activado
BM25 / SPLADE top-508 ms20 msElasticsearch o Tantivy local
Dense embedding del query12 ms35 msbge-m3 con batch=1
Dense top-50 (HNSW filterable)6 ms18 msQdrant con quantization escalar
RRF fusion → top-30<1 ms<1 msaritmética
Cross-encoder rerank top-3060 ms180 msbge-reranker-v2-m3 batched
Selección top-5 + format<1 ms<1 ms
Total sin rewrite/HyDE~90 ms~270 msrango realista en H100
Total con rewrite + HyDE~240 ms~640 ms+ ~150-370 ms

En RTX 4090 (~3× más lento que H100 en cross-encoder grande, similar en lo demás) los números se desplazan: ~180 ms p50 sin rewrite, ~400 ms p50 con rewrite + HyDE. Es perfectamente servible para un asistente síncrono con SLO de 2-3s, pero ajustado para uno con SLO de 500 ms.

Métricas: nDCG, MRR, Recall@k

Hay tres métricas clásicas que cualquier eval de retrieval reporta:

Recall@k: ¿el chunk relevante está entre los top-k? Métrica binaria, ignora orden. La que importa para la capa ancha.

$$ \mathrm{Recall@k} = \frac{|{\text{relevantes}} \cap {\text{top-k}}|}{|{\text{relevantes}}|} $$

Mean Reciprocal Rank (MRR@k): ¿en qué posición aparece el primer chunk relevante? Penaliza colocarlo lejos.

$$ \mathrm{MRR@k} = \frac{1}{|Q|} \sum_{q \in Q} \frac{1}{\mathrm{rank}_q} $$

donde $\mathrm{rank}_q$ es la posición del primer relevante para la query $q$ (o $\infty$ si no aparece en top-k).

nDCG@k: tiene en cuenta múltiples chunks relevantes con grados de relevancia. La métrica más usada para evaluar rerankers:

$$ \mathrm{DCG@k} = \sum_{i=1}^{k} \frac{2^{\mathrm{rel}_i} - 1}{\log_2(i+1)} \quad \mathrm{nDCG@k} = \frac{\mathrm{DCG@k}}{\mathrm{IDCG@k}} $$

La heurística operativa: Recall@50 mide la calidad de tu capa ancha (queremos > 95% — si no, la capa 2 nunca podrá recuperar lo perdido), nDCG@5 mide la calidad de tu reranker (queremos > 0,75 sobre golden set para considerar el sistema “bueno”) y MRR@5 mide la calidad de tu top-1 (importa especialmente en sistemas con top_k=1 o donde la respuesta del LLM se basa principalmente en el primer chunk).

Costes y throughput

Asumiendo cross-encoder bge-reranker-v2-m3 (568M params) en una H100 SXM con TEI 1.7 y batch dinámico:

  • Throughput: ~280 queries/seg con top-30 cada una (= ~8.400 pares chunk-query/seg).
  • VRAM ocupada: ~3.5 GB del modelo + ~6 GB de KV cache + activations con batch=32.
  • Coste energético: una H100 SXM consume ~700 W. A 8.400 pares/seg, el coste energético del reranking puro es ~0,083 mJ por par. En cifras de cuenta de la luz industrial española (~0,12 €/kWh, mayo 2026): ~0,000003 € por rerank de 30 candidatos. La factura es despreciable comparada con el coste del LLM downstream.

En RTX 4090 con TEI: ~95 queries/seg top-30 (~2.850 pares/seg), VRAM ~3.5 GB + ~5 GB. Servible para asistente interno con tráfico modesto (~5 QPS sostenidos, picos a 20-30).

El patrón canónico — qué pieza va dónde

El stack de referencia operativa 2026 para un RAG hybrid + rerank on-premise bajo soberanía de datos:

                       ┌─────────────────┐
  Query del usuario ──▶│ Query rewriter  │  (opcional, LLM 3B)
                       │ (Qwen2.5-7B-IT) │
                       └────────┬────────┘
                                ▼
                       ┌─────────────────┐
                       │     HyDE        │  (opcional)
                       │ (mismo LLM)     │
                       └────────┬────────┘
              ┌─────────────────┼─────────────────┐
              ▼                 ▼                 ▼
       ┌──────────────┐  ┌──────────────┐  ┌──────────────┐
       │ BM25/SPLADE  │  │ Dense bi-enc │  │ (filtros     │
       │ Elasticsearch│  │ bge-m3 + TEI │  │  por metadata│
       │ top-50       │  │ + Qdrant     │  │  por tenant) │
       │              │  │ top-50       │  │              │
       └──────┬───────┘  └──────┬───────┘  └──────┬───────┘
              └─────────┬───────┘                 │
                        ▼                         │
                ┌──────────────┐                  │
                │   RRF k=60   │◀─────────────────┘
                │   → top-30   │
                └──────┬───────┘
                       ▼
              ┌──────────────────┐
              │ Cross-encoder    │
              │ bge-reranker-v2  │
              │ TEI batch=32     │
              │ → top-5 final    │
              └──────┬───────────┘
                     ▼
              ┌──────────────────┐
              │  Contexto LLM    │
              │  ~2.000 tokens   │
              └──────────────────┘

Servicios clave:

  • Sparse index: Elasticsearch 8.16, OpenSearch 2.18 o Tantivy embebido. BM25 nativo, SPLADE opcional vía plugin.
  • Vector index: Qdrant 1.13 (HNSW filterable, scalar quantization, multivectors). Cubierto en detalle en el post de PostgreSQL + Qdrant en ingestión.
  • Embedder serving: TEI (Text Embeddings Inference) de HuggingFace para bge-m3, multilingual-e5 y compatibles. Endpoint OpenAI-compatible.
  • Reranker serving: TEI también para los rerankers de la familia bge-reranker, jina-reranker, mxbai-rerank. Endpoint /rerank separado.
  • LLM para rewriter/HyDE: vLLM 0.7+ sirviendo Qwen2.5-7B-Instruct o Phi-4-mini-Instruct. Si el LLM principal es el mismo, comparte recursos; si no, pod dedicado con quantization GPTQ-INT4.
  • Orquestación: el cliente RAG (FastAPI, LangChain, LlamaIndex) compone los tres carriles. Cada llamada paralela (Sparse + Dense) se hace concurrente; rewriter, HyDE y rerank son secuenciales.

Manifests mínimos para Kubernetes ya están desarrollados en el post de vLLM en Kubernetes y la pieza completa de orquestación encaja en el patrón del cluster H100 multi-tenant.

Hardware on-premise: qué cabe en RTX 4090 vs configuración genérica 4×H100 SXM

La realidad operacional importa. Dos perfiles típicos:

Configuración modesta — 1× RTX 4090 (24 GB Ada Lovelace)

Sirve cómodamente:

  • bge-m3 (568M) para dense embedding del query → ~1 GB VRAM
  • bge-reranker-v2-m3 (568M) cross-encoder → ~3.5 GB VRAM con batch=32
  • LLM ligero para rewriter/HyDE (Qwen2.5-7B GPTQ-INT4) → ~5-6 GB VRAM
  • LLM principal (Llama-3.1-8B-Instruct FP8) → ~11-12 GB VRAM

Total ocupación VRAM: ~21 GB. Quedan ~3 GB para KV cache del LLM principal, suficiente para 4-8 concurrent users con contextos ~4K tokens. Para asistente interno corporativo es viable. Si necesitas contexto mayor o más concurrencia, hay que repartir: o el LLM principal se sirve fuera del 4090 (otra GPU, otro nodo), o se cae al cuantizar el reranker (no recomendable: pierdes los puntos de nDCG que justifican el coste de añadirlo).

Configuración genérica — 4×H100 SXM (320 GB total, NVLink)

Sirve cómodamente todo lo anterior multiplicado por 30-50× en throughput y abre la puerta a:

  • LLM principal grande (Llama 3.3 70B FP8 o Qwen2.5-72B FP8) en 2 GPUs con tensor parallelism
  • Rerankers grandes (bge-reranker-v2-gemma 2B) con calidad ~2 puntos por encima
  • Late interaction (ColBERT-v2) en uno de los nodos para casos críticos
  • Múltiples LLM ligeros paralelos para rewriter, HyDE y judge de evals

La regla práctica: el reranker es de las cosas más eficientes que puedes meter en una GPU. La VRAM ocupada es modesta, el throughput es alto, y la mejora de calidad por euro de hardware extra es de las mejores del stack RAG.

Las siete trampas que matan el retrieval

Trampa 1 — Top-k al LLM sin reranker. El dense retrieval devuelve top-5, esos 5 chunks van al LLM directamente. Sin reranker, el orden de los 5 es el orden de cosine similarity, que no es el orden de relevancia real. El LLM responde basándose mucho más en los primeros chunks que en los últimos, y los matices se pierden. Síntoma: respuestas confiadas con citas que no son las más relevantes del corpus.

Trampa 2 — Dense-only sin BM25. Queries cortas y queries con jerga interna funcionan mal, los demos quedan bonitos, el tráfico real se queja. Síntoma: en QA con tickets de soporte real, el sistema falla específicamente en queries de menos de 5 palabras o con códigos de producto.

Trampa 3 — Reranker mal calibrado en top-k. Top-k de capa 1 demasiado pequeño (top-10) recorta antes de que el reranker pueda hacer su trabajo. Top-k demasiado grande (top-200) dispara latencia sin mejorar nDCG. El sweet spot canónico es top-30 a top-50 del hybrid → top-5 a top-10 del reranker. Síntoma: nDCG@5 del reranker estancado sin importar el modelo de reranker que pruebes.

Trampa 4 — Embedder y reranker en idiomas distintos. El corpus es español + catalán, el embedder es all-MiniLM (inglés only), el reranker es bge-reranker-v2-m3 (multilingüe). La capa ancha recupera mal por el embedder, el reranker tiene poco que rerankear. Síntoma: Recall@50 < 80%, irrecuperable cambiando el reranker.

Trampa 5 — RRF con k=1. Alguien lee el paper, copia la fórmula y pone k=1 “porque no entiende qué hace”. El resultado: las primeras posiciones del ranking peor (sea cual sea) dominan. La fusión deja de promediar y se vuelve “winner-takes-all”. Síntoma: el ranking final es casi idéntico al de sólo uno de los retrievers.

Trampa 6 — Query rewriter que cambia el sentido. El rewriter LLM “mejora” "cómo cancelo" a "procedimiento de baja del servicio premium para clientes corporativos". Recupera chunks sobre clientes corporativos cuando el usuario era particular. La hiperespecificación del rewriter es peor que ningún rewriter. Síntoma: las queries de usuarios “normales” devuelven peor que las antiguas. Mitigación: el rewriter debe mantener la intención del usuario y sólo añadir contexto, nunca asumir.

Trampa 7 — Sin telemetría del retrieval. El sistema sirve, el usuario se queja, no sabes si la culpa fue del corpus, del retrieval, del reranker o del LLM. Sin emitir trazas con retrieved_chunks_ids, retrieved_chunks_scores, rerank_scores, query_rewritten_to, selected_top_k, el debugging es teatro. Síntoma: cada incidente tarda días en investigarse. La pieza de tracing con OTel y MCP cubre el patrón canónico.

Las siete son operacionales. Igual que con el corpus, el retrieval no se rompe porque las matemáticas estén mal: se rompe porque la disciplina se relaja. Y como en Eval, las métricas pueden subir mientras la experiencia real empeora — porque el golden set se acomoda al sistema en lugar del sistema acomodarse al golden set.

Lo que no hemos cubierto (próximos posts)

  • Embedding model selection y fine-tuning: cómo elegir entre los embedders del MTEB-leaderboard sin caer en goodharting, cuándo y cómo fine-tunear un embedder sobre dominio propio con MNR loss o triplet loss, qué dataset sintético se genera con LLM para entrenar (GPL, InPars, Promptagator), y los gotchas del re-embedding masivo cuando cambias de modelo.
  • Semantic cache para RAG: cómo cachear queries semánticamente similares para servir respuestas sin pasar por el retrieval ni el LLM, GPTCache, MeanCache, y el trade-off precisión/cobertura del cache. Ahorra 30-70% del coste en cargas con queries repetidas.
  • Multi-vector y ColBERT-v2 a escala: cómo se diseñan los índices PLAID y CITADEL para servir corpus de 100M+ chunks con late interaction sin quemar el presupuesto de memoria.
  • RAG eval específico — RAGAS deep dive: faithfulness, answer relevance, context precision, context recall, noise sensitivity. Cómo se construye el golden set específico para RAG (con chunks etiquetados como relevantes/irrelevantes) y qué métricas correlacionan con la satisfacción real del usuario.
  • Function calling y tool-augmented retrieval: cuándo el LLM decide qué retriever invocar (SQL para datos estructurados, vector para no estructurados, web search para tiempo real), patrón ReAct, manejo de tool errors.
  • Agentic retrieval loops: cuando una sola pasada de retrieval no basta y el agente itera (planning, sub-queries, summary-and-refine). El trade-off latencia / calidad y los anti-patrones (loops infinitos, sub-queries que divergen).

Ver también

Referencias