Sin monitoreo (anti‑patrón) + qué loggear + código

  • Reconoce la trampa antes de enviarla a prod.
  • Ve qué se rompe cuando el modelo se equivoca seguro.
  • Copia defaults seguros: permisos, budgets, idempotency.
  • Sabe cuándo no usar un agente.
Señales de detección
  • Tool calls por run suben (o repiten mismo args hash).
  • Gasto/tokens suben sin mejorar el resultado.
  • Retries pasan de raros a constantes (429/5xx).
Si no puedes responder “¿qué hizo el agente?”, no puedes operarlo en producción. Mínimo: traces, stop reasons, spend y logs de tool calls.
En esta página
  1. El problema (en producción)
  2. El momento 03:00
  3. Por qué esto se rompe en producción
  4. 1) Los agentes son sistemas distribuidos con pasos extra
  5. 2) “Success rate” esconde los fallos interesantes
  6. 3) No puedes arreglar lo que no puedes replay
  7. 4) El monitoreo es parte de la gobernanza
  8. Invariantes duras (no negociables)
  9. Ejemplo de implementación (código real)
  10. Caso de fallo (concreto)
  11. 🚨 Incidente: “todo está lento” (y no sabíamos por qué)
  12. Dashboards + alerts (ejemplos para robar)
  13. PromQL (Grafana)
  14. SQL (estilo Postgres/BigQuery)
  15. Alert rules (plain English)
  16. Trade-offs
  17. Cuándo NO usarlo
  18. Checklist (copiar/pegar)
  19. Config segura por defecto
  20. FAQ
  21. Páginas relacionadas
  22. Takeaway de producción
  23. Qué se rompe sin esto
  24. Qué funciona con esto
  25. Mínimo para shippear
En resumen

En resumen: Sin observabilidad, cada fallo se vuelve “el modelo estuvo raro” — un diagnóstico que no se puede testear ni arreglar. Necesitas: traces de tool calls, stop reasons, tracking de costos y capacidad de replay. No es infraestructura opcional.

Aprenderás: Requisitos mínimos de monitoreo • Un schema unificado de eventos • Taxonomía de stop reasons • Replay básico • Un incidente concreto que vas a reconocer

Concrete metric

Sin monitoreo: los usuarios reportan primero • debugging por vibes • sin replay
Con monitoreo mínimo: detectar drift temprano • debug con traces + stop reasons • replay de los últimos runs
Impacto: respuesta a incidentes más rápida + menos fallos repetidos (porque puedes arreglar el root cause)


El problema (en producción)

Un run de agente sale mal.

Un usuario reporta: “envió el email equivocado”.

Abres logs y tienes:

  • El texto final (quizá)
  • Un stack trace (quizá)
  • Vibes (seguro)

Si no puedes responder estas cinco preguntas, el sistema no es operable:

Incident questions
  1. ¿Qué tools se llamaron (y en qué orden)?
  2. ¿Con qué argumentos (o al menos args hashes)?
  3. ¿Qué volvió (o al menos snapshot hashes)?
  4. ¿Qué versión de model/prompt/tools corría?
  5. ¿Por qué se detuvo?
Truth

Eso no es “faltan dashboards”. Eso es “este sistema no es operable”.

El momento 03:00

Así se siente “sin monitoreo”:

TEXT
03:12 — Support: "Agent emailed the wrong customer. Please stop it."

Grepeas logs y encuentras… nada que puedas joinear.

TEXT
2026-02-07T03:11:58Z INFO sent email to customer@example.com
2026-02-07T03:11:59Z INFO sent email to customer@example.com
2026-02-07T03:12:01Z WARN http.get 429
2026-02-07T03:12:03Z INFO Agent completed task

Sin run_id. Sin step trace. Sin stop reason. Sin hash de args. Sin versión de model/tool.

Y haces el peor debugging: grep por una dirección de email y rezar que sea única.


Por qué esto se rompe en producción

Failure analysis

1) Los agentes son sistemas distribuidos con pasos extra

Cuando un agente llama tools, construiste:

  • múltiples dependencias (HTTP, DB, APIs)
  • múltiples failure modes (timeouts, 502s, rate limits)
  • múltiples retries (y retry storms)

Si no loggeas cada paso, debuggeas por storytelling.

2) “Success rate” esconde los fallos interesantes

El drift aparece como:

  • más tool calls por run (se loopea, no falla)
  • más tokens por request (el modelo “explica” errores)
  • más latencia (retries, tools lentos)
  • stop reasons distintos (budgets, denials, timeouts)

3) No puedes arreglar lo que no puedes replay

Si no puedes replay (o al menos reconstruir) un run desde logs, no puedes confiar en un “fix”. Solo vas a adivinar.

4) El monitoreo es parte de la gobernanza

Budgets, allowlists y kill switches son inútiles si no puedes ver cuándo se activaron.


Invariantes duras (no negociables)

  • Cada run tiene run_id.
  • Cada step tiene step_id.
  • Cada tool call loggea: nombre del tool, args hash, duración, status, clase de error.
  • Cada run termina con un stop event: stop_reason.
  • Si no puedes replay (aunque sea parcialmente), no puedes confiar en un fix.

Ejemplo de implementación (código real)

El fallo típico aquí es tener dos formatos de log distintos:

  • los eventos de tools son estructurados
  • los stop events son “especiales”

Eso mata la posibilidad de join.

Este sample usa un schema unificado para tool calls y stop events.

PYTHON
from __future__ import annotations

from dataclasses import dataclass, asdict
import hashlib
import json
import time
from typing import Any, Literal


EventKind = Literal["tool_result", "stop"]


def sha(obj: Any) -> str:
  raw = json.dumps(obj, sort_keys=True, separators=(",", ":"), ensure_ascii=False).encode("utf-8")
  return hashlib.sha256(raw).hexdigest()[:24]


@dataclass(frozen=True)
class Event:
  run_id: str
  kind: EventKind
  ts_ms: int

  # optional fields
  step_id: int | None = None
  tool: str | None = None
  args_sha: str | None = None
  duration_ms: int | None = None
  status: Literal["ok", "error"] | None = None
  error: str | None = None

  stop_reason: str | None = None
  usage: dict[str, Any] | None = None


def log_event(ev: Event) -> None:
  print(json.dumps(asdict(ev), ensure_ascii=False))


def call_tool(run_id: str, step_id: int, tool: str, args: dict[str, Any]) -> Any:
  started = time.time()
  try:
      out = tool_impl(tool, args=args)  # (pseudo)
      dur = int((time.time() - started) * 1000)
      log_event(
          Event(
              run_id=run_id,
              kind="tool_result",
              ts_ms=int(time.time() * 1000),
              step_id=step_id,
              tool=tool,
              args_sha=sha(args),
              duration_ms=dur,
              status="ok",
              error=None,
          )
      )
      return out
  except Exception as e:
      dur = int((time.time() - started) * 1000)
      log_event(
          Event(
              run_id=run_id,
              kind="tool_result",
              ts_ms=int(time.time() * 1000),
              step_id=step_id,
              tool=tool,
              args_sha=sha(args),
              duration_ms=dur,
              status="error",
              error=type(e).__name__,
          )
      )
      raise


def stop(run_id: str, *, reason: str, usage: dict[str, Any]) -> dict[str, Any]:
  log_event(
      Event(
          run_id=run_id,
          kind="stop",
          ts_ms=int(time.time() * 1000),
          stop_reason=reason,
          usage=usage,
      )
  )
  return {"status": "stopped", "stop_reason": reason, "usage": usage}
JAVASCRIPT
import crypto from "node:crypto";

export function sha(obj) {
const raw = JSON.stringify(obj, Object.keys(obj || {}).sort());
return crypto.createHash("sha256").update(raw, "utf8").digest("hex").slice(0, 24);
}

export function logEvent(ev) {
console.log(JSON.stringify(ev));
}

export async function callTool(runId, stepId, tool, args) {
const started = Date.now();
try {
  const out = await toolImpl(tool, { args }); // (pseudo)
  logEvent({
    run_id: runId,
    kind: "tool_result",
    ts_ms: Date.now(),
    step_id: stepId,
    tool,
    args_sha: sha(args),
    duration_ms: Date.now() - started,
    status: "ok",
    error: null,
  });
  return out;
} catch (e) {
  logEvent({
    run_id: runId,
    kind: "tool_result",
    ts_ms: Date.now(),
    step_id: stepId,
    tool,
    args_sha: sha(args),
    duration_ms: Date.now() - started,
    status: "error",
    error: e?.name || "Error",
  });
  throw e;
}
}

export function stop(runId, { reason, usage }) {
logEvent({
  run_id: runId,
  kind: "stop",
  ts_ms: Date.now(),
  stop_reason: reason,
  usage,
});
return { status: "stopped", stop_reason: reason, usage };
}

Caso de fallo (concreto)

Incident

🚨 Incidente: “todo está lento” (y no sabíamos por qué)

Date: 2024-10-08
Duration: 3 días sin detectar, ~2 horas de debug cuando agregamos visibilidad
System: agente de soporte al cliente


Qué pasó en realidad

El tool http.get empezó a devolver 429/503 intermitentes.

Nuestra capa de tools reintentó hasta 8× por call (antes 2×) sin jitter. El agente interpretó esos fallos como “prueba otra query” y terminó haciendo más tool calls por run.

En 3 días (números ilustrativos, pero este patrón es común):

  • avg tool calls/run: 4.3 → 11.7
  • latencia p95: 2.1s → 8.4s
  • spend/run: ~2×

Nada “crasheó”. El success rate siguió ~91%, así que el drift se veía como “los usuarios están impacientes” hasta que Support escaló.


Root cause (la versión aburrida)

  • retries + sin jitter → thundering herd
  • sin stop reasons en logs → “success” ocultó el drift
  • sin tool-call trace → no pudimos probar dónde se iba el tiempo/spend

Fix

  1. Event logs estructurados (run_id, step_id, tool, args hash, duración, status)
  2. Stop reasons expuestos al caller/UI
  3. Dashboards + alerts sobre señales de drift (tool calls/run, latencia P95, stop reasons)

Dashboards + alerts (ejemplos para robar)

No necesitas observabilidad perfecta. Necesitas observabilidad útil.

PromQL (Grafana)

PROMQL
# Tool calls per run (p95)
histogram_quantile(0.95, sum(rate(agent_tool_calls_bucket[5m])) by (le))

# Stop reasons over time
sum(rate(agent_stop_total[10m])) by (stop_reason)

# Latency p95
histogram_quantile(0.95, sum(rate(agent_run_latency_ms_bucket[5m])) by (le))

SQL (estilo Postgres/BigQuery)

SQL
-- Alert: tool_calls/run spike vs baseline
SELECT
  date_trunc('hour', created_at) AS hour,
  avg(tool_calls) AS avg_tool_calls
FROM agent_runs
WHERE created_at > now() - interval '7 days'
GROUP BY 1
HAVING avg(tool_calls) > 2 * (
  SELECT avg(tool_calls)
  FROM agent_runs
  WHERE created_at BETWEEN now() - interval '14 days' AND now() - interval '7 days'
);

Alert rules (plain English)

  • Si tool_calls_per_run_p95 es 2× baseline por 10 minutos → investiga (y considera kill writes).
  • Si aparece stop_reason=loop_detected por encima de baseline → investiga (tool spam / prompt malo / outage).
  • Si spikea stop_reason=tool_timeout → tienes problemas upstream, no “model weirdness”.

Trade-offs

Trade-offs
  • Loggear cuesta dinero (storage, indexación). Sigue siendo más barato que incidentes a ciegas.
  • Evita loggear PII/secrets crudos. Hashea args y redacta agresivamente.
  • Replay requiere política de retención + controles de acceso.

Cuándo NO usarlo

Don’t
  • No construyas una plataforma pesada de tracing antes de tener logs estructurados. Empieza pequeño.
  • No loggees args crudos si contienen PII/secrets. Nunca.
  • No shippees agentes sin stop reasons. Estás creando retry loops.

Checklist (copiar/pegar)

Production checklist
  • [ ] run_id / step_id para cada run
  • [ ] Schema unificado de eventos (tool results + stop events)
  • [ ] Tool-call logs: tool, args_hash, duración, status, error class
  • [ ] Stop reason devuelto al usuario + loggeado
  • [ ] Métricas por run: tokens/tool calls/spend
  • [ ] Dashboards: latencia P95, tool_calls/run, distribución de stop_reason
  • [ ] Datos para replay: snapshot hashes (con retención + access control)

Config segura por defecto

YAML
logging:
  events:
    enabled: true
    schema: "unified"
    store_args: false
    store_args_hash: true
    include: ["run_id", "step_id", "tool", "duration_ms", "status", "error", "stop_reason"]
metrics:
  track: ["tokens_per_request", "tool_calls_per_run", "latency_p95", "spend_per_run", "stop_reason"]
retention:
  tool_snapshot_days: 14
  logs_days: 30

FAQ

FAQ
¿Cuál es el mínimo de monitoreo que necesitamos?
Tool call logs + stop reasons + métricas básicas de uso. Si no puedes responder “¿qué hizo?”, no puedes operarlo.
¿Podemos loggear args crudos de tools?
Normalmente no. Hashea args, redacta agresivamente, y guarda raw solo en sistemas muy controlados si de verdad lo necesitas.
¿Necesitamos distributed tracing?
Eventualmente sí. Empieza con logs estructurados que incluyan run_id, step_id y duraciones. Eso te da la mayor parte del valor.
¿Cómo monitoreamos drift?
Mira tokens, tool calls, latencia y stop reasons. Se mueven antes de las quejas de correctness.

Páginas relacionadas

Related

Takeaway de producción

Production takeaway

Qué se rompe sin esto

  • ❌ No puedes explicar incidentes
  • ❌ El drift se ve como “model weirdness”
  • ❌ Los overruns de costo aparecen tarde

Qué funciona con esto

  • ✅ Puedes join, replay y debuggear runs
  • ✅ El drift se vuelve un gráfico, no un debate
  • ✅ Kill switches se activan por señales reales

Mínimo para shippear

  1. Logs estructurados unificados
  2. Stop reasons
  3. Métricas + dashboards básicos
  4. Alerts sobre drift

No sabes si este es tu caso?

Disena tu agente ->
⏱️ 9 min de lecturaActualizado Mar, 2026Dificultad: ★★★
Implementar en OnceOnly
Safe defaults for tool permissions + write gating.
Usar en OnceOnly
# onceonly guardrails (concept)
version: 1
tools:
  default_mode: read_only
  allowlist:
    - search.read
    - kb.read
    - http.get
writes:
  enabled: false
  require_approval: true
  idempotency: true
controls:
  kill_switch: { enabled: true, mode: disable_writes }
audit:
  enabled: true
Integrado: control en producciónOnceOnly
Guardrails para agentes con tool-calling
Lleva este patrón a producción con gobernanza:
  • Presupuestos (pasos / topes de gasto)
  • Permisos de herramientas (allowlist / blocklist)
  • Kill switch y parada por incidente
  • Idempotencia y dedupe
  • Audit logs y trazabilidad
Mención integrada: OnceOnly es una capa de control para sistemas de agentes en producción.
Autor

Esta documentación está curada y mantenida por ingenieros que despliegan agentes de IA en producción.

El contenido es asistido por IA, con responsabilidad editorial humana sobre la exactitud, la claridad y la relevancia en producción.

Los patrones y las recomendaciones se basan en post-mortems, modos de fallo e incidentes operativos en sistemas desplegados, incluido durante el desarrollo y la operación de infraestructura de gobernanza para agentes en OnceOnly.