Logging semantico para agentes

Los logs semanticos estructuran eventos de agentes para su analisis.
En esta página
  1. Idea en 30 segundos
  2. Problema principal
  3. Como funciona
  4. Vocabulario minimo de eventos
  5. Cuando usar
  6. Ejemplo de implementacion
  7. Errores tipicos
  8. Eventos nombrados distinto en diferentes servicios
  9. Texto libre en lugar de campos normalizados
  10. No hay event_version
  11. Se loggean raw prompts o raw args sin redaccion
  12. Autoevaluacion
  13. FAQ
  14. Paginas relacionadas

Idea en 30 segundos

El logging semantico para agentes significa que los eventos no solo tienen formato JSON, sino tambien un significado estable.

Es decir, pasos equivalentes en distintos runs se registran igual: el mismo event, los mismos campos clave, los mismos estados.

Esto hace que los logs sirvan para busqueda, alertas, analitica y debugging en production.

Problema principal

Muchos equipos ya escriben logs estructurados, pero eso a menudo no alcanza.

En distintos servicios y versiones del agente, el mismo evento puede tener nombres y campos distintos: tool_called, call_tool, tool.invoke. Como resultado, los logs existen, pero comparar runs entre si es dificil.

El logging semantico es una solucion a nivel de diseno de eventos, no solo una capa tecnica encima del logging.

En production esto suele verse asi:

  • una consulta en el sistema de logs devuelve demasiado ruido;
  • las alertas se comportan de forma inestable por nombres distintos de eventos;
  • durante un incidente hay que mapear manualmente eventos de varios formatos.

Por eso, para sistemas de agentes son clave un vocabulario comun de eventos y un esquema estable de campos.

Como funciona

El logging semantico se basa en tres cosas:

  • un vocabulario acordado de eventos (event taxonomy);
  • campos estables para cada evento;
  • valores normalizados (status, error_class, stop_reason).

event taxonomy es un contrato entre runtime, logs, dashboards y alertas. Romper este contrato rompe la observabilidad. status normalmente tiene un conjunto limitado de valores (por ejemplo: ok, error, timeout, cancelled; en este ejemplo se usa la variante simplificada: ok / error).

El logging semantico no reemplaza el tracing, lo complementa. Hace que los eventos no solo sean visibles, sino comparables entre servicios y releases. El logging responde "que paso", el tracing "como paso", y el semantic logging "que significa".

Vocabulario minimo de eventos

EventoSignificado semanticoCampos clave
run_startedse inicio un nuevo runrun_id, trace_id, request_id, task_hash
agent_stepel agente paso al siguiente pasostep_index, step_type, actor
tool_callinicio de una invocacion de herramientatool_name, args_hash
tool_resultresultado de una invocacion de herramientatool_name, latency_ms, status, error_class
llm_resultresultado de un paso del modelomodel, token_usage, latency_ms, status
policy_decisiondecision de policy/guardrailsrule_id, decision, reason_code
run_finishedrun finalizadostop_reason, total_steps, total_latency_ms

policy_decision permite ver no solo errores, sino tambien causas de bloqueo y decisiones de guardrails.

event_version permite evolucionar el esquema de eventos sin romper dashboards ni alertas existentes.

Cuando usar

El logging semantico completo no siempre es necesario.

Para un escenario simple single-shot sin tools y sin bucle de ejecucion, logs basicos suelen ser suficientes.

Pero el logging semantico se vuelve critico cuando:

  • hay varios agentes o servicios en el sistema;
  • hay que construir alertas y dashboards estables;
  • es importante comparar comportamiento entre releases;
  • los incidentes deben analizarse rapido, sin mapeo manual de eventos.

Ejemplo de implementacion

Abajo hay un ejemplo simplificado de logging semantico en runtime. La idea es simple: registramos solo eventos del vocabulario acordado y normalizamos valores de campos.

PYTHON
import hashlib
import json
import logging
import time
import uuid
from enum import StrEnum

logger = logging.getLogger("agent")


class EventName(StrEnum):
    RUN_STARTED = "run_started"
    AGENT_STEP = "agent_step"
    TOOL_CALL = "tool_call"
    TOOL_RESULT = "tool_result"
    LLM_RESULT = "llm_result"
    POLICY_DECISION = "policy_decision"
    RUN_FINISHED = "run_finished"


def stable_hash(value):
    # default=str da compatibilidad base
    # en sistemas criticos conviene serializacion explicita (por ejemplo ISO 8601)
    payload = json.dumps(
        value,
        sort_keys=True,
        ensure_ascii=False,
        default=str,
    ).encode("utf-8")
    return hashlib.sha256(payload).hexdigest()


def normalize_status(ok):
    return "ok" if ok else "error"


def normalize_error(error):
    if error is None:
        return None
    return type(error).__name__


def log_semantic(event_name: EventName, **fields):
    logger.info(
        event_name.value,
        extra={
            "event": event_name.value,
            "event_version": 1,
            "timestamp_ms": int(time.time() * 1000),
            **fields,
        },
    )


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

    log_semantic(
        EventName.RUN_STARTED,
        run_id=run_id,
        trace_id=trace_id,
        request_id=request_id,
        task_hash=stable_hash(task),
    )

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

            log_semantic(
                EventName.AGENT_STEP,
                run_id=run_id,
                trace_id=trace_id,
                step_index=step_index,
                step_type=step_type,
                actor=getattr(step, "actor", "agent_runtime"),
            )

            if step_type == "tool_call":
                args = getattr(step, "args", {})
                log_semantic(
                    EventName.TOOL_CALL,
                    run_id=run_id,
                    trace_id=trace_id,
                    step_index=step_index,
                    tool_name=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_semantic(
                        EventName.TOOL_RESULT,
                        run_id=run_id,
                        trace_id=trace_id,
                        step_index=step_index,
                        tool_name=tool_name,
                        latency_ms=latency_ms,
                        status=normalize_status(True),
                        error_class=None,
                    )
                else:
                    log_semantic(
                        EventName.LLM_RESULT,
                        run_id=run_id,
                        trace_id=trace_id,
                        step_index=step_index,
                        model=getattr(step, "model", None),
                        token_usage=getattr(result, "token_usage", None),
                        latency_ms=latency_ms,
                        status=normalize_status(True),
                    )

                # policy_decision se registra despues de ejecutar el paso
                # (cuando ya se conoce resultado o error)
                if getattr(step, "policy_decision", None) is not None:
                    decision = step.policy_decision
                    log_semantic(
                        EventName.POLICY_DECISION,
                        run_id=run_id,
                        trace_id=trace_id,
                        step_index=step_index,
                        rule_id=decision.rule_id,
                        decision=decision.value,
                        reason_code=decision.reason_code,
                    )

            except Exception as error:
                latency_ms = int((time.time() - step_started_at) * 1000)
                run_status = "error"

                if step_type == "tool_call":
                    stop_reason = "tool_error"
                    log_semantic(
                        EventName.TOOL_RESULT,
                        run_id=run_id,
                        trace_id=trace_id,
                        step_index=step_index,
                        tool_name=tool_name,
                        latency_ms=latency_ms,
                        status=normalize_status(False),
                        error_class=normalize_error(error),
                    )
                else:
                    stop_reason = "step_error"
                    log_semantic(
                        EventName.LLM_RESULT,
                        run_id=run_id,
                        trace_id=trace_id,
                        step_index=step_index,
                        model=getattr(step, "model", None),
                        latency_ms=latency_ms,
                        status=normalize_status(False),
                        error_class=normalize_error(error),
                    )

                if getattr(step, "policy_decision", None) is not None:
                    decision = step.policy_decision
                    log_semantic(
                        EventName.POLICY_DECISION,
                        run_id=run_id,
                        trace_id=trace_id,
                        step_index=step_index,
                        rule_id=decision.rule_id,
                        decision=decision.value,
                        reason_code=decision.reason_code,
                    )

                raise

            if result.is_final:
                stop_reason = "completed"
                break

    finally:
        log_semantic(
            EventName.RUN_FINISHED,
            run_id=run_id,
            trace_id=trace_id,
            status=run_status,
            stop_reason=stop_reason,
            total_steps=step_index,
            total_latency_ms=int((time.time() - started_at) * 1000),
        )

En production, estos eventos normalmente se envian a un sistema centralizado de logging (por ejemplo ELK, Datadog o ClickHouse), donde se construyen consultas, dashboards y alertas.

Por ejemplo, un semantic event en JSON puede verse asi:

JSON
{
  "timestamp_ms": 1774106220000,
  "event": "policy_decision",
  "event_version": 1,
  "run_id": "run_9fd2",
  "trace_id": "tr_9fd2",
  "step_index": 3,
  "rule_id": "email_external_domain",
  "decision": "deny",
  "reason_code": "missing_user_confirmation"
}

Errores tipicos

Incluso si ya hay logs estructurados, el logging semantico suele romperse por errores tipicos como estos.

Eventos nombrados distinto en diferentes servicios

Cuando una misma accion tiene nombres de event distintos, las consultas de logs se vuelven inestables. Como resultado, es mas dificil detectar a tiempo un fallo de herramienta o una fase temprana de spam de herramientas.

Texto libre en lugar de campos normalizados

Campos como "error": "something failed" casi no sirven para analitica. Es mejor tener campos separados status, error_class, reason_code con valores fijos.

No hay event_version

Sin version de evento, cambios de esquema rompen silenciosamente dashboards, saved queries y alertas. Por eso, la evolucion de esquema debe hacerse de forma explicita.

Se loggean raw prompts o raw args sin redaccion

Es un riesgo de seguridad y cumplimiento. Es mas seguro guardar hash o versiones anonimizadas de esos campos.

Autoevaluacion

Abajo tienes un checklist corto de logging semantico basico 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

P: En que se diferencia semantic logging del logging JSON normal?
R: El logging JSON define solo el formato. Semantic logging define el significado: nombres de evento estables, campos consistentes y valores normalizados.

P: Semantic logging reemplaza el tracing?
R: No. El tracing muestra el camino de ejecucion, y semantic logging hace que los eventos de ese camino sean entendibles para busqueda, alertas y analitica.

P: Cual es el minimo de semantic logging para un primer release de production?
R: Vocabulario base de eventos (run_started, tool_call, tool_result, run_finished), run_id/trace_id estables, status, error_class y stop_reason.

P: Hay que migrar todos los logs viejos de inmediato?
R: No. Es mejor empezar por eventos nuevos y rutas criticas de run, y luego migrar gradualmente formatos antiguos.

Paginas relacionadas

Siguiente sobre el tema:

⏱️ 7 min de lecturaActualizado 20 de marzo 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.