Evaluar LLMs en Producción: Métricas, Benchmarks y Sistemas de Prueba Continuos
Entrenar o ajustar un modelo está bien. Desplegarlo en producción sin saber si es mejor que la versión anterior es un desastre.
La mayoría de los equipos que implementan soluciones con LLMs caen en la trampa de usar métricas proxy: "el modelo respondió rápido", "el usuario no se quejó". Eso no es evaluación. Eso es esperanza.
El Problema: ¿Cómo Sabes que tu LLM Mejoró?
Los números de validación durante el desarrollo no significan nada en producción. Una clasificación de 92% en MMLU no te dice si tu modelo RAG está hallucinating menos. Una latencia baja no te confirma que las respuestas son mejores. Necesitas un sistema que:
- Mida la calidad de forma consistente
- Detecte degradación antes de que afecte usuarios
- Permita comparaciones justas entre modelos
- Automatice la evaluación en cada cambio
Las Métricas que Importan (y las que No)
BLEU Score: La Métrica Heredada que Aún Se Usa
BLEU (Bilingual Evaluation Understudy) compara n-gramas entre respuesta generada y respuesta de referencia. Rango 0-1.
from nltk.translate.bleu_score import sentence_bleu
reference = [['el', 'gato', 'está', 'en', 'la', 'mesa']]
candidate = ['el', 'gato', 'está', 'en', 'una', 'mesa']
score = sentence_bleu(reference, candidate)
# Score: ~0.65 (perdió 1 de 6 palabras exactas)
Problema: Penaliza paráfrasis válidas. "El felino está sobre la mesa" obtiene 0. Es demasiado estricta para lenguaje natural.
Cuándo usarla: Traducción automática. No para chatbots o generación abierta.
ROUGE Score: Mejor para Resúmenes
ROUGE (Recall-Oriented Understudy for Gisting Evaluation) mide overlap de n-gramas entre generado y referencia, enfocándose en recall.
from rouge_score import rouge_scorer
scorer = rouge_scorer.RougeScorer(['rouge1', 'rougeL'])
reference = "El cambio climático amenaza los ecosistemas"
candidate = "El cambio climático es una amenaza para ecosistemas"
scores = scorer.score(reference, candidate)
print(scores['rouge1'].fmeasure) # ~0.75
print(scores['rougeL'].fmeasure) # ~0.83
ROUGE-1: Unigrams (palabras individuales)
ROUGE-L: Longest common subsequence (orden importa)
Cuándo usarla: Resúmenes, abstractos, extractos de documentos.
BERTScore: La Métrica Semántica
BERTScore usa embeddings de BERT para comparar similitud semántica, no exactitud de palabras.
from bert_score import score
cand = ["El gato está comiendo"]
refs = ["El felino está consumiendo alimento"]
P, R, F1 = score(cand, refs, lang="es", model_type="bert-base-multilingual-cased")
print(F1) # ~0.92 (mucho mejor que BLEU/ROUGE porque entiende significado)
Ventaja: Entiende sinónimos y paráfrasis. No es n-gram based.
Limitación: No mide factualidad. "2+2=5" y "2+2=4" tendrían alta similitud semántica.
Cuándo usarla: Generación abierta, paráfrasis, respuestas de chatbot donde múltiples formulaciones son válidas.
Métricas Específicas de Dominio
Para RAG: Mean Reciprocal Rank (MRR), Normalized Discounted Cumulative Gain (NDCG)
# MRR: Posición del primer resultado relevante
# Si el documento correcto está en posición 2: MRR = 1/2 = 0.5
# NDCG: Evalúa ranking considerando relevancia graduada
# Mejor que exactitud binaria (relevante/no relevante)
Para Clasificación: Precision, Recall, F1, Macro-average
Para Generación Controlada (JSON, Code): Syntactic correctness, schema validation
import json
from pydantic import BaseModel, ValidationError
class Response(BaseModel):
action: str
confidence: float
def is_valid_output(text: str) -> bool:
try:
data = json.loads(text)
Response(**data)
return True
except (json.JSONDecodeError, ValidationError):
return False
score = sum(is_valid_output(output) for output in outputs) / len(outputs)
# Métrica: "% de salidas válidas"
Benchmarks Estándar para Comparación
MMLU (Massive Multitask Language Understanding)
57 categorías de preguntas de elección múltiple: matemática, historia, medicina, derecho, etc. 12,500+ preguntas.
Qué mide: Conocimiento general y razonamiento
Limitación: Solo elección múltiple. No mide generación.
- GPT-4: 86%
- Claude 3.5 Sonnet: 88%
- Llama 3.1 70B: 85%
HumanEval (OpenAI)
164 problemas de programación Python. Mide si el código generado pasa test cases.
Qué mide: Razonamiento lógico y codificación
- GPT-4: 88%
- Claude 3.5 Sonnet: 92%
- Llama 3.1 70B: 83%
HellaSwag
70,000 escenarios de "qué pasa después" con 4 opciones. Mide sense of sequence y common sense.
Qué mide: Comprensión de secuencia y coherencia temporal
TruthfulQA
817 preguntas donde modelos tienden a producir respuestas que "suenan bien" pero son falsas.
Qué mide: Faithfulness (fidelidad a hechos vs. plausibilidad)
Por qué importa: Un LLM puede sonar confiable mientras alucina completamente.
Diseño de Evaluación en Producción
Sistema de 3 Capas
Capa 1: Métrica Automática (Diaria)
Suite de 100-500 casos de prueba con respuestas esperadas. Se ejecuta automáticamente.
# Pseudocódigo
test_cases = [
{
"input": "¿Cuál es la capital de Francia?",
"expected_output": "París",
"evaluator": "exact_match"
},
{
"input": "Resume este artículo en 2 líneas",
"reference": "Resumen experto",
"evaluator": "rouge_l_score"
}
]
for test in test_cases:
result = model.generate(test['input'])
score = test['evaluator'](result, test['expected_output'])
log_metric(score, test['input'])
Capa 2: Evaluación por LLM (Semanal)
Usar otro LLM (más fuerte o diferente) para evaluar calidad. Ejemplo: Claude 3.5 evalúa Llama.
evaluator_prompt = f"""
Evalúa la siguiente respuesta en escala 1-5:
Pregunta: {question}
Respuesta Esperada: {reference}
Respuesta Generada: {candidate}
Criterios:
- ¿Es factuamente correcta?
- ¿Es completa?
- ¿Es clara y bien estructurada?
Responde solo con número (1-5) y explicación breve.
"""
score = evaluator_llm.generate(evaluator_prompt)
Capa 3: Evaluación Humana (Mensual)
Muestreo aleatorio de 50-100 ejemplos evaluados por domain experts.
- ¿Fue útil la respuesta?
- ¿Contiene información falsa?
- ¿Necesita mejoras?
Detección Automática de Degradación
import numpy as np
from datetime import datetime, timedelta
# Trackear métrica diaria
daily_scores = [0.87, 0.89, 0.88, 0.90, 0.86, 0.84, 0.82, 0.81, 0.79]
dates = [datetime.now() - timedelta(days=i) for i in range(len(daily_scores))]
# Calcular tendencia
window = 7 # últimos 7 días
recent_avg = np.mean(daily_scores[:window])
older_avg = np.mean(daily_scores[window:])
degradation = (older_avg - recent_avg) / older_avg * 100
if degradation > 5: # Si cayó más de 5%
alert("Critical: Model performance degraded by {:.1f}%".format(degradation))
rollback_to_previous_version()
Implementación: Pipeline Completo con Evidencia Real
Aquí está un ejemplo funcional minimalista:
#!/usr/bin/env python3
import json
import subprocess
from datetime import datetime
from anthropic import Anthropic
client = Anthropic()
# Test cases con respuestas esperadas
TEST_CASES = [
{
"id": "qa_1",
"question": "¿Qué es un embedding?",
"type": "knowledge",
"rubric": [
"Menciona representación vectorial",
"Menciona similitud semántica",
"Menciona aplicaciones (RAG, búsqueda)"
]
},
{
"id": "code_1",
"question": "Escribe una función que invierta una lista en Python",
"type": "code",
"expected_code": "def reverse_list(lst): return lst[::-1]"
},
{
"id": "summary_1",
"question": "Resume en 1 frase: Los transformers revolucionaron NLP",
"type": "summary"
}
]
def evaluate_knowledge(response: str, rubric: list) -> float:
"""Evalúa usando LLM"""
eval_prompt = f"""
Evalúa si la siguiente respuesta cubre estos puntos:
Rubric: {rubric}
Respuesta: {response}
Responde con JSON: {{"covered_points": int, "total_points": int}}
"""
result = client.messages.create(
model="claude-3-5-sonnet-20241022",
max_tokens=100,
messages=[{"role": "user", "content": eval_prompt}]
)
try:
data = json.loads(result.content[0].text)
return data["covered_points"] / data["total_points"]
except:
return 0.5
def evaluate_code(response: str, expected: str) -> float:
"""Evalúa corrección sintáctica"""
try:
compile(response, '', 'exec')
# Bonus: ejecutar y comparar con esperado
return 0.8 if 'def ' in response else 0.3
except:
return 0.0
def run_evaluation():
results = {
"timestamp": datetime.now().isoformat(),
"model": "claude-3-5-sonnet-20241022",
"tests": [],
"overall_score": 0
}
for test in TEST_CASES:
# Generar respuesta
response = client.messages.create(
model="claude-3-5-sonnet-20241022",
max_tokens=500,
messages=[{"role": "user", "content": test["question"]}]
)
text = response.content[0].text
# Evaluar según tipo
if test["type"] == "knowledge":
score = evaluate_knowledge(text, test["rubric"])
elif test["type"] == "code":
score = evaluate_code(text, test.get("expected_code", ""))
else: # summary
score = 0.7 # Placeholder
results["tests"].append({
"id": test["id"],
"question": test["question"],
"score": round(score, 3),
"response_preview": text[:100]
})
results["overall_score"] = round(
sum(t["score"] for t in results["tests"]) / len(results["tests"]), 3
)
# Guardar resultado
with open(f"eval_result_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json", "w") as f:
json.dump(results, f, indent=2)
return results
if __name__ == "__main__":
results = run_evaluation()
print(f"Overall Score: {results['overall_score']}")
print(f"Tests passed: {sum(1 for t in results['tests'] if t['score'] > 0.7)}/{len(results['tests'])}")
Casos Reales: Lo Que Funciona en Producción
Caso 1: RAG Evaluation
Una empresa de atención al cliente usa RAG para responder preguntas sobre documentos internos. Métrica elegida: NDCG@5 en retrieval + Human Rating de respuesta final.
Setup:
- 100 preguntas frecuentes con documentos relevantes marcados
- Evaluar ranking de documentos: NDCG
- Evaluar respuesta generada: Human rating 1-5
Resultado: Detectaron que cambiar embedding model de sentence-transformers a OpenAI's text-embedding-3-large mejoró NDCG de 0.82 a 0.94. Cambio deployeado.
Caso 2: Code Generation Monitoring
Plataforma que genera código Python. Métrica: % de código que compila + % de test cases pasados.
Setup:
- 50 problemas de HumanEval
- Ejecutar código generado contra test cases
- Grabar métrica diaria
Resultado: Notaron degradación de 85% a 72% en una semana. Investigación: el model context window se agotaba en problemas complejos. Solución: agregar retrieval de ejemplos similares.
Caso 3: Factualidad en Chatbot
Chatbot empresarial. Métrica: TruthfulQA adaptada + fact checking externo.
Setup:
- 500 preguntas sobre políticas de empresa
- Evaluar con LLM más fuerte si la respuesta es factualmente correcta
- Alertar si % de alucinaciones sube
Resultado: Con context window pequeño (4K), 18% alucinaciones. Aumentando a 128K, bajó a 3%. Cambio justificado por métrica.
Anti-Patrones a Evitar
❌ No confundas velocidad con calidad
Un modelo más rápido que alucina no es mejor. Mide ambos: latency Y quality.
❌ Una única métrica nunca es suficiente
BLEU = 0.85 puede significar cualquier cosa. Necesitas contexto: ROUGE, BERTScore, Human eval.
❌ No copies benchmarks sin adaptarlos
MMLU sirve para comparar modelos base. No te dice si tu RAG mejoró o si tu chatbot es mejor.
❌ Olvidar el costo de evaluación
Ejecutar evaluación por LLM 1000 veces/día puede costar miles de dólares. Muestrea.
❌ No monitorear en producción
Si no mides continuamente, no sabes si degradaste. La evaluación no termina en deployment.
Herramientas Existentes
- OpenAI Evals: Framework para definir evals custom
- RAGAS: Evaluación específica para RAG
- LangChain Evaluation: Integración con LLMs
- DeepEval: Metrics automáticas con LLM judge
- Braintrust: Plataforma completa (evaluación + versioning)
Conclusión: La Evaluación es Arquitectura
No es un "nice to have" para al final. Es parte fundamental del sistema. Sin evaluación:
- No sabes si mejoró
- No detectas degradación
- No comparas modelos objetivamente
- No convences a stakeholders de cambios
La buena noticia: las herramientas son simples. La inversión inicial en diseñar test cases y métricas se recupera en la primera degradación detectada automáticamente.
Próximo paso: Define 3-5 métricas específicas para tu use case, automatiza su cálculo semanal, y monitorea tendencias. Es así de simple.
¿Cómo estás evaluando tus LLMs en producción? ¿Qué métricas usas? Comparte en los comentarios.