Logging für KI‑Agenten (was du loggen musst, was du redigierst, worauf du alertest)

Agent‑Logging, das in Incidents hilft: Trace IDs, Tool‑Call Events, Stop Reasons, Redaction und Alerts. Mit Python + JS Snippets.
Auf dieser Seite
  1. Problem (warum du hier bist)
  2. Warum das in Prod scheitert
  3. Diagramm: die minimale Event‑Pipeline
  4. Echter Code: Tool‑Gateway instrumentieren (Python + JS)
  5. Realer Ausfall (incident-style, mit Zahlen)
  6. Abwägungen
  7. Wann du das NICHT so machen solltest
  8. Copy/Paste Checkliste
  9. Sicheres Default‑Config‑Snippet (YAML)
  10. In OnceOnly umsetzen (optional)
  11. FAQ (3–5)
  12. Verwandte Seiten (3–6 Links)

Problem (warum du hier bist)

In dev „funktioniert“ dein Agent.

In prod macht er einmal in 200 Runs etwas Komisches:

  • ein Ticket sagt „er hat die falsche Mail rausgeschickt“
  • Kosten schießen 15 Minuten hoch
  • er looped auf einer flaky API bis zum Timeout

Und du hast… fast nichts:

  • eine „final answer“
  • ein paar Console Logs
  • vielleicht einen Tool‑Error ohne Kontext

Dann beginnt die schlimmste Debug‑Sorte: Raten mit Kreditkarte.

Diese Seite ist Logging, das Incidents wieder langweilig macht.

Warum das in Prod scheitert

Agents scheitern wie Distributed Systems, weil sie welche sind:

  • das Modell ist ein unzuverlässiger Planner
  • Tools sind Side Effects (HTTP/DB/Ticketing/E‑Mail)
  • Retries und Timeouts erzeugen emergentes Verhalten

Wenn du die Loop nicht loggst, kannst du keine Incident‑Basics beantworten:

  • Welche Tool Calls liefen? In welcher Reihenfolge?
  • Welche Args (oder wenigstens welcher args_hash)?
  • Was hat das Tool zurückgegeben (oder was wurde redigiert)?
  • Warum hat der Run gestoppt (stop_reason)?
  • Welche Anfrage / welcher User war’s?

Ohne stop_reason beobachtest du nichts. Du sammelst Vibes.

Diagramm: die minimale Event‑Pipeline

Echter Code: Tool‑Gateway instrumentieren (Python + JS)

Fang an der Boundary an. Tools sind dort, wo Geld und Schaden entstehen.

Wir loggen:

  • run_id, trace_id, tool_name
  • args_hash (nicht raw args per Default)
  • Latenz + Status
  • error_class (normalisiert)

Und wir machen es schwer, das Logging „aus Versehen zu vergessen“, indem alles durch ein Gateway muss.

PYTHON
import hashlib
import json
import time
from dataclasses import dataclass
from typing import Any, Callable, Dict, Optional


def stable_hash(obj: Any) -> str:
  raw = json.dumps(obj, sort_keys=True, ensure_ascii=False).encode("utf-8")
  return hashlib.sha256(raw).hexdigest()


@dataclass(frozen=True)
class RunCtx:
  run_id: str
  trace_id: str
  user_id: Optional[str] = None
  request_id: Optional[str] = None


class Logger:
  def event(self, name: str, fields: Dict[str, Any]) -> None: ...


class ToolGateway:
  def __init__(self, *, impls: dict[str, Callable[..., Any]], logger: Logger):
      self.impls = impls
      self.logger = logger

  def call(self, ctx: RunCtx, name: str, args: Dict[str, Any]) -> Any:
      fn = self.impls.get(name)
      if not fn:
          self.logger.event("tool_call", {
              "run_id": ctx.run_id,
              "trace_id": ctx.trace_id,
              "tool": name,
              "args_hash": stable_hash(args),
              "ok": False,
              "error_class": "unknown_tool",
          })
          raise RuntimeError(f"unknown tool: {name}")

      t0 = time.time()
      self.logger.event("tool_call", {
          "run_id": ctx.run_id,
          "trace_id": ctx.trace_id,
          "tool": name,
          "args_hash": stable_hash(args),
      })

      try:
          out = fn(**args)
          self.logger.event("tool_result", {
              "run_id": ctx.run_id,
              "trace_id": ctx.trace_id,
              "tool": name,
              "latency_ms": int((time.time() - t0) * 1000),
              "ok": True,
          })
          return out
      except TimeoutError:
          self.logger.event("tool_result", {
              "run_id": ctx.run_id,
              "trace_id": ctx.trace_id,
              "tool": name,
              "latency_ms": int((time.time() - t0) * 1000),
              "ok": False,
              "error_class": "timeout",
          })
          raise
      except Exception as e:
          self.logger.event("tool_result", {
              "run_id": ctx.run_id,
              "trace_id": ctx.trace_id,
              "tool": name,
              "latency_ms": int((time.time() - t0) * 1000),
              "ok": False,
              "error_class": type(e).__name__,
          })
          raise
JAVASCRIPT
import crypto from "node:crypto";

export function stableHash(obj) {
const raw = JSON.stringify(obj);
return crypto.createHash("sha256").update(raw).digest("hex");
}

export class ToolGateway {
constructor({ impls = {}, logger }) {
  this.impls = impls;
  this.logger = logger;
}

call(ctx, name, args) {
  const fn = this.impls[name];
  const argsHash = stableHash(args);

  if (!fn) {
    this.logger.event("tool_call", {
      run_id: ctx.run_id,
      trace_id: ctx.trace_id,
      tool: name,
      args_hash: argsHash,
      ok: false,
      error_class: "unknown_tool",
    });
    throw new Error("unknown tool: " + name);
  }

  const t0 = Date.now();
  this.logger.event("tool_call", {
    run_id: ctx.run_id,
    trace_id: ctx.trace_id,
    tool: name,
    args_hash: argsHash,
  });

  try {
    const out = fn(args);
    this.logger.event("tool_result", {
      run_id: ctx.run_id,
      trace_id: ctx.trace_id,
      tool: name,
      latency_ms: Date.now() - t0,
      ok: true,
    });
    return out;
  } catch (e) {
    this.logger.event("tool_result", {
      run_id: ctx.run_id,
      trace_id: ctx.trace_id,
      tool: name,
      latency_ms: Date.now() - t0,
      ok: false,
      error_class: e?.name || "Error",
    });
    throw e;
  }
}

Wenn du’s noch nicht machst, kombiniere das mit:

  • Budgets (/de/governance/budget-controls)
  • Dedupe gegen Tool Spam (/de/failures/tool-spam)
  • Unit Tests, die Stop Reasons stabil halten (/de/testing-evaluation/unit-testing-agents)

Realer Ausfall (incident-style, mit Zahlen)

Wir haben einen „read-only“ Research Agent shipped, der http.get nutzt.

Dann hat ein Partner‑API begonnen, 200er mit Error‑Payloads zurückzugeben (ja). Unser Tool‑Wrapper hat „200 == ok“ interpretiert und nur „success“ geloggt.

Impact:

  • ~18% Runs lieferten confident falsche Summaries für ~2 Stunden
  • ~30 Tickets
  • On‑Call: ~4 Stunden, um zu beweisen, dass es nicht „nur Halluzination“ war

Fix:

  1. normalisierte error_class + Validation‑Failures loggen
  2. args_hash + Latenz speichern, um Hotspots zu finden
  3. Alert: validation_fail_rate > 2% für 5 Minuten

Du brauchst keine perfekten Logs. Du brauchst Logs, die „was ist passiert?“ in <10 Minuten beantworten.

Abwägungen

  • Raw Tool Args sind hilfreich und auch der schnellste Weg zu PII‑Leaks. Default: args_hash.
  • Volle Tool Results machen Debugging leicht und Compliance schwer. Sampling + Redaction.
  • Zu viel Logging ist eine eigene Outage. Starte mit Events, auf die du alertest.

Wann du das NICHT so machen solltest

  • Wenn der Agent nur lokal/vertrauenswürdig läuft, kannst du (kurz) laxer sein.
  • Wenn du die Loop‑Form täglich umbaust: Logs leicht halten, aber IDs + Stop Reasons stabil.
  • Bau kein Custom Tracing, wenn du’s nicht betreiben kannst. Nimm was Langweiliges.

Copy/Paste Checkliste

  • [ ] run_id, trace_id, request_id, user_id auf jedem Event
  • [ ] tool_call + tool_result (name, args_hash, latency, ok, error_class)
  • [ ] stop_reason + Budgets am Run‑Ende
  • [ ] Redaction‑Policy (PII/Secrets) + Hashes als Default
  • [ ] Alerts: tool calls/run, timeouts, validation fails
  • [ ] Eine „Incident Query“ pro Top‑Failure (gespeicherte Suche/Dashboard)

Sicheres Default‑Config‑Snippet (YAML)

YAML
logging:
  ids:
    run_id: required
    trace_id: required
    request_id: required
  tool_calls:
    enabled: true
    store_args: false
    store_args_hash: true
    store_results: "sampled"
    result_sample_rate: 0.01
  pii:
    redact_fields: ["email", "phone", "token", "authorization", "cookie"]
  stop_reasons:
    enabled: true
alerts:
  tool_calls_per_run_p95: { warn: 10, critical: 20 }
  timeout_rate: { warn: 0.02, critical: 0.05 }
  validation_fail_rate: { warn: 0.02, critical: 0.05 }

In OnceOnly umsetzen (optional)

In OnceOnly umsetzen
Tool Calls mit args_hash + stop reasons loggen (safe by default).
In OnceOnly nutzen
# onceonly-python: governed audit logs + metrics
import os
from onceonly import OnceOnly

client = OnceOnly(api_key=os.environ["ONCEONLY_API_KEY"])
agent_id = "support-bot"

# Pull last 50 actions (includes args_hash + decisions)
for e in client.gov.agent_logs(agent_id, limit=50):
    print(e.ts, e.tool, e.decision, e.args_hash, e.spend_usd, e.reason)

# Rollups for dashboards/alerts
m = client.gov.agent_metrics(agent_id, period="day")
print("spend_usd=", m.total_spend_usd, "blocked=", m.blocked_actions)

FAQ (3–5)

Soll ich raw tool args loggen?
Default: nein. Log args_hash + safe Felder. Raw args nur kurz im Incident-Fenster (mit Redaction), danach wieder aus.
Welches Feld bringt am meisten?
Ein stabiler run_id/trace_id auf jedem Event. Ohne das kannst du nichts rekonstruieren.
Wie erkenne ich Loops schnell?
Alert auf tool_calls/run und wiederholte args_hash für das gleiche Tool. Kombiniere das mit stop_reason Taxonomie.
Brauche ich Distributed Tracing?
Wenn Tools andere Services callen: ja. Starte simpel mit trace_id propagation + ein paar Spans um Tool Calls.

Q: Soll ich raw tool args loggen?
A: Default: nein. Log args_hash + safe Felder. Raw args nur kurz im Incident‑Fenster, dann wieder aus.

Q: Welches Feld bringt am meisten?
A: Ein stabiler run_id/trace_id auf jedem Event.

Q: Wie erkenne ich Loops schnell?
A: Alert auf tool_calls/run und wiederholte (tool, args_hash). Lies dazu /failures/tool-spam.

Q: Brauche ich Distributed Tracing?
A: Wenn Tools andere Services callen: ja. Fang mit Trace IDs + Tool‑Spans an.

⏱️ 7 Min. LesezeitAktualisiert Mär, 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

Diese Dokumentation wird von Engineers kuratiert und gepflegt, die AI-Agenten in der Produktion betreiben.

Die Inhalte sind KI-gestützt, mit menschlicher redaktioneller Verantwortung für Genauigkeit, Klarheit und Produktionsrelevanz.

Patterns und Empfehlungen basieren auf Post-Mortems, Failure-Modes und operativen Incidents in produktiven Systemen, auch bei der Entwicklung und dem Betrieb von Governance-Infrastruktur für Agenten bei OnceOnly.