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.