Semantisches Logging fuer Agenten

Semantische Logs strukturieren Agent-Events fuer die Analyse.
Auf dieser Seite
  1. Idee in 30 Sekunden
  2. Hauptproblem
  3. Wie es funktioniert
  4. Minimales Event-Vokabular
  5. Wann einsetzen
  6. Implementierungsbeispiel
  7. Typische Fehler
  8. Events sind in verschiedenen Services unterschiedlich benannt
  9. Freitext statt normalisierter Felder
  10. Kein event_version
  11. Raw prompts oder raw args werden ohne Redaction geloggt
  12. Selbstcheck
  13. FAQ
  14. Verwandte Seiten

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

EventSemantische BedeutungSchluesselfelder
run_startedein neuer Run wurde gestartetrun_id, trace_id, request_id, task_hash
agent_stepAgent ist zum naechsten Schritt uebergegangenstep_index, step_type, actor
tool_callStart eines Tool-Aufrufstool_name, args_hash
tool_resultErgebnis eines Tool-Aufrufstool_name, latency_ms, status, error_class
llm_resultErgebnis eines Modellschrittsmodel, token_usage, latency_ms, status
policy_decisionPolicy/Guardrail-Entscheidungrule_id, decision, reason_code
run_finishedRun wurde beendetstop_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.

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 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:

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"
}

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:

⏱ 6 Min. Lesezeit ‱ Aktualisiert 20. MĂ€rz 2026Schwierigkeit: ★★★
Integriert: Production ControlOnceOnly
Guardrails fĂŒr Tool-Calling-Agents
Shippe dieses Pattern mit Governance:
  • Budgets (Steps / Spend Caps)
  • Tool-Permissions (Allowlist / Blocklist)
  • Kill switch & Incident Stop
  • Idempotenz & Dedupe
  • Audit logs & Nachvollziehbarkeit
Integrierter Hinweis: OnceOnly ist eine Control-Layer fĂŒr Production-Agent-Systeme.

Autor

Nick — Engineer, der Infrastruktur fĂŒr KI-Agenten in Produktion aufbaut.

Fokus: Agent-Patterns, Failure-Modes, Runtime-Steuerung und SystemzuverlÀssigkeit.

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


Redaktioneller Hinweis

Diese Dokumentation ist KI-gestĂŒtzt, mit menschlicher redaktioneller Verantwortung fĂŒr Genauigkeit, Klarheit und Produktionsrelevanz.

Der Inhalt basiert auf realen AusfÀllen, Post-Mortems und operativen VorfÀllen in produktiv eingesetzten KI-Agenten-Systemen.