Problema (por qué estás aquí)
En dev, tu agente “funciona”.
En producción, una vez cada ~200 runs:
- un usuario dice “envió lo equivocado”
- el coste se dispara 15 minutos
- entra en loop con una API flaky hasta timeout
Y tú tienes… casi nada:
- una “respuesta final”
- dos logs sueltos
- quizá un error del tool sin contexto
Eso te deja con el peor debugging: adivinar con una tarjeta conectada.
Esta página es logging que vuelve los incidentes aburridos (en el buen sentido).
Por qué esto falla en producción
Los agentes fallan como sistemas distribuidos porque lo son:
- el modelo es un planner poco fiable
- los tools son side effects (HTTP/DB/tickets/email)
- retries + timeouts generan comportamientos emergentes
Si no logueas el loop, no puedes responder:
- ¿qué tool calls ocurrieron y en qué orden?
- ¿con qué args (o al menos args_hash)?
- ¿qué devolvió el tool (o qué redaccionamos)?
- ¿por qué terminó el run (
stop_reason)? - ¿qué request/user lo disparó?
Si no logueas stop_reason, no estás observando. Estás coleccionando vibes.
Diagrama: pipeline mínimo de eventos
Código real: instrumenta el tool gateway (Python + JS)
Empieza por la frontera. Los tools son donde vive el gasto y el daño.
Logueamos:
run_id,trace_id,tool_nameargs_hash(no args en claro por defecto)- latencia + estado
error_class(normalizada)
Y lo forzamos vía gateway para que no exista el “se me olvidó loguear”.
import hashlib
import json
import time
from dataclasses import dataclass
from typing import Any, Callable, Dict, Optional
def stable_hash(obj: Any) -> str:
raw = json.dumps(obj, sort_keys=True, ensure_ascii=False).encode("utf-8")
return hashlib.sha256(raw).hexdigest()
@dataclass(frozen=True)
class RunCtx:
run_id: str
trace_id: str
user_id: Optional[str] = None
request_id: Optional[str] = None
class Logger:
def event(self, name: str, fields: Dict[str, Any]) -> None: ...
class ToolGateway:
def __init__(self, *, impls: dict[str, Callable[..., Any]], logger: Logger):
self.impls = impls
self.logger = logger
def call(self, ctx: RunCtx, name: str, args: Dict[str, Any]) -> Any:
fn = self.impls.get(name)
if not fn:
self.logger.event("tool_call", {
"run_id": ctx.run_id,
"trace_id": ctx.trace_id,
"tool": name,
"args_hash": stable_hash(args),
"ok": False,
"error_class": "unknown_tool",
})
raise RuntimeError(f"unknown tool: {name}")
t0 = time.time()
self.logger.event("tool_call", {
"run_id": ctx.run_id,
"trace_id": ctx.trace_id,
"tool": name,
"args_hash": stable_hash(args),
})
try:
out = fn(**args)
self.logger.event("tool_result", {
"run_id": ctx.run_id,
"trace_id": ctx.trace_id,
"tool": name,
"latency_ms": int((time.time() - t0) * 1000),
"ok": True,
})
return out
except TimeoutError:
self.logger.event("tool_result", {
"run_id": ctx.run_id,
"trace_id": ctx.trace_id,
"tool": name,
"latency_ms": int((time.time() - t0) * 1000),
"ok": False,
"error_class": "timeout",
})
raise
except Exception as e:
self.logger.event("tool_result", {
"run_id": ctx.run_id,
"trace_id": ctx.trace_id,
"tool": name,
"latency_ms": int((time.time() - t0) * 1000),
"ok": False,
"error_class": type(e).__name__,
})
raiseimport crypto from "node:crypto";
export function stableHash(obj) {
const raw = JSON.stringify(obj);
return crypto.createHash("sha256").update(raw).digest("hex");
}
export class ToolGateway {
constructor({ impls = {}, logger }) {
this.impls = impls;
this.logger = logger;
}
call(ctx, name, args) {
const fn = this.impls[name];
const argsHash = stableHash(args);
if (!fn) {
this.logger.event("tool_call", {
run_id: ctx.run_id,
trace_id: ctx.trace_id,
tool: name,
args_hash: argsHash,
ok: false,
error_class: "unknown_tool",
});
throw new Error("unknown tool: " + name);
}
const t0 = Date.now();
this.logger.event("tool_call", {
run_id: ctx.run_id,
trace_id: ctx.trace_id,
tool: name,
args_hash: argsHash,
});
try {
const out = fn(args);
this.logger.event("tool_result", {
run_id: ctx.run_id,
trace_id: ctx.trace_id,
tool: name,
latency_ms: Date.now() - t0,
ok: true,
});
return out;
} catch (e) {
this.logger.event("tool_result", {
run_id: ctx.run_id,
trace_id: ctx.trace_id,
tool: name,
latency_ms: Date.now() - t0,
ok: false,
error_class: e?.name || "Error",
});
throw e;
}
}Combínalo con:
- budgets (
/es/governance/budget-controls) - dedupe contra tool spam (
/es/failures/tool-spam) - unit tests que fijen stop reasons (
/es/testing-evaluation/unit-testing-agents)
Fallo real (con números)
Shipeamos un agente de research “read-only” que llamaba http.get.
Un partner empezó a devolver 200 con payload de error (sí). Nuestro wrapper trató “200 == ok” y logueábamos solo “success”.
Impacto:
- ~18% de runs dieron resúmenes incorrectos pero confiados por ~2 horas
- ~30 tickets
- on-call: ~4 horas para demostrar que no era “el modelo alucinando”
Arreglo:
- log de
error_classnormalizada + fallos de validación - guardar
args_hash+ latencia para localizar hot spots - alerta: validation_fail_rate > 2% por 5 minutos
No necesitas logs perfectos. Necesitas logs que respondan “¿qué pasó?” en <10 minutos.
Trade-offs
- Loguear args en claro ayuda y también filtra PII. Por defecto:
args_hash. - Guardar resultados completos facilita debugging y complica compliance. Sampling + redacción.
- Mucho logging puede ser su propia caída. Empieza por lo que vas a alertar.
Cuándo NO hacer esto
- Si el agente corre solo en un entorno local/trust, puedes ser más laxo (por un rato).
- Si estás cambiando el loop cada día: logging ligero pero consistente (IDs + stop reasons).
- No te fabriques un tracing casero si no lo puedes operar.
Checklist (copy-paste)
- [ ]
run_id,trace_id,request_id,user_iden cada evento - [ ]
tool_call+tool_result(name, args_hash, latency, ok, error_class) - [ ]
stop_reason+ budgets al final del run - [ ] policy de redacción (PII/secrets) + hashes por defecto
- [ ] alertas: tool calls/run, timeouts, validation fails
- [ ] una query “incidente” por failure frecuente (dashboard / saved search)
Config segura por defecto (YAML)
logging:
ids:
run_id: required
trace_id: required
request_id: required
tool_calls:
enabled: true
store_args: false
store_args_hash: true
store_results: "sampled"
result_sample_rate: 0.01
pii:
redact_fields: ["email", "phone", "token", "authorization", "cookie"]
stop_reasons:
enabled: true
alerts:
tool_calls_per_run_p95: { warn: 10, critical: 20 }
timeout_rate: { warn: 0.02, critical: 0.05 }
validation_fail_rate: { warn: 0.02, critical: 0.05 }
Implementar en OnceOnly (opcional)
# onceonly-python: governed audit logs + metrics
import os
from onceonly import OnceOnly
client = OnceOnly(api_key=os.environ["ONCEONLY_API_KEY"])
agent_id = "support-bot"
# Pull last 50 actions (includes args_hash + decisions)
for e in client.gov.agent_logs(agent_id, limit=50):
print(e.ts, e.tool, e.decision, e.args_hash, e.spend_usd, e.reason)
# Rollups for dashboards/alerts
m = client.gov.agent_metrics(agent_id, period="day")
print("spend_usd=", m.total_spend_usd, "blocked=", m.blocked_actions)
FAQ (3–5)
Usado por patrones
Fallos relacionados
- AI Agent Infinite Loop (Detectar + arreglar, con código)
- Explosión de presupuesto (cuando un agente quema dinero) + fixes + código
- Tool Spam Loops (fallo del agente + fixes + código)
- Incidentes de exceso de tokens (prompt bloat) + fixes + código
- Corrupción de respuestas de tools (schema drift + truncation) + código
Gobernanza requerida
Q: ¿Debería loguear args en claro?
A: Por defecto no. Log args_hash + campos seguros. Args en claro solo temporalmente durante incidentes.
Q: ¿Cuál es el campo más útil?
A: run_id/trace_id estable en cada evento.
Q: ¿Cómo detecto loops rápido?
A: Alertra tool_calls/run + repetición (tool, args_hash). Luego lee /failures/tool-spam.
Q: ¿Necesito distributed tracing?
A: Si cruzas servicios, sí. Trace IDs + spans de tool primero.
Páginas relacionadas (3–6 links)
- Fundamentos: Tool calling de un agente de IA (con código) · Qué hace a un agente apto para producción (guardrails + código)
- Fallos: Tool Spam Loops (fallo del agente + fixes + código) · Explosión de presupuesto (cuando un agente quema dinero) + fixes + código
- Gobernanza: Budget Controls para agentes IA (pasos, tiempo, $) + Código · Kill switch (diseño + policy + código)
- Pruebas: Unit tests para agentes de IA (deterministas, baratos, realmente útiles)
- Stack de producción: Stack de producción para agentes de IA (lo que hay entre tu agente y el desastre)