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
| Evento | Significado semantico | Campos clave |
|---|---|---|
| run_started | se inicio un nuevo run | run_id, trace_id, request_id, task_hash |
| agent_step | el agente paso al siguiente paso | step_index, step_type, actor |
| tool_call | inicio de una invocacion de herramienta | tool_name, args_hash |
| tool_result | resultado de una invocacion de herramienta | tool_name, latency_ms, status, error_class |
| llm_result | resultado de un paso del modelo | model, token_usage, latency_ms, status |
| policy_decision | decision de policy/guardrails | rule_id, decision, reason_code |
| run_finished | run finalizado | stop_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.
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:
{
"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:
- Observabilidad para agentes de IA — modelo general de tracing, logging y metricas.
- Logging de agentes — que eventos registrar en runtime.
- Tracing de agente — como ver la ruta completa de un run.
- Tracing distribuido de agentes — como conectar eventos entre servicios.
- Debugging de runs de agentes — analisis practico de incidentes.