Empezar aquí
Índice de la sección
Lección 40.6 · práctica

Depurar un agente leyendo sus trazas

Registra cada paso de tu agente en una traza JSONL sencilla y aprende a leerla para encontrar herramientas mal elegidas, bucles, errores ignorados y contexto desbordado.

Publicado 45 min
Al terminar podrás
  • Definir un formato de traza con los campos necesarios para reconstruir una ejecución.
  • Implementar un registrador de trazas en el bucle agéntico sin cambiar su lógica.
  • Diagnosticar los fallos más comunes de un agente a partir de su traza.
  • Convertir un fallo encontrado en una traza en un caso de prueba.

Requiere: Un agente con varias herramientas

Al terminar esta lección cada ejecución de tu agente dejará una traza que puedes leer en dos minutos, y sabrás qué buscar en ella cuando algo sale mal. Con un agente, la respuesta final casi nunca explica el fallo: la explicación está en los pasos.

La idea

Un agente que falla rara vez lanza una excepción. Responde algo plausible pero incorrecto, tarda mucho más de lo normal o gasta muchos más tokens. Para saber por qué necesitas su trayectoria: qué vio en cada paso, qué decidió y qué le devolvieron las herramientas.

Una traza es el registro ordenado de esa trayectoria. No hace falta una plataforma de observabilidad para empezar: un archivo JSONL (un objeto JSON por línea) por ejecución basta para depurar en local, y es fácil de llevar después a la herramienta que uses en producción.

flowchart LR
  B[Bucle agéntico] --> T[Traza JSONL]
  T --> L[Leer y diagnosticar]
  L --> C[Caso de prueba]
  C --> F[Arreglo]
  F --> B

Qué guardar

Dos tipos de evento cubren casi todo:

CampoEn modeloEn herramienta
ejecucion, paso, tipo, tSíSí
stop_reasonSí
tokens_entrada, tokens_salidaSí, de usage
msDuración de la llamadaDuración de la herramienta
textoTexto del asistente (recortado)
nombre, entradaHerramienta y argumentos
salida, es_error, charsResultado recortado, si fue error y su tamaño real

Guarda también, al principio, un evento inicio con la tarea, el modelo y la versión de tu prompt y tus herramientas. Sin eso no podrás comparar trazas de antes y después de un cambio.

Un registrador mínimo

import json
import time
import uuid
from pathlib import Path

class Traza:
    def __init__(self, carpeta: str = "trazas", recorte: int = 500):
        self.id = uuid.uuid4().hex[:8]
        self.recorte = recorte
        Path(carpeta).mkdir(exist_ok=True)
        self.archivo = open(Path(carpeta) / f"{self.id}.jsonl", "a", encoding="utf-8")

    def evento(self, tipo: str, paso: int, **datos) -> None:
        linea = {"ejecucion": self.id, "paso": paso, "tipo": tipo, "t": round(time.time(), 3), **datos}
        self.archivo.write(json.dumps(linea, ensure_ascii=False, default=str) + "\n")
        self.archivo.flush()            # si el proceso muere, la traza llega hasta el último paso

    def corto(self, texto: str) -> str:
        return texto if len(texto) <= self.recorte else texto[: self.recorte] + f"… [+{len(texto) - self.recorte}]"

Y en el bucle, sin tocar su lógica, registras alrededor de cada llamada y cada herramienta:

def agente(pregunta: str, presupuesto: Presupuesto, traza: Traza) -> dict:
    traza.evento("inicio", 0, tarea=pregunta, modelo=MODEL, version_prompt="soporte-v3")
    messages = [{"role": "user", "content": pregunta}]
    paso = 0
    while (motivo := presupuesto.agotado()) is None:
        paso += 1
        t0 = time.perf_counter()
        resp = client.messages.create(model=MODEL, max_tokens=2048, system=SYSTEM,
                                      tools=TOOLS, messages=messages)
        presupuesto.registrar(resp.usage)
        traza.evento("modelo", paso, stop_reason=resp.stop_reason,
                     tokens_entrada=resp.usage.input_tokens, tokens_salida=resp.usage.output_tokens,
                     ms=round((time.perf_counter() - t0) * 1000),
                     texto=traza.corto("".join(b.text for b in resp.content if b.type == "text")))
        messages.append({"role": "assistant", "content": resp.content})
        if resp.stop_reason == "end_turn":
            traza.evento("fin", paso, estado="completo")
            texto = "".join(b.text for b in resp.content if b.type == "text")
            return {"estado": "completo", "respuesta": texto}
        if resp.stop_reason != "tool_use":
            traza.evento("fin", paso, estado="error", motivo=resp.stop_reason)
            return {"estado": "error", "motivo": resp.stop_reason}
        resultados = []
        for b in (b for b in resp.content if b.type == "tool_use"):
            t0 = time.perf_counter()
            r = ejecutar_con_control(b, presupuesto)
            traza.evento("herramienta", paso, nombre=b.name, entrada=b.input,
                         salida=traza.corto(str(r["content"])), es_error=r["is_error"],
                         chars=len(str(r["content"])), ms=round((time.perf_counter() - t0) * 1000))
            resultados.append(r)
        messages.append({"role": "user", "content": resultados})
    traza.evento("fin", paso, estado="incompleto", motivo=motivo)
    return {"estado": "incompleto", "motivo": motivo}

Para simplificar, aquí pause_turn se trata como error; si usas herramientas del servidor, añade su rama como en El bucle agéntico.

Qué buscar

Abre la traza y léela de arriba abajo con estas preguntas. Cada síntoma suele apuntar a una causa concreta:

Síntoma en la trazaCausa probableDónde arreglarlo
Primera herramienta equivocadaDescripciones ambiguas o solapadasDescripción de la herramienta
La misma llamada con los mismos argumentos varias vecesLa salida no le sirve o no entiende el errorSalida o mensaje de error de la herramienta
es_error: true y el modelo sigue como si nadaEl error no dice qué hacerTexto del error
tokens_entrada da un salto grande en un pasoUna herramienta devolvió demasiado (chars alto)Limitar y resumir la salida
stop_reason: max_tokensmax_tokens bajo o respuesta desmesuradaParámetro o instrucciones de longitud
Termina con end_turn en el paso 1 sin herramientasNo sabe que debe consultar datosPrompt de sistema o descripciones
Argumentos inventados (ids que nunca aparecieron)Falta un paso previo de búsquedaDescripción: «requiere el id de…»
Muchos pasos para una tarea sencillaHerramientas demasiado finasDiseño del conjunto de herramientas

La regla práctica: busca el primer paso donde la ejecución se tuerce. Lo que pasa después suele ser consecuencia.

Un ejemplo leído

# salida de ejemplo
{"paso": 0, "tipo": "inicio", "tarea": "Soy ana@example.com, me habéis cobrado dos veces el pedido"}
{"paso": 1, "tipo": "modelo", "stop_reason": "tool_use", "tokens_entrada": 1840, "tokens_salida": 61}
{"paso": 1, "tipo": "herramienta", "nombre": "ver_pedidos", "entrada": {"cliente_id": "ana@example.com"}, "es_error": true, "salida": "Error: cliente_id inválido"}
{"paso": 2, "tipo": "modelo", "stop_reason": "tool_use", "tokens_entrada": 1950, "tokens_salida": 58}
{"paso": 2, "tipo": "herramienta", "nombre": "ver_pedidos", "entrada": {"cliente_id": "ana"}, "es_error": true, "salida": "Error: cliente_id inválido"}
{"paso": 3, "tipo": "modelo", "stop_reason": "tool_use", "tokens_entrada": 2060, "tokens_salida": 55}
{"paso": 3, "tipo": "herramienta", "nombre": "buscar_ayuda", "entrada": {"consulta": "cobro duplicado"}, "es_error": false, "chars": 14210}
{"paso": 4, "tipo": "modelo", "stop_reason": "end_turn", "tokens_entrada": 5720, "tokens_salida": 240}

Lectura: el primer fallo está en el paso 1. El modelo pasó un email a ver_pedidos sin llamar antes a buscar_cliente, y el error «cliente_id inválido» no le dijo qué hacer, así que probó otra cosa al azar. Luego buscar_ayuda devolvió más de 14.000 caracteres, y eso explica el salto de tokens del paso 4. Dos arreglos: un error que diga «usa primero buscar_cliente» y un límite en la salida de buscar_ayuda. Exactamente los principios de Un agente con varias herramientas.

De la traza al caso de prueba

Cada fallo que encuentres en una traza es un caso de prueba gratis. Guarda la tarea, lo que esperabas («primera herramienta: buscar_cliente») y la versión que fallaba. Cuando lo arregles, el caso se queda en tu conjunto para que no vuelva a pasar. Así nacen las evals de trayectoria.

Errores frecuentes

  • Registrar solo la respuesta final.
  • Guardar salidas de herramientas completas: las trazas se vuelven ilegibles y caras de almacenar. Recorta y guarda el tamaño real aparte.
  • No registrar la versión del prompt y de las herramientas.
  • Leer la traza desde el final y arreglar el síntoma en lugar de la causa.

Ejercicio

EjercicioPistas

Un resumen rápido ayuda: un script que, por traza, imprima pasos, tokens totales, herramientas usadas en orden y número de errores. Con jq también puedes filtrar: jq -c 'select(.tipo=="herramienta")' trazas/*.jsonl.

Resumen

  • La respuesta final no explica por qué falló un agente; la trayectoria sí.
  • Un JSONL por ejecución con eventos de modelo y de herramienta basta para empezar.
  • Registra stop_reason, tokens, duración, argumentos, errores y tamaño de las salidas.
  • Busca el primer paso donde la ejecución se tuerce y usa la tabla de síntomas.
  • Cada fallo encontrado se convierte en un caso de prueba.

Para seguir