Evals — Módulo 5: evaluar el camino, no solo la respuesta
Quinto módulo del Curso de Evals: un agente puede acertar la respuesta por el camino equivocado, y ese agente falla mañana. Qué es una trayectoria, cómo se mide su eficiencia, cómo se detectan bucles y pasos redundantes, las tres formas de compararla contra una referencia y cómo atribuir el fallo al paso exacto donde se rompió.
☰ Índice del contenido
- La tesis del módulo
- Qué es una trayectoria
- Dos caminos al mismo resultado
- Las métricas de trayectoria
- 1. Éxito de tarea (task success)
- 2. Eficiencia del camino (path efficiency)
- 3. Tasa de bucles (loop rate)
- 4. Pasos redundantes
- 5. Costo por tarea
- Comparar contra una trayectoria de referencia
- Coincidencia exacta (exact match)
- Coincidencia como subsecuencia (in-order match)
- Coincidencia de conjunto (any-order match)
- Atribución del fallo al paso N
- Código: representar y puntuar una trayectoria
- Cómo se instrumenta esto en la práctica
- Errores comunes
- La frase para llevarse
- Qué viene
- Material relacionado
En el módulo 4 dejamos un juez calibrado: un LLM que puntúa la salida final con una rúbrica que correlaciona razonablemente con lo que diría un humano. Ese juez mira una cosa y una sola: el texto que el agente entregó al final. Y funciona bien mientras el sistema sea un chatbot que responde en un turno. Cuando el sistema es un agente —algo que razona, llama herramientas, observa lo que devuelven y vuelve a decidir— ese juez se queda ciego frente a la mitad del sistema. Este módulo abre esa mitad.
La tesis del módulo
Un agente que llega a la respuesta correcta por el camino equivocado no es un agente que funciona: es un agente que todavía no falla. La respuesta final es el resultado de una lotería de N pasos; evaluar solo el resultado es evaluar la lotería, no el sistema.
Qué es una trayectoria
Una trayectoria (trajectory) es la secuencia completa de pasos que el agente ejecutó desde que recibió la tarea hasta que la dio por terminada. En el patrón ReAct que vimos en el módulo 5 de LangGraph, cada paso es una vuelta del ciclo:
- Razonar (thought): el modelo decide qué hacer a continuación.
- Actuar (action): llama a una herramienta con ciertos argumentos.
- Observar (observation): recibe el resultado de esa herramienta.
- Vuelve al paso 1, hasta que decide que ya puede responder.
La trayectoria es el registro de esas vueltas. Si el agente resolvió “¿cuánto vendimos en marzo comparado con febrero?” llamando primero a consultar_ventas(mes="2026-03"), después a consultar_ventas(mes="2026-02") y finalmente respondiendo, su trayectoria tiene dos pasos de herramienta y una respuesta.
En términos prácticos, una trayectoria es lo que ya está en tu sistema de trazas —LangSmith, Phoenix o el stream() de LangGraph— pero convertido en un objeto que se puede puntuar. La diferencia entre observabilidad y evaluación de trayectorias es exactamente esa: la observabilidad te deja mirar un caso; la evaluación te deja puntuar los doscientos y comparar la corrida de hoy con la de ayer.
Dos caminos al mismo resultado
Este diagrama es el módulo entero en una imagen. Las dos trayectorias responden bien la misma pregunta. Solo una de las dos es un sistema que puedes poner en producción.
TRAYECTORIA SANA (2 tools + respuesta)
START ──► ┌──────────────────────────┐
│ 1 buscar_cliente(rut) │
└────────────┬─────────────┘
▼
┌──────────────────────────┐
│ 2 ver_saldo(id=8812) │
└────────────┬─────────────┘
▼
┌──────────────────────────┐
│ 3 responder("$142.900") │ ──► END
└──────────────────────────┘
TRAYECTORIA PATOLÓGICA (6 tools + respuesta)
START ──► ┌──────────────────────────┐
│ 1 ver_saldo(id=null) │
└────────────┬─────────────┘
▼ error: id requerido
┌──────────────────────────┐
│ 2 ver_saldo(id=null) │ ◄──┐
└────────────┬─────────────┘ │
▼ mismo error │ bucle
┌──────────────────────────┐ │ x3
│ 3 ver_saldo(id=null) │ ───┘
└────────────┬─────────────┘
▼
┌──────────────────────────┐
│ 4 listar_clientes() │ 40k tokens
└────────────┬─────────────┘
▼
┌──────────────────────────┐
│ 5 buscar_cliente(rut) │
└────────────┬─────────────┘
▼
┌──────────────────────────┐
│ 6 ver_saldo(id=8812) │
└────────────┬─────────────┘
▼
┌──────────────────────────┐
│ 7 responder("$142.900") │ ──► END
└──────────────────────────┘
Mismo output. Un juez de salida final les pone 5/5 a ambas.
La segunda trayectoria tiene tres problemas que el juez de salida final no ve, y los tres son bugs reales:
- El bucle: repitió tres veces la misma llamada con el mismo argumento inválido. Con un
max_iterationsun poco más generoso, ese bucle es infinito y el caso se cae por timeout. - El paso caro:
listar_clientes()sin filtro trajo cuarenta mil tokens al contexto. Ese caso cuesta veinte veces lo que debería y empuja al agente hacia la ventana de contexto. - La fragilidad: llegó al resultado correcto porque
buscar_clienteestaba disponible y el modelo se acordó tarde. Mañana, con el contexto un poco más lleno o con el proveedor actualizando el modelo, no se acuerda.
Ese último punto es la razón profunda para evaluar trayectorias: la trayectoria patológica no es un caso que pasó, es un caso que va a fallar y que todavía no falló. Medirla es la única forma de arreglarlo antes.
Las métricas de trayectoria
Cinco métricas cubren casi todo lo que necesitas. Todas son deterministas —se calculan con aritmética sobre la traza, sin llamar a ningún LLM— y por lo tanto son baratas y corren en cada pull request, igual que las métricas de retrieval del módulo de evaluación de RAG.
1. Éxito de tarea (task success)
Éxito de tarea = fracción de casos donde el estado final cumple el criterio de éxito definido en el módulo 2.
Es la métrica principal y sigue siendo binaria. Todo lo demás en este módulo la condiciona, pero no la reemplaza: un agente eficientísimo que no resuelve la tarea vale cero.
2. Eficiencia del camino (path efficiency)
Eficiencia = pasos mínimos / pasos ejecutados, acotada a 1.0.
Si la tarea se resuelve en 2 llamadas a herramientas y el agente usó 6, la eficiencia es 2/6 = 0.33. Si usó 2, es 1.0. Los “pasos mínimos” salen de tu trayectoria de referencia, que escribiste tú a mano cuando armaste el caso.
Es la métrica que más rápido detecta una degradación silenciosa: un cambio de prompt que sube el éxito de 0.88 a 0.89 pero baja la eficiencia de 0.81 a 0.52 no es una mejora, es un agente que aprendió a compensar a fuerza de intentos.
3. Tasa de bucles (loop rate)
Tasa de bucles = fracción de casos donde la misma llamada (nombre + argumentos normalizados) se repite N o más veces sin que cambie la observación.
La condición “sin que cambie la observación” es la que separa el bucle real del reintento legítimo: llamar dos veces a consultar_estado(pedido=77) para ver si ya cambió es normal; llamarla tres veces recibiendo el mismo error no lo es. Con N=3 se pesca casi todo sin falsos positivos.
4. Pasos redundantes
Pasos redundantes = llamadas cuya observación no aportó información nueva que el agente usara después.
En la práctica se aproxima con dos reglas baratas: llamadas duplicadas exactas, y llamadas cuyo resultado nunca aparece citado ni referenciado en los pasos posteriores. Es la métrica que más se ensucia; úsala como señal de investigación, no como umbral que bloquea.
5. Costo por tarea
Costo por tarea = tokens de entrada × precio_in + tokens de salida × precio_out, sumado sobre todos los pasos.
Es la métrica que convence al que paga la cuenta, y la que hace visible el listar_clientes() del ejemplo. Repórtala siempre junto al éxito: un agente con 0.94 de éxito a $0.08 por tarea y otro con 0.91 a $0.006 son decisiones de negocio distintas, no un ganador y un perdedor.
| Métrica | Determinista | Qué diagnostica | Umbral inicial razonable |
|---|---|---|---|
| Éxito de tarea | Depende del criterio | Si el sistema sirve | Lo que hayas definido en el módulo 2 |
| Eficiencia del camino | Sí | Prompt vago, tools mal descritas | > 0.70 |
| Tasa de bucles | Sí | Manejo de errores inexistente | < 0.02 |
| Pasos redundantes | Sí (aproximada) | Contexto que el agente no lee | Señal, no umbral |
| Costo por tarea | Sí | Tools que devuelven demasiado | Presupuesto del proyecto |
Ojo: la eficiencia del camino no se optimiza sola, se optimiza contra el éxito. Un agente al que castigas por dar pasos aprende a rendirse antes. Siempre se reportan juntas y siempre se lee la eficiencia condicionada a los casos exitosos: la eficiencia de una trayectoria que falló no significa nada.
Comparar contra una trayectoria de referencia
Para las métricas anteriores basta con la traza. Pero muchas veces quieres algo más fuerte: ¿siguió el camino que yo esperaba? Ahí necesitas una trayectoria de referencia (reference trajectory), que es la secuencia de llamadas que un experto escribió como camino correcto para ese caso.
La pregunta operativa es cómo se comparan dos secuencias. Hay tres formas, y elegir mal es la causa más común de evals de trayectoria que nadie mira porque “siempre salen rojos”.
Coincidencia exacta (exact match)
La trayectoria observada tiene que ser idéntica a la de referencia: mismas herramientas, mismo orden, sin nada de más ni de menos.
referencia : [buscar_cliente, ver_saldo]
observada : [buscar_cliente, ver_saldo] ✓ pasa
observada : [buscar_cliente, ver_perfil, ver_saldo] ✗ falla
Es la más estricta y casi siempre es demasiado estricta. Un agente que consultó un dato extra antes de responder no hizo nada malo; con exact match, ese caso es rojo. El resultado típico es una suite con 40% de aprobación que el equipo aprende a ignorar, que es peor que no tener suite.
Coincidencia como subsecuencia (in-order match)
La referencia tiene que aparecer en orden dentro de la observada, pero se permiten pasos extra intercalados.
referencia : [buscar_cliente, ver_saldo]
observada : [buscar_cliente, ver_perfil, ver_saldo] ✓ pasa
observada : [ver_saldo, buscar_cliente] ✗ falla
Es el modo por defecto para la gran mayoría de los casos. Captura lo único que de verdad importa cuando hay dependencias —“tienes que buscar al cliente antes de pedir su saldo”— y no castiga la exploración razonable. Los pasos extra no se ignoran: se contabilizan en la eficiencia del camino, que es donde corresponde.
Coincidencia de conjunto (any-order match)
Solo importa que las herramientas de la referencia hayan sido llamadas, sin importar el orden ni lo que haya en medio.
referencia : {consultar_clima, consultar_vuelos}
observada : [consultar_vuelos, consultar_clima] ✓ pasa
observada : [consultar_clima] ✗ falla
Es la correcta cuando los pasos son genuinamente independientes: consultar el clima y consultar los vuelos no tienen relación de dependencia, y exigir un orden es inventar un requisito que no existe.
| Modo | Permite pasos extra | Exige orden | Cuándo usarlo | Riesgo |
|---|---|---|---|---|
| Exacto | No | Sí | Flujos regulados con secuencia auditada (KYC, pagos) | Falsos rojos; la suite se abandona |
| Subsecuencia | Sí | Sí | Por defecto. Hay dependencias reales entre pasos | Deja pasar caminos ineficientes (mídelos aparte) |
| Conjunto | Sí | No | Pasos independientes, consultas paralelas | No detecta inversiones de orden peligrosas |
Cuidado: el modo de comparación se define por caso, no por suite. En un mismo golden dataset conviven un caso de reembolso que exige orden exacto (verificar identidad → verificar compra → emitir nota de crédito) y un caso de consulta que no exige nada. Un campo
modoen cada caso del dataset es la diferencia entre un eval que la gente respeta y uno que la gente silencia.
Atribución del fallo al paso N
Saber que un caso falló no arregla nada. Lo que arregla es saber en qué paso se rompió, y eso no se deduce de la respuesta final: se instrumenta.
La regla es que cada paso de la trayectoria guarde lo suficiente para reconstruirlo sin volver a ejecutar nada:
ANATOMÍA DE UN PASO INSTRUMENTADO
┌──────────────────────────────────────────────┐
│ indice 3 │
│ herramienta ver_saldo │
│ argumentos {"id": null} │
│ observacion ERROR: id es obligatorio │
│ ok false │
│ tokens_in 1842 │
│ tokens_out 31 │
│ latencia_ms 410 │
└──────────────────────────────────────────────┘
primer paso con ok=false ──► paso culpable = 3
todo lo posterior ──► consecuencia, no causa
Con eso, la atribución es una regla simple y determinista: el paso culpable es el primer paso donde ok es falso o donde la trayectoria se desvía de la referencia. Todo lo que viene después es consecuencia. Es exactamente el mismo principio que “el primer error del compilador es el único que hay que leer”.
El valor de la atribución no es por caso: es agregado. Cuando cruzas cien casos fallidos por paso culpable y por herramienta, aparece la tabla que decide tu semana:
| Paso culpable | Herramienta | Casos | Causa dominante |
|---|---|---|---|
| 1 | buscar_cliente | 3 | El usuario dio el RUT con puntos |
| 2 | ver_saldo | 31 | Argumento id nulo: el agente no encadena |
| 4 | emitir_nota | 6 | Timeout del servicio externo |
Treinta y uno de cuarenta fallos en un solo paso, por una sola causa. Ese es el arreglo de la semana, y sin atribución habrías leído cuarenta trazas a mano para descubrirlo.
Código: representar y puntuar una trayectoria
Nada de esto necesita un framework. Son dataclasses y aritmética.
"""Evaluación de trayectorias de agentes: representación y puntuación.
Sin dependencias externas. La idea es que puedas pegar esto en tu
repo y adaptarlo al formato de traza que ya emite tu agente.
"""
from __future__ import annotations
import json
from collections import Counter
from dataclasses import dataclass, field
from statistics import mean
@dataclass
class Paso:
"""Un paso de la trayectoria: una llamada a herramienta y su resultado."""
herramienta: str
argumentos: dict
observacion: str = ""
ok: bool = True
tokens_in: int = 0
tokens_out: int = 0
latencia_ms: int = 0
def firma(self) -> str:
"""Identidad normalizada del paso: nombre + argumentos ordenados.
Sirve para detectar repeticiones. `sort_keys` evita que
{"a":1,"b":2} y {"b":2,"a":1} cuenten como llamadas distintas.
"""
return f"{self.herramienta}({json.dumps(self.argumentos, sort_keys=True)})"
@dataclass
class Trayectoria:
pasos: list[Paso] = field(default_factory=list)
respuesta_final: str = ""
exito: bool = False # lo fija el criterio de éxito del caso
@property
def nombres(self) -> list[str]:
return [p.herramienta for p in self.pasos]
def costo_usd(self, precio_in: float, precio_out: float) -> float:
"""Precios en USD por 1M de tokens (como los publican los proveedores)."""
t_in = sum(p.tokens_in for p in self.pasos)
t_out = sum(p.tokens_out for p in self.pasos)
return (t_in * precio_in + t_out * precio_out) / 1_000_000
# --- Comparación contra una trayectoria de referencia -------------------
def coincide_exacto(obs: list[str], ref: list[str]) -> bool:
return obs == ref
def coincide_subsecuencia(obs: list[str], ref: list[str]) -> bool:
"""¿Aparece `ref` en orden dentro de `obs`, permitiendo pasos extra?
Recorre `obs` una sola vez avanzando un puntero sobre `ref`.
O(n). Si el puntero llega al final, la referencia está contenida.
"""
i = 0
for nombre in obs:
if i < len(ref) and nombre == ref[i]:
i += 1
return i == len(ref)
def coincide_conjunto(obs: list[str], ref: list[str]) -> bool:
"""Ignora orden y repeticiones: solo exige que se hayan llamado todas."""
return set(ref).issubset(set(obs))
COMPARADORES = {
"exacto": coincide_exacto,
"subsecuencia": coincide_subsecuencia,
"conjunto": coincide_conjunto,
}
# --- Métricas de trayectoria -------------------------------------------
def detectar_bucle(t: Trayectoria, n: int = 3) -> bool:
"""Bucle = misma firma repetida n+ veces CON la misma observación.
La condición sobre la observación es la que distingue el bucle
patológico del polling legítimo (consultar un estado que cambia).
"""
conteo = Counter((p.firma(), p.observacion) for p in t.pasos)
return any(c >= n for c in conteo.values())
def pasos_redundantes(t: Trayectoria) -> int:
"""Llamadas duplicadas exactas: la primera cuenta, las demás sobran."""
vistas: set[str] = set()
redundantes = 0
for p in t.pasos:
if p.firma() in vistas:
redundantes += 1
vistas.add(p.firma())
return redundantes
def eficiencia(t: Trayectoria, pasos_minimos: int) -> float:
"""pasos_minimos / pasos_ejecutados, acotado a 1.0.
Se acota porque una trayectoria más corta que la referencia no es
'mejor que perfecta': o el caso se resolvió por otro camino válido
o el criterio de éxito está mal definido.
"""
if not t.pasos:
return 0.0
return min(1.0, pasos_minimos / len(t.pasos))
def paso_culpable(t: Trayectoria, ref: list[str] | None = None) -> int | None:
"""Índice (1-indexado) del primer paso que se rompió o se desvió.
Prioridad: (1) primer paso con ok=False; (2) si hay referencia y no
coincide como subsecuencia, la primera posición donde se desvía.
Devuelve None si la trayectoria es sana.
"""
for i, p in enumerate(t.pasos, start=1):
if not p.ok:
return i
if ref and not coincide_subsecuencia(t.nombres, ref):
# Primer paso que no pertenece a la referencia; si todos
# pertenecen (faltó uno), el culpable es el último ejecutado.
for i, nombre in enumerate(t.nombres, start=1):
if nombre not in ref:
return i
return len(t.pasos) or None
return None
def evaluar_caso(t: Trayectoria, caso: dict) -> dict:
"""Puntúa una trayectoria contra la definición de un caso del golden set.
El caso trae: trayectoria_referencia, modo de comparación y
pasos_minimos (por defecto, el largo de la referencia).
"""
ref = caso["trayectoria_referencia"]
modo = caso.get("modo", "subsecuencia")
minimos = caso.get("pasos_minimos", len(ref))
comparar = COMPARADORES[modo]
return {
"id": caso["id"],
"exito": t.exito,
"camino_ok": comparar(t.nombres, ref),
"eficiencia": round(eficiencia(t, minimos), 3),
"bucle": detectar_bucle(t),
"redundantes": pasos_redundantes(t),
"pasos": len(t.pasos),
"costo_usd": round(t.costo_usd(precio_in=3.0, precio_out=15.0), 5),
"paso_culpable": paso_culpable(t, ref),
}
def agregar(resultados: list[dict]) -> dict:
"""Agregado de la corrida. Ojo: la eficiencia se promedia SOLO sobre
los casos exitosos —la eficiencia de un caso fallido no significa nada."""
exitosos = [r for r in resultados if r["exito"]]
return {
"n": len(resultados),
"exito_tarea": round(mean(r["exito"] for r in resultados), 3),
"camino_ok": round(mean(r["camino_ok"] for r in resultados), 3),
"eficiencia_media": (
round(mean(r["eficiencia"] for r in exitosos), 3) if exitosos else 0.0
),
"tasa_bucles": round(mean(r["bucle"] for r in resultados), 3),
"costo_medio_usd": round(mean(r["costo_usd"] for r in resultados), 5),
"fallos_por_paso": dict(
Counter(r["paso_culpable"] for r in resultados if r["paso_culpable"])
),
}
Y así se ve corriéndolo sobre las dos trayectorias del diagrama:
if __name__ == "__main__":
caso = {
"id": "saldo_001",
"trayectoria_referencia": ["buscar_cliente", "ver_saldo"],
"modo": "subsecuencia",
"pasos_minimos": 2,
}
sana = Trayectoria(
pasos=[
Paso("buscar_cliente", {"rut": "12345678-9"}, "id=8812",
tokens_in=900, tokens_out=40),
Paso("ver_saldo", {"id": 8812}, "142900",
tokens_in=1000, tokens_out=30),
],
respuesta_final="El saldo es $142.900",
exito=True,
)
patologica = Trayectoria(
pasos=[
*[Paso("ver_saldo", {"id": None}, "ERROR: id es obligatorio",
ok=False, tokens_in=1800, tokens_out=30)] * 3,
Paso("listar_clientes", {}, "[... 40k tokens ...]",
tokens_in=42000, tokens_out=25),
Paso("buscar_cliente", {"rut": "12345678-9"}, "id=8812",
tokens_in=44000, tokens_out=40),
Paso("ver_saldo", {"id": 8812}, "142900",
tokens_in=45000, tokens_out=30),
],
respuesta_final="El saldo es $142.900",
exito=True,
)
for t in (sana, patologica):
print(evaluar_caso(t, caso))
# {'id': 'saldo_001', 'exito': True, 'camino_ok': True, 'eficiencia': 1.0,
# 'bucle': False, 'redundantes': 0, 'pasos': 2, 'costo_usd': 0.00675,
# 'paso_culpable': None}
#
# {'id': 'saldo_001', 'exito': True, 'camino_ok': True, 'eficiencia': 0.333,
# 'bucle': True, 'redundantes': 2, 'pasos': 6, 'costo_usd': 0.41197,
# 'paso_culpable': 1}
Léelo con calma, porque es el punto del módulo. Las dos trayectorias tienen exito: True y camino_ok: True —el modo subsecuencia acepta los pasos extra—. Un eval de salida final las declara idénticas. Pero la segunda tiene eficiencia 0.333 en vez de 1.0, un bucle detectado, dos pasos redundantes y un costo de $0.412 contra $0.0068: 61 veces más cara por la misma respuesta. Ese factor 61 es invisible sin evaluar el camino.
Ojo: los precios del ejemplo (3.0 y 15.0 USD por millón de tokens de entrada y salida) son solo placeholders para que la aritmética se pueda seguir. Pon los de tu proveedor y tu modelo, y ponlos en una constante versionada: cuando cambien, quieres que tu histórico de costos siga siendo comparable o al menos que se note el corte.
Cómo se instrumenta esto en la práctica
No hace falta reescribir el agente. Hay tres puntos de enganche, de menos a más invasivo:
| Enganche | Cómo | Cuándo conviene |
|---|---|---|
| Callbacks del framework | stream(mode="updates") en LangGraph, callbacks en LangChain | Ya usas un framework; es lo más barato |
| Trazas OpenTelemetry | Cada tool es un span; la trayectoria es el árbol de spans | Ya tienes observabilidad montada en producción |
| Wrapper de tools | Un decorador que registra nombre, args, resultado y tokens | Agente artesanal, sin framework |
El wrapper es el que menos gente conoce y el que más rinde cuando el agente es propio:
"""Wrapper mínimo para capturar la trayectoria sin tocar la lógica."""
import functools
import time
def instrumentar(registro: list[Paso]):
"""Decorador de fábrica: envuelve una tool y anota cada llamada.
`registro` es la lista donde se van acumulando los pasos de ESTA
ejecución. En concurrencia usa un contextvar en vez de una lista suelta.
"""
def decorador(fn):
@functools.wraps(fn) # conserva __name__ y __doc__: el LLM lee la docstring
def envuelta(**kwargs):
t0 = time.perf_counter()
try:
resultado = fn(**kwargs)
ok = True
except Exception as e: # el error TAMBIÉN es un paso
resultado, ok = f"ERROR: {e}", False
registro.append(Paso(
herramienta=fn.__name__,
argumentos=kwargs,
observacion=str(resultado)[:500], # trunca: no guardes 40k
ok=ok,
latencia_ms=int((time.perf_counter() - t0) * 1000),
))
return resultado
return envuelta
return decorador
Dos detalles que se pagan solos: functools.wraps conserva la docstring, que es literalmente lo que el modelo lee para decidir si usa la tool (volveremos sobre esto en el módulo 6), y capturar la excepción como paso con ok=False en vez de dejarla propagar es lo que hace posible medir la recuperación tras un error.
Errores comunes
- Evaluar solo la salida final en un sistema agéntico. Es el error madre del módulo. La respuesta correcta por el camino equivocado es una bomba de tiempo: aprobó hoy y va a reprobar cuando cambie el contexto, el modelo o el orden de las tools.
- Usar coincidencia exacta por defecto. Genera una suite con 40% de aprobación donde nadie distingue el rojo real del rojo de siempre. Subsecuencia por defecto, exacto solo donde la secuencia sea un requisito auditado.
- Escribir la trayectoria de referencia mirando lo que hizo el agente. Es el equivalente a escribir el examen después de ver las respuestas. La referencia la define quien sabe cuál es el camino correcto, antes de correr nada.
- Promediar la eficiencia sobre todos los casos. Un caso que falló en el paso 1 tiene eficiencia altísima y contamina el promedio hacia arriba. La eficiencia se lee condicionada al éxito, siempre.
- Optimizar el número de pasos como si fuera el objetivo. Presiona a un agente por dar menos pasos y aprenderá a responder sin verificar. Los pasos son un costo, no un mal; el objetivo es el éxito al costo mínimo, no el costo mínimo.
- No guardar los argumentos de cada llamada. Sin argumentos no hay firma, sin firma no hay detección de bucles ni de redundancia, y la traza se vuelve una lista de nombres de función que no diagnostica nada. Guardar los argumentos (anonimizados si traen datos personales) es el requisito mínimo.
- Guardar la observación completa. Cuarenta mil tokens por paso × doscientos casos × cada corrida es una factura de almacenamiento y un JSON que no abre. Trunca a unos cientos de caracteres y guarda aparte el hash de lo completo si necesitas auditoría.
La frase para llevarse
“La respuesta final te dice si el agente acertó hoy. La trayectoria te dice si va a acertar mañana.”
Qué viene
Módulo 6: evaluar el uso de herramientas, que es donde los agentes se rompen de verdad. Vamos a desarmar los cinco fallos que hay que medir por separado —elegir la tool equivocada, pasarle argumentos mal tipados, inventarse tools que no existen, no saber qué hacer cuando la tool devuelve un error, y romper el orden de las dependencias— cada uno con su métrica propia. Y vamos a montar un harness con las tools mockeadas que inyecta errores a propósito para medir la recuperación, porque un agente que nunca vio fallar una herramienta no es un agente probado. Vas a descubrir, además, que buena parte de los fallos de selección no se arreglan cambiando de modelo: se arreglan reescribiendo la descripción de la tool.
Material relacionado
Preguntas sobre este curso
Regístrate gratis para hacer preguntas y guardar tu progreso.
✉ Entérate de lo nuevo
Recibe un aviso cuando publique nuevos cursos, workshops o investigaciones. Sin spam.