Fran Rodrigo
Evaluación, seguridad y gobernanzaW1019 min de lectura

Evaluación y benchmarking de agentes

Por qué evaluar agentes es más duro que evaluar NLP. Evaluación de trayectorias, proceso frente a resultado. Benchmarks principales: GAIA, AgentBench, WebArena, SWE-bench, ToolBench. LLM-como-juez con controles de fiabilidad. Análisis de coste y latencia.

Conceptos núcleoEvaluación de trayectoriaLLM-como-juezMétricas de coste

01Objetivos de aprendizaje

Al finalizar esta clase, los estudiantes serán capaces de:

  1. Explicar por qué evaluar agentes de IA es fundamentalmente más difícil que evaluar modelos de NLP estándar.
  2. Diseñar protocolos de evaluación basados en tareas que midan tanto la tasa de éxito como la calidad de la completitud.
  3. Implementar evaluación basada en proceso que valore la calidad del razonamiento, la eficiencia en el uso de herramientas y los pasos intermedios.
  4. Describir y comparar los principales benchmarks de agentes: SWE-bench, GAIA, AgentBench, WebArena y HumanEval.
  5. Aplicar técnicas de LLM-as-judge para la evaluación automatizada de las salidas de agentes.
  6. Diseñar protocolos de evaluación humana con guías de anotación apropiadas y medidas de fiabilidad inter-evaluador.
  7. Incorporar métricas de coste y latencia en la evaluación de agentes para determinar la preparación para despliegue en producción.
  8. Construir un arnés de evaluación automatizada sencillo en Python.

021. Por qué evaluar agentes es difícil

La brecha de evaluación

Evaluar modelos tradicionales de NLP es relativamente sencillo: dada una entrada, comparar la salida del modelo con una referencia de gold-standard. La accuracy, el F1 score, BLEU y ROUGE proporcionan métricas claras y cuantitativas. La evaluación de agentes es fundamentalmente diferente, y esta diferencia es uno de los retos más subestimados del campo.

Consideremos la analogía de evaluar a un estudiante. Evaluar un examen tipo test es fácil: comparar las respuestas con la clave. Evaluar un ensayo es más difícil: se necesitan rúbricas, criterio y posiblemente múltiples evaluadores. Evaluar el rendimiento de un estudiante en un proyecto de investigación de un semestre es lo más difícil de todo: hay que valorar no solo el informe final, sino el proceso de investigación, las decisiones tomadas en el camino, la capacidad de manejar contratiempos y la calidad del razonamiento. La evaluación de agentes es como evaluar el proyecto de investigación, y a menudo intentamos hacerlo con las herramientas diseñadas para exámenes tipo test.

Idea clave: El reto fundamental de la evaluación de agentes es que los agentes no son funciones que mapean entradas a salidas. Son procesos que interactúan con entornos a lo largo del tiempo, tomando secuencias de decisiones con resultados inciertos. Evaluar un proceso es fundamentalmente más difícil que evaluar una función.

Fuentes de dificultad

No determinismo

Los agentes basados en LLM son estocásticos. El mismo agente con la misma entrada puede producir diferentes salidas en distintas ejecuciones debido a:

  • Temperatura de muestreo: Incluso temperaturas pequeñas introducen variabilidad.
  • Variabilidad en las respuestas de herramientas: Las búsquedas web, llamadas a APIs y consultas a bases de datos pueden devolver resultados diferentes en momentos diferentes.
  • Dependencia del camino: Las primeras elecciones aleatorias se propagan en cascada a través de los pasos de razonamiento posteriores.

Esto significa que la evaluación de una sola ejecución no es fiable. La evaluación estadística a través de múltiples ejecuciones es necesaria, lo que incrementa el coste.

python
def evaluate_with_variance(
    agent_fn, test_case: dict, num_runs: int = 5
) -> dict:
    """Run an agent multiple times and report variance.

    Args:
        agent_fn: The agent function to evaluate.
        test_case: Dict with 'input' and 'expected_output' keys.
        num_runs: Number of independent runs.

    Returns:
        Dict with mean score, standard deviation, and individual results.
    """
    scores = []
    outputs = []

    for run in range(num_runs):
        output = agent_fn(test_case["input"])
        score = compute_score(output, test_case["expected_output"])
        scores.append(score)
        outputs.append(output)

    return {
        "mean_score": sum(scores) / len(scores),
        "std_dev": (sum((s - sum(scores)/len(scores))**2 for s in scores) / len(scores)) ** 0.5,
        "min_score": min(scores),
        "max_score": max(scores),
        "num_runs": num_runs,
        "all_scores": scores,
    }

Comportamientos complejos y de múltiples pasos

Los agentes no sólo producen texto; realizan acciones a través de múltiples pasos. Un agente de codificación podría:

  1. Leer la descripción del problema.
  2. Buscar documentación relevante.
  3. Escribir código inicial.
  4. Ejecutar el código y observar un error.
  5. Depurar y corregir el código.
  6. Ejecutar pruebas para verificar.

Evaluar solo la salida final pierde información importante sobre la calidad del proceso. Dos agentes podrían producir el mismo código final, pero uno podría haberlo hecho en 3 pasos mientras el otro necesitó 15, incluyendo varios intentos fallidos.

Salidas abiertas

Muchas tareas de agentes tienen múltiples soluciones válidas. "Escribe una función en Python para ordenar una lista" tiene docenas de implementaciones correctas. "Planifica un viaje de una semana a Japón" tiene millones de respuestas razonables. No hay una única salida de gold-standard contra la cual comparar.

Interacción con el entorno

Los agentes interactúan con entornos externos (navegadores web, intérpretes de código, bases de datos, APIs). Estas interacciones son:

  • Difíciles de reproducir: El contenido web cambia, las APIs evolucionan, las bases de datos se actualizan.
  • Costosas de ejecutar: Las llamadas reales a APIs cuestan dinero y tardan tiempo.
  • Difíciles de aislar: Los agentes con acceso al mundo real pueden causar efectos en el mundo real.

Comportamientos emergentes e inesperados

Los agentes pueden exhibir comportamientos no anticipados por el protocolo de evaluación:

  • Encontrar soluciones creativas que el evaluador no consideró.
  • Explotar lagunas en la configuración de evaluación.
  • Producir resultados correctos a través de un razonamiento incorrecto.
  • Aparentar tener éxito mientras en realidad toman atajos dañinos.

032. Evaluación basada en tareas

La evaluación basada en tareas responde a la pregunta más práctica: "¿Consigue realmente el agente hacer las cosas?" Es el resultado final de la evaluación de agentes. Un agente con trazas de razonamiento hermosas y uso elegante de herramientas que no produce la respuesta correcta sigue siendo un agente que falla.

Tasa de éxito

La métrica más fundamental: ¿completo el agente la tarea? Esta es la calificación de "aprobado/suspenso" de la evaluación de agentes.

python
def task_success_rate(
    results: list[dict],
) -> dict:
    """Compute task-level success metrics.

    Args:
        results: List of dicts with 'task_id', 'completed', and 'correct' fields.

    Returns:
        Dict with success metrics.
    """
    n = len(results)
    completed = sum(1 for r in results if r["completed"])
    correct = sum(1 for r in results if r["correct"])

    return {
        "total_tasks": n,
        "completion_rate": completed / n if n > 0 else 0.0,
        "success_rate": correct / n if n > 0 else 0.0,
        "completed_but_incorrect": completed - correct,
    }

Sin embargo, el exito/fracaso binario pierde matices importantes. Un agente que completa correctamente el 80% de una tarea es diferente de uno que falla completamente.

Calidad de completitud

La puntuación graduada captura el éxito parcial:

python
from dataclasses import dataclass


@dataclass
class TaskResult:
    """Result of a single task evaluation."""

    task_id: str
    score: float          # 0.0 to 1.0 (partial credit)
    completed: bool       # Did the agent declare completion?
    steps_taken: int      # Number of steps to reach the result
    errors_encountered: int
    tools_used: list[str]
    wall_time_seconds: float
    tokens_consumed: int
    notes: str = ""


def evaluate_task_quality(
    result: TaskResult,
    max_steps: int = 20,
    max_time: float = 300.0,
) -> dict:
    """Evaluate task completion quality with multiple dimensions.

    Combines correctness with efficiency metrics.
    """
    # Correctness (primary metric)
    correctness = result.score

    # Efficiency: penalize excessive steps
    step_efficiency = max(0.0, 1.0 - (result.steps_taken / max_steps))

    # Time efficiency
    time_efficiency = max(0.0, 1.0 - (result.wall_time_seconds / max_time))

    # Error rate (lower is better)
    error_rate = result.errors_encountered / max(result.steps_taken, 1)

    # Combined quality score (weighted)
    quality_score = (
        0.6 * correctness +
        0.2 * step_efficiency +
        0.1 * time_efficiency +
        0.1 * (1 - error_rate)
    )

    return {
        "task_id": result.task_id,
        "correctness": correctness,
        "step_efficiency": step_efficiency,
        "time_efficiency": time_efficiency,
        "error_rate": error_rate,
        "quality_score": quality_score,
    }

Evaluación basada en rúbricas

Para tareas abiertas, definir una rúbrica con criterios específicos:

python
CODING_TASK_RUBRIC = {
    "correctness": {
        "description": "Does the code produce the correct output?",
        "weight": 0.40,
        "levels": {
            1.0: "All test cases pass",
            0.75: "Most test cases pass (>80%)",
            0.5: "Some test cases pass (40-80%)",
            0.25: "Few test cases pass (<40%)",
            0.0: "No test cases pass or code does not run",
        },
    },
    "code_quality": {
        "description": "Is the code clean, readable, and well-structured?",
        "weight": 0.20,
        "levels": {
            1.0: "Excellent: clean, well-documented, good variable names",
            0.75: "Good: mostly clean with minor issues",
            0.5: "Acceptable: works but has style issues",
            0.25: "Poor: hard to read, bad structure",
            0.0: "Very poor: incomprehensible",
        },
    },
    "efficiency": {
        "description": "Is the solution reasonably efficient?",
        "weight": 0.15,
        "levels": {
            1.0: "Optimal time and space complexity",
            0.75: "Near-optimal",
            0.5: "Acceptable for the problem size",
            0.25: "Unnecessarily slow or memory-intensive",
            0.0: "Unacceptably inefficient",
        },
    },
    "error_handling": {
        "description": "Does the code handle edge cases and errors?",
        "weight": 0.15,
        "levels": {
            1.0: "Comprehensive error handling",
            0.5: "Basic error handling",
            0.0: "No error handling",
        },
    },
    "process_quality": {
        "description": "Was the agent's problem-solving process reasonable?",
        "weight": 0.10,
        "levels": {
            1.0: "Systematic: understood, planned, implemented, tested",
            0.5: "Somewhat systematic but with unnecessary steps",
            0.0: "Chaotic: random trial and error",
        },
    },
}


def rubric_evaluate(scores: dict[str, float], rubric: dict) -> float:
    """Compute a weighted score from rubric evaluations."""
    total = 0.0
    total_weight = 0.0
    for criterion, config in rubric.items():
        if criterion in scores:
            total += scores[criterion] * config["weight"]
            total_weight += config["weight"]
    return total / total_weight if total_weight > 0 else 0.0

043. Evaluación basada en proceso

Más allá de las salidas finales

La evaluación basada en tareas te dice si el agente tuvo éxito. La evaluación basada en proceso te dice por qué tuvo éxito o fracasó. Esta distinción importa enormemente para mejorar los agentes.

La analogía es calificar un examen de matemáticas. Si un estudiante obtiene la respuesta correcta a través de un razonamiento erróneo (por ejemplo, dos errores que se cancelan), fallará en el siguiente problema donde los errores no se cancelen. De forma similar, un agente que obtiene la respuesta correcta a través de un proceso defectuoso (resultados de búsqueda afortunados, salidas de herramientas casuales) fallará en tareas ligeramente diferentes.

La evaluación basada en proceso examina cómo el agente llegó a su respuesta, no solo la respuesta en sí. Esto es crucial porque:

  • Una respuesta final correcta lograda a través de un razonamiento defectuoso puede no generalizar.
  • Una respuesta final incorrecta con buen razonamiento puede necesitar solo ajustes menores.
  • Comprender el proceso permite mejoras dirigidas.

Calidad del razonamiento

Evaluar la calidad de la cadena de pensamiento del agente:

python
REASONING_EVALUATION_PROMPT = """Evaluate the quality of the following agent's
reasoning process. Score each dimension from 1-5.

Agent's reasoning trace:
{trace}

Correct answer: {correct_answer}

Dimensions to evaluate:
1. **Logical Coherence** (1-5): Does each step follow logically from the previous?
2. **Relevance** (1-5): Are the reasoning steps relevant to the problem?
3. **Completeness** (1-5): Does the reasoning address all aspects of the problem?
4. **Efficiency** (1-5): Is the reasoning concise without unnecessary tangents?
5. **Self-Correction** (1-5): When errors occur, does the agent recognize and fix them?

For each dimension, provide a score and brief justification.
"""


def evaluate_reasoning(
    trace: str, correct_answer: str, llm_call
) -> dict:
    """Evaluate reasoning quality using an LLM judge."""
    response = llm_call(
        prompt=REASONING_EVALUATION_PROMPT.format(
            trace=trace, correct_answer=correct_answer
        )
    )
    return parse_dimension_scores(response)


def parse_dimension_scores(response: str) -> dict:
    """Parse dimension scores from judge response."""
    dimensions = [
        "logical_coherence",
        "relevance",
        "completeness",
        "efficiency",
        "self_correction",
    ]
    scores = {}
    for dim in dimensions:
        for line in response.split("\n"):
            dim_readable = dim.replace("_", " ")
            if dim_readable.lower() in line.lower() and any(c.isdigit() for c in line):
                digits = [int(c) for c in line if c.isdigit()]
                if digits:
                    scores[dim] = digits[0] / 5.0  # Normalize to 0-1
                    break
        if dim not in scores:
            scores[dim] = 0.5  # Default if parsing fails
    return scores

Eficiencia en el uso de herramientas

¿Qué tan eficazmente utiliza el agente sus herramientas disponibles?

python
def evaluate_tool_use(
    tool_calls: list[dict], optimal_calls: list[dict] | None = None
) -> dict:
    """Evaluate the efficiency and appropriateness of tool usage."""
    total_calls = len(tool_calls)

    # Count redundant calls (same tool with same/similar arguments)
    seen = set()
    redundant = 0
    for call in tool_calls:
        key = f"{call['tool']}:{call.get('args', '')}"
        if key in seen:
            redundant += 1
        seen.add(key)

    # Count failed calls
    failed = sum(1 for call in tool_calls if call.get("status") == "error")

    # Tool diversity (number of unique tools used)
    unique_tools = len({call["tool"] for call in tool_calls})

    metrics = {
        "total_calls": total_calls,
        "unique_tools_used": unique_tools,
        "redundant_calls": redundant,
        "failed_calls": failed,
        "redundancy_rate": redundant / total_calls if total_calls > 0 else 0.0,
        "failure_rate": failed / total_calls if total_calls > 0 else 0.0,
    }

    # Compare to optimal if available
    if optimal_calls is not None:
        optimal_count = len(optimal_calls)
        metrics["optimal_calls"] = optimal_count
        metrics["call_overhead"] = (total_calls - optimal_count) / optimal_count if optimal_count > 0 else 0.0

    return metrics

Análisis de trayectoria

Examinar la trayectoria completa de acciones del agente:

python
def analyze_trajectory(
    trajectory: list[dict],
) -> dict:
    """Analyze an agent's complete action trajectory."""
    total_steps = len(trajectory)

    # Categorize steps
    reasoning_steps = [s for s in trajectory if s["type"] == "thought"]
    action_steps = [s for s in trajectory if s["type"] == "action"]
    observation_steps = [s for s in trajectory if s["type"] == "observation"]

    # Detect backtracking (similar actions repeated)
    backtrack_count = 0
    for i in range(1, len(action_steps)):
        if action_steps[i]["content"] == action_steps[i-1]["content"]:
            backtrack_count += 1

    # Compute timing if timestamps available
    if trajectory and "timestamp" in trajectory[0]:
        from datetime import datetime
        start = datetime.fromisoformat(trajectory[0]["timestamp"])
        end = datetime.fromisoformat(trajectory[-1]["timestamp"])
        duration = (end - start).total_seconds()
    else:
        duration = None

    return {
        "total_steps": total_steps,
        "reasoning_steps": len(reasoning_steps),
        "action_steps": len(action_steps),
        "observation_steps": len(observation_steps),
        "reasoning_to_action_ratio": (
            len(reasoning_steps) / len(action_steps) if action_steps else 0
        ),
        "backtrack_count": backtrack_count,
        "duration_seconds": duration,
    }

054. Principales benchmarks de agentes

Los benchmarks cumplen un papel crítico en el ecosistema de IA: proporcionan evaluaciones estandarizadas y reproducibles que permiten la comparación entre diferentes agentes, modelos y enfoques. Sin benchmarks, cada equipo evalúa en sus propios casos de prueba, haciendo imposible comparar resultados entre artículos.

Sin embargo, los benchmarks también tienen un lado peligroso. Cuando un benchmark se convierte en el objetivo principal, los equipos optimizan para ese benchmark específico en lugar de para la capacidad general; esta es la Ley de Goodhart en acción. Mantén esta tensión en mente mientras revisamos los principales benchmarks.

Idea clave: Los benchmarks miden capacidades específicas, no inteligencia general. Un agente que logra el 100% en SWE-bench podría fallar espectacularmente en tareas de navegación web. Evalúa siempre con benchmarks que coincidan con tu escenario de despliegue, y trata las puntuaciones de benchmark como una señal entre muchas, no como la medida definitiva de la calidad del agente.

SWE-bench

Artículo: Jiménez et al. (2024), "SWE-bench: Can Language Models Resolve Real-World GitHub Issues?"

Qué evalúa: La capacidad de los agentes para resolver issues reales de GitHub en repositorios populares de Python (Django, Flask, scikit-learn, sympy, etc.).

Cómo funciona:

  1. Cada tarea es un issue real de GitHub con un pull request correspondiente que lo resuelve.
  2. Al agente se le da la descripción del issue y el repositorio en el commit anterior a la corrección.
  3. El agente debe modificar el código para resolver el issue.
  4. El éxito se mide por si la suite de pruebas del repositorio pasa tras las modificaciones del agente.

Características clave:

  • 2.294 instancias de tareas de 12 repositorios de Python.
  • Requiere comprender bases de código de más de 10.000 líneas.
  • Las pruebas son objetivas: pasan o no pasan.
  • SWE-bench Lite es un subconjunto curado de 300 instancias más fáciles.

Estado del arte (a principios de 2025): Los mejores agentes resuelven aproximadamente el 40-50% de las tareas de SWE-bench Lite.

GAIA

Artículo: Mialon et al. (2023), "GAIA: A Benchmark for General AI Assistants."

Qué evalúa: Capacidades de asistente de propósito general que requieren razonamiento de múltiples pasos, uso de herramientas y navegación web.

Cómo funciona:

  • 466 preguntas de dificultad creciente (Niveles 1, 2, 3).
  • Las preguntas están diseñadas para que los humanos puedan responderlas fácilmente pero los sistemas de IA necesiten herramientas (calculadora, búsqueda web, lectura de archivos).
  • Cada pregunta tiene una única respuesta correcta e inequívoca.
  • Nivel 1: Requiere 1-2 pasos. Nivel 3: Requiere más de 10 pasos con razonamiento complejo.

Ejemplo (Nivel 2): "¿Cuál fue el precio de cierre de las acciones de Apple el día en que se anunció el primer iPhone?" (Requiere conocer la fecha del anunció y buscar datos históricos de acciones.)

Idea clave: Incluso con acceso a herramientas, lograr un rendimiento a nivel humano en GAIA sigue siendo un reto abierto.

AgentBench

Artículo: Liu et al. (2024), "AgentBench: Evaluating LLMs as Agents."

Qué evalúa: Capacidades de agentes en 8 entornos diferentes:

  1. Sistema operativo (comandos bash)
  2. Base de datos (consultas SQL)
  3. Grafo de conocimiento (consultas SPARQL)
  4. Juego de cartas digital
  5. Puzzles de pensamiento lateral
  6. Tareas domésticas (ALFWorld)
  7. Compras web (WebShop)
  8. Navegación web

Idea clave: El rendimiento varía dramáticamente entre entornos. Un agente que destaca en codificación puede tener dificultades con la navegación web.

WebArena

Artículo: Zhou et al. (2024), "WebArena: A Realistic Web Environment for Building Autonomous Agents."

Qué evalúa: La capacidad de los agentes para completar tareas realistas en sitios web reales.

Cómo funciona:

  • Réplicas autoalojadas de sitios web reales (Reddit, GitLab, sitios de compras, plataformas CMS, mapas).
  • 812 tareas en los sitios web alojados.
  • Las tareas requieren navegación, rellenado de formularios, recuperación de información e interacciones de múltiples pasos.
  • La evaluación comprueba el estado final del entorno (por ejemplo, ¿se creó realmente la publicación? ¿se añadió el artículo al carrito?).

Ejemplo de tarea: "Pública un comentario en el hilo de discusión más reciente del subreddit de Machine Learning diciéndo 'Great insight, thanks for sharing!'"

HumanEval

Artículo: Chen et al. (2021), "Evaluating Large Language Models Trained on Code."

Qué evalúa: Generación de código a partir de docstrings.

Cómo funciona:

  • 164 problemas de programación en Python con firmas de funciones y docstrings.
  • El modelo genera el cuerpo de la función.
  • La evaluación es pass@k: la probabilidad de que al menos una de k soluciones generadas pase todos los casos de prueba.

Por qué importa para los agentes: HumanEval prueba la capacidad de generación de código que sustenta los agentes de programación. Aunque evalúa generación en lugar de agencia, es un benchmark fundamental para agentes capaces de escribir código.

Comparación de benchmarks

BenchmarkDominioTareasEvaluaciónRequiere herramientas
SWE-benchIngeniería de software2.294Pasan/no pasan las pruebasEditor de código, terminal
GAIAAsistente general466Coincidencia exacta de respuestaWeb, calculadora, archivos
AgentBench8 entornos1.000+Específica del entornoVarios
WebArenaNavegación web812Estado del entornoNavegador web
HumanEvalGeneración de código164pass@k en casos de pruebaNinguna (generación pura)

065. LLM-as-judge

El concepto

Cuando no hay referencias de gold-standard disponibles o al evaluar salidas abiertas, otro LLM puede servir como evaluador (juez). Este enfoque fue formalizado por Zheng et al. (2023) en su trabajo sobre MT-Bench y Chatbot Arena.

La idea es al mismo tiempo atractiva e inquietante: usar una IA para evaluar a otra IA. El atractivo es la escalabilidad; se pueden evaluar miles de salidas automáticamente. La preocupación es la circularidad; si el juez tiene los mismos sesgos que el agente evaluado, esos sesgos serán invisibles para la evaluación. Es como pedir a un estudiante que califique su propio examen.

A pesar de estas preocupaciones, LLM-as-judge se ha convertido en el enfoque dominante para evaluar salidas abiertas de agentes porque la alternativa (evaluación humana) es demasiado lenta y costosa para la mayoría de los flujos de desarrollo. La clave es comprender y mitigar los sesgos conocidos.

Concepto erróneo común: "LLM-as-judge no es fiable." La investigación muestra que los jueces LLM fuertes (como GPT-4) correlacionan bien con el juicio humano en la mayoría de las dimensiones de evaluación. La correlación no es perfecta, pero es lo suficientemente buena para ser útil en desarrollo e iteración. La mejor práctica es usar LLM-as-judge para iteración rápida y evaluación humana para validación final.

LLM-as-judge básico

python
JUDGE_PROMPT = """You are an expert judge evaluating the quality of an AI agent's
response to a task.

Task: {task}

Agent's response:
{response}

Evaluate on a scale of 1-10 across these dimensions:
1. **Correctness**: Is the response factually accurate and logically sound?
2. **Completeness**: Does the response fully address all aspects of the task?
3. **Clarity**: Is the response well-organized and easy to understand?
4. **Usefulness**: Would this response be practically helpful to the user?

For each dimension, provide:
- Score (1-10)
- Brief justification

Finally, provide an overall score (1-10).
"""


def llm_as_judge(
    task: str,
    response: str,
    llm_call,
    judge_model: str = "gpt-4",
) -> dict:
    """Use an LLM to evaluate an agent's response."""
    prompt = JUDGE_PROMPT.format(task=task, response=response)
    judgment = llm_call(model=judge_model, prompt=prompt)
    return parse_judgment(judgment)


def parse_judgment(judgment: str) -> dict:
    """Parse a judge's response into structured scores."""
    dimensions = ["correctness", "completeness", "clarity", "usefulness"]
    scores = {}

    for dim in dimensions:
        for line in judgment.split("\n"):
            if dim.lower() in line.lower() and any(c.isdigit() for c in line):
                digits = [int(c) for c in line if c.isdigit()]
                if digits:
                    scores[dim] = min(digits[0], 10) / 10.0  # Normalize to 0-1
                    break

    # Extract overall score
    for line in judgment.split("\n"):
        if "overall" in line.lower() and any(c.isdigit() for c in line):
            digits = [int(c) for c in line if c.isdigit()]
            if digits:
                scores["overall"] = min(digits[0], 10) / 10.0
                break

    if "overall" not in scores:
        scores["overall"] = sum(scores.values()) / len(scores) if scores else 0.5

    scores["raw_judgment"] = judgment
    return scores

Comparación por pares

En lugar de puntuación absoluta (que es subjetiva), comparar dos respuestas cabeza a cabeza:

python
PAIRWISE_PROMPT = """Compare the following two responses to the same task.

Task: {task}

Response A:
{response_a}

Response B:
{response_b}

Which response is better? Consider correctness, completeness, clarity, and
usefulness.

Respond with:
- "A" if Response A is clearly better
- "B" if Response B is clearly better
- "TIE" if they are roughly equivalent

Then explain your reasoning in 2-3 sentences."""


def pairwise_compare(
    task: str,
    response_a: str,
    response_b: str,
    llm_call,
    num_comparisons: int = 3,
) -> dict:
    """Compare two responses with position debiasing.

    Runs the comparison multiple times, swapping positions to reduce
    position bias (LLMs tend to prefer the first response).
    """
    a_wins = 0
    b_wins = 0
    ties = 0

    for i in range(num_comparisons):
        # Alternate positions to reduce position bias
        if i % 2 == 0:
            first, second = response_a, response_b
            label_first, label_second = "A", "B"
        else:
            first, second = response_b, response_a
            label_first, label_second = "B", "A"

        prompt = PAIRWISE_PROMPT.format(
            task=task, response_a=first, response_b=second
        )
        result = llm_call(prompt=prompt).strip()

        if result.startswith("A"):
            if label_first == "A":
                a_wins += 1
            else:
                b_wins += 1
        elif result.startswith("B"):
            if label_second == "A":
                a_wins += 1
            else:
                b_wins += 1
        else:
            ties += 1

    if a_wins > b_wins:
        winner = "A"
    elif b_wins > a_wins:
        winner = "B"
    else:
        winner = "TIE"

    return {
        "winner": winner,
        "a_wins": a_wins,
        "b_wins": b_wins,
        "ties": ties,
        "comparisons": num_comparisons,
    }

Sesgos conocidos de los jueces LLM

LLM-as-judge tiene varios sesgos documentados (Zheng et al., 2023):

  • Sesgo de posición: Tendencia a preferir la respuesta presentada en primer lugar.
  • Sesgo de verbosidad: Tendencia a preferir respuestas más largas y detalladas.
  • Sesgo de auto-mejora: Los modelos tienden a puntuar mejor sus propias salidas.
  • Sesgo de estilo: Preferencia por ciertos estilos de escritura (por ejemplo, listas con viñetas, formato estructurado).

Mitigaciones:

  • Intercambiar posiciones y promediar resultados (como se muestra arriba).
  • Usar un modelo diferente como juez del que se está evaluando.
  • Combinar el juicio del LLM con métricas objetivas.
  • Reportar intervalos de confianza, no solo estimaciones puntuales.

076. Pipelines de evaluación automatizada

Pipeline de evaluación integral

Interactive · Pipeline de evaluacion de agentes

Embudo de evaluación

De cobertura a profundidad

La evaluación de agentes empieza ancha y barata, y termina estrecha y cara. Pulsa cada nivel para ver qué entra y qué cae.

Cobertura amplia

10,000 casos

Métricas baratas y automáticas sobre todo el conjunto. Sirve para detectar regresiones.

← AmplitudProfundidad →

Un pipeline de evaluación en producción debe ser automatizado, reproducible y exhaustivo:

python
"""
An automated evaluation pipeline for AI agents.

This pipeline:
1. Loads test cases from a dataset
2. Runs the agent on each test case (with retry and timeout)
3. Evaluates results using multiple metrics
4. Generates a comprehensive report
"""

import json
import time
from dataclasses import dataclass, field
from datetime import datetime
from pathlib import Path


@dataclass
class TestCase:
    """A single test case for agent evaluation."""

    id: str
    input: str
    expected_output: str | None = None
    metadata: dict = field(default_factory=dict)
    difficulty: str = "medium"
    category: str = "general"


@dataclass
class EvaluationResult:
    """Result of evaluating an agent on a test case."""

    test_case_id: str
    agent_output: str
    scores: dict[str, float]
    trajectory: list[dict]
    wall_time_seconds: float
    tokens_used: int
    error: str | None = None


class EvaluationPipeline:
    """Automated evaluation pipeline for AI agents.

    Supports:
    - Multiple evaluation metrics (exact match, LLM judge, custom)
    - Retry logic with timeout
    - Parallel evaluation (optional)
    - Comprehensive reporting
    """

    def __init__(
        self,
        agent_fn,
        evaluators: dict[str, callable],
        timeout_seconds: float = 300.0,
        max_retries: int = 2,
    ):
        self.agent_fn = agent_fn
        self.evaluators = evaluators
        self.timeout_seconds = timeout_seconds
        self.max_retries = max_retries
        self.results: list[EvaluationResult] = []

    def load_test_cases(self, path: str) -> list[TestCase]:
        """Load test cases from a JSON file."""
        with open(path) as f:
            data = json.load(f)
        return [TestCase(**case) for case in data]

    def run_agent(self, test_case: TestCase) -> dict:
        """Run the agent on a single test case with timing and error handling."""
        start_time = time.time()
        trajectory = []
        error = None
        output = ""

        for attempt in range(self.max_retries + 1):
            try:
                result = self.agent_fn(
                    test_case.input,
                    timeout=self.timeout_seconds,
                )
                output = result.get("output", "")
                trajectory = result.get("trajectory", [])
                break
            except TimeoutError:
                error = f"Timeout after {self.timeout_seconds}s (attempt {attempt + 1})"
            except Exception as e:
                error = f"Error: {str(e)} (attempt {attempt + 1})"

        wall_time = time.time() - start_time

        return {
            "output": output,
            "trajectory": trajectory,
            "wall_time": wall_time,
            "tokens_used": result.get("tokens_used", 0) if not error else 0,
            "error": error,
        }

    def evaluate_single(self, test_case: TestCase) -> EvaluationResult:
        """Run and evaluate a single test case."""
        run_result = self.run_agent(test_case)

        scores = {}
        for eval_name, eval_fn in self.evaluators.items():
            try:
                score = eval_fn(
                    input=test_case.input,
                    output=run_result["output"],
                    expected=test_case.expected_output,
                    trajectory=run_result["trajectory"],
                )
                scores[eval_name] = score
            except Exception as e:
                scores[eval_name] = 0.0
                print(f"  Evaluator '{eval_name}' failed: {e}")

        return EvaluationResult(
            test_case_id=test_case.id,
            agent_output=run_result["output"],
            scores=scores,
            trajectory=run_result["trajectory"],
            wall_time_seconds=run_result["wall_time"],
            tokens_used=run_result["tokens_used"],
            error=run_result["error"],
        )

    def run_evaluation(self, test_cases: list[TestCase]) -> dict:
        """Run the full evaluation pipeline."""
        print(f"Starting evaluation: {len(test_cases)} test cases")
        print(f"Evaluators: {list(self.evaluators.keys())}")
        print("=" * 60)

        self.results = []
        for i, test_case in enumerate(test_cases):
            print(f"[{i+1}/{len(test_cases)}] Evaluating {test_case.id}...", end=" ")
            result = self.evaluate_single(test_case)
            self.results.append(result)

            if result.error:
                print(f"ERROR: {result.error}")
            else:
                score_str = ", ".join(
                    f"{k}: {v:.2f}" for k, v in result.scores.items()
                )
                print(f"Scores: {score_str} ({result.wall_time_seconds:.1f}s)")

        report = self.generate_report(test_cases)
        return report

    def generate_report(self, test_cases: list[TestCase]) -> dict:
        """Generate a comprehensive evaluation report."""
        if not self.results:
            return {"error": "No results to report."}

        all_scores = {}
        for eval_name in self.evaluators:
            scores = [r.scores.get(eval_name, 0.0) for r in self.results]
            all_scores[eval_name] = {
                "mean": sum(scores) / len(scores),
                "min": min(scores),
                "max": max(scores),
                "std": (sum((s - sum(scores)/len(scores))**2 for s in scores) / len(scores)) ** 0.5,
            }

        errors = [r for r in self.results if r.error]
        times = [r.wall_time_seconds for r in self.results]
        tokens = [r.tokens_used for r in self.results]

        # Per-category breakdown
        categories = {}
        for tc, result in zip(test_cases, self.results):
            cat = tc.category
            if cat not in categories:
                categories[cat] = []
            categories[cat].append(result)

        category_scores = {}
        for cat, cat_results in categories.items():
            for eval_name in self.evaluators:
                scores = [r.scores.get(eval_name, 0.0) for r in cat_results]
                key = f"{cat}/{eval_name}"
                category_scores[key] = sum(scores) / len(scores) if scores else 0.0

        report = {
            "timestamp": datetime.now().isoformat(),
            "total_test_cases": len(self.results),
            "overall_scores": all_scores,
            "error_count": len(errors),
            "error_rate": len(errors) / len(self.results),
            "timing": {
                "mean_seconds": sum(times) / len(times),
                "total_seconds": sum(times),
                "max_seconds": max(times),
            },
            "tokens": {
                "mean": sum(tokens) / len(tokens) if tokens else 0,
                "total": sum(tokens),
            },
            "by_category": category_scores,
        }

        return report

    def save_report(self, report: dict, path: str) -> None:
        """Save the evaluation report to a JSON file."""
        with open(path, "w") as f:
            json.dump(report, f, indent=2)
        print(f"Report saved to {path}")

087. Evaluación humana

Cuando la evaluación humana es necesaria

LLM-as-judge y las métricas automatizadas tienen limitaciones. La evaluación humana sigue siendo necesaria para:

  • Dominios novedosos donde no existe benchmark.
  • Evaluación de calidad subjetiva (creatividad, tono, poder de persuasión).
  • Evaluación de seguridad (detectar salidas dañinas que las comprobaciones automatizadas pasan por alto).
  • Calibrar métricas automatizadas (asegurar que correlacionán con el juicio humano).

Diseño de protocolos de anotación

Un protocolo de anotación bien diseñado incluye:

python
ANNOTATION_GUIDELINES = """
## Agent Response Evaluation Guidelines

### Task
You will evaluate AI agent responses to user tasks. For each response,
rate the following dimensions on a 1-5 scale.

### Dimensions

**1. Task Completion (1-5)**
- 5: Fully completes the task with no omissions
- 4: Mostly complete with minor omissions
- 3: Partially complete; addresses the main point but misses aspects
- 2: Minimally addresses the task
- 1: Does not address the task at all

**2. Accuracy (1-5)**
- 5: All information is accurate
- 4: Nearly all accurate, one minor error
- 3: Mostly accurate, some errors
- 2: Multiple significant errors
- 1: Predominantly inaccurate

**3. Helpfulness (1-5)**
- 5: Exceptionally helpful; would fully satisfy the user
- 4: Helpful; addresses the user's needs well
- 3: Somewhat helpful; provides partial value
- 2: Minimally helpful
- 1: Not helpful at all

### Instructions
- Read the task description carefully before evaluating.
- Evaluate the response independently (do not compare to other responses).
- Provide a brief justification (1-2 sentences) for each score.
- If you are unsure, err on the side of a lower score.
- Flag any responses that contain harmful, biased, or inappropriate content.
"""

Fiabilidad inter-evaluador

Cuando múltiples anotadores evalúan las mismas salidas, su concordancia debe medirse:

python
def cohens_kappa(rater1: list[int], rater2: list[int]) -> float:
    """Compute Cohen's Kappa for inter-rater reliability.

    Cohen's Kappa measures agreement between two raters beyond
    what would be expected by chance.

    Interpretation:
    - < 0.00: Poor agreement
    - 0.00-0.20: Slight agreement
    - 0.21-0.40: Fair agreement
    - 0.41-0.60: Moderate agreement
    - 0.61-0.80: Substantial agreement
    - 0.81-1.00: Almost perfect agreement

    Args:
        rater1: List of ratings from rater 1.
        rater2: List of ratings from rater 2.

    Returns:
        Cohen's Kappa coefficient.
    """
    assert len(rater1) == len(rater2), "Raters must evaluate the same items"

    n = len(rater1)
    categories = sorted(set(rater1) | set(rater2))

    # Observed agreement
    agreement = sum(1 for a, b in zip(rater1, rater2) if a == b)
    p_observed = agreement / n

    # Expected agreement by chance
    p_expected = 0.0
    for cat in categories:
        p1 = sum(1 for r in rater1 if r == cat) / n
        p2 = sum(1 for r in rater2 if r == cat) / n
        p_expected += p1 * p2

    # Kappa
    if p_expected == 1.0:
        return 1.0  # Perfect agreement
    return (p_observed - p_expected) / (1 - p_expected)
  • Kappa de Cohen: Mide la concordancia entre dos evaluadores más allá de lo que se esperaría por azar. Valores por encima de 0.61 indican concordancia sustancial.
  • Alfa de Krippendorff: Soporta cualquier número de evaluadores, datos faltantes y diferentes niveles de medición (nominal, ordinal, intervalo, razón).
python
def krippendorffs_alpha(ratings: list[list[int | None]], level: str = "ordinal") -> float:
    """Compute Krippendorff's Alpha for multiple raters.

    Krippendorff's Alpha supports:
    - Any number of raters
    - Missing data (None values)
    - Different measurement levels (nominal, ordinal, interval, ratio)

    This is a simplified implementation for nominal data.

    Args:
        ratings: Matrix where ratings[i][j] is rater i's rating for item j.
                 None indicates a missing rating.
        level: Measurement level ("nominal" supported in this implementation).

    Returns:
        Krippendorff's Alpha coefficient.
    """
    n_raters = len(ratings)
    n_items = len(ratings[0])

    # Collect observed disagreement
    observed_disagreement = 0.0
    n_pairs = 0

    for j in range(n_items):
        item_ratings = [ratings[i][j] for i in range(n_raters) if ratings[i][j] is not None]
        m = len(item_ratings)
        if m < 2:
            continue
        for a_idx in range(m):
            for b_idx in range(a_idx + 1, m):
                if item_ratings[a_idx] != item_ratings[b_idx]:
                    observed_disagreement += 1
                n_pairs += 1

    if n_pairs == 0:
        return 1.0

    d_observed = observed_disagreement / n_pairs

    # Expected disagreement
    all_valid = [r for rater in ratings for r in rater if r is not None]
    n_total = len(all_valid)
    value_counts = {}
    for v in all_valid:
        value_counts[v] = value_counts.get(v, 0) + 1

    d_expected = 0.0
    values = list(value_counts.keys())
    for i, v1 in enumerate(values):
        for j, v2 in enumerate(values):
            if v1 != v2:
                d_expected += value_counts[v1] * value_counts[v2]
    d_expected /= (n_total * (n_total - 1))

    if d_expected == 0:
        return 1.0

    return 1.0 - (d_observed / d_expected)

098. Métricas de coste y latencia

Por qué importan el coste y la latencia

Hay un dicho en ingeniería: "Rápido, barato, bueno: elige dos." La evaluación de agentes tradicionalmente se centra en "bueno" (precisión, calidad) mientras ignora "rápido" (latencia) y "barato" (coste). En producción, los tres importan.

Idea clave: El coste y la latencia no son métricas secundarias; son criterios de evaluación de primera clase. Un agente que logra el 95% de precisión pero tarda 5 minutos y cuesta 2porinteraccioˊnpuedesermenospraˊcticoqueunoquelograel85 por interacción puede ser menos práctico que uno que logra el 85% de precisión en 10 segundos por 0.05. El compromiso correcto depende del caso de uso, pero no puedes hacer ese compromiso si no mides las tres dimensiones.

Seguimiento de costés

python
# Approximate pricing (as of early 2025)
MODEL_PRICING = {
    "gpt-4-turbo": {"input": 10.00, "output": 30.00},     # per million tokens
    "gpt-4o": {"input": 2.50, "output": 10.00},
    "gpt-4o-mini": {"input": 0.15, "output": 0.60},
    "claude-3-5-sonnet": {"input": 3.00, "output": 15.00},
    "claude-3-5-haiku": {"input": 0.80, "output": 4.00},
}


@dataclass
class CostMetrics:
    """Track costs for an agent interaction."""

    model: str
    input_tokens: int = 0
    output_tokens: int = 0
    tool_calls: int = 0
    tool_cost: float = 0.0  # External API costs

    @property
    def llm_cost(self) -> float:
        """Compute LLM API cost in USD."""
        pricing = MODEL_PRICING.get(self.model, {"input": 0, "output": 0})
        input_cost = (self.input_tokens / 1_000_000) * pricing["input"]
        output_cost = (self.output_tokens / 1_000_000) * pricing["output"]
        return input_cost + output_cost

    @property
    def total_cost(self) -> float:
        return self.llm_cost + self.tool_cost

    def __repr__(self) -> str:
        return (
            f"CostMetrics(model={self.model}, "
            f"tokens={self.input_tokens}+{self.output_tokens}, "
            f"llm_cost=${self.llm_cost:.4f}, "
            f"total=${self.total_cost:.4f})"
        )


def cost_adjusted_score(
    task_score: float,
    cost: float,
    max_acceptable_cost: float = 1.0,
    cost_penalty_weight: float = 0.2,
) -> float:
    """Compute a cost-adjusted performance score.

    Penalizes agents that are expensive relative to their accuracy.

    Args:
        task_score: Raw task performance score (0-1).
        cost: Cost of the interaction in USD.
        max_acceptable_cost: Cost threshold above which penalty increases.
        cost_penalty_weight: How much to weight cost penalty (0-1).

    Returns:
        Adjusted score.
    """
    cost_ratio = min(cost / max_acceptable_cost, 2.0)  # Cap at 2x
    cost_penalty = cost_ratio * cost_penalty_weight
    return max(0.0, task_score - cost_penalty)

Análisis de latencia

python
@dataclass
class LatencyMetrics:
    """Track latency for an agent interaction."""

    time_to_first_token: float = 0.0     # Seconds until first output
    time_to_completion: float = 0.0       # Total interaction time
    llm_call_times: list[float] = field(default_factory=list)
    tool_call_times: list[float] = field(default_factory=list)

    @property
    def total_llm_time(self) -> float:
        return sum(self.llm_call_times)

    @property
    def total_tool_time(self) -> float:
        return sum(self.tool_call_times)

    @property
    def overhead_time(self) -> float:
        """Time spent on orchestration (not LLM or tools)."""
        return self.time_to_completion - self.total_llm_time - self.total_tool_time

    def summary(self) -> dict:
        return {
            "total_seconds": self.time_to_completion,
            "time_to_first_token": self.time_to_first_token,
            "llm_time": self.total_llm_time,
            "tool_time": self.total_tool_time,
            "overhead_time": self.overhead_time,
            "num_llm_calls": len(self.llm_call_times),
            "num_tool_calls": len(self.tool_call_times),
            "avg_llm_call_time": (
                self.total_llm_time / len(self.llm_call_times)
                if self.llm_call_times else 0
            ),
        }

La frontera de Pareto coste-calidad

Al comparar agentes, representar costé frente a calidad para encontrar la frontera de Pareto: el conjunto de agentes donde ningún otro agente es simultáneamente más barato y mejor. Este es uno de los instrumentos más potentes para tomar decisiones de despliegue.

El concepto está tomado de la economía. Imagina representar todos los agentes disponibles en un gráfico con el coste en el eje x y la calidad en el eje y. La frontera de Pareto es el "límite exterior" de puntos donde no se puede mejorar la calidad sin aumentar el coste, o reducir el coste sin sacrificar calidad. Los agentes en la frontera son las opciones racionales; los agentes por debajo de la frontera están dominados (existe otro agente que es mejor y más barato a la vez).

Pruébalo tú mismo: Imagina que tienes cinco agentes: A (calidad=0.95, coste=1.50),B(calidad=0.90,coste=0.80), B (calidad=0.90, coste=0.80), C (calidad=0.85, coste=0.40),D(calidad=0.80,coste=0.30), D (calidad=0.80, coste=0.30), E (calidad=0.70, coste=0.90$). ¿Qué agentes están en la frontera de Pareto? ¿Cuáles están dominados? (Pista: E está dominado tanto por C como por D.)

python
def find_pareto_frontier(
    agents: list[dict],
) -> list[dict]:
    """Find the Pareto-optimal agents (cost vs. quality).

    An agent is Pareto-optimal if no other agent is both cheaper
    and higher quality.
    """
    # Sort by cost (ascending)
    sorted_agents = sorted(agents, key=lambda a: a["cost"])

    pareto = []
    best_quality = -1

    for agent in sorted_agents:
        if agent["quality"] > best_quality:
            pareto.append(agent)
            best_quality = agent["quality"]

    return pareto

109. Frameworks de evaluación y mejores prácticas

Mejores prácticas para la evaluación de agentes

  1. Definir criterios de éxito claros antes de construir el agente. Los objetivos vagos conducen a evaluaciones vagas.

  2. Usar múltiples métodos de evaluación. Ninguna métrica única captura todos los aspectos de la calidad del agente. Combinar:

    • Métricas automatizadas (coincidencia exacta, pass@k, BLEU) para eficiencia.
    • LLM-as-judge para evaluación abierta.
    • Evaluación humana para validación final.
  3. Reportar varianza, no solo medias. Ejecutar las evaluaciones múltiples veces y reportar desviaciones estándar e intervalos de confianza.

  4. Evaluar con datos reservados (held-out). Asegurar que el agente no haya visto los casos de prueba durante el desarrollo.

  5. Probar casos extremos y entradas adversarias. Los agentes a menudo fallan con entradas inusuales, instrucciones contradictorias o intentos de manipulación.

  6. Rastrear costé y latencia junto con la precisión. Una mejora del 2% en precisión que triplica el costé puede no valer la pena.

  7. Versionar las evaluaciones. Registrar qué versión del agente se evaluó con qué versión del conjunto de pruebas.

  8. Incluir análisis de fallos. Comprender por qué un agente falla es más valioso que saber con qué frecuencia falla.

Anti-patrones de evaluación

  • Evaluar solo en la distribución de entrenamiento: Probar con entradas diversas y fuera de distribución.
  • Seleccionar ejemplos a conveniencia: Siempre reportar métricas agregadas, no éxitos seleccionados.
  • Ignorar el eje de coste: Un agente infinitamente caro no es útil.
  • Evaluar una sola vez: Los resultados de una única ejecución no son fiables para agentes estocásticos.
  • Confundir capacidad con fiabilidad: Un agente que puede resolver una tarea el 30% de las veces es diferente de uno que la resuelve el 95% de las veces.

1110. Ejemplo práctico: Construcción de un arnés de evaluación automatizada

Construyamos un arnés de evaluación completo que pueda evaluar un agente en múltiples dimensiones.

python
"""
A complete automated evaluation harness for AI agents.

This harness:
1. Loads test cases from a JSON file
2. Runs the agent on each test case
3. Applies multiple evaluation metrics
4. Generates a detailed report with breakdowns

Requirements:
    pip install sentence-transformers
"""

import json
import time
from dataclasses import dataclass, field
from datetime import datetime


# ── Evaluator Functions ────────────────────────────────────────────

def exact_match_evaluator(
    input: str, output: str, expected: str | None, **kwargs
) -> float:
    """Check if output exactly matches expected output.

    Case-insensitive, whitespace-normalized comparison.
    """
    if expected is None:
        return 0.0
    return 1.0 if output.strip().lower() == expected.strip().lower() else 0.0


def contains_evaluator(
    input: str, output: str, expected: str | None, **kwargs
) -> float:
    """Check if the expected answer is contained in the output."""
    if expected is None:
        return 0.0
    return 1.0 if expected.strip().lower() in output.strip().lower() else 0.0


def step_efficiency_evaluator(
    input: str, output: str, expected: str | None,
    trajectory: list[dict] | None = None, **kwargs
) -> float:
    """Evaluate how efficiently the agent reached its answer."""
    if not trajectory:
        return 0.5  # Unknown efficiency

    n_steps = len(trajectory)
    n_errors = sum(1 for step in trajectory if step.get("status") == "error")
    n_retries = sum(
        1 for i in range(1, len(trajectory))
        if trajectory[i].get("action") == trajectory[i-1].get("action")
    )

    step_penalty = max(0, n_steps - 5) * 0.05
    error_penalty = n_errors * 0.15
    retry_penalty = n_retries * 0.10

    score = max(0.0, 1.0 - step_penalty - error_penalty - retry_penalty)
    return score


# ── Evaluation Harness ─────────────────────────────────────────────

class AgentEvaluationHarness:
    """A comprehensive evaluation harness for AI agents."""

    def __init__(self, agent_fn):
        self.agent_fn = agent_fn
        self.evaluators = {
            "exact_match": exact_match_evaluator,
            "contains": contains_evaluator,
            "efficiency": step_efficiency_evaluator,
        }
        self.results = []

    def add_evaluator(self, name: str, evaluator_fn) -> None:
        """Register a custom evaluator."""
        self.evaluators[name] = evaluator_fn

    def run(
        self,
        test_cases: list[dict],
        num_runs: int = 1,
        verbose: bool = True,
    ) -> dict:
        """Run the evaluation harness."""
        if verbose:
            print(f"Evaluation started: {len(test_cases)} cases, {num_runs} run(s) each")
            print(f"Evaluators: {list(self.evaluators.keys())}")
            print("=" * 70)

        self.results = []

        for i, test_case in enumerate(test_cases):
            case_id = test_case.get("id", f"case_{i}")
            if verbose:
                print(f"\n[{i+1}/{len(test_cases)}] {case_id}: {test_case['input'][:60]}...")

            run_results = []
            for run in range(num_runs):
                start = time.time()
                try:
                    agent_result = self.agent_fn(test_case["input"])
                    output = agent_result if isinstance(agent_result, str) else agent_result.get("output", "")
                    trajectory = agent_result.get("trajectory", []) if isinstance(agent_result, dict) else []
                    error = None
                except Exception as e:
                    output = ""
                    trajectory = []
                    error = str(e)
                elapsed = time.time() - start

                scores = {}
                for eval_name, eval_fn in self.evaluators.items():
                    try:
                        score = eval_fn(
                            input=test_case["input"],
                            output=output,
                            expected=test_case.get("expected_output"),
                            trajectory=trajectory,
                        )
                        scores[eval_name] = score
                    except Exception as e:
                        scores[eval_name] = 0.0

                run_results.append({
                    "output": output,
                    "scores": scores,
                    "wall_time": elapsed,
                    "error": error,
                })

            # Aggregate across runs
            avg_scores = {}
            for eval_name in self.evaluators:
                run_scores = [r["scores"].get(eval_name, 0.0) for r in run_results]
                avg_scores[eval_name] = {
                    "mean": sum(run_scores) / len(run_scores),
                    "std": (sum((s - sum(run_scores)/len(run_scores))**2 for s in run_scores) / len(run_scores)) ** 0.5 if num_runs > 1 else 0.0,
                }

            case_result = {
                "test_case_id": case_id,
                "category": test_case.get("category", "general"),
                "difficulty": test_case.get("difficulty", "medium"),
                "scores": avg_scores,
                "num_runs": num_runs,
                "avg_wall_time": sum(r["wall_time"] for r in run_results) / num_runs,
                "error_rate": sum(1 for r in run_results if r["error"]) / num_runs,
                "sample_output": run_results[0]["output"][:300],
            }
            self.results.append(case_result)

            if verbose:
                score_str = " | ".join(
                    f"{k}: {v['mean']:.2f}" for k, v in avg_scores.items()
                )
                print(f"  Scores: {score_str} ({case_result['avg_wall_time']:.1f}s)")

        report = self._generate_report()

        if verbose:
            print("\n" + "=" * 70)
            print("EVALUATION SUMMARY")
            print("=" * 70)
            self._print_report(report)

        return report

    def _generate_report(self) -> dict:
        """Generate the evaluation report."""
        overall = {}
        for eval_name in self.evaluators:
            means = [r["scores"][eval_name]["mean"] for r in self.results]
            overall[eval_name] = {
                "mean": sum(means) / len(means),
                "std": (sum((m - sum(means)/len(means))**2 for m in means) / len(means)) ** 0.5,
                "min": min(means),
                "max": max(means),
            }

        by_category = {}
        for result in self.results:
            cat = result["category"]
            if cat not in by_category:
                by_category[cat] = []
            by_category[cat].append(result)

        category_scores = {}
        for cat, cat_results in by_category.items():
            category_scores[cat] = {}
            for eval_name in self.evaluators:
                means = [r["scores"][eval_name]["mean"] for r in cat_results]
                category_scores[cat][eval_name] = sum(means) / len(means)
            category_scores[cat]["count"] = len(cat_results)

        by_difficulty = {}
        for result in self.results:
            diff = result["difficulty"]
            if diff not in by_difficulty:
                by_difficulty[diff] = []
            by_difficulty[diff].append(result)

        difficulty_scores = {}
        for diff, diff_results in by_difficulty.items():
            difficulty_scores[diff] = {}
            for eval_name in self.evaluators:
                means = [r["scores"][eval_name]["mean"] for r in diff_results]
                difficulty_scores[diff][eval_name] = sum(means) / len(means)
            difficulty_scores[diff]["count"] = len(diff_results)

        times = [r["avg_wall_time"] for r in self.results]

        return {
            "timestamp": datetime.now().isoformat(),
            "total_cases": len(self.results),
            "overall_scores": overall,
            "by_category": category_scores,
            "by_difficulty": difficulty_scores,
            "timing": {
                "mean_seconds": sum(times) / len(times),
                "total_seconds": sum(times),
                "max_seconds": max(times),
            },
            "error_rate": sum(r["error_rate"] for r in self.results) / len(self.results),
        }

    def _print_report(self, report: dict) -> None:
        """Pretty-print the evaluation report."""
        print(f"\nTotal test cases: {report['total_cases']}")
        print(f"Error rate: {report['error_rate']:.1%}")
        print(f"Mean time per case: {report['timing']['mean_seconds']:.1f}s")

        print("\nOverall Scores:")
        for metric, stats in report["overall_scores"].items():
            print(f"  {metric}: {stats['mean']:.3f} (std: {stats['std']:.3f}, range: {stats['min']:.2f}-{stats['max']:.2f})")

        if report["by_category"]:
            print("\nBy Category:")
            for cat, scores in report["by_category"].items():
                count = scores.pop("count", "?")
                score_str = ", ".join(f"{k}: {v:.2f}" for k, v in scores.items())
                print(f"  {cat} (n={count}): {score_str}")

        if report["by_difficulty"]:
            print("\nBy Difficulty:")
            for diff, scores in report["by_difficulty"].items():
                count = scores.pop("count", "?")
                score_str = ", ".join(f"{k}: {v:.2f}" for k, v in scores.items())
                print(f"  {diff} (n={count}): {score_str}")

Ejemplo de uso

python
def main():
    """Demonstrate the evaluation harness."""

    # A simple mock agent for demonstration
    def mock_agent(input_text: str) -> dict:
        """A mock agent that returns predefined answers."""
        answers = {
            "capital of france": "The capital of France is Paris.",
            "2 + 2": "4",
            "sort": "def sort_list(lst): return sorted(lst)",
        }

        output = "I don't know."
        for key, answer in answers.items():
            if key in input_text.lower():
                output = answer
                break

        return {
            "output": output,
            "trajectory": [
                {"action": "think", "content": "Processing query..."},
                {"action": "respond", "content": output},
            ],
        }

    # Define test cases
    test_cases = [
        {
            "id": "geo_001",
            "input": "What is the capital of France?",
            "expected_output": "Paris",
            "category": "geography",
            "difficulty": "easy",
        },
        {
            "id": "math_001",
            "input": "What is 2 + 2?",
            "expected_output": "4",
            "category": "math",
            "difficulty": "easy",
        },
        {
            "id": "code_001",
            "input": "Write a Python function to sort a list.",
            "expected_output": "def sort_list(lst): return sorted(lst)",
            "category": "coding",
            "difficulty": "medium",
        },
        {
            "id": "geo_002",
            "input": "What is the capital of Bhutan?",
            "expected_output": "Thimphu",
            "category": "geography",
            "difficulty": "hard",
        },
        {
            "id": "reason_001",
            "input": "If all roses are flowers and some flowers fade quickly, can we conclude that some roses fade quickly?",
            "expected_output": "No",
            "category": "reasoning",
            "difficulty": "hard",
        },
    ]

    # Create and run harness
    harness = AgentEvaluationHarness(mock_agent)
    report = harness.run(test_cases, num_runs=1, verbose=True)

    # Save report
    print(f"\nFull report:")
    print(json.dumps(report, indent=2, default=str))


if __name__ == "__main__":
    main()

Salida esperada

El arnés produce informes como:

text
Evaluation started: 5 cases, 1 run(s) each
Evaluators: ['exact_match', 'contains', 'efficiency']
======================================================================

[1/5] geo_001: What is the capital of France?...
  Scores: exact_match: 0.00 | contains: 1.00 | efficiency: 1.00 (0.0s)

[2/5] math_001: What is 2 + 2?...
  Scores: exact_match: 1.00 | contains: 1.00 | efficiency: 1.00 (0.0s)

[3/5] code_001: Write a Python function to sort a list....
  Scores: exact_match: 1.00 | contains: 1.00 | efficiency: 1.00 (0.0s)

[4/5] geo_002: What is the capital of Bhutan?...
  Scores: exact_match: 0.00 | contains: 0.00 | efficiency: 1.00 (0.0s)

[5/5] reason_001: If all roses are flowers and some flowers fade quickly, ...
  Scores: exact_match: 0.00 | contains: 0.00 | efficiency: 1.00 (0.0s)

======================================================================
EVALUATION SUMMARY
======================================================================

Total test cases: 5
Error rate: 0.0%
Mean time per case: 0.0s

Overall Scores:
  exact_match: 0.400 (std: 0.490, range: 0.00-1.00)
  contains: 0.600 (std: 0.490, range: 0.00-1.00)
  efficiency: 1.000 (std: 0.000, range: 1.00-1.00)

By Category:
  geography (n=2): exact_match: 0.00, contains: 0.50
  math (n=1): exact_match: 1.00, contains: 1.00
  coding (n=1): exact_match: 1.00, contains: 1.00
  reasoning (n=1): exact_match: 0.00, contains: 0.00

By Difficulty:
  easy (n=2): exact_match: 0.50, contains: 1.00
  medium (n=1): exact_match: 1.00, contains: 1.00
  hard (n=2): exact_match: 0.00, contains: 0.00

12Preguntas de discusión

  1. La Ley de Goodhart en la evaluación de agentes: "Cuando una medida se convierte en un objetivo, deja de ser una buena medida." ¿Cómo se aplica esto a los benchmarks de agentes? ¿Pueden los agentes aprender a manipular benchmarks específicos? Pista: considera cómo los agentes de SWE-bench podrían aprender a enfocarse en pasar pruebas en lugar de realmente corregir el problema subyacente. ¿Cómo sería un issue de GitHub "verdaderamente resuelto" frente a uno que simplemente pasa la suite de pruebas?

  2. La brecha de evaluación: Los benchmarks actuales prueban capacidades específicas, pero el despliegue real de agentes implica interacciones impredecibles. ¿Cómo podemos evaluar agentes para la "larga cola" de escenarios del mundo real? Pista: considera pruebas de estrés con entradas adversarias, evaluación en tareas fuera de distribución y monitorización del rendimiento en producción a lo largo del tiempo en lugar de depender únicamente de benchmarks previos al despliegue.

  3. Evaluación de seguridad: ¿Cómo deberíamos evaluar si un agente es seguro? ¿Es suficiente la ausencia de comportamiento dañino en las pruebas, o necesitamos garantías más fuertes? Pista: considera la diferencia entre "probamos 1000 casos y ninguno fue dañino" y "podemos demostrar que el agente nunca tomará acciones dañinas." ¿Qué nivel de garantía es apropiado para diferentes niveles de riesgo?

  4. Jueces humanos vs. jueces LLM: ¿En qué circunstancias deberíamos preferir la evaluación humana sobre LLM-as-judge? ¿Cuándo es suficiente LLM-as-judge? Pista: considera el compromiso coste-precisión, la experiencia de dominio requerida y si la evaluación requiere comprender matices qué los LLM podrían pasar por alto (contexto cultural, tono emocional, preocupaciones de seguridad).

  5. Compromisos coste-calidad: ¿Es ético desplegar un agente más barato y menos preciso cuando existe uno más caro y más preciso? ¿Cómo deberíamos pensar sobre los compromisos coste-calidad en dominios de alto riesgo? Pista: considera la diferencia entre un chatbot de servicio al cliente (donde una tasa de error del 5% es molesta pero no dañina) y un asistente de diagnóstico médico (donde una tasa de error del 5% podría ser peligrosa).

  6. Saturación de benchmarks: ¿Qué debería hacer la comunidad cuando los agentes logran puntuaciones casi perfectas en los benchmarks existentes? ¿Cómo diseñamos benchmarks que sigan siendo desafiantes a medida que las capacidades mejoran? Pista: mira la historia de la IA en ajedrez y Go; una vez que los benchmarks fueron "resueltos", el enfoque se trasladó a dominios más complejos. ¿Cuál es la trayectoria equivalente para los benchmarks de agentes?


13Resumen y conclusiones clave

  1. La evaluación de agentes es fundamentalmente más difícil que la evaluación de modelos debido al no determinismo, los comportamientos de múltiples pasos, las salidas abiertas y la interacción con el entorno. Las métricas individuales son insuficientes.

  2. La evaluación basada en tareas mide lo que más importa (¿tuvo éxito el agente?) pero debe complementarse con métricas de calidad, rúbricas y evaluación de proceso para una comprensión más profunda.

  3. La evaluación basada en proceso revela cómo funciona el agente, no solo si funciona. La calidad del razonamiento, la eficiencia en el uso de herramientas y el análisis de trayectoria proporcionan perspectivas accionables para la mejora.

  4. Los benchmarks proporcionan comparación estandarizada pero cada uno tiene limitaciones. SWE-bench prueba codificación, GAIA prueba asistencia general, WebArena prueba interacción web. Usa benchmarks que coincidan con tu escenario de despliegue.

  5. LLM-as-judge permite evaluación escalable de salidas abiertas pero tiene sesgos conocidos. El intercambio de posiciones, el uso de diferentes modelos juez y la calibración con evaluación humana mitigan estos sesgos.

  6. La evaluación humana sigue siendo el estándar de oro para calidad subjetiva y seguridad. Los protocolos de anotación bien diseñados con medidas de fiabilidad inter-evaluador aseguran consistencia.

  7. El coste y la latencia son métricas de evaluación de primera clase para agentes en producción. La frontera de Pareto de coste vs. calidad guía las decisiones prácticas de despliegue.

  8. La evaluación debe ser automatizada, reproducible y exhaustiva. Un arnés de evaluación que combine múltiples métricas, rastree la varianza y genere informes detallados es infraestructura esencial para el desarrollo de agentes.


14Referencias

  1. Jiménez, C. E., Yang, J., Wettig, A., Yao, S., Pei, K., Press, O., & Narasimhan, K. (2024). SWE-bench: Can Language Models Resolve Real-World GitHub Issues? International Conference on Learning Representations (ICLR).

  2. Mialon, G., Dessi, R., Lomeli, M., Nalmpantis, C., Pasunuru, R., Raber, R., Roziere, B., Schick, T., Dwivedi-Yu, J., Celikyilmaz, A., Grave, E., LeCun, Y., & Scialom, T. (2023). GAIA: A Benchmark for General AI Assistants. arXiv préprint arXiv:2311.12983.

  3. Liu, X., Yu, H., Zhang, H., Xu, Y., Leí, X., Lai, H., Gu, Y., Ding, H., Men, K., Yang, K., Zhang, S., Deng, X., Zeng, A., Du, Z., Zhang, C., Shen, S., Zhang, T., Su, Y., Sun, H., Huang, M., Dong, Y., & Tang, J. (2024). AgentBench: Evaluating LLMs as Agents. International Conference on Learning Representations (ICLR).

  4. Zhou, S., Xu, F. F., Zhu, H., Zhou, X., Lo, R., Sridhar, A., Cheng, X., Ou, T., Bisk, Y., Fried, D., Alon, U., & Neubig, G. (2024). WebArena: A Realistic Web Environment for Building Autonomous Agents. International Conference on Learning Representations (ICLR).

  5. Chen, M., Tworek, J., Jun, H., Yuan, Q., Pinto, H. P. de O., Kaplan, J., Edwards, H., Burda, Y., Joseph, N., Brockman, G., Ray, A., Puri, R., Krueger, G., Petrov, M., Khlaaf, H., Sastry, G., Mishkin, P., Chan, B., Gray, S., Ryder, N., Pavlov, M., Power, A., Kaiser, L., Bavarian, M., Winter, C., Tíllet, P., Such, F. P., Cummings, D., Plappert, M., Chanez, F., Barnes, E., Herbert-Voss, A., Guss, W. H., Nichol, A., Paiño, A., Tezak, N., Tang, J., Babuschkin, I., Balaji, S., Jain, S., Saunders, W., Hesse, C., Carr, A. N., Leike, J., Achiam, J., Misra, V., Morikawa, E., Radford, A., Knight, M., Brundage, M., Murati, M., Mayer, K., Welinder, P., McGrew, B., Amodei, D., McCandlish, S., Sutskever, I., & Zaremba, W. (2021). Evaluating Large Language Models Trained on Code. arXiv preprint arXiv:2107.03374.

  6. Zheng, L., Chiang, W.-L., Sheng, Y., Zhuang, S., Wu, Z., Zhuang, Y., Lin, Z., Li, Z., Li, D., Xing, E. P., Zhang, H., González, J. E., & Stoica, I. (2023). Judging LLM-as-a-Judge with MT-Bench and Chatbot Arena. Advances in Neural Information Processing Systems (NeurIPS), 36.

  7. Es, S., James, J., Espinosa-Anke, L., & Schockaert, S. (2024). RAGAS: Automated Evaluation of Retrieval Augmented Generation. Proceedings of the 18th Conference of the European Chapter of the Association for Computational Linguistics (EACL): System Demonstrations.

  8. Shinn, N., Cassano, F., Gopinath, A., Narasimhan, K., & Yao, S. (2023). Reflexión: Language Agents with Verbal Reinforcement Learning. Advances in Neural Information Processing Systems (NeurIPS), 36.


Parte de "IA Agentica: Fundamentos, Arquitecturas y Aplicaciones" (CC BY-SA 4.0).