Logging de agentes

Los logs registran acciones del agente, prompts y llamadas a herramientas.
En esta página
  1. Idea en 30 segundos
  2. Problema principal
  3. Cómo funciona
  4. Qué eventos loggear primero
  5. Cuándo usar
  6. Ejemplo de implementación
  7. Errores típicos
  8. Se loggea solo la respuesta final
  9. No hay identificadores estables (run_id, trace_id)
  10. Se loggean raw prompts o raw args sin redaccion
  11. No se loggean tool_result y stop_reason
  12. Autoevaluación
  13. FAQ
  14. Páginas relacionadas

Idea en 30 segundos

El logging de agentes responde una pregunta simple: qué pasó exactamente durante un run.

Para eso se necesitan eventos estructurados con correlación mediante run_id y trace_id.

Sin eso, en incidentes normalmente se ve solo la respuesta final, pero no el camino que llevó a ella.

Problema principal

En un backend normal, pocos logs por solicitud suelen ser suficientes.

En sistemas de agentes, una solicitud puede incluir reasoning, tool calls, retries y varios pasos de modelo. Si solo se loggea el final, cuesta entender dónde exactamente se rompió el sistema.

En production normalmente se ve así:

  • un usuario reporta una respuesta incorrecta;
  • costos o latency suben en olas;
  • en logs aparece un error aislado sin contexto de run.

Por eso los agentes necesitan logging estructurado de eventos en todo el ciclo de vida del run, no logs sueltos.

Cómo funciona

La idea base es simple: cada paso importante se registra como un evento estructurado separado.

Mínimo para cada evento:

  • run_id y trace_id para correlación;
  • event (qué pasó);
  • timestamp;
  • status (ok / error) donde aplique;
  • campos clave del paso (tool, latency, stop_reason, etc.).

Qué eventos loggear primero

EventoQue conviene registrar
run_startedrun_id, trace_id, request_id, user_id
agent_stepstep_type, step_index, tool
tool_calltool_name, args_hash
tool_resulttool_name, latency_ms, status, error_class
llm_resultmodel, token usage, latency_ms, status
run_finishedstop_reason, total_steps, total_latency_ms

En sistemas de production, raw prompts y raw tool args normalmente no se guardan sin redaccion. Lo mas comun es guardar hash o version anonimizada.

Cuándo usar

El logging profundo no siempre es necesario.

Para un escenario simple single-shot, a veces alcanza con logs minimos request -> response.

Pero en cuanto hay tools, retries, varios pasos o costos altos, sin logging estructurado se vuelve dificil:

  • debugear incidentes;
  • explicar costos;
  • ajustar alertas de forma estable.

Ejemplo de implementación

Abajo tienes un ejemplo simplificado de structured logging en runtime y tool gateway. En el ejemplo no se guardan raw args: se guarda args_hash. agent_step registra el paso del agente, mientras tool_call y tool_result detallan por separado inicio y resultado de la llamada de herramienta.

PYTHON
import hashlib
import json
import logging
import time
import uuid

logger = logging.getLogger("agent")


def stable_hash(value):
    payload = json.dumps(
        value,
        sort_keys=True,
        ensure_ascii=False,
        default=str,  # para datetime y tipos complejos; en sistemas criticos conviene formato estable (por ejemplo ISO 8601)
    ).encode("utf-8")
    return hashlib.sha256(payload).hexdigest()


def log_event(event, **fields):
    logger.info(event, extra={"event": event, **fields})


def run_agent(agent, task, user_id=None, request_id=None):
    run_id = str(uuid.uuid4())
    trace_id = str(uuid.uuid4())
    started_at = time.time()
    steps = 0
    stop_reason = "max_steps"
    run_status = "ok"

    log_event(
        "run_started",
        run_id=run_id,
        trace_id=trace_id,
        user_id=user_id,
        request_id=request_id,
        task_hash=stable_hash(task),
    )

    try:
        for step in agent.iter(task):  # step: reasoning o tool execution
            steps += 1
            step_started_at = time.time()
            step_type = step.type
            tool_name = getattr(step, "tool_name", None)

            log_event(
                "agent_step",
                run_id=run_id,
                trace_id=trace_id,
                step_index=steps,
                step_type=step_type,
                tool=tool_name,
            )

            if step_type == "tool_call":
                args = getattr(step, "args", {})
                log_event(
                    "tool_call",
                    run_id=run_id,
                    trace_id=trace_id,
                    tool=tool_name,
                    args_hash=stable_hash(args),
                )

            try:
                result = step.execute()
                latency_ms = int((time.time() - step_started_at) * 1000)

                if step_type == "tool_call":
                    log_event(
                        "tool_result",
                        run_id=run_id,
                        trace_id=trace_id,
                        tool=tool_name,
                        latency_ms=latency_ms,
                        status="ok",
                    )
                else:
                    token_usage = getattr(result, "token_usage", None)
                    log_event(
                        "llm_result",
                        run_id=run_id,
                        trace_id=trace_id,
                        step_type=step_type,
                        model=getattr(step, "model", None),
                        token_usage=token_usage,
                        latency_ms=latency_ms,
                        status="ok",
                    )
            except Exception as error:
                latency_ms = int((time.time() - step_started_at) * 1000)
                result_event = "tool_result" if step_type == "tool_call" else "llm_result"
                log_event(
                    result_event,
                    run_id=run_id,
                    trace_id=trace_id,
                    step_type=step_type,
                    tool=tool_name,
                    model=getattr(step, "model", None),
                    latency_ms=latency_ms,
                    status="error",
                    error_class=type(error).__name__,
                    error_message=str(error),
                )
                run_status = "error"
                stop_reason = "tool_error" if step_type == "tool_call" else "step_error"
                raise

            if result.is_final:
                stop_reason = "completed"
                break
    finally:
        log_event(
            "run_finished",
            run_id=run_id,
            trace_id=trace_id,
            status=run_status,
            stop_reason=stop_reason,
            total_steps=steps,
            total_latency_ms=int((time.time() - started_at) * 1000),
        )

En production estos eventos suelen enviarse a un sistema centralizado de logs (por ejemplo ELK, Datadog o ClickHouse), y luego se usan para dashboards y alertas.

Este ejemplo alcanza para:

  • encontrar un tool call problemático;
  • calcular latency por paso;
  • entender por qué se detuvo un run.

Por ejemplo, un registro JSON puede verse asi:

JSON
{
  "timestamp": "2026-03-21T15:17:00Z",
  "event": "tool_result",
  "run_id": "run_9fd2",
  "trace_id": "tr_9fd2",
  "tool": "search_docs",
  "latency_ms": 410,
  "status": "ok"
}

Errores típicos

Incluso con logging agregado, los incidentes suelen ser dificiles de analizar por errores tipicos como estos.

Se loggea solo la respuesta final

Sin eventos intermedios no se ve como el agente llego al resultado. En ese modo, incluso un incidente simple tarda demasiado.

No hay identificadores estables (run_id, trace_id)

Cuando los eventos no se correlacionan, no se puede reconstruir el run completo. En production esto suele convertir el debugging en busqueda manual entre servicios.

Se loggean raw prompts o raw args sin redaccion

Eso es un riesgo directo de fuga de datos personales o sensibles. Es mas seguro guardar hash, campos redactados o version anonimizada.

No se loggean tool_result y stop_reason

Si faltan tool_result y stop_reason, cuesta entender que se rompio exactamente. Esos huecos suelen ocultar fallo de herramienta o una fase temprana de spam de herramientas.

Autoevaluación

Abajo tienes un checklist corto de logging base de agentes antes del release.

Progreso: 0/9

⚠ Falta observability base

Será difícil depurar el sistema en production. Empieza con run_id, structured logs y tracing de tool calls.

FAQ

Q: En qué se diferencia logging de tracing?
A: Logging responde "que paso" y registra eventos. Tracing muestra "como paso exactamente" mediante secuencia de pasos y relaciones.

Q: Qué conviene loggear primero si casi no hay logging?
A: Empieza por base: run_id, trace_id, run_started, tool_call, tool_result, run_finished, stop_reason. Eso ya da base de debugging.

Q: ¿Se pueden loggear prompts completos?
A: Por defecto, mejor no. En production los prompts suelen contener datos sensibles. Es mas seguro hash o version redactada.

Q: ¿Cómo saber si el logging ya alcanza?
A: Si puedes reconstruir la secuencia de un run problematico y ubicar el punto de fallo en 5-10 minutos, el nivel base ya funciona.

Páginas relacionadas

Sigue con estos temas:

⏱️ 6 min de lecturaActualizado 9 de abril de 2026Dificultad: ★★★
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

Nick — ingeniero que construye infraestructura para agentes de IA en producción.

Enfoque: patrones de agentes, modos de fallo, control del runtime y fiabilidad del sistema.

🔗 GitHub: https://github.com/mykolademyanov


Nota editorial

Esta documentación está asistida por IA, con responsabilidad editorial humana sobre la exactitud, la claridad y la relevancia en producción.

El contenido se basa en fallos reales, post-mortems e incidentes operativos en sistemas de agentes de IA desplegados.