Módulo 1: ¿Por Qué Evaluar Sistemas de AI?
3. Tipos de Evaluación: Offline, Online, Human-in-the-Loop
Descripción
No toda evaluación es igual. Existen tres grandes familias de evaluación para sistemas de AI, y cada una responde a una pregunta diferente. La evaluación offline usa datasets fijos antes del deploy para responder "¿mi sistema mantiene o mejora la calidad?". La evaluación online mide en producción con usuarios reales para responder "¿mi sistema funciona para la gente que lo usa?". La evaluación human-in-the-loop incorpora evaluadores humanos para responder "¿qué tan buena es realmente esta respuesta, según un criterio que solo un humano puede juzgar?". Cada tipo tiene su momento, sus ventajas y sus limitaciones — y los tres se necesitan mutuamente.
La mayoría de equipos comete el error de pensar que con una sola de estas tres ya está cubierto. Un equipo que solo hace offline pierde la realidad de producción. Un equipo que solo mira métricas online no puede diagnosticar por qué algo falló. Un equipo que depende solo de evaluadores humanos no puede escalar ni automatizar. El pipeline maduro de evaluación — el que vas a construir a lo largo de esta guía — combina los tres tipos en un flujo donde cada uno alimenta al siguiente.
En esta cápsula vas a entender en profundidad cada tipo, ver implementaciones con código ejecutable, aprender cuándo usar cada uno, y practicar con ejercicios que te preparan para el proyecto del módulo. Cuando termines, vas a poder diseñar un pipeline de evaluación que integre offline, online y HITL de forma coherente para cualquier sistema de AI.
Los tres tipos de evaluación
Antes de entrar en cada uno, piensa en esta analogía. Un restaurante evalúa la calidad de su comida de tres formas:
- En la cocina (offline): El chef prueba cada plato antes de sacarlo. Tiene recetas de referencia y criterios claros.
- En la mesa (online): Observa si los clientes dejan comida en el plato, si piden repetir, si vuelven al restaurante.
- Con críticos (HITL): Invita a críticos gastronómicos para una evaluación detallada con rubrics profesionales.
Las tres son necesarias. Sin la primera, salen platos malos. Sin la segunda, no sabes si a la gente le gusta. Sin la tercera, no calibras tu propio criterio. En AI Engineering, la lógica es la misma.
1. Evaluación offline
Qué es
Evaluar tu sistema con un dataset fijo (golden dataset) antes de desplegarlo. Las preguntas están definidas, las respuestas esperadas también, y el sistema genera outputs que comparas contra esas referencias usando métricas cuantitativas. Es el equivalente a los tests unitarios de software, pero para la calidad de outputs de AI.
Cuándo usar
- Pre-deploy: Antes de cada merge o release, para verificar que la calidad no bajó.
- En CI/CD: Como un gate automático — si las métricas bajan del threshold, el deploy se bloquea.
- Comparación A/B técnica: Modelo A vs modelo B, prompt v1 vs v2, retriever viejo vs nuevo.
- Detección de regresiones: ¿El cambio que hice rompió algo que antes funcionaba?
Ventajas
- Reproducible: Mismo dataset, mismas métricas, resultados comparables entre versiones.
- Automatizable: Se ejecuta en CI/CD sin intervención humana.
- Barato: Una vez que tienes el dataset, el costo es solo compute.
- Rápido: Puedes correr 100 evaluaciones en minutos.
Limitaciones
- El dataset puede no representar producción: Si tus 50 preguntas de prueba no cubren lo que los usuarios realmente preguntan, los resultados son engañosos.
- No captura el comportamiento real: Un score de 0.92 en tu golden dataset no garantiza que los usuarios estén satisfechos.
- Dataset estático en un mundo dinámico: Tu sistema evoluciona, tus usuarios cambian, pero tu dataset se queda igual si no lo actualizas.
Ejemplo básico: evaluación offline con un dataset simple
from typing import Callable
def run_offline_evaluation(
model_fn: Callable[[str], str],
dataset: list[dict],
metric_fn: Callable[[str, str], float],
) -> dict:
"""Patrón generar → evaluar → reportar: el núcleo de toda
evaluación offline, desde la más simple hasta RAGAS o TruLens."""
results = []
for item in dataset:
output = model_fn(item["input"])
score = metric_fn(output, item["expected"])
results.append({"input": item["input"], "output": output, "score": score})
scores = [r["score"] for r in results]
return {"mean_score": sum(scores) / len(scores), "min_score": min(scores), "results": results}
dataset = [
{"input": "¿Qué es RAG?", "expected": "Retrieval-Augmented Generation"},
{"input": "¿Qué es un embedding?", "expected": "representación vectorial"},
{"input": "¿Qué es fine-tuning?", "expected": "ajuste de un modelo pre-entrenado"},
]
def simple_metric(output: str, expected: str) -> float:
return 1.0 if expected.lower() in output.lower() else 0.0
def mock_model(question: str) -> str:
return f"Respuesta simulada sobre {question}"
report = run_offline_evaluation(mock_model, dataset, simple_metric)
print(f"Score promedio: {report['mean_score']:.2f}")
Ejemplo intermedio: evaluación con múltiples métricas y quality gates
En la práctica, nunca evalúas con una sola métrica. Necesitas capturar múltiples dimensiones de calidad. Este patrón es el que expandirás en el proyecto del módulo.
from typing import Callable
from dataclasses import dataclass, field
@dataclass
class EvalMetric:
name: str
fn: Callable[[str, str], float]
threshold: float = 0.7
def run_multi_metric_evaluation(
model_fn: Callable[[str], str],
dataset: list[dict],
metrics: list[EvalMetric],
) -> dict:
"""Evalúa con N métricas y genera un reporte pass/fail.
El threshold por métrica permite definir "quality gates":
si alguna métrica cae por debajo, la evaluación falla.
Esto es lo que usarás en CI/CD para bloquear deploys.
"""
all_scores: dict[str, list[float]] = {m.name: [] for m in metrics}
for item in dataset:
output = model_fn(item["input"])
for metric in metrics:
score = metric.fn(output, item["expected"])
all_scores[metric.name].append(score)
results = {}
passed = True
failures = []
for metric in metrics:
mean_score = sum(all_scores[metric.name]) / len(all_scores[metric.name])
results[metric.name] = round(mean_score, 3)
if mean_score < metric.threshold:
passed = False
failures.append(f"{metric.name}: {mean_score:.3f} < {metric.threshold}")
return {"metrics": results, "passed": passed, "failures": failures}
metrics = [
EvalMetric("contains_reference", lambda out, exp: 1.0 if exp.lower() in out.lower() else 0.0, 0.8),
EvalMetric("min_length", lambda out, _: 1.0 if len(out) > 20 else 0.0, 0.9),
]
report = run_multi_metric_evaluation(mock_model, dataset, metrics)
print(f"Passed: {report['passed']}, Metrics: {report['metrics']}")
Ejemplo avanzado: evaluación offline con LLM-as-judge
Cuando las métricas heurísticas no bastan, usas un LLM como evaluador. Preview del módulo 7 — el patrón básico es simple.
from openai import OpenAI
def llm_judge_relevance(question: str, answer: str, client: OpenAI | None = None) -> float:
"""LLM evalúa relevancia 1-5. Escala mejor que humanos pero
introduce sesgo del evaluador. Módulo 7 cubre calibración."""
if client is None:
client = OpenAI()
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=[
{"role": "system", "content": (
"Evalúa la relevancia de la respuesta a la pregunta. "
"Responde SOLO con un número del 1 al 5."
)},
{"role": "user", "content": f"Pregunta: {question}\nRespuesta: {answer}"},
],
temperature=0,
)
try:
return int(response.choices[0].message.content.strip()) / 5.0
except ValueError:
return 0.0
# score = llm_judge_relevance("¿Qué es RAG?", "RAG combina retrieval con generation.")
2. Evaluación online
Qué es
Medir la calidad de tu sistema en producción, con requests reales de usuarios. No tienes "respuestas correctas" contra las que comparar — en cambio, mides señales de comportamiento que actúan como proxies de calidad: ¿el usuario dio thumbs up? ¿Regeneró la respuesta? ¿Abandonó la conversación?
Cuándo usar
- Siempre, como complemento a offline. Es la única forma de saber si tu sistema funciona para usuarios reales.
- Monitoring continuo: Dashboards con métricas que te alertan si algo se degrada.
- A/B testing: Comparar dos versiones del sistema con tráfico real.
- Detectar drift: La distribución de inputs cambia con el tiempo, y tus métricas offline no capturan eso.
Ventajas
- Refleja la realidad: Los datos vienen de usuarios reales con necesidades reales.
- Captura distribución real de inputs: Descubres preguntas que nunca incluiste en tu golden dataset.
- Detecta drift: Si la calidad baja gradualmente, las métricas online lo captan.
Limitaciones
- No tienes ground truth: No sabes cuál era la "respuesta correcta" para cada input.
- Las métricas son proxies: Thumbs up mide satisfacción, no calidad objetiva. Un usuario puede dar thumbs up a una hallucination convincente.
- Sesgo de selección: Solo mides usuarios que dan feedback. Los que abandonan silenciosamente no aparecen.
Ejemplo básico: métricas de feedback explícito e implícito
from dataclasses import dataclass
from datetime import datetime
@dataclass
class UserInteraction:
timestamp: datetime
query: str
response: str
feedback: str | None = None # "positive", "negative", None
retry_count: int = 0
response_time_ms: int = 0
conversation_turns: int = 1
def compute_online_metrics(events: list[UserInteraction]) -> dict:
"""Calcula métricas online combinando señales explícitas e implícitas.
Las explícitas (feedback) son más directas pero tienen baja cobertura.
Las implícitas (retries, abandonment) cubren a todos los usuarios
pero son ambiguas. Combinarlas da la imagen más completa.
"""
total = len(events)
if total == 0:
return {"error": "No events"}
with_feedback = [e for e in events if e.feedback is not None]
thumbs_up = sum(1 for e in with_feedback if e.feedback == "positive")
retries = sum(1 for e in events if e.retry_count > 0)
single_turn = sum(1 for e in events if e.conversation_turns == 1)
return {
"satisfaction_rate": thumbs_up / len(with_feedback) if with_feedback else None,
"feedback_rate": len(with_feedback) / total,
"retry_rate": retries / total,
"single_turn_rate": single_turn / total,
"avg_response_time_ms": sum(e.response_time_ms for e in events) / total,
}
sample_events = [
UserInteraction(datetime.now(), "¿Qué es RAG?", "RAG es...", "positive", 0, 1200, 3),
UserInteraction(datetime.now(), "Explica embeddings", "Los embeddings...", "positive", 0, 980, 2),
UserInteraction(datetime.now(), "¿Precio del plan?", "El plan cuesta...", "negative", 2, 1500, 1),
UserInteraction(datetime.now(), "¿Cómo hago fine-tuning?", "Para fine-tuning...", None, 0, 1100, 4),
UserInteraction(datetime.now(), "Ayuda con mi código", "Aquí tienes...", "negative", 1, 2300, 1),
]
metrics = compute_online_metrics(sample_events)
for key, value in metrics.items():
print(f" {key}: {value}")
Ejemplo intermedio: alertas de degradación
La evaluación online cobra su mayor valor cuando te alerta automáticamente si algo se degrada.
from dataclasses import dataclass
@dataclass
class AlertConfig:
metric_name: str
threshold: float
direction: str # "above" o "below"
def check_alerts(
current: dict[str, float],
alerts: list[AlertConfig],
) -> list[str]:
"""Compara métricas actuales contra thresholds y genera alertas.
Detectar degradación antes de que los usuarios se quejen
— o antes de que dejen de usarte en silencio — es el
valor principal de la evaluación online.
"""
triggered = []
for alert in alerts:
value = current.get(alert.metric_name)
if value is None:
continue
if alert.direction == "below" and value < alert.threshold:
triggered.append(f"{alert.metric_name} bajó a {value:.3f} (min: {alert.threshold})")
elif alert.direction == "above" and value > alert.threshold:
triggered.append(f"{alert.metric_name} subió a {value:.3f} (max: {alert.threshold})")
return triggered
alerts = [
AlertConfig("satisfaction_rate", threshold=0.75, direction="below"),
AlertConfig("retry_rate", threshold=0.20, direction="above"),
]
current_metrics = {"satisfaction_rate": 0.70, "retry_rate": 0.25}
for msg in check_alerts(current_metrics, alerts):
print(f"⚠ ALERTA: {msg}")
3. Evaluación human-in-the-loop (HITL)
Qué es
Humanos evalúan los outputs de tu sistema según criterios definidos (rubrics). Puede ser pre-deploy (un equipo de annotators evalúa un batch de respuestas) o en producción (sampling + evaluación asíncrona). Es el gold standard de calidad: cuando no estás seguro de si tu métrica automatizada captura lo que importa, la evaluación humana es tu fuente de verdad.
Cuándo usar
- Crear golden datasets: Los humanos etiquetan las respuestas "correctas" que luego usas en evaluación offline.
- Calibrar LLM-as-judge: ¿Tu evaluador automatizado coincide con los humanos? HITL te permite validarlo (módulo 7).
- Casos donde la subjetividad importa: Tono, estilo, "helpfulness" — conceptos que las métricas heurísticas no capturan bien.
- Dominios de alto riesgo: Médico, legal, financiero — donde el costo de un error es alto y necesitas verificación humana.
- Bootstrap inicial: Cuando empiezas y no tienes ni golden dataset ni métricas calibradas, HITL es tu punto de partida.
Ventajas
- Gold standard: Si el humano dice que la respuesta es buena, es buena (asumiendo guidelines claros).
- Captura matices: "La respuesta es técnicamente correcta pero condescendiente" — eso no lo captura ninguna métrica.
- Base para automatización: Los datos de HITL alimentan la calibración de evaluadores automatizados.
Limitaciones
- Costoso y lento: No puedes evaluar 10,000 ejemplos con humanos en una hora.
- No escala: A medida que crece tu tráfico, HITL no puede ser tu evaluación principal.
- Subjetividad entre evaluadores: Dos humanos pueden discrepar. Necesitas inter-annotator agreement para saber si tus rubrics son claras.
Ejemplo básico: rubric de evaluación
from dataclasses import dataclass
@dataclass
class RubricDimension:
name: str
scale: dict[int, str]
weight: float = 1.0
# Un rubric bien diseñado es la base de toda evaluación HITL.
# Cada dimensión debe ser no-ambigua para que diferentes
# evaluadores lleguen al mismo score.
EVALUATION_RUBRIC = [
RubricDimension(
name="relevance",
scale={
1: "No responde la pregunta en absoluto",
2: "Toca el tema pero no responde directamente",
3: "Responde la pregunta parcialmente",
4: "Responde la pregunta completamente",
},
weight=1.5,
),
RubricDimension(
name="accuracy",
scale={
1: "Información incorrecta o inventada",
2: "Mezcla de información correcta e incorrecta",
3: "Mayormente correcto con imprecisiones menores",
4: "Completamente correcto y verificable",
},
weight=2.0,
),
RubricDimension(
name="helpfulness",
scale={
1: "No útil — el usuario no gana nada",
2: "Poco útil — información genérica",
3: "Útil — respuesta práctica y aplicable",
4: "Muy útil — respuesta excepcional con ejemplos claros",
},
weight=1.0,
),
]
Ejemplo intermedio: inter-annotator agreement
Si tus evaluadores no se ponen de acuerdo, tus rubrics no están suficientemente claras. Medir el agreement te dice cuándo refinar tus guidelines.
from dataclasses import dataclass
from statistics import mean, stdev
@dataclass
class HumanEvaluation:
example_id: str
evaluator_id: str
scores: dict[str, int]
def compute_inter_annotator_agreement(evaluations: list[HumanEvaluation]) -> dict:
"""Mide cuánto concuerdan los evaluadores entre sí.
Si el acuerdo es bajo (< 0.6), tus rubrics necesitan
refinamiento antes de confiar en los datos generados.
"""
by_example: dict[str, list[HumanEvaluation]] = {}
for ev in evaluations:
by_example.setdefault(ev.example_id, []).append(ev)
agreements = []
for evals in by_example.values():
if len(evals) < 2:
continue
for dim in evals[0].scores:
scores = [e.scores[dim] for e in evals]
agreement = 1 - (max(scores) - min(scores)) / 3 # escala 1-4
agreements.append(agreement)
return {
"mean_agreement": round(mean(agreements), 3) if agreements else 0,
"std": round(stdev(agreements), 3) if len(agreements) > 1 else 0,
}
evaluations = [
HumanEvaluation("q1", "ann_a", {"relevance": 4, "accuracy": 3, "helpfulness": 3}),
HumanEvaluation("q1", "ann_b", {"relevance": 4, "accuracy": 4, "helpfulness": 3}),
HumanEvaluation("q2", "ann_a", {"relevance": 2, "accuracy": 2, "helpfulness": 2}),
HumanEvaluation("q2", "ann_b", {"relevance": 3, "accuracy": 2, "helpfulness": 1}),
]
result = compute_inter_annotator_agreement(evaluations)
print(f"Agreement: {result['mean_agreement']:.2f} (> 0.8 = bueno, < 0.6 = revisar rubrics)")
Cuándo usar cada tipo: la tabla de decisión
| Situación | Offline | Online | HITL |
|---|---|---|---|
| Pre-deploy, detectar regresiones | ✅ Principal | ❌ | Opcional |
| Comparar modelo A vs B | ✅ Principal | ✅ A/B test | Opcional |
| Crear golden dataset | ❌ | ❌ | ✅ Principal |
| Monitoring continuo | ✅ Regression suite | ✅ Principal | Sampling |
| Calibrar LLM-as-judge | ❌ | ❌ | ✅ Principal |
| Evaluar subjetividad (tono, estilo) | Limitado | Proxies | ✅ Principal |
| Bootstrap de un proyecto nuevo | ❌ | ❌ | ✅ Punto de partida |
| Evaluar safety y toxicidad | ✅ Edge cases | ✅ Monitoring | ✅ Verificación |
| Diagnosticar caída de calidad | ✅ Comparar versiones | Detecta problema | ✅ Investiga causa |
La regla general: offline para automatizar, online para monitorear, HITL para calibrar y crear datos.
El flujo típico: cómo los tres se conectan
Los tres tipos no son alternativas — son fases de un ciclo continuo. Cada uno alimenta al siguiente.
┌──────────────────────────────────────────────────────────┐
│ CICLO DE EVALUACIÓN │
│ │
│ 1. HITL → Crear golden dataset │
│ Humanos anotan 50-100 ejemplos con rubrics claras │
│ ↓ │
│ 2. Offline → Evaluar en cada PR/merge │
│ Golden dataset + métricas automáticas en CI/CD │
│ ↓ │
│ 3. Online → Monitoring en producción │
│ Métricas de comportamiento + alertas de degradación │
│ ↓ │
│ 4. HITL (sampling) → Cerrar el loop │
│ Revisar 10-20 casos/semana de producción │
│ Actualizar golden dataset con nuevos patrones │
│ ↓ │
│ (Volver a paso 2 con dataset actualizado) │
└──────────────────────────────────────────────────────────┘
Este ciclo es lo que separa un pipeline de evaluación maduro de uno ad-hoc. No es lineal — es un loop que se repite y mejora con cada iteración.
Comparación detallada: Offline vs Online vs HITL
| Aspecto | Offline | Online | HITL |
|---|---|---|---|
| Momento | Pre-deploy | En producción | Pre o post-deploy |
| Dataset | Fijo, conocido | Dinámico, real | Curado por humanos |
| Ground truth | Sí (golden) | No (proxies) | Sí (juicio humano) |
| Automatización | Total | Parcial | Mínima |
| Costo | Bajo (compute) | Bajo (ya en prod) | Alto (humanos) |
| Escalabilidad | Alta | Alta | Baja |
| Velocidad | Minutos | Horas/días | Días/semanas |
| Captura subjetividad | No | Indirectamente | Sí |
Si estás empezando y solo puedes hacer una cosa, haz offline con un golden dataset pequeño (20-30 ejemplos). Es el mejor retorno por esfuerzo. De ahí, añade online cuando estés en producción y HITL cuando necesites calibrar tu dataset.
Troubleshooting
Problema 1: "Nuestro dataset offline no representa producción"
Síntomas: Las métricas offline son excelentes pero los usuarios se quejan.
Solución: Incluye samples de producción en tu golden dataset. Configura un proceso periódico (semanal) donde tomas N interacciones reales, las anotas con HITL, y las añades al dataset. Complementa con synthetic data para edge cases que no aparecen frecuentemente pero son críticos.
def refresh_golden_dataset(
current_dataset: list[dict],
production_samples: list[dict],
max_additions: int = 10,
) -> list[dict]:
"""Añade ejemplos de producción al golden dataset gradualmente."""
existing_inputs = {item["input"] for item in current_dataset}
new_items = [s for s in production_samples if s["input"] not in existing_inputs][:max_additions]
print(f"Dataset: {len(current_dataset)} → {len(current_dataset) + len(new_items)} ejemplos")
return current_dataset + new_items
Problema 2: "No tenemos recursos para HITL"
Síntomas: Equipo pequeño, sin presupuesto para annotators, nadie tiene tiempo.
Solución: Empieza con LLM-as-judge (módulo 7) para el día a día. Usa HITL solo para calibrar el judge y crear el golden dataset inicial. Un set de 50-100 ejemplos anotados por alguien del equipo es suficiente como base. Invierte 2 horas una vez, no 20 horas cada semana.
Problema 3: "Las métricas online no correlacionan con satisfacción"
Síntomas: Tu retry_rate bajó y pensaste que mejoraste, pero una encuesta muestra que los usuarios están menos satisfechos.
Solución: Una sola métrica nunca cuenta toda la historia. Combina múltiples proxies: thumbs + retry rate + abandonment + conversation depth. Valida periódicamente con encuestas cortas. Busca tendencias más que valores absolutos.
Problema 4: "Los evaluadores humanos no se ponen de acuerdo"
Síntomas: Inter-annotator agreement bajo (< 0.6). Dos evaluadores dan scores muy diferentes.
Solución: El problema casi siempre está en los rubrics. Añade ejemplos concretos para cada nivel ("un 3 en relevancia se ve así: ..."). Haz una sesión de calibración donde todos evalúan los mismos 10 ejemplos y discuten discrepancias. Después, re-evalúa el agreement.
Problema 5: "No sé cuántos ejemplos necesita mi golden dataset"
Síntomas: Parálisis por no saber el tamaño "correcto".
Solución: Empieza con 20-30 ejemplos que cubran los escenarios más comunes. Crece incrementalmente: cada sprint, añade 5-10 de producción. No necesitas cobertura exhaustiva al inicio — necesitas un loop que mejore con el tiempo. El módulo 3 profundiza en esto.
Ejercicios
Ejercicio 1: Clasificar escenarios
Para cada escenario, identifica si es principalmente offline, online o HITL:
a) Ejecutar 100 preguntas de un JSON antes de cada deploy b) Medir % de thumbs up en la app esta semana c) Un anotador etiqueta 50 respuestas como "correcta/incorrecta" d) Tu CI/CD bloquea un merge porque el score promedio bajó de 0.85 a 0.72 e) Mides que el 30% de los usuarios regeneran la primera respuesta
Ver solución
a) Offline — Dataset fijo (JSON), pre-deploy, automatizable. b) Online — Métrica en producción derivada de feedback real. c) HITL — Humano evalúa directamente la calidad de outputs. d) Offline — CI/CD gate con evaluación automatizada y thresholds. e) Online — Retry rate es una métrica implícita de comportamiento en producción.
Ejercicio 2: Diseñar un pipeline de tres capas
Diseña un pipeline de evaluación para un chatbot de atención al cliente (productos, precios, devoluciones). Define qué harías en cada capa y en qué orden.
Ver solución
- HITL bootstrap: El equipo de soporte anota 60 conversaciones con rubrics (relevance, accuracy, tone 1-4). Frecuencia: una vez, luego actualizaciones mensuales.
- Offline CI: Correr golden dataset en cada PR. Métricas: contains_reference, llm_judge_relevance, response_length. Threshold: score >= 0.80 para merge.
- Online monitoring: Métricas explícitas (thumbs, escalation_to_human) + implícitas (retry_rate, abandonment). Alertas si satisfaction < 0.75 o retry_rate > 0.20.
- HITL sampling: Samplear 15 conversaciones/semana, priorizar feedback negativo y casos sin feedback. Actualizar golden dataset con hallazgos.
Ejercicio 3: Implementar métricas online
Implementa una función que calcule al menos 4 métricas online (explícitas e implícitas) a partir de una lista de interacciones.
Ver solución
from dataclasses import dataclass
from datetime import datetime
@dataclass
class Interaction:
query: str
feedback: str | None = None
retry_count: int = 0
conversation_turns: int = 1
time_to_next_msg_sec: float | None = None
def compute_metrics(interactions: list[Interaction]) -> dict:
n = len(interactions)
with_fb = [i for i in interactions if i.feedback is not None]
return {
"satisfaction": sum(1 for i in with_fb if i.feedback == "positive") / len(with_fb) if with_fb else None,
"retry_rate": sum(1 for i in interactions if i.retry_count > 0) / n,
"avg_turns": sum(i.conversation_turns for i in interactions) / n,
"quick_dismiss_rate": sum(
1 for i in interactions
if i.time_to_next_msg_sec is not None and i.time_to_next_msg_sec < 3.0
) / n,
"feedback_participation": len(with_fb) / n,
}
sample = [
Interaction("q1", "positive", 0, 3, 15.0),
Interaction("q2", "negative", 2, 1, 2.0),
Interaction("q3", None, 0, 5, 45.0),
Interaction("q4", "positive", 0, 2, 10.0),
Interaction("q5", None, 1, 1, 1.5),
]
for k, v in compute_metrics(sample).items():
print(f" {k}: {v}")
Ejercicio 4: Diseñar un rubric HITL
Crea un rubric con 3 dimensiones para un chatbot de documentación técnica. Cada dimensión: 4 niveles con descripciones claras.
Ver solución
rubric = {
"technical_accuracy": {
"weight": 2.0,
"scale": {
1: "Info incorrecta que llevaría a errores (API inexistente, params inventados)",
2: "Mezcla correcta/incorrecta — usuario tendría que verificar todo",
3: "Mayormente correcto con imprecisiones menores",
4: "Completamente correcto y verificable contra docs oficiales",
},
},
"completeness": {
"weight": 1.5,
"scale": {
1: "No responde la pregunta o da info tangencial",
2: "Parcial — omite pasos clave o info necesaria",
3: "Completa para el caso base, omite edge cases relevantes",
4: "Completa con edge cases, warnings y next steps",
},
},
"actionability": {
"weight": 1.0,
"scale": {
1: "Abstracta sin pasos concretos — usuario necesita buscar más",
2: "Indicaciones generales sin código ni comandos",
3: "Código que funciona pero sin explicación del contexto",
4: "Código funcional con explicación, prerrequisitos y verificación",
},
},
}
# Score ponderado de ejemplo
scores = {"technical_accuracy": 3, "completeness": 4, "actionability": 3}
total_w = sum(d["weight"] for d in rubric.values())
weighted = sum(scores[dim] * rubric[dim]["weight"] for dim in rubric) / total_w
print(f"Score ponderado: {weighted:.2f} / 4.00")
Ejercicio 5: Trade-off analysis
Para cada par, argumenta cuál elegirías y por qué:
a) 50 ejemplos evaluados por humanos vs 500 evaluados por LLM-as-judge b) Retry rate como métrica principal vs Thumbs up como métrica principal c) Golden dataset fijo 6 meses vs Actualizado cada 2 semanas
Ver solución
a) Depende de la fase. Para bootstrap/calibración, los 50 humanos son tu ground truth. Para CI/CD diario, los 500 con LLM-as-judge dan mejor cobertura. Lo ideal: usa los 50 humanos para calibrar el judge, luego usa los 500 con confianza.
b) Thumbs up es más directo pero tiene sesgo de selección. Retry rate cubre a todos pero es ambiguo. Mejor práctica: usar ambos — thumbs como señal primaria donde existe, retry como cobertura universal.
c) Actualizado cada 2 semanas, siempre. Un dataset estático se vuelve obsoleto. El costo (30 min quincenal) es mínimo vs. el riesgo de evaluar contra datos obsoletos.
Ejercicio 6: Implementar el ciclo completo
Implementa una función que ejecute el ciclo offline → online → recomendaciones y devuelva un reporte consolidado.
Ver solución
from typing import Callable
def run_eval_cycle(
model_fn: Callable[[str], str],
golden: list[dict],
prod_events: list[dict],
threshold: float = 0.8,
) -> dict:
# Offline: evaluar contra golden dataset
scores = [1.0 if item["expected"].lower() in model_fn(item["input"]).lower() else 0.0 for item in golden]
offline_score = sum(scores) / len(scores) if scores else 0
# Online: métricas de producción
n = len(prod_events)
satisfaction = sum(1 for e in prod_events if e.get("feedback") == "positive") / n if n else 0
retry_rate = sum(1 for e in prod_events if e.get("retries", 0) > 0) / n if n else 0
# Recomendaciones automáticas
recs = []
if offline_score < threshold:
recs.append(f"Offline ({offline_score:.2f}) bajo threshold. Revisar cambios.")
if retry_rate > 0.2:
recs.append("Retry rate alto. Investigar queries problemáticos.")
return {"offline": {"score": offline_score, "passed": offline_score >= threshold},
"online": {"satisfaction": satisfaction, "retry_rate": retry_rate},
"recommendations": recs}
golden = [{"input": "¿Qué es RAG?", "expected": "retrieval"}, {"input": "¿Embedding?", "expected": "vector"}]
prod = [{"feedback": "positive", "retries": 0}, {"feedback": "negative", "retries": 1},
{"feedback": "positive", "retries": 0}, {"feedback": None, "retries": 2}]
report = run_eval_cycle(lambda q: f"Respuesta sobre {q}", golden, prod)
print(f"Offline: {report['offline']}, Online: {report['online']}")
Conexión con el proyecto del módulo
El proyecto del módulo 1 te pide construir un pipeline de evaluación end-to-end que genera, evalúa y reporta. Los tres tipos de evaluación que aprendiste en esta cápsula son el marco conceptual de ese pipeline:
- Tu proyecto implementa evaluación offline: Toma un dataset fijo de 20 preguntas, genera respuestas y las evalúa con métricas. Es la base más importante.
- El patrón se extiende a online: En el módulo 8, el mismo pipeline correrá contra datos de producción con alertas de degradación.
- HITL es tu fuente de verdad: Las respuestas "esperadas" de tu golden dataset vienen de juicio humano. Cuando construyas el dataset del proyecto, estás haciendo HITL tú mismo.
Cuando implementes el proyecto, piensa en cada componente como una instancia del ciclo:
Proyecto Módulo 1:
golden_dataset.json ← HITL (tú anotaste las respuestas esperadas)
run_evaluation() ← Offline (evalúas contra el dataset)
generate_report() ← Output que en producción alimentaría monitoring (online)
En los módulos siguientes, cada capa se sofistica: el golden dataset crece (módulo 3), las métricas se especializan por dominio (módulos 4-6), el evaluador humano se reemplaza parcialmente por LLM-as-judge (módulo 7), y el pipeline se integra en CI/CD con monitoring continuo (módulo 8).
Resumen
- Evaluación offline usa datasets fijos pre-deploy. Es reproducible, automatizable y barata. Ideal para regresiones y comparación de versiones. Limitada por la representatividad del dataset.
- Evaluación online mide en producción con métricas de comportamiento (thumbs, retries, abandonment). Es la única forma de saber si tu sistema funciona para usuarios reales. Las métricas son proxies, no medidas directas de calidad.
- Evaluación human-in-the-loop usa evaluadores humanos con rubrics claras. Es el gold standard pero no escala. Úsala para crear golden datasets, calibrar evaluadores automatizados y evaluar dimensiones subjetivas.
- Los tres tipos se complementan en un ciclo continuo: HITL crea datos → offline automatiza → online monitorea → HITL recalibra.
- Si solo puedes hacer una cosa, empieza con offline + golden dataset pequeño (20-30 ejemplos). Es el mejor retorno por esfuerzo.
- Las métricas online más útiles combinan señales explícitas (feedback) con implícitas (retry rate, abandonment, conversation depth).
- El inter-annotator agreement es clave en HITL: si los evaluadores no concuerdan, el problema está en los rubrics.
- Un pipeline maduro integra los tres tipos y los mejora iterativamente — es un loop, no un proceso lineal.
Recursos adicionales
- Human-in-the-Loop Machine Learning (Google) — Guía completa de HITL y annotation strategies
- Online vs Offline Evaluation of Recommender Systems — Paper comparativo con lecciones aplicables a LLMs
- A/B Testing for AI Systems (ExP Platform) — Framework de Microsoft para experimentación en producción
- Annotation Guidelines Best Practices — Cómo diseñar rubrics que maximicen inter-annotator agreement
- Your AI Product Needs Evals (Eugene Yan) — Artículo práctico sobre por qué todo producto de AI necesita evaluación
- Hamel Husain - Creating a LLM-as-Judge — Evaluación automatizada y calibración con datos humanos
- RAGAS Documentation - Metrics — Framework que usarás en el módulo 5, cuya filosofía aplica desde ahora
- Inter-Annotator Agreement (Wikipedia) — Cohen's Kappa y métricas de acuerdo entre evaluadores