Inicio / LLMOps / LLMOps: De Prototipo a Producción / Evaluación de LLMs

Evaluación de LLMs

ROUGE, LLM-as-judge, comparación pairwise, RAGAS y regression testing.

Intermedio
🔒 Solo lectura
📖

Estás en modo lectura

Puedes leer toda la lección, pero para marcar progreso, hacer ejercicios y ganar XP necesitas una cuenta Pro.

Desbloquear por $9/mes

Evaluación de LLMs

¿Por Qué es Difícil Evaluar LLMs?

A diferencia del ML tradicional donde hay métricas claras (accuracy, F1), evaluar LLMs es complejo porque las respuestas pueden ser correctas de múltiples formas, el lenguaje es subjetivo, y la calidad depende del contexto. Aún así, la evaluación sistemática es imprescindible para LLMOps.


Tipos de Evaluación

┌────────────────────────────────────────────┐
│          PIRÁMIDE DE EVALUACIÓN            │
│                                             │
│              ┌───────┐                      │
│              │ Human │  Costoso, lento      │
│              │ Eval  │  pero gold standard  │
│             ┌┴───────┴┐                     │
│             │LLM-as-a │  Rápido, escalable  │
│             │  Judge  │  buena correlación   │
│            ┌┴─────────┴┐                    │
│            │ Automated  │  Rápido, barato   │
│            │  Metrics  │  pero limitado      │
│           ┌┴───────────┴┐                   │
│           │  Unit Tests  │  Base mínima     │
│           │  (assertions)│  determinista     │
│           └──────────────┘                   │
└────────────────────────────────────────────┘

Métricas Automatizadas

Para Texto Generado

from rouge_score import rouge_scorer

def calculate_rouge(reference: str, generated: str) -> dict:
    scorer = rouge_scorer.RougeScorer(['rouge1', 'rouge2', 'rougeL'])
    scores = scorer.score(reference, generated)
    return {
        "rouge1": scores["rouge1"].fmeasure,  # Unigrams
        "rouge2": scores["rouge2"].fmeasure,  # Bigrams
        "rougeL": scores["rougeL"].fmeasure,  # Longest common subsequence
    }

Para Clasificación

def evaluate_classification(predictions, ground_truth):
    correct = sum(p == gt for p, gt in zip(predictions, ground_truth))
    accuracy = correct / len(predictions)
    
    # Per-class metrics
    classes = set(ground_truth)
    metrics = {}
    for cls in classes:
        tp = sum(p == cls and gt == cls for p, gt in zip(predictions, ground_truth))
        fp = sum(p == cls and gt != cls for p, gt in zip(predictions, ground_truth))
        fn = sum(p != cls and gt == cls for p, gt in zip(predictions, ground_truth))
        
        precision = tp / (tp + fp) if (tp + fp) > 0 else 0
        recall = tp / (tp + fn) if (tp + fn) > 0 else 0
        f1 = 2 * precision * recall / (precision + recall) if (precision + recall) > 0 else 0
        
        metrics[cls] = {"precision": precision, "recall": recall, "f1": f1}
    
    return {"accuracy": accuracy, "per_class": metrics}

LLM-as-a-Judge

Usar un LLM para evaluar las respuestas de otro LLM. Tiene buena correlación con evaluación humana.

def llm_judge(question: str, response: str, criteria: list[str]) -> dict:
    criteria_text = "\n".join(f"- {c}" for c in criteria)
    
    judge_response = client.chat.completions.create(
        model="gpt-4o",
        messages=[{
            "role": "system",
            "content": """Eres un evaluador experto. Evalúa la respuesta en cada criterio 
de 1 a 5 (1=muy malo, 5=excelente). Responde en JSON."""
        }, {
            "role": "user",
            "content": f"""Pregunta: {question}

Respuesta a evaluar: {response}

Criterios:
{criteria_text}

Evalúa cada criterio con un score de 1 a 5 y una justificación breve.
Formato JSON: {{"criterio": {{"score": N, "reason": "..."}}}}"""
        }],
        response_format={"type": "json_object"},
        temperature=0,
    )
    
    return json.loads(judge_response.choices[0].message.content)

# Uso
result = llm_judge(
    question="¿Qué es Docker?",
    response="Docker es una plataforma de contenedores...",
    criteria=["Precisión técnica", "Completitud", "Claridad", "Relevancia"]
)

Pairwise Comparison

def pairwise_judge(question: str, response_a: str, response_b: str) -> str:
    """Comparar dos respuestas lado a lado."""
    result = client.chat.completions.create(
        model="gpt-4o",
        messages=[{
            "role": "user",
            "content": f"""Pregunta: {question}

Respuesta A: {response_a}

Respuesta B: {response_b}

¿Cuál respuesta es mejor? Responde "A", "B" o "empate".
Justifica brevemente."""
        }],
        temperature=0,
    )
    return result.choices[0].message.content

Evaluación de RAG (RAGAS)

def evaluate_rag_pipeline(test_cases: list[dict]) -> dict:
    """
    Cada test_case tiene:
    - question: pregunta del usuario
    - ground_truth: respuesta esperada
    - retrieved_docs: documentos recuperados
    - generated_answer: respuesta generada
    """
    metrics = {
        "faithfulness": [],      # ¿Se basa en los docs?
        "answer_relevancy": [],  # ¿Responde la pregunta?
        "context_precision": [], # ¿Los docs son relevantes?
    }
    
    for tc in test_cases:
        # Faithfulness: ¿La respuesta se apoya en los documentos?
        faith = llm_judge(
            question=f"¿La respuesta se basa fielmente en estos documentos?\n{tc['retrieved_docs']}",
            response=tc["generated_answer"],
            criteria=["Fidelidad al contexto"]
        )
        metrics["faithfulness"].append(faith)
        
        # Answer relevancy
        relevancy = llm_judge(
            question=tc["question"],
            response=tc["generated_answer"],
            criteria=["Relevancia de la respuesta"]
        )
        metrics["answer_relevancy"].append(relevancy)
    
    return {k: sum(v) / len(v) for k, v in metrics.items() if v}

Framework de Evaluación Completo

class LLMEvalFramework:
    def __init__(self, model, test_suite_path):
        self.model = model
        self.test_suite = self._load_tests(test_suite_path)
    
    def _load_tests(self, path):
        with open(path) as f:
            return json.load(f)
    
    def run_evaluation(self) -> dict:
        results = {
            "total_tests": len(self.test_suite),
            "passed": 0,
            "failed": 0,
            "scores": [],
            "failures": [],
        }
        
        for test in self.test_suite:
            response = self._call_model(test["input"])
            score = self._score_response(test, response)
            
            results["scores"].append(score)
            if score >= test.get("threshold", 0.7):
                results["passed"] += 1
            else:
                results["failed"] += 1
                results["failures"].append({
                    "input": test["input"],
                    "expected": test.get("expected"),
                    "actual": response,
                    "score": score,
                })
        
        results["avg_score"] = sum(results["scores"]) / len(results["scores"])
        results["pass_rate"] = results["passed"] / results["total_tests"]
        
        return results
    
    def _score_response(self, test, response) -> float:
        if "exact_match" in test:
            return 1.0 if response.strip() == test["exact_match"] else 0.0
        if "contains" in test:
            return 1.0 if test["contains"].lower() in response.lower() else 0.0
        if "llm_judge" in test:
            judge = llm_judge(test["input"], response, test["llm_judge"]["criteria"])
            return sum(v["score"] for v in judge.values()) / (5 * len(judge))
        return 0.5  # Default

Regression Testing

def regression_test(old_model, new_model, test_suite):
    """Verificar que el nuevo modelo no empeora."""
    old_results = evaluate(old_model, test_suite)
    new_results = evaluate(new_model, test_suite)
    
    regressions = []
    for old, new in zip(old_results, new_results):
        if new["score"] < old["score"] - 0.1:  # 10% de margen
            regressions.append({
                "test": old["test_name"],
                "old_score": old["score"],
                "new_score": new["score"],
            })
    
    if regressions:
        print(f"⚠️ {len(regressions)} regresiones detectadas!")
    else:
        print("✓ Sin regresiones")
    
    return regressions

Resumen

Evaluar LLMs requiere múltiples capas: unit tests para assertions básicas, métricas automatizadas, LLM-as-a-judge para evaluación escalable, y evaluación humana para validación final. Un pipeline de evaluación robusto es la columna vertebral de LLMOps.

🔒

Ejercicio práctico disponible

Métricas de evaluación para LLMs

Desbloquear ejercicios
// Métricas de evaluación para LLMs
// Desbloquea Pro para acceder a este ejercicio
// y ganar +50 XP al completarlo

function ejemplo() {
    // Tu código aquí...
}

¿Te gustó esta lección?

Con Pro puedes marcar progreso, hacer ejercicios, tomar quizzes, ganar XP y obtener tu constancia.

Ver planes desde $9/mes