← Ruta Perico
Etapa 14

Tests que no ejecutas a mano

2ª categoría Publicada Medición
14

Hasta ahora todo lo que había que comprobar tenía respuesta correcta. ¿Se bajaron los 137 mensajes? ¿Son literales las 3.531 frases? ¿Vuelve una frase cuando la buscas con su propia paráfrasis? Son preguntas con un sí y un no.

En cuanto un modelo entra en el producto, esas preguntas se acaban. «¿Suena a Perico?» no tiene respuesta correcta, y assert salida == esperado no sirve para nada: la misma entrada da una salida distinta cada vez, y las dos pueden estar bien.

La tentación entonces es mirar cuatro salidas, decidir que están muy logradas y publicar. Que es exactamente lo que hace todo el mundo, y por eso casi ningún proyecto con un LLM dentro sabe si funciona.

La escalera

Lo que sigue no es una técnica, son tres, ordenadas de más barata y objetiva a menos:

PeldañoQué preguntaCosteQué tan objetivo
1. Comprobaciones con respuesta¿Funciona el mecanismo?0 € y segundosTotal: hay respuesta correcta
2. Juez a ciegas¿Cuál de las dos suena más a él?Dinero y minutosNinguno: es un gusto
3. Solapamiento de palabras¿Reutiliza lo que recuperó?Dinero y minutosTotal: es un hecho

La regla de la escalera es subirla en orden, porque cada peldaño es más caro y menos fiable que el anterior. Y saltarse el primero para ir directo al juez es lo que hace la gente.

Peldaño 1 — las que tienen respuesta correcta

Son las dos de la etapa 12: la auto-recuperación y la tasa de repetición. Deterministas, gratuitas, segundos.

No miden calidad. Miden que el mecanismo funciona: que el espacio de embeddings encuentra lo que tiene que encontrar, y que la búsqueda no se ha colapsado sobre las mismas cuatro frases. Son el suelo. Si no se pasan no hay nada que juzgar, porque lo que falla está más abajo.

Con el suelo pasado, empieza lo difícil.

Peldaño 2 — el juez a ciegas, y por qué no zanjó nada

La pregunta de fondo del proyecto es la que se planteó en la etapa 0: ¿la recuperación se gana su sitio, o cincuenta ejemplos en el prompt harían el mismo trabajo? Todo lo construido desde entonces es un argumento a favor de la primera opción. Toca medirlo en vez de afirmarlo.

El diseño es una comparación a ciegas. El mismo texto traducido dos veces:

las dos versiones
A   núcleo + frases recuperadas      el pipeline completo
B   solo núcleo (--no-rag)           la línea base, que es few-shot con 50 ejemplos

Un juez ve las dos, sin saber cuál es cuál, y dice cuál suena más a él. Dos detalles del montaje hacen que el veredicto valga algo:

  • El orden se baraja en cada consulta y las etiquetas no dicen nada, así que el juez no puede favorecer una posición ni un nombre.
  • Se le permite empatar, y se le dice que el empate es lo esperado:
rag_judge.py — final del prompt del juez
Si las dos suenan igual de bien o igual de mal, di "empate". No fuerces una
preferencia que no tienes: un empate es un resultado válido y frecuente.

Sin esa frase, el juez tiene que elegir. Y un juez obligado a elegir inventa una preferencia cuando no la hay: sale un cara o cruz disfrazado de dictamen, que en el informe se lee exactamente igual que un resultado.

Primera ejecución, 12 consultas: línea base 7, RAG 5. La premisa del proyecto parecía estar fallando.

Ampliando a 48 consultas: RAG 27, línea base 21. El número se dio la vuelta.

Ninguno de los dos resultados significa nada

27 de 48 es un 56% de victorias. Una moneda produce 27 o más aproximadamente una de cada cuatro veces (23,5%). Con 12 consultas es aún peor: 7 de 12 sale casi cuatro de cada diez veces.

Y aquí está lo que importa. Si 7-5 se descartó por ruido — que lo era —, 27-21 es ruido también. Dar el segundo por bueno y el primero por casualidad no es medir: es quedarse con el número que confirma lo que ya querías creer, que es precisamente contra lo que sirve un experimento.

La prueba está en darle la vuelta: si el resultado hubiera salido 21-27, ¿lo habrías aceptado como prueba de que la recuperación no sirve? Si la respuesta es no, tampoco vale al revés.

Lo interesante de esta ejecución no estaba en el marcador.

Cero empates en 48 comparaciones. De un juez al que se le dijo, con esas palabras, que el empate era válido y frecuente. Un juez que siempre encuentra un ganador está encontrando diferencias que a lo mejor no existen.

Y sus motivos lo delataban. Los que daba una y otra vez: «más muletillas», «más digresiva», «más comparaciones típicas». Las tres describen densidad de tics — y los tics los pone el núcleo, que va idéntico en las dos versiones. El juez estaba midiendo, con mucha seguridad, la parte que no variaba entre A y B.

Peldaño 3 — cambiar el gusto por un hecho

El juez falló por lo que es: un juez de gustos. Así que la salida no es un juez mejor, es otra pregunta — una que tenga respuesta comprobable.

¿El texto generado con recuperación reutiliza las frases que recuperó?

Fíjate en lo que se gana. Si no las reutiliza, la recuperación no aporta nada por mucho que un juez la prefiera: la búsqueda se ejecuta, se paga, y el modelo la ignora. Y si las reutiliza, el solapamiento dice cuánto.

rag_overlap.py
MIN_WORD_LENGTH = 4


def content_words(text):
    """Lowercase, unaccented content words — what actually carries meaning."""
    text = unicodedata.normalize("NFD", text.lower())
    text = "".join(c for c in text if unicodedata.category(c) != "Mn")
    words = re.findall(r"[a-z]+", text)
    return {w for w in words
            if w not in STOPWORDS and len(w) >= MIN_WORD_LENGTH}


def overlap(retrieved_text, output_text):
    """Share of the output's content words that appear in the retrieved phrases.

    Measured over the output rather than over the retrieved set: the question
    is how much of what was written came from there, not how much of what was
    fetched got used.
    """
    source = content_words(retrieved_text)
    produced = content_words(output_text)
    if not produced:
        return 0.0, set()
    shared = produced & source
    return len(shared) / len(produced), shared

Dos decisiones del diseño merecen explicación, porque sin ellas el número no diría nada:

Solo palabras con contenido. «que», «de», «un» salen en cualquier texto español. Contarlas ahogaría la señal bajo el ruido de la gramática: dos textos sin ninguna relación comparten la mitad de sus palabras si cuentas esas.

La línea base es la misma medida sobre la salida sin recuperación. Este es el punto fino. Dos textos sobre el mismo asunto, en el mismo registro, comparten palabras por casualidad: si el texto va de frío, las dos versiones dirán «frío». Ese solapamiento de fondo hay que medirlo, y es exactamente lo que se obtiene aplicando la misma cuenta a la versión que nunca vio las frases recuperadas. La diferencia entre los dos números es la parte atribuible a la recuperación, y no el número con RAG a secas, que siempre saldría alto.

Resultado sobre 24 consultas:

Tipo de peticiónCon RAGSin RAGDiferencia
Cotidiano20,2 %11,1 %+9,1
Ciclismo17,9 %11,5 %+6,5
Cansancio21,9 %11,0 %+11,0
Total20,0 %11,2 %+8,8

La versión con recuperación reutiliza más en 19 de 24 consultas. Una moneda da un reparto así de desigual el 0,7 % de las veces (p = 0,007), que es la primera vez en toda esta escalera que un número sale del ruido.

La columna que contesta es la tercera

El 20 % no dice nada por sí solo. Dos textos sobre cansancio comparten «cuerpo», «piernas» y «semana» sin que nadie haya recuperado nada, y esa casualidad vale 11,2 puntos — es la segunda columna.

Lo que la recuperación aporta es la diferencia: +8,8 puntos. Casi el doble de reutilización que por azar. Lo recuperado llega a la salida.

Y llega en las tres categorías, incluida ciclismo, que es donde el corpus tiene más vecinos y donde cabría esperar más solapamiento de fondo — lo tiene, 11,5 %, y aun así la recuperación suma 6,5 encima.

Este resultado además descarta una hipótesis que parecía razonable. El prompt lleva esta regla:

translate.py — dentro de STYLE
- No imites las frases de ejemplo palabra por palabra. Son para que veas cómo
hablas, no para copiarlas.

Escrita para el núcleo, pero el modelo no sabe que es solo para el núcleo: la ve y tiene delante también las frases recuperadas. Se sospechaba que esa frase estuviera anulando la recuperación por la puerta de atrás. Con +8,8 puntos de reutilización medida, no la anula.

Lo que se eligió, y qué fijó cada cosa

Se eligióY con eso quedó fijado
12 consultas, luego 48La resolución del experimento. Con 12 no se distingue nada del azar; con 48, tampoco lo suficiente
Seis categorías de ochoQue se pudiera ver el reparto por tipo de petición — a costa de que cada grupo sea demasiado pequeño para concluir
Sonnet 5 como juezQué criterio de «suena a él» se está usando. No es el de un aficionado: es el de un modelo
Empate permitido y esperadoQue un «no hay diferencia» pueda salir. Cero empates en 48 fue el dato más informativo de la prueba
Orden barajado por consultaQue la posición no arrastre el voto
Palabras de 4 letras o más, sin vacíasQué cuenta como reutilización. Con las vacías dentro, todo solapa con todo
La versión sin RAG como baseCuánto solapamiento es casualidad. Sin esa resta, el número con RAG no significa nada

La cuarta fila es la que más sorprende: la decisión que más información dio fue permitir la respuesta que no quería nadie.

Lo que se lleva uno de esta etapa

  1. Una métrica que siempre da veredicto es sospechosa. Permitir «no hay diferencia» no es blandura: es lo que hace que las demás respuestas signifiquen algo.
  2. Pregúntate qué está comparando el juez de verdad. Aquí las dos versiones compartían cincuenta frases de núcleo, así que todo lo que dijo sobre muletillas describía la parte que no cambiaba.
  3. Un hecho antes que una preferencia. «¿Reutilizó estas palabras?» tiene respuesta; «¿cuál suena más a él?» tiene estado de ánimo.
  4. El tamaño de muestra no es un trámite. El mismo experimento dio respuestas opuestas con 12 y con 48, y las dos estaban dentro del ruido.
  5. Si no aceptarías el resultado al revés, no has medido nada. Es la comprobación más rápida que hay y casi nadie se la hace.
  6. Una ejecución a medias no es una muestra. Si las consultas van agrupadas por tipo, las primeras que terminan son un tipo, no un resumen.