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_nameargs_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.
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__,
})
raiseimport 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:
- normalisierte
error_class+ Validation‑Failures loggen args_hash+ Latenz speichern, um Hotspots zu finden- 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_idauf 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)
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)
# 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)
Von Patterns genutzt
Verwandte Failures
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.
Verwandte Seiten (3–6 Links)
- Grundlagen: Tool‑Calling für KI‑Agenten (mit Code) · Was einen Agent production-ready macht (Guardrails + Code)
- Failures: Tool-Spam Loops (Agent Failure Mode + Fixes + Code) · Budget Explosion (Wenn Agents Geld verbrennen) + Fixes + Code
- Governance: Budget Controls für AI Agents (Steps, Time, $) + Code · Kill Switch Design (Policy + Code)
- Testing: Unit Tests für KI‑Agenten (deterministisch, billig, wirklich nützlich)
- Production stack: Production‑Stack für KI‑Agenten (das Zeug zwischen Agent und Desaster)