Idee in 30 Sekunden
Semantisches Logging (semantic logging) fuer Agenten bedeutet: Events haben nicht nur JSON-Format, sondern auch stabile Bedeutung.
Das heisst, gleiche Schritte in unterschiedlichen Runs werden gleich geloggt: derselbe event, dieselben Schluesselfelder, dieselben Statuswerte.
So werden Logs fuer Suche, Alerts, Analytics und Debugging in Production nutzbar.
Hauptproblem
Viele Teams schreiben bereits strukturierte Logs, aber das reicht oft nicht.
In unterschiedlichen Services und Agent-Versionen kann dasselbe Event verschiedene Namen und Felder haben:
tool_called, call_tool, tool.invoke.
Dadurch sind Logs zwar da, aber Runs lassen sich schlecht vergleichen.
Semantisches Logging ist eine Loesung auf Event-Design-Ebene, nicht nur eine technische Schicht ueber Logs.
In Production zeigt sich das oft so:
- Queries im Log-System liefern zu viel Rauschen;
- Alerts verhalten sich instabil wegen unterschiedlicher Event-Namen;
- im Incident muessen Events aus mehreren Formaten manuell zusammengefuehrt werden.
Deshalb brauchen Agentensysteme ein gemeinsames Event-Vokabular und stabile Feldschemata.
Wie es funktioniert
Semantisches Logging stuetzt sich auf drei Dinge:
- ein abgestimmtes Event-Vokabular (
event taxonomy); - stabile Felder fuer jedes Event;
- normalisierte Werte (
status,error_class,stop_reason).
event taxonomy ist ein Vertrag zwischen Runtime, Logs, Dashboards und Alerts.
Wenn dieser Vertrag gebrochen wird, bricht Observability.
status hat typischerweise einen begrenzten Wertebereich (zum Beispiel: ok, error, timeout, cancelled; in diesem Beispiel wird die vereinfachte Variante ok / error genutzt).
Semantisches Logging ersetzt Tracing nicht, sondern ergaenzt es. Es macht Events nicht nur sichtbar, sondern zwischen Services und Releases vergleichbar. Logging beantwortet "was ist passiert", Tracing "wie ist es passiert", und semantic logging "was bedeutet es".
Minimales Event-Vokabular
| Event | Semantische Bedeutung | Schluesselfelder |
|---|---|---|
| run_started | ein neuer Run wurde gestartet | run_id, trace_id, request_id, task_hash |
| agent_step | Agent ist zum naechsten Schritt uebergegangen | step_index, step_type, actor |
| tool_call | Start eines Tool-Aufrufs | tool_name, args_hash |
| tool_result | Ergebnis eines Tool-Aufrufs | tool_name, latency_ms, status, error_class |
| llm_result | Ergebnis eines Modellschritts | model, token_usage, latency_ms, status |
| policy_decision | Policy/Guardrail-Entscheidung | rule_id, decision, reason_code |
| run_finished | Run wurde beendet | stop_reason, total_steps, total_latency_ms |
policy_decision macht nicht nur Fehler sichtbar, sondern auch Blockierungsgruende und Guardrail-Entscheidungen.
event_version erlaubt Schema-Aenderungen ohne bestehende Dashboards und Alerts zu brechen.
Wann einsetzen
Volles semantisches Logging ist nicht immer noetig.
Fuer einfache Single-Shot-Szenarien ohne Tools und ohne Ausfuehrungsschleife reichen oft Basis-Logs.
Kritisch wird semantisches Logging, wenn:
- mehrere Agenten oder Services beteiligt sind;
- Alerts und Dashboards stabil gebaut werden muessen;
- Verhalten zwischen Releases verglichen werden soll;
- Incidents schnell analysiert werden muessen, ohne manuelles Event-Mapping.
Implementierungsbeispiel
Unten ist ein vereinfachtes Beispiel fuer semantisches Logging in der Runtime. Die Idee: nur Events aus einem abgestimmten Vokabular loggen und Feldwerte normalisieren.
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 gibt Basis-Kompatibilitaet
# in kritischen Systemen ist explizite Serialisierung besser (z. B. 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 oder 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 wird nach dem Schritt geloggt
# (wenn Ergebnis oder Fehler bekannt ist)
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),
)
In Production werden solche Events meist in zentrale Logging-Systeme gesendet (zum Beispiel ELK, Datadog oder ClickHouse), wo darauf Queries, Dashboards und Alerts aufbauen.
Ein einzelnes semantisches Event in JSON kann zum Beispiel so aussehen:
{
"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"
}
Typische Fehler
Auch wenn strukturierte Logs vorhanden sind, bricht semantisches Logging oft durch typische Fehler unten.
Events sind in verschiedenen Services unterschiedlich benannt
Wenn dieselbe Aktion unterschiedliche event-Namen hat, werden Log-Queries instabil.
Dadurch ist es schwerer, Tool-Ausfall oder eine fruehe Phase von Tool-Spam rechtzeitig zu erkennen.
Freitext statt normalisierter Felder
Felder wie "error": "something failed" sind fuer Analytics kaum nutzbar.
Besser sind getrennte Felder wie status, error_class, reason_code mit festen Werten.
Kein event_version
Ohne Event-Version brechen Schema-Aenderungen unbemerkt Dashboards, gespeicherte Queries und Alerts. Darum sollte Schema-Evolution explizit erfolgen.
Raw prompts oder raw args werden ohne Redaction geloggt
Das ist ein Sicherheits- und Compliance-Risiko. Sicherer sind Hashes oder anonymisierte Feldversionen.
Selbstcheck
Unten ist eine kurze Checkliste fuer semantisches Basis-Logging vor Release.
Fortschritt: 0/9
â Grundlegende Observability fehlt
Das System wird in production schwer zu debuggen sein. Starten Sie mit run_id, structured logs und tracing von tool calls.
FAQ
Q: Worin unterscheidet sich semantic logging von normalem JSON-Logging?
A: JSON-Logging definiert nur das Format. Semantic logging definiert die Bedeutung: stabile Event-Namen, einheitliche Felder und normalisierte Werte.
Q: Ersetzt semantic logging Tracing?
A: Nein. Tracing zeigt den Ausfuehrungspfad, semantic logging macht Events auf diesem Pfad fuer Suche, Alerts und Analytics verstaendlich.
Q: Was ist das Minimum an semantic logging fuer den ersten Production-Release?
A: Basis-Vokabular (run_started, tool_call, tool_result, run_finished), stabile run_id/trace_id, status, error_class und stop_reason.
Q: Muss man alle alten Logs sofort migrieren?
A: Nein. Besser mit neuen Events und kritischen Run-Pfaden starten und alte Formate schrittweise migrieren.
Verwandte Seiten
Weiter zum Thema:
- Observability fuer KI-Agenten â Gesamtmodell aus Tracing, Logging und Metriken.
- Agent-Logging â welche Events in der Runtime erfasst werden sollten.
- Agent Tracing â wie man den vollstaendigen Pfad eines Runs sieht.
- Verteiltes Agent-Tracing â wie Events zwischen Services verknuepft werden.
- Debugging von Agent-Runs â praktische Incident-Analyse.