Kein Monitoring (Anti-Pattern) + Was du loggen musst + Code

  • Erkenne die Falle, bevor sie in Prod landet.
  • Sieh, was bricht, wenn das Modell Ăźberzeugt danebenliegt.
  • Sichere Defaults kopieren: Permissions, Budgets, Idempotency.
  • Wissen, wann du keinen Agent brauchst.
Erkennungs-Signale
  • Tool-Calls pro Run steigen (oder wiederholen sich mit args-hash).
  • Kosten/Tokens pro Request steigen ohne bessere Ergebnisse.
  • Retries kippen von selten zu konstant (429/5xx).
Wenn du nicht beantworten kannst: 'Was hat der Agent getan?', kannst du ihn nicht in Production betreiben. Minimal: Traces, stop reasons, spend, tool call logs.
Auf dieser Seite
  1. Problem (zuerst)
  2. Der 03:00-Moment
  3. Warum das in Production bricht
  4. 1) Agenten sind Distributed Systems mit extra Schritten
  5. 2) „Success rate“ versteckt die interessanten Fehler
  6. 3) Du kannst nicht fixen, was du nicht replayen kannst
  7. 4) Monitoring ist Teil von Governance
  8. Hard invariants (nicht verhandelbar)
  9. Implementierung (echter Code)
  10. Example failure case (konkret)
  11. 🚨 Incident: „Alles ist langsam“ (und wir wussten nicht warum)
  12. Dashboards + Alerts (Beispiele zum Klauen)
  13. PromQL-Beispiele (Grafana)
  14. SQL-Beispiel (Postgres/BigQuery-Style)
  15. Alert-Regeln (Plain English)
  16. Abwägungen
  17. Wann NICHT nutzen
  18. Checklist (Copy-Paste)
  19. Safe default config
  20. FAQ
  21. Related pages
  22. Production takeaway
  23. What breaks without this
  24. What works with this
  25. Minimum to ship
Kurzfazit

Kurzfazit: Ohne Observability wird jeder Agent-Failure zu „das Modell war weird“ — untestbar und unfixbar. Du brauchst: Tool-Aufruf-Traces, stop reasons, Cost Tracking und Replay. Das ist nicht optional.

Du lernst: Minimum Monitoring • ein einheitliches Event-Schema • stop reason taxonomy • Replay-Basics • ein konkreter Incident, den du wiedererkennst

Concrete metric

Ohne Monitoring: Nutzer melden Probleme zuerst • Debugging nach Gefühl • kein Replay
Mit minimalem Monitoring: Drift früh sehen • debuggen via Traces + stop reasons • letzte Runs replayen
Impact: schnellere Incident Response + weniger Wiederholungsfehler (weil du Root Cause fixen kannst)


Problem (zuerst)

Ein Agent-Run geht schief.

Ein User meldet: „er hat die falsche Email geschickt.“

Du Ăśffnest Logs und hast:

  • final answer text (vielleicht)
  • stack trace (vielleicht)
  • vibes (safe)

Wenn du diese fĂźnf Fragen nicht beantworten kannst, ist das System nicht operierbar:

Incident questions
  1. Welche Tools wurden aufgerufen (und in welcher Reihenfolge)?
  2. Mit welchen Args (oder wenigstens args hashes)?
  3. Was kam zurĂźck (oder wenigstens snapshot hashes)?
  4. Welche Version von model/prompt/tools lief?
  5. Warum hat es gestoppt?
Truth

Das ist nicht „fehlende Dashboards“. Das ist „dieses System ist nicht operierbar“.

Der 03:00-Moment

So fühlt sich „kein Monitoring“ an:

TEXT
03:12 — Support: "Agent emailed the wrong customer. Please stop it."

Du greppst Logs und findest… nichts, was du joinen kannst.

TEXT
2026-02-07T03:11:58Z INFO sent email to customer@example.com
2026-02-07T03:11:59Z INFO sent email to customer@example.com
2026-02-07T03:12:01Z WARN http.get 429
2026-02-07T03:12:03Z INFO Agent completed task

Kein run_id. Kein Step Trace. Kein stop reason. Kein args hash. Keine Tool/Model-Version.


Warum das in Production bricht

Failure analysis

1) Agenten sind Distributed Systems mit extra Schritten

Sobald ein Agent Tools aufruft, hast du mehrere Dependencies, mehrere Failure Modes und mehrere Retries. Ohne Step-Level Logs wird Debugging zu Storytelling.

2) „Success rate“ versteckt die interessanten Fehler

Drift zeigt sich als:

  • mehr tool calls per run
  • mehr tokens per request
  • hĂśhere Latenz
  • andere stop reasons

3) Du kannst nicht fixen, was du nicht replayen kannst

Wenn du einen Run nicht replayen oder zumindest aus Logs rekonstruieren kannst, kannst du einem „Fix“ nicht trauen. Du rätst nur.

4) Monitoring ist Teil von Governance

Budgets, Allowlists und Kill-Switches sind wertlos, wenn du nicht siehst, wann sie triggern.


Hard invariants (nicht verhandelbar)

  • Jeder Run hat run_id.
  • Jeder Step hat step_id.
  • Jeder Tool Call loggt: tool name, args hash, duration, status, error class.
  • Jeder Run endet mit einem stop event: stop_reason.
  • Ohne (teilweises) Replay kannst du einem Fix nicht trauen.

Implementierung (echter Code)

Der häufigste Fehler: zwei Log-Formate:

  • Tool-Events sind strukturiert
  • Stop-Events sind „special“

Das killt Joinability.

Dieses Sample nutzt ein einheitliches Event-Schema fĂźr Tool-Results und Stop-Events.

PYTHON
from __future__ import annotations

from dataclasses import dataclass, asdict
import hashlib
import json
import time
from typing import Any, Literal


EventKind = Literal["tool_result", "stop"]


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


@dataclass(frozen=True)
class Event:
  run_id: str
  kind: EventKind
  ts_ms: int

  # optional fields
  step_id: int | None = None
  tool: str | None = None
  args_sha: str | None = None
  duration_ms: int | None = None
  status: Literal["ok", "error"] | None = None
  error: str | None = None

  stop_reason: str | None = None
  usage: dict[str, Any] | None = None


def log_event(ev: Event) -> None:
  print(json.dumps(asdict(ev), ensure_ascii=False))


def call_tool(run_id: str, step_id: int, tool: str, args: dict[str, Any]) -> Any:
  started = time.time()
  try:
      out = tool_impl(tool, args=args)  # (pseudo)
      dur = int((time.time() - started) * 1000)
      log_event(
          Event(
              run_id=run_id,
              kind="tool_result",
              ts_ms=int(time.time() * 1000),
              step_id=step_id,
              tool=tool,
              args_sha=sha(args),
              duration_ms=dur,
              status="ok",
              error=None,
          )
      )
      return out
  except Exception as e:
      dur = int((time.time() - started) * 1000)
      log_event(
          Event(
              run_id=run_id,
              kind="tool_result",
              ts_ms=int(time.time() * 1000),
              step_id=step_id,
              tool=tool,
              args_sha=sha(args),
              duration_ms=dur,
              status="error",
              error=type(e).__name__,
          )
      )
      raise


def stop(run_id: str, *, reason: str, usage: dict[str, Any]) -> dict[str, Any]:
  log_event(
      Event(
          run_id=run_id,
          kind="stop",
          ts_ms=int(time.time() * 1000),
          stop_reason=reason,
          usage=usage,
      )
  )
  return {"status": "stopped", "stop_reason": reason, "usage": usage}
JAVASCRIPT
import crypto from "node:crypto";

export function sha(obj) {
const raw = JSON.stringify(obj, Object.keys(obj || {}).sort());
return crypto.createHash("sha256").update(raw, "utf8").digest("hex").slice(0, 24);
}

export function logEvent(ev) {
console.log(JSON.stringify(ev));
}

export async function callTool(runId, stepId, tool, args) {
const started = Date.now();
try {
  const out = await toolImpl(tool, { args }); // (pseudo)
  logEvent({
    run_id: runId,
    kind: "tool_result",
    ts_ms: Date.now(),
    step_id: stepId,
    tool,
    args_sha: sha(args),
    duration_ms: Date.now() - started,
    status: "ok",
    error: null,
  });
  return out;
} catch (e) {
  logEvent({
    run_id: runId,
    kind: "tool_result",
    ts_ms: Date.now(),
    step_id: stepId,
    tool,
    args_sha: sha(args),
    duration_ms: Date.now() - started,
    status: "error",
    error: e?.name || "Error",
  });
  throw e;
}
}

export function stop(runId, { reason, usage }) {
logEvent({
  run_id: runId,
  kind: "stop",
  ts_ms: Date.now(),
  stop_reason: reason,
  usage,
});
return { status: "stopped", stop_reason: reason, usage };
}

Example failure case (konkret)

Incident

🚨 Incident: „Alles ist langsam“ (und wir wussten nicht warum)

Date: 2024-10-08
Duration: 3 Tage unbemerkt, ~2 Stunden Debug, sobald wir Visibility hatten
System: Customer-Support-Agent


What actually happened

Das http.get-Tool hat intermittierend 429s/503s zurĂźckgegeben.

Unser Tool-Layer hat bis zu 8× pro Call retried (vorher 2×) ohne Jitter. Der Agent hat diese Failures als „versuch eine andere Query“ interpretiert und mehr tool calls pro run gemacht.

Über 3 Tage (illustrative Zahlen, aber das Muster ist typisch):

  • avg tool calls/run: 4.3 → 11.7
  • p95 latency: 2.1s → 8.4s
  • spend/run: ~2×

Nichts ist „gecrasht“. Success rate blieb ~91%, also sah der Drift aus wie „User sind ungeduldig“, bis Support eskaliert hat.


Root cause (die langweilige Version)

  • retries ohne jitter → thundering herd
  • keine stop reasons in Logs → „success“ maskiert Drift
  • kein tool-call trace → wir konnten nicht beweisen, wo Zeit/Spend hinging

Fix

  1. Structured event logs (run_id, step_id, tool, args hash, duration, status)
  2. stop reasons an Caller/UI zurĂźckgeben
  3. Dashboards + Alerts auf Drift-Signale (tool calls/run, latency P95, stop reasons)

Dashboards + Alerts (Beispiele zum Klauen)

Du brauchst nicht perfekte Observability. Du brauchst nĂźtzliche Observability.

PromQL-Beispiele (Grafana)

PROMQL
# Tool calls per run (p95)
histogram_quantile(0.95, sum(rate(agent_tool_calls_bucket[5m])) by (le))

# Stop reasons over time
sum(rate(agent_stop_total[10m])) by (stop_reason)

# Latency p95
histogram_quantile(0.95, sum(rate(agent_run_latency_ms_bucket[5m])) by (le))

SQL-Beispiel (Postgres/BigQuery-Style)

SQL
-- Alert: tool_calls/run spike vs baseline
SELECT
  date_trunc('hour', created_at) AS hour,
  avg(tool_calls) AS avg_tool_calls
FROM agent_runs
WHERE created_at > now() - interval '7 days'
GROUP BY 1
HAVING avg(tool_calls) > 2 * (
  SELECT avg(tool_calls)
  FROM agent_runs
  WHERE created_at BETWEEN now() - interval '14 days' AND now() - interval '7 days'
);

Alert-Regeln (Plain English)

  • Wenn tool_calls_per_run_p95 2× baseline fĂźr 10 Minuten → investigate (und ggf. Writes killen).
  • Wenn stop_reason=loop_detected Ăźber baseline → investigate (tool spam / outage / bad prompt).
  • Wenn stop_reason=tool_timeout spiked → Upstream ist kaputt, nicht „model weirdness“.

Abwägungen

Trade-offs
  • Logging kostet Geld (Storage, Indexing). Trotzdem billiger als blind incidents.
  • Logge keine rohen PII/Secrets. Hash args und redact aggressiv.
  • Replay braucht Retention Policy + Access Controls.

Wann NICHT nutzen

Don’t
  • Bau keine schwere Tracing-Plattform, bevor du structured logs hast. Start small.
  • Logge keine rohen tool args, wenn sie PII/Secrets enthalten. Nie.
  • Shippe keine Agenten ohne stop reasons. Du baust Retry Loops.

Checklist (Copy-Paste)

Production checklist
  • [ ] run_id / step_id fĂźr jeden run
  • [ ] einheitliches Event-Schema (tool results + stop events)
  • [ ] tool-call logs: tool, args_hash, duration, status, error class
  • [ ] stop reason an User zurĂźckgeben + loggen
  • [ ] tokens/tool calls/spend pro run als Metrics
  • [ ] Dashboards: latency P95, tool_calls/run, stop_reason distribution
  • [ ] Replay-Daten: snapshot hashes (mit Retention + Access Control)

Safe default config

YAML
logging:
  events:
    enabled: true
    schema: "unified"
    store_args: false
    store_args_hash: true
    include: ["run_id", "step_id", "tool", "duration_ms", "status", "error", "stop_reason"]
metrics:
  track: ["tokens_per_request", "tool_calls_per_run", "latency_p95", "spend_per_run", "stop_reason"]
retention:
  tool_snapshot_days: 14
  logs_days: 30

FAQ

FAQ
Was ist das Minimum an Monitoring?
Tool-call logs + stop reasons + basic usage metrics. Wenn du nicht beantworten kannst „was hat es getan?“, kannst du es nicht betreiben.
KĂśnnen wir rohe tool args loggen?
Meistens nein. Hash args, redact aggressiv und speichere raw nur in streng kontrollierten Systemen, falls Ăźberhaupt.
Brauchen wir distributed tracing?
Später ja. Starte mit structured logs inkl. run_id, step_id und durations. Das bringt den Großteil des Werts.
Wie monitoren wir Drift?
Tokens, tool calls, latency, stop reasons. Die bewegen sich vor correctness complaints.

Related

Production takeaway

Production takeaway

What breaks without this

  • ❌ Incidents sind nicht erklärbar
  • ❌ Drift ist „model weirdness“
  • ❌ Cost overruns fallen zu spät auf

What works with this

  • ✅ Runs sind joinable, replayable, debuggable
  • ✅ Drift ist ein Graph, kein Streit
  • ✅ Kill-Switches triggern auf echte Signale

Minimum to ship

  1. Unified structured logs
  2. Stop reasons
  3. Basic metrics + dashboards
  4. Alerts auf Drift

Nicht sicher, ob das dein Fall ist?

Agent gestalten ->
⏱️ 8 Min. Lesezeit • Aktualisiert Mär, 2026Schwierigkeit: ★★★
In OnceOnly umsetzen
Safe defaults for tool permissions + write gating.
In OnceOnly nutzen
# onceonly guardrails (concept)
version: 1
tools:
  default_mode: read_only
  allowlist:
    - search.read
    - kb.read
    - http.get
writes:
  enabled: false
  require_approval: true
  idempotency: true
controls:
  kill_switch: { enabled: true, mode: disable_writes }
audit:
  enabled: true
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.