Calidad: evaluar, re-rankear y los fallos típicos
Un RAG que funciona en la demo y decepciona en producción casi nunca falla por el LLM: falla porque nadie midió la recuperación por separado, porque se confundió más contexto con mejor contexto, o porque un detalle de la ingesta envenenó el índice en silencio. Esta lección cierra el nivel con la disciplina que lo sostiene: medir la recuperación aparte de la generación con un conjunto dorado de preguntas, recuperar amplio y re-rankear con un cross-encoder para quedarse con lo mejor, y reconocer de un vistazo el catálogo de patologías que arruinan un RAG mal montado.
Todo RAG parece funcionar el primer día. Se prueban seis preguntas, las seis salen bien, y el sistema se declara listo. Semanas después llegan las quejas: responde de más, responde de menos, cita lo que no toca, y nadie sabe si la culpa es del modelo, del troceo o del topK. La diferencia entre esos dos momentos no es tecnología, es medición. Un RAG es un sistema de recuperación de información con un redactor al final, y las disciplinas de medir recuperación existen desde mucho antes que los LLM. Esta lección te da esas métricas, la técnica que más sube la precisión por euro invertido, y el catálogo de fallos que reconocerás en tu propio sistema.
- Medir la recuperación por separado con un conjunto dorado y métricas de cobertura y orden.
- Evaluar la generación por fidelidad al contexto, relevancia y tasa de abstención correcta.
- Recuperar amplio y re-rankear con un cross-encoder para elevar la precisión del contexto final.
- Diagnosticar los fallos típicos de un RAG mal montado por su síntoma característico.
Medir la recuperación aparte de la generación
La primera regla de la evaluación es no evaluar el sistema entero de una vez. Si solo miras la respuesta final, cada fallo es un misterio con dos sospechosos y ningún indicio. Separa: mide primero si el fragmento correcto llegó al contexto, y solo después si el modelo lo usó bien.
Para lo primero necesitas un conjunto dorado: entre cincuenta y doscientas preguntas reales de tus usuarios, cada una anotada con el chunk o el documento que de verdad la responde. Construirlo lleva una tarde y es la inversión más rentable de todo el nivel, porque convierte cada cambio de troceo, modelo o topK en un experimento con resultado en vez de una opinión.
Con ese conjunto, tres métricas bastan. Cobertura en k, o recall@k, es la proporción de preguntas cuyo fragmento correcto aparece entre los topK recuperados; es la métrica reina porque marca el techo absoluto del sistema, ya que lo que no se recupera no se puede responder. MRR mide en qué posición apareció el primer acierto y premia que esté arriba, lo cual importa porque el modelo atiende mejor al principio del contexto. Precisión en k cuenta cuánta de la basura acompaña al acierto, y es la que empeora cuando subes el topK a lo bruto.
Anotar el conjunto dorado es más fácil de lo que parece. Coge las preguntas que la gente ya hizo —del buscador interno, de los tickets, del canal de soporte—, busca a mano el fragmento que las responde y guarda su identificador junto al texto de la pregunta. Doscientos casos son más que suficientes para detectar diferencias reales, y el ejercicio de anotarlos te enseña sobre tu corpus más que cualquier métrica posterior.
Para la generación, las preguntas son otras tres. La fidelidad mide si cada afirmación de la respuesta está respaldada por el contexto entregado, y es lo contrario de alucinar. La relevancia mide si la respuesta contesta lo que se preguntó y no un tema vecino. Y la abstención correcta mide qué hace el sistema cuando el contexto no basta: un RAG que nunca dice no lo sé está mintiendo con una frecuencia que aún no has medido.
Puntuar a mano cien respuestas cada vez que tocas un parámetro no es sostenible, y por eso se usa un modelo como evaluador: se le da la pregunta, el contexto y la respuesta, y se le pide juzgar si cada afirmación está respaldada. Funciona razonablemente para la fidelidad, que es una tarea de verificación acotada. Pero tiene sesgos conocidos —premia respuestas largas, se autofavorece, se deja llevar por el tono seguro—, así que calíbralo: puntúa tú treinta ejemplos, compara con su veredicto y comprueba que coinciden antes de fiarte de las otras mil.
Contexto y re-ranking
La intuición de que más contexto es mejor contexto es falsa y cara. Cada fragmento irrelevante compite por la atención del modelo, empuja lo bueno hacia el centro poco atendido de la entrada, alarga la latencia y multiplica el coste por consulta. Existe un óptimo, y suele estar entre tres y ocho fragmentos bien elegidos. Las ventanas de contexto enormes no cambian esta conclusión: poder meter medio corpus no significa que meterlo mejore la respuesta.
De ahí viene la técnica que más precisión regala por unidad de esfuerzo: recuperar amplio y re-rankear. El índice vectorial usa un bi-encoder, que calcula el vector de la pregunta y el del documento por separado y los compara con una operación geométrica. Es rapidísimo, porque los vectores del corpus ya estaban calculados, y por eso escala a millones de fragmentos. Pero paga esa velocidad con precisión: la pregunta y el pasaje nunca se miran a la vez.
Un cross-encoder hace justo lo contrario. Recibe el par entero —pregunta y fragmento juntos— y produce una puntuación de relevancia mirando las dos cosas en relación. Es mucho más preciso y mucho más caro, porque exige una pasada por cada candidato y no se puede precalcular. La combinación aprovecha lo mejor de ambos: el bi-encoder filtra millones a treinta, y el cross-encoder ordena esos treinta para quedarte con los cinco mejores.
const candidatos = await env.INDICE.query(vector, { topK: 30, filter: { tenant } });
const textos = await rehidratar(env, candidatos.matches);
const orden = await env.AI.run("@cf/baai/bge-reranker-base", {
query: pregunta,
contexts: textos.map((t) => ({ text: t.texto })),
});
const finales = orden.response
.sort((a, b) => b.score - a.score)
.slice(0, 5)
.map((r) => textos[r.id]);
flowchart LR Q[pregunta] --> BI[bi-encoder busca en Vectorize] BI --> C30[30 candidatos rapidos] C30 --> CE[cross-encoder puntua cada par] CE --> TOP5[5 fragmentos precisos] TOP5 --> GEN[generacion con contexto limpio] style CE fill:#f9e2af,color:#11111b style GEN fill:#a6e3a1,color:#11111b
Conviene entender por qué funciona tan bien la combinación. El bi-encoder tiene que resumir un fragmento en un vector sin saber qué le van a preguntar, así que comprime pensando en todas las preguntas posibles a la vez. El cross-encoder puntúa ya conociendo la pregunta concreta, y por eso distingue lo que el vector no podía distinguir: si ese pasaje responde esta duda o solo habla del mismo tema. Es la diferencia entre archivar un libro por materia y leer la página buscando una respuesta.
El coste es una llamada extra de inferencia y unas decenas de milisegundos. A cambio, la precisión del contexto sube de forma notable y muchas veces te permite bajar el número de fragmentos entregados, lo que recorta a su vez el coste de generación. Es de las pocas optimizaciones que mejoran calidad y factura a la vez.
Los fallos típicos
Casi todos los RAG rotos que verás padecen una de estas patologías, y cada una tiene un síntoma reconocible. Antes de consultar la tabla, haz siempre la misma pregunta de triaje: ¿estaba el fragmento correcto en el contexto que se le entregó al modelo? Imprime el contexto de una respuesta fallida y míralo. Si el fragmento no estaba, el problema vive aguas arriba, en el troceo, el índice o la consulta; si estaba y la respuesta lo ignoró, vive en el prompt o en el generador. Esa bifurcación resuelve la mitad de los diagnósticos en un minuto.
| Síntoma | Causa probable | Arreglo |
|---|---|---|
| Respuestas plausibles sin relación con el corpus | Modelo de embedding distinto en ingesta y consulta |
Unificar modelo y reindexar entero |
| El fragmento correcto nunca aparece | Troceo sin solape o demasiado grueso | Recortar el chunk, añadir solape, reindexar |
| Cita documentos que ya no existen | Falta de reingesta o trozos huérfanos | Borrar por prefijo antes de reinsertar |
| Un usuario ve datos de otro | Filtro de tenant fuera de la consulta | Filtrar en la query con metadata indexada |
| Responde siempre, incluso sin fuentes | Sin instrucción de abstención ni umbral | Autorizar el no lo sé y fijar un mínimo de score |
| Falla con códigos e identificadores | Solo búsqueda semántica | Añadir índice léxico y fusionar rankings |
Empeoró al subir el topK |
Dilución del contexto y perdido en el medio | Recuperar amplio, re-rankear y entregar pocos |
Hay dos más que no caben en la tabla porque no se manifiestan de golpe. Una es la deriva del corpus: el sistema funcionaba y hoy responde peor, porque los documentos cambiaron y nadie reindexó, o porque llegaron miles de documentos nuevos que compiten por las mismas plazas del topK. La otra es el envenenamiento del índice: alguien subió material duplicado, obsoleto o directamente falso, y como el recuperador no juzga la verdad de lo que devuelve, tu sistema empezó a citar con toda formalidad una fuente que no debería existir. Contra las dos vale lo mismo: una medición periódica y la costumbre de mirar qué entró en el corpus, no solo qué salió del modelo.
Y por encima de todas está el fallo que las hace posibles: no medir. Un equipo sin conjunto dorado no puede distinguir una mejora de una casualidad, así que ajusta parámetros por intuición, discute por anécdotas y descubre las regresiones cuando las cuenta un usuario enfadado. Todas las patologías de la tabla son detectables en minutos con cincuenta preguntas anotadas; sin ellas, cualquiera de las siete puede vivir meses en producción sin que nadie sepa nombrarla.
Queda una patología que ninguna tabla arregla y conviene nombrar: hay preguntas que RAG no puede responder por construcción. Cuántos contratos vencen este trimestre, cuál es el proveedor más caro, cómo evolucionó el gasto mes a mes. Son agregaciones sobre el conjunto entero, y la búsqueda por similitud devuelve unos pocos fragmentos parecidos, jamás un cómputo global. Esas preguntas se responden con SQL sobre D1, no con vectores. Un sistema maduro las detecta y las enruta a la herramienta correcta en lugar de fingir que un puñado de chunks bastan.
Conjunto dorado
Cincuenta preguntas anotadas con su fragmento correcto. Convierte cada ajuste en un experimento medible.
Recuperar amplio
Treinta candidatos baratos, cinco finales precisos. El re-ranking mejora calidad y coste a la vez.
Abstención medida
La tasa de no lo sé es un indicador, no un defecto. Si es cero, tu sistema está inventando en silencio.
Lo que no es RAG
Agregaciones, cálculos y comparaciones globales van a SQL. La similitud no cuenta ni suma.
Un bucle de evaluación que corre solo
Una evaluación que hay que ejecutar a mano se ejecuta dos veces y se abandona. Guarda el conjunto dorado en D1 y convierte la medición en código que cualquiera pueda lanzar, o mejor, que se lance solo desde un scheduled handler cada noche.
type Caso = { pregunta: string; idEsperado: string };
export async function coberturaEnK(env: Env, casos: Caso[], k: number): Promise<number> {
let aciertos = 0;
for (const caso of casos) {
const { data } = await env.AI.run("@cf/baai/bge-base-en-v1.5", { text: [caso.pregunta] });
const res = await env.INDICE.query(data[0], { topK: k });
if (res.matches.some((m) => m.id === caso.idEsperado)) aciertos++;
}
return aciertos / casos.length; // el techo de tu sistema, en un numero
}
La evaluación de laboratorio se complementa con señales de producción, que son gratis y nadie recoge. Si el usuario pulsó una cita, esa fuente le pareció pertinente. Si reformuló la misma pregunta dos veces seguidas, la primera respuesta no le sirvió. Si copió un fragmento de la respuesta, algo acertaste. Registrar esos gestos convierte el uso real en un conjunto de evaluación que crece solo y que, a diferencia del dorado, refleja lo que la gente pregunta de verdad.
Escribe cada resultado en una tabla con su fecha y la configuración que lo produjo: modelo de embedding, tamaño de chunk, solape, topK, si hubo re-ranking. Lo que obtienes es una serie temporal de la calidad de tu recuperador, y con ella dejas de discutir por intuiciones. Cuando alguien cambie el troceo un jueves, el lunes verás si la cobertura subió o cayó tres puntos, y sabrás a qué commit culpar.
Mide también el presupuesto de cada consulta, porque la calidad sin coste ni latencia es media verdad. Un ciclo completo suma la inferencia del embedding, la búsqueda vectorial, la lectura en D1, el re-ranking opcional y la generación, y conviene conocer el percentil noventa y cinco de cada tramo por separado: casi siempre el generador domina el tiempo total, pero un re-ranking mal dimensionado o un topK de cincuenta con rehidratación completa pueden colarse en el primer puesto sin que nadie lo note. Los registros de AI Gateway te dan el coste por llamada; el resto lo instrumentas tú desde el Worker.
Vale la pena terminar el nivel con la conclusión que ordena todas sus piezas: en un sistema RAG, la calidad final está acotada por arriba por la recuperación, y ninguna mejora aguas abajo puede superar ese límite. Si el fragmento que contiene la respuesta no entró en el contexto, da igual qué modelo elijas, cuánto pulas el prompt o cuántos euros gastes en tokens: la respuesta correcta es literalmente inalcanzable. Esta observación, tan elemental que casi ofende, es la que casi ningún equipo interioriza a tiempo, y explica el patrón que se repite en toda la industria: meses invertidos en iterar el prompt y en probar modelos más caros para arreglar un problema que vivía en el troceo, en el modelo de embedding o en un topK demasiado corto. El diagnóstico correcto empieza siempre por la misma pregunta, y es una pregunta que solo se puede contestar si has construido el conjunto dorado: cuando el sistema falló, ¿estaba el fragmento bueno en el contexto? Si no estaba, el trabajo está en la ingesta y en la búsqueda, y todo lo demás es teatro. Si estaba y el modelo no lo usó, entonces sí toca mirar el prompt, el orden del contexto o el modelo. Esa bifurcación convierte una queja difusa en una tarea concreta, y es la razón por la que separar las métricas no es burocracia sino método. De ahí se sigue la tesis que hereda todo lo que venga después, cuando estos sistemas dejen de ser un solo disparo y se conviertan en agentes que buscan varias veces, reformulan, comparan fuentes y deciden cuándo tienen bastante: incluso entonces, cada iteración del bucle seguirá siendo una recuperación, y cada recuperación seguirá teniendo su cobertura, su precisión y su techo. Los sistemas de este tipo se ganan o se pierden en la capa de datos —cómo troceas, qué indexas, cómo filtras, cómo ordenas— y no en la capa de modelo, que es la que todo el mundo mira porque es la que habla. Cloudflare te da las cuatro piezas del bucle a un binding de distancia y a milisegundos del usuario; lo que ninguna plataforma puede darte es la disciplina de medir cuál de ellas te está fallando hoy.
- Construye un conjunto dorado de cincuenta preguntas reales anotadas con el fragmento que las responde.
- Calcula la cobertura en 5, en 10 y en 30 sobre ese conjunto. Ese número es el techo de tu sistema: anótalo antes de tocar nada más.
- Añade re-ranking con un
cross-encodersobre treinta candidatos y vuelve a medir cobertura y precisión con cinco fragmentos finales. - Mide la tasa de abstención con preguntas cuya respuesta no está en el corpus. Si es cero, tienes un problema mayor que cualquier métrica de relevancia.
- Recupera la pregunta que guardaste en la primera lección —la agregación que RAG no resuelve— y construye la ruta alternativa hacia SQL en D1.