El test negativo de tu RAG que pasaba por el motivo equivocado
Un cambio de modelo de embeddings puso en verde dos tests que llevaban meses fallando. No era una mejora: el retriever había dejado de recuperar la trampa.

Un equipo cambió su modelo de embeddings en febrero —de text-embedding-3-small a bge-large— y su golden set siguió en verde. Dos tests negativos que llevaban meses fallando pasaron de golpe. Parecía una victoria. No lo era: lo que había cambiado es que el retriever dejó de recuperar los fragmentos trampa.
Un verde que no significa nada
El fallo es de forma. Un test negativo comprueba que el sistema responde "no está en los documentos" ante una pregunta cuya respuesta no existe en el corpus. Si el retriever no recupera el chunk que contiene la trampa, el LLM no llega a verla nunca. Un modelo educado se niega a responder y el test pasa. Pero ha pasado porque la recuperación se rompió, no porque el modelo acertara. El test estaba verde con el sistema roto.
La solución que adoptaron: dos sellos por cada test.
El trap_chunk_id es el identificador del fragmento que contiene la trampa, fijado cuando se escribe el test. Un paso de CI comprueba si ese chunk está en el conjunto recuperado y, si no lo está, marca el test como "no ejecutado", un tercer veredicto junto a pasa y falla, diga lo que diga el LLM.
El embedder_tag es el modelo de embeddings con el que el test se validó por última vez. Tras cualquier cambio de modelo, las líneas con la etiqueta vieja quedan obsoletas. No se borran, pero el resumen de CI las marca como "sin verificar con el embedder actual" hasta que alguien las vuelva a ejecutar y a fechar.
Con eso, los dos tests que "pasaban" al cambiar a bge-large se pusieron rojos al instante: el trap_chunk_id no aparecía en el conjunto recuperado. El embedder nuevo era peor en similitud semántica para esas trampas concretas. Revirtieron el cambio, arreglaron la estrategia de chunking y solo entonces revalidaron bajo el modelo nuevo.
El coste que nadie cuenta
El trap_chunk_id se mueve cada vez que se re-chunka o se re-parsea un documento. Cualquier cambio de pipeline obliga a re-sellarlo: relanzar la ingesta completa, localizar el nuevo id que contiene la trampa, actualizar el conjunto de tests, re-ejecutar bajo el embedder actual y refrescar etiqueta y fecha. Son unos veinte minutos por cambio, y no es opcional. Si se salta el paso, las conclusiones describen una recuperación que ya no existe.
¿Por qué no basta con la similitud del coseno? Porque las distribuciones de preguntas respondibles e irrespondibles se solapan y ningún umbral las separa. Una pregunta que va del tema puntúa alto aunque no sea contestable. En lugar de perseguir ese umbral, el equipo afirma directamente que el chunk trampa tiene que estar recuperado; si no lo está, el test no se ha ejecutado. Es la misma lógica que empuja a la búsqueda híbrida: BM25 para coincidencias exactas, vectores para semántica, filtros de metadatos para contexto estructural.
Un test negativo que pasa porque el modelo se niega a responder no demuestra que la recuperación funcione: demuestra que el modelo es educado. Es la misma distinción que en producción entre recibir un 200 OK y comprobar que el servicio hizo de verdad el trabajo. Si tu evaluación no marca de dónde salió cada fragmento recuperado, es probable que estés enviando fallos de recuperación disfrazados de fallos de modelo.
