Índice de la sección
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.
Roadmap- 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:
| Campo | En modelo | En herramienta |
|---|---|---|
ejecucion, paso, tipo, t | Sí | Sí |
stop_reason | Sí | |
tokens_entrada, tokens_salida | Sí, de usage | |
ms | Duración de la llamada | Duración de la herramienta |
texto | Texto del asistente (recortado) | |
nombre, entrada | Herramienta y argumentos | |
salida, es_error, chars | Resultado 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 traza | Causa probable | Dónde arreglarlo |
|---|---|---|
| Primera herramienta equivocada | Descripciones ambiguas o solapadas | Descripción de la herramienta |
| La misma llamada con los mismos argumentos varias veces | La salida no le sirve o no entiende el error | Salida o mensaje de error de la herramienta |
es_error: true y el modelo sigue como si nada | El error no dice qué hacer | Texto del error |
tokens_entrada da un salto grande en un paso | Una herramienta devolvió demasiado (chars alto) | Limitar y resumir la salida |
stop_reason: max_tokens | max_tokens bajo o respuesta desmesurada | Parámetro o instrucciones de longitud |
Termina con end_turn en el paso 1 sin herramientas | No sabe que debe consultar datos | Prompt de sistema o descripciones |
| Argumentos inventados (ids que nunca aparecieron) | Falta un paso previo de búsqueda | Descripción: «requiere el id de…» |
| Muchos pasos para una tarea sencilla | Herramientas demasiado finas | Diseñ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.