M1 — Evaluación: medir antes de optimizar
Outcome del módulo: construir un eval harness reproducible sobre el corpus real de scholar-rag que, con un comando, te diga qué tan bueno es tu retrieval (recall@k, MRR) y tu generación (faithfulness). Al terminar tienes un número baseline que vas a intentar superar el resto del curso.
Este es el módulo palanca: sin él, ningún otro se puede medir.
Concepto 1 — El dataset gold: sin ground truth no hay métrica
Idea núcleo: una métrica compara lo que tu sistema devolvió contra lo que debería haber devuelto. Ese “debería” es el dataset gold, y hay que construirlo a mano una vez.
Entender (capa 1)
Texto: un dataset gold para RAG es una lista de preguntas donde, por cada una, anotaste cuál es el chunk o documento que la responde (para medir retrieval) y, opcionalmente, una respuesta de referencia (para medir generación). No necesitas miles: 50 a 100 preguntas bien elegidas sobre tu corpus real ya te dan señal confiable. La clave es que cubran los casos que importan: preguntas fáciles, preguntas con términos exactos (códigos, nombres), preguntas cuya respuesta está partida en dos chunks.
La tentación de junior es saltarse esto y “probar a ojo”. El problema: a ojo caben 5 preguntas y tu sesgo. Con 80 preguntas anotadas, un cambio que sube recall del 60% al 78% es un hecho, no una impresión.
Visual:
flowchart TB
C[Corpus real: tesis Unicordoba] --> Q[Escribes 50-100 preguntas reales]
Q --> A[Anotas el chunk correcto por pregunta]
A --> G[(dataset gold: pregunta -> doc_id correcto)]
G --> E[Eval harness: mide tu sistema contra esto]
Ejemplo:
# eval/dataset.py — un dataset gold es solo esto
GOLD = [
{"q": "que metodologia uso la tesis sobre riego en el valle del sinu",
"relevant_ids": ["thesis_a1_chunk_12"]},
{"q": "codigo de error del sensor DHT22 mencionado en el capitulo 4",
"relevant_ids": ["thesis_a1_chunk_47"]}, # caso de termino exacto
{"q": "que autores citan el modelo de van genuchten",
"relevant_ids": ["thesis_b3_chunk_3", "thesis_b3_chunk_9"]}, # respuesta partida
]
Fijar (capa 2)
Nota atómica:
- Front: ¿por qué no basta “probar el RAG a ojo”?
- Back: porque a ojo pruebas pocas preguntas y con sesgo; un dataset gold de 50-100 preguntas anotadas convierte “creo que mejoró” en “recall subió de 0.60 a 0.78”, que es lo único que un cambio de ingeniería puede afirmar.
Feynman: “El dataset gold es el examen con respuestas al final del libro. Sin las respuestas, corriges el examen a tu gusto y siempre apruebas.”
Aplicar (capa 3)
Escribe 30 preguntas reales sobre el corpus de scholar-rag y anota, abriendo el documento, cuál chunk las responde. Incluye al menos 5 con términos exactos y 5 cuya respuesta esté en dos chunks. Guárdalas en eval/dataset.py. Ese archivo es tu instrumento; el resto del módulo lo usa.
Concepto 2 — recall@k y MRR: medir el retrieval
Idea núcleo: el retrieval tiene dos preguntas medibles: ¿trajo el chunk correcto (recall)? y ¿lo trajo arriba (MRR)?
Entender (capa 1)
Texto: recall@k responde “de los documentos relevantes, cuántos aparecieron en el top-k”. Si para una pregunta el chunk correcto es thesis_a1_chunk_12 y tu retrieval lo trae en el top-5, recall@5 = 1.0 para esa pregunta; si no lo trae, 0.0. Promedias sobre todas las preguntas del gold.
MRR (Mean Reciprocal Rank) va más fino: no solo si apareció, sino en qué posición. Si el chunk correcto salió #1, aporta 1/1 = 1.0; si salió #4, aporta 1/4 = 0.25. Promedias sobre las preguntas. Un retrieval que trae lo correcto pero en la posición 8 tiene recall alto y MRR bajo, y eso importa: el LLM presta más atención a los primeros chunks del contexto.
Visual:
flowchart LR
subgraph pregunta
R[relevante: chunk_12]
end
T["top-5 devuelto: [c9, c12, c3, c1, c7]"] --> RC["recall@5 = 1.0 (esta)"]
T --> MR["MRR = 1/2 = 0.5 (chunk_12 en pos 2)"]
Video: RAG Retrieval Evaluation Metrics: Recall@K, Precision@K, MRR, MAP, NDCG
Ejemplo:
# eval/metrics.py
def recall_at_k(relevant: set[str], retrieved: list[str], k: int) -> float:
if not relevant:
return 0.0
return len(relevant & set(retrieved[:k])) / len(relevant)
def reciprocal_rank(relevant: set[str], retrieved: list[str]) -> float:
for i, doc_id in enumerate(retrieved, start=1):
if doc_id in relevant:
return 1.0 / i
return 0.0
def evaluate(gold, retrieve_fn, k: int = 5) -> dict:
recalls, rrs = [], []
for item in gold:
got = retrieve_fn(item["q"]) # tu retrieval real
rel = set(item["relevant_ids"])
recalls.append(recall_at_k(rel, got, k))
rrs.append(reciprocal_rank(rel, got))
return {"recall@k": sum(recalls) / len(gold),
"mrr": sum(rrs) / len(gold)}
Fijar (capa 2)
Nota atómica:
- Front: ¿qué mide MRR que recall@k no?
- Back: la posición. recall@k solo dice si el chunk correcto entró en el top-k; MRR premia que salga arriba (1/posición). Importa porque el LLM pondera más los primeros chunks del contexto.
Feynman: “recall es ‘¿estaba el libro en el estante que revisé?’. MRR es ‘¿estaba a la altura de los ojos o tuve que agacharme hasta el último puesto?’.”
Aplicar (capa 3)
Conecta la función evaluate a la búsqueda actual de scholar-rag (solo-vector primero) y corre contra tu dataset.py. Anota recall@5 y mrr. Ese par de números es tu baseline oficial. Todo lo que hagas en M2-M4 se compara contra él.
Concepto 3 — faithfulness: medir que no alucina
Idea núcleo: una respuesta puede sonar perfecta y estar inventada. Faithfulness mide qué parte de la respuesta está de verdad soportada por el contexto recuperado.
Entender (capa 1)
Texto: el retrieval puede ser bueno y la respuesta igual mala si el LLM agrega cosas que no estaban en los chunks. Para medir faithfulness: descompones la respuesta en afirmaciones atómicas (“la tesis usó metodología X”, “el sensor es DHT22”, “lo publicaron en 2021”) y verificas, una por una, si el contexto recuperado las respalda. Faithfulness = afirmaciones soportadas / afirmaciones totales. Un valor de 1.0 significa que todo lo que dijo sale del contexto; 0.7 significa que un 30% se lo inventó o lo trajo de su memoria pre-entrenada, que en un corpus académico específico es justo lo que no quieres.
Se puede calcular con un LLM como juez (le pasas afirmación + contexto y respondes “¿soportada sí/no?”). Es lo que hace RAGAS por dentro. Vale entender el mecanismo antes de usar la librería.
Visual:
flowchart TB
A[Respuesta del LLM] --> B[Partir en afirmaciones atomicas]
B --> C{Cada afirmacion:\nel contexto la soporta?}
C -->|si| S[soportada]
C -->|no| N[inventada / alucinada]
S --> F["faithfulness = soportadas / total"]
N --> F
Ejemplo:
# eval/faithfulness.py — el juez es un LLM, el mecanismo es simple
async def faithfulness(answer: str, context: str, judge) -> float:
claims = await judge.split_into_claims(answer)
if not claims:
return 0.0
checks = [await judge.is_supported(c, context) for c in claims]
return sum(checks) / len(checks)
Fijar (capa 2)
Nota atómica:
- Front: ¿puede un RAG con buen recall dar respuestas malas?
- Back: sí; el retrieval puede traer el chunk correcto y el LLM igual agregar afirmaciones no soportadas por el contexto (alucinación). Faithfulness lo mide descomponiendo la respuesta en afirmaciones y verificando cuántas respalda el contexto.
Feynman: “Recall es que el testigo correcto esté en la sala. Faithfulness es que el abogado solo repita lo que el testigo dijo, y no invente frases en su boca.”
Aplicar (capa 3)
Toma 10 preguntas del gold, corre el RAG completo de scholar-rag, y calcula faithfulness (a mano o con un LLM juez). Anota el número. Si es alto pero tu recall es bajo, tu problema es retrieval; si el recall es bueno pero faithfulness baja, tu problema es el prompt o la falta de un paso de verificación (eso es M4).
Cierre del módulo
Ya tienes el instrumento: un dataset gold y tres números —recall@k, mrr, faithfulness— que definen el estado actual de scholar-rag sin opiniones. Guárdalos. Ese es el “antes” contra el que vas a medir cada mejora del resto del curso.
Lo que puedes responder ahora que antes no: “¿qué tan bueno es tu RAG?” — con tres números y cómo los mediste. Esa frase, dicha con datos, ya te separa del 90% que dice “funciona bien”.
Siguiente: M2 — Embeddings e indexado vectorial, donde atacas el mayor cuello de latencia (el índice ANN que hoy falta) midiendo contra este baseline.