Un cross-encoder como juez: filtra tu pipeline RAG con puntuaciones de relevancia
Cómo la atención cruzada permite a un cross-encoder puntuar entre 0 y 1 la relevancia de una consulta frente a un documento, y cómo un umbral impide que las recuperaciones flojas lleguen siquiera al LLM.
La mayoría de los tutoriales de RAG tratan el cross-encoder como un paso de reordenación y nada más. Pero puede hacer un trabajo más interesante. Usado como juez, decide si el texto recuperado es lo bastante bueno como para pasárselo al generador.
Una alternativa habitual es un LLM como juez, pero sigue siendo un modelo generativo con todas sus rarezas de siempre. Un juez basado en cross-encoder se sitúa en un punto de encuentro útil entre tres preocupaciones: cómo evoluciona la arquitectura, cuánto cuesta el pipeline y cuánto esperan los usuarios.
Por qué un cross-encoder es un buen juez
Los montajes básicos de RAG se apoyan por completo en un bi-encoder, la familia sentence-transformer que se usa para generar embeddings. Un bi-encoder comprime cada texto en su propio vector independiente. Mide lo cerca que están dos vectores en el espacio y se le escapa la lógica más fina, como condiciones, negaciones e inversiones estructurales.
Hay además un límite más profundo. Los bi-encoders están ajustados para el recall, no para la precisión. Encuentran documentos sobre el mismo tema, pero no pueden confirmar que un documento responda a la pregunta que de verdad se hizo.
Hoy lo habitual para evaluar la recuperación es pedir a un LLM, como GPT o Claude, que lea los fragmentos recuperados y les ponga nota. En producción ese enfoque tiene tres inconvenientes serios:
- Latencia. Esperar a una llamada pesada a la API antes de que empiece la generación arruina la experiencia de un usuario que espera una respuesta rápida.
- Coste. Enviar miles de tokens de contexto recuperado a un modelo juez en cada consulta dispara la factura de la API.
- Inestabilidad. Los prompts derivan y la salida del modelo no es determinista, lo que rompe cualquier umbral fijo. También puede alucinar, así que un pasaje calificado como relevante el lunes puede salir irrelevante o medio relevante el martes.
Un cross-encoder elimina los tres problemas. Lee la consulta y el documento juntos en una sola pasada del transformer, de modo que cada token puede atender a cualquier otro. El resultado es una puntuación precisa y repetible entre 0.0 y 1.0.
Qué pasa por dentro: atención cruzada
La forma más fácil de ver por qué juzga mejor es comparar cómo lee el texto con cómo lo hace un bi-encoder.
Un bi-encoder codifica la consulta (Q) y el documento (D) por separado. Sale un vector para Q y otro para D. Dentro del transformer, los tokens de la consulta solo atienden a tokens de la consulta, y los del documento solo a tokens del documento. Los dos lados se encuentran al final del todo, mediante un producto escalar o una similitud coseno.
Un cross-encoder funciona de otra manera. Construye una única secuencia de entrada que contiene ambos textos:
[CLS] User Query [SEP] Retrieved Document Chunk [SEP]
Como ambos están en la misma secuencia, el modelo aplica atención cruzada completa. La fórmula estándar de atención sigue valiendo:
Attention(Q, K, V) = softmax( (Q * Kᵀ) / √dₖ ) * V
La diferencia es que cada token de la consulta calcula un peso de atención frente a cada token del fragmento del documento, todo a la vez. Las palabras de la pregunta quedan ligadas directamente a las palabras que la responden o la contradicen.
Como contraste, este es el esquema del bi-encoder, donde los dos lados no se ven hasta la comparación final:
Bi-Encoder Architecture:
Query Tokens ──► [ Encoder A ] ──► [ Vector Q ] ──┐
├──► Cosine Distance ──► Score
Doc Tokens ──► [ Encoder B ] ──► [ Vector D ] ──┘
(No token-to-token interaction)
Cross-Encoder Architecture:
Query Tokens ──┐
├──► [ Full Self-Attention Matrix ] ──► [ [CLS] Token ] ──► Sigmoid ──► Relevance
Doc Tokens ──┘ (Every token attends to every) (Dense Output) Probability
Un ejemplo resuelto
Este es un caso en el que un bi-encoder se equivoca y un cross-encoder acierta.
Supongamos que una base de conocimiento tiene tres documentos:
- Documento A: «Fuertes tormentas y lluvias torrenciales alteraron los desplazamientos en toda la región, y el aeropuerto canceló todos los vuelos previstos para la tarde-noche».
- Documento B: «Pese al aguacero y al cielo oscuro, el aeropuerto funcionó con normalidad y no se retrasó ni un solo vuelo».
- Documento C: «Un fallo en el software de control del tráfico aéreo provocó grandes retrasos, y la lluvia de la tarde no tuvo nada que ver».
Entonces un usuario pregunta: «¿La lluvia causó retrasos en los vuelos?»
Dónde falla el bi-encoder
Un modelo de embeddings estándar trata la consulta como un todo y puntúa cada documento por separado. Los tres textos contienen los términos de mucho peso «lluvia», «vuelo» y «retrasos». Un bi-encoder solo mide la cercanía general de los vectores, así que los documentos B y C obtienen una similitud coseno muy alta. A menudo quedan justo al lado del documento A y a veces por encima. El recuperador ve palabras coincidentes, supone que el tema coincide y los da por relevantes.
A qué conclusión llega el cross-encoder
Ahora pasa los mismos candidatos al juez cross-encoder. La matriz de atención cruzada hace que los tokens de la consulta se alineen frente al texto del documento y formen relaciones reales. El resultado es muy distinto:
[Query] + [Doc A] ---> Score: 0.94 (High Confidence)
Mechanics: The model explicitly links "rain" -> "caused" -> "cancel/delays".
[Query] + [Doc B] ---> Score: 0.21 (Irrelevant Noise)
Mechanics: The cross-attention catches the modifier "Despite the heavy rain... continued smoothly without a single flight delay". It recognizes a structural negation.
[Query] + [Doc C] ---> Score: 0.42 (Topical Context Only)
Mechanics: The model notes that while both events happened, the text explicitly declares them "unrelated".
Si tratas esas puntuaciones como probabilidades reales, puedes quitar enseguida los documentos B y C del prompt. El generador recibe entonces un único fragmento con mucha señal, el documento A. Eso elimina cualquier posibilidad de que el modelo se invente una afirmación contradictoria o falsa sobre lo que hizo el tiempo.
Cómo se obtiene la puntuación
Un bi-encoder compara la distancia espacial entre dos vectores separados. Un cross-encoder nunca crea embeddings independientes. Llega a su puntuación analizando toda la secuencia de forma conjunta:
- Procesado de tokens. La consulta y el documento recuperado se empaquetan en una secuencia y se separan con un token especial:
[CLS] User Query [SEP] Document Chunk [SEP]. - La matriz de atención. Mientras la secuencia atraviesa el transformer, cada token de ambos textos calcula su relación con todos los demás. El resultado es una matriz de interacción densa dentro del modelo.
- El estado oculto. El modelo reúne su comprensión final en el token
[CLS]al principio de la secuencia, que actúa como resumen único de cómo se relacionan la consulta y el documento. - La conversión en probabilidad. Ese estado oculto final de
[CLS]pasa por una capa lineal de clasificación que reduce las dimensiones ocultas en bruto a un solo número. Después lo transforma una función sigmoide,Score = 1 / (1 + e^-x).
La sigmoide comprime cualquier salida en bruto al rango de 0.0 a 1.0. El resultado se lee como una probabilidad de relevancia calibrada, una medida de cuán seguro está el modelo de que el documento contiene de verdad la respuesta a la pregunta.
Cómo interpretar la puntuación
Como el valor está acotado y es absoluto, puedes clasificarlo en niveles y actuar sobre cada uno en tiempo de ejecución:
- De 0.80 a 1.00, una señal de alta confianza. El documento resuelve el núcleo de la pregunta. La respuesta está en el fragmento y la estructura de las afirmaciones encaja con la consulta.
- De 0.50 a 0.80, solo contexto temático. El documento es del área correcta y menciona los conceptos adecuados, pero no tiene una respuesta definitiva o trae mucho ruido de fondo.
- De 0.00 a 0.50, un desajuste estructural o ruido. El texto comparte muchas palabras clave con la consulta, pero tiene lógica opuesta, negaciones o un tema ajeno.
Convertir al juez en un disyuntor
En un RAG corriente, el cross-encoder es un ordenador de resultados. Reordena bloques de texto antes de que sigan adelante. Lo verdaderamente nuevo del papel de juez es convertir su probabilidad en una barrera rígida que puede saltar como un disyuntor.
En lugar de dejar que el generador lea lo que haya quedado primero por casualidad, fijas para el pipeline un suelo de calidad firme, por ejemplo 0.75.
Supongamos que el recuperador devuelve veinte documentos y el juez los puntúa todos. La mejor puntuación de todo el lote es 0.42. El disyuntor salta y el pipeline cancela en el acto la llamada de generación.
No hay que dejar que un modelo de lenguaje enorme y caro adivine a partir de material irrelevante, porque eso es justo lo que provoca alucinaciones. En su lugar, el sistema envía al usuario una respuesta de reserva preparada, algo como: «Lo sentimos, no hay documentación interna verificada para responder a esa pregunta con fiabilidad».
Las probabilidades calibradas usadas como límite programático rígido convierten un bucle impredecible de generación de texto en un software predecible. El generador solo se ejecuta cuando las pruebas son buenas, y el juez sale barato porque es un modelo codificador pequeño, no otra ronda de generación cara.