Production‑Stack für KI‑Agenten (das Zeug zwischen Agent und Desaster)

Dein Agent ist kein Prompt. Er ist ein Stack: Budgets, Tools, State, Logs, Controls. Das ist der Kleber, der Incidents verhindert.
Auf dieser Seite
  1. Das Problem
  2. Warum das in echten Systemen passiert
  3. Was kaputtgeht, wenn du’s ignorierst
  4. Der Stack (was wir wirklich betreiben)
  5. Diagramm (wo die Control Layer sitzt)
  6. Layer für Layer: was wir schmerzhaft gelernt haben
  7. Entry point
  8. Orchestrator
  9. Model layer
  10. Tool layer
  11. State layer
  12. Observability
  13. Control layer
  14. Code: Orchestrierungs‑Skeleton (TypeScript)
  15. Was wir messen (weil „wirkt ok“ keine Metrik ist)
  16. Stop‑Reason‑Taxonomie (damit du ohne Vibes debuggen kannst)
  17. Multi‑Tenant‑Realität (wo die meisten Incidents wohnen)
  18. Rate Limits & Circuit Breakers (weil Agents Outages verstärken)
  19. Rollout, wie wir ihn machen (weil Agents kein Vertrauen am Tag 1 verdienen)
  20. Wo „Memory“ hingehört (Spoiler: nicht in Prompts)
  21. Incident‑Response (was du vor dem ersten Pager bereit haben willst)
  22. Tests & Replay (weil „works on my prompt“ kein Test ist)
  23. Build Order: „boring first“
  24. Realer Ausfall
  25. Abwägungen
  26. Wann du keinen Agent‑Stack bauen solltest
  27. Links

Das Problem

In dev „funktioniert“ dein Agent.

In prod:

  • er looped auf einer flaky API
  • er macht 200 Tool Calls, weil „nur noch einen“
  • du kannst nicht erklären, was passiert ist, weil dein einziges Log der Final Answer ist

Das ist kein LLM‑Problem. Das ist ein Stack‑Problem.

Warum das in echten Systemen passiert

Agents sind im Kern:

  • ein Planner (LLM)
  • eine Runtime (dein Code)
  • Side Effects (Tools)
  • State (Memory/Artifacts)
  • Constraints (Budgets/Policy)
  • Observability (Logs/Audit)

Wenn du nur den Planner baust, wirst du gepaged.

Was kaputtgeht, wenn du’s ignorierst

  • Kein Audit = kein Postmortem (oder das Postmortem ist „das Modell war’s“)
  • Keine Budgets = unbounded Kosten
  • Keine Policy‑Boundary = versehentliche Writes mit Prod‑Creds
  • Kein State = wiederholte Arbeit, doppelte Tool Calls, Prompt‑Bloat

Der Stack (was wir wirklich betreiben)

  1. Entry point: UI/API, Auth, Request‑ID
  2. Orchestrator: Routing, Retries, Budgets, Tracing
  3. Model layer: LLM Calls (mit Spend Tracking)
  4. Tool layer: APIs, Browser, DB (mit Allowlists)
  5. State: Memory, Artifacts, Caches, Idempotency Keys
  6. Observability: Structured Logs, Traces, Audit Events
  7. Control layer: Policy Engine, Kill Switch, Incident Stop

Diagramm (wo die Control Layer sitzt)

Das ist das Mental Model, das wir nutzen:

Wenn du „Control“ in einen Prompt packst, hast du keine Control Layer. Du hast eine Empfehlung.

Layer für Layer: was wir schmerzhaft gelernt haben

Entry point

Am Entry point entscheidest du über den Blast Radius.

Gute Defaults:

  • authentifizieren, bevor der Agent läuft
  • eine Request‑ID erzeugen
  • Tenant/Environment an diese Request‑ID binden
  • Budget upfront setzen (lass das Modell nicht über Budgets „verhandeln“)

Wenn du das Modell Tenant oder Environment auswählen lässt, schreibst du irgendwann in das falsche.

Orchestrator

Das ist die Runtime, die deinen Agent „ehrlich“ hält:

  • step loop
  • timeouts
  • retry policy
  • tool allowlists
  • trace collection
  • stop reasons

Wenn du das nicht baust, wird jeder Agent eine Snowflake, die anders kaputtgeht. Snowflakes sind süß – bis du sie betreiben musst.

Model layer

Deine Model‑Layer ist meistens:

  • provider fallbacks (if you have them)
  • spend tracking
  • predictable output formats (tool actions)

Das Modell ist nicht der einzige „unzuverlässige“ Teil. Aber es ist der Teil, den alle beschuldigen – weil es leichter ist, als zuzugeben, dass die Runtime fehlt.

Tool layer

Tools sind der Ort, an dem Side Effects leben. Hier enforce’st du:

  • allowlists (what can be called)
  • permissions (what can be written)
  • idempotency keys (what can be repeated safely)
  • timeouts (what can’t hang)
  • rate limits (what can’t DDoS your dependencies)

Die Tool‑Layer darf nicht „mach das Ding“ als Input akzeptieren. Sie soll strukturierte Args mit Validation akzeptieren.

State layer

State ist nicht ein Bucket.

Wir splitten das in:

  • scratch: kurzlebige Run‑Notizen (klein, strukturiert)
  • artifacts: Outputs, die du später brauchst (Drafts, Extracts, Pläne)
  • memory: was du über Runs hinweg tragen willst (vorsichtig)
  • cache: dedupe teure Reads (URLs, KB‑Lookups)

Wenn du alles in „memory“ kippst, bekommst du Prompt‑Bloat und schlechtere Antworten. Wenn du nichts mitnimmst, bekommst du wiederholte Arbeit und doppelte Tool Calls.

Observability

Wenn du die Frage „was hat er getan?“ nicht beantworten kannst, kannst du’s nicht in Production betreiben.

Minimum‑Observability:

  • action trace (steps, tool calls, stop reason)
  • structured tool logs (args hash, duration, status)
  • spend/cost estimation
  • per-tenant usage metrics

Wenn du’s ernst meinst, add Tracing (Model‑Call‑Spans, Tool‑Spans). Aber selbst simple Structured Logs schlagen „das Modell hat’s gesagt“.

Control layer

Die Control Layer ist das Stück, das existieren soll, wenn du schläfst:

  • budgets (hard limits)
  • tool permissions (least privilege)
  • approvals (for writes)
  • kill switch (operator stop)
  • incident stop (circuit breakers)

Das ist kein „Security Theater“. Das ist der Unterschied zwischen einer LLM‑Demo und einem System, das du laufen lassen kannst.

Code: Orchestrierungs‑Skeleton (TypeScript)

Du brauchst kein massives Framework. Du brauchst explizite Control Points.

TS
type Budget = { maxSteps: number; maxSeconds: number; maxUsd: number };
type ToolName = "web.search" | "http.get" | "ticket.create";

type Policy = {
  allowTools: ToolName[];
  budget: Budget;
  requireApprovalFor: ToolName[];
};

type AuditEvent =
  | { type: "tool.call"; tool: ToolName; args: unknown; ms: number }
  | { type: "budget.stop"; reason: string }
  | { type: "kill"; reason: string };

export async function runAgent(input: string, policy: Policy) {
  const started = Date.now();
  const events: AuditEvent[] = [];

  for (let step = 0; step < policy.budget.maxSteps; step++) {
    if (Date.now() - started > policy.budget.maxSeconds * 1000) {
      events.push({ type: "budget.stop", reason: "time" });
      break;
    }
    if (await killSwitchIsOn()) {
      events.push({ type: "kill", reason: "operator" });
      break;
    }

    const action = await llmDecideNext(input); // returns {tool, args} or {finish}
    if (action.type === "finish") return { output: action.text, events };

    if (!policy.allowTools.includes(action.tool)) {
      throw new Error(`tool not allowed: ${action.tool}`);
    }
    if (policy.requireApprovalFor.includes(action.tool)) {
      await waitForHumanApproval(action); // (pseudo)
    }

    const t0 = Date.now();
    const obs = await callTool(action.tool, action.args); // must enforce timeouts + idempotency
    events.push({ type: "tool.call", tool: action.tool, args: action.args, ms: Date.now() - t0 });

    input = updateState(input, action, obs); // keep state small, structured
  }

  return { output: "stopped", events };
}

Was wir messen (weil „wirkt ok“ keine Metrik ist)

Wenn du Agents in Production betreiben willst, miss das langweilige Zeug:

  • completion rate (fertig vs Budget gerissen?)
  • p50/p95 runtime
  • p50/p95 tool calls per run
  • cost per run (tokens + tool credits)
  • loop rate (runs stopped by loop guard)
  • policy deny rate (how often your allowlist blocks it)

Wenn du Policy Denies nicht misst, „fixst“ du den Agent, indem du Permissions aufreißt – statt die Aufgabe zu fixen.

Stop‑Reason‑Taxonomie (damit du ohne Vibes debuggen kannst)

Wenn du Agents ohne explizite Stop Reasons shippst, sind deine Dashboards 100% Vibes: “it didn’t work” → “it timed out” → “maybe the model was bad”.

Wir loggen pro Run genau einen stop_reason und behandeln ihn wie einen Vertrag. Das ist der Unterschied zwischen:

  • “agent feels flaky”
  • “60% of runs stop on tool_timeout:http.get because the upstream is dying”

Typische Stop Reasons, die wir wirklich sehen:

  • finish
  • max_steps, max_seconds, max_usd
  • policy_deny:<tool>
  • approval_timeout
  • tool_timeout:<tool>
  • tool_error_exhausted:<tool>
  • loop_detected
  • operator_kill

Beispiel‑Event (so eine langweilige Zeile spart dir später gerne mal einen Tag):

JSON
{
  "request_id": "req_9f2c",
  "tenant": "acme-prod",
  "steps": 25,
  "tool_calls": 17,
  "usd_estimate": 1.03,
  "stop_reason": "max_usd"
}

Ja, du kannst fancy werden mit „partial success“ und „degraded mode“. Start mit einem Stop Reason. Mach ihn konsistent. Dein On‑Call‑Ich wird’s dir danken.

Multi‑Tenant‑Realität (wo die meisten Incidents wohnen)

Multi‑Tenant‑Agent‑Systeme scheitern sehr vorhersehbar:

  • falscher Tenant‑Context
  • Cross‑Tenant‑Caches
  • geteilte Credentials
  • “globale” Tools, die still alles sehen

Schutzmaßnahmen:

  • Tenant‑ID wird vom Entry point gesetzt, nie vom Modell
  • Caches sind nach Tenant + Environment gekeyed
  • Credentials sind nach Tenant + Environment gescoped
  • Audit Logs enthalten immer die Tenant‑ID

Wenn eins davon fehlt, leakst du irgendwann Daten.

Rate Limits & Circuit Breakers (weil Agents Outages verstärken)

Wenn eine Dependency flaky ist, ist ein Agent im Grunde ein Failure‑Amplifier: er retried, sucht Alternativen, probiert nochmal, “verifiziert”, probiert nochmal.

So machst du aus:

  • “Upstream‑API liefert 2 Minuten lang 500er” →
  • “wir senden 80k Requests und werden eine Stunde rate‑limited”

Wir machen drei langweilige Dinge:

  1. Per‑Tool Concurrency Caps (pro Tenant). Beispiel: Browser‑Tool max 2 parallele Runs. Mehr ist Self‑DDoS.
  2. Rate Limiting an der Tool‑Boundary. Nicht im Modell.
  3. Circuit Breakers, die schnell failen, wenn die Error Rate spiked.

Pseudo‑Code:

TS
const httpGet = rateLimit({ perTenantRps: 5 }, async (url: string) => {
  return fetch(url, { signal: AbortSignal.timeout(8000) });
});

const breaker = new CircuitBreaker({
  windowMs: 30_000,
  failureRate: 0.5,
  cooldownMs: 60_000,
});

const res = await breaker.exec(() => httpGet("https://api.example.com/health"));

Wenn der Breaker offen ist, stoppen wir den Run mit einem klaren Reason (tool_unhealthy:http.get), und wir tun nicht so, als könnte das Modell sich „durch ein Outage durch‑reasonen“. Kann es nicht. Es verbrennt nur Budget.

Rollout, wie wir ihn machen (weil Agents kein Vertrauen am Tag 1 verdienen)

Shipping nach Prod ist kein Binary‑Switch.

Wir shippen so:

  1. nur interne Nutzer
  2. nur Read‑Only‑Tools
  3. kleiner Canary‑Anteil
  4. Permissions schrittweise erweitern (mit Approvals für Writes)
  5. und erst dann über “autonomous” Verhalten nachdenken

Und ja: der Kill Switch liegt die ganze Zeit griffbereit.

Wo „Memory“ hingehört (Spoiler: nicht in Prompts)

Wenn du alles in den Prompt packst, bekommst du:

  • aufgeblasene Context Windows
  • schlechtere Antworten (das Modell ersäuft im Rauschen)
  • höhere Kosten

Wir bevorzugen:

  • kleines strukturiertes Scratchpad pro Run
  • Artifacts extern gespeichert (Drafts, Notes, Zitate)
  • optionales Long‑Term‑Memory mit strengem Scoping + TTL

Memory ist ein Product Feature. Behandle es so. Teste es. Auditiere es. Scope es.

Incident‑Response (was du vor dem ersten Pager bereit haben willst)

Agents werden failen. Die Frage ist, ob du den Schaden schnell stoppen kannst.

Bevor du shippst, stell sicher, dass du kannst:

  • ein Tool (Browser, E‑Mail, Payments) deaktivieren, ohne Code zu deployen
  • einen Tenant deaktivieren, ohne alle anderen mitzureißen
  • einen einzelnen Run per Request‑ID finden
  • einen Run in einer sicheren Umgebung replayen
  • die Frage „welche Tool Calls sind passiert?“ in unter einer Minute beantworten

Wenn du das nicht kannst, wird der erste Incident langsam und schmerzhaft.

Tests & Replay (weil „works on my prompt“ kein Test ist)

Die nervige Wahrheit: Agent‑Verhalten ändert sich, wenn du irgendwas änderst. Model‑Version. Prompt. Tool‑Schema. Upstream‑API‑Responses. Sogar Timeouts.

Darum testen wir den Stack, nicht nur den Prompt:

  • record/replay von Tool‑Responses in einer Sandbox (gleiche Inputs, stabile Outputs)
  • eine kleine Suite “golden” Tasks auf jedem Deploy
  • Asserts auf Traces, nicht nur auf den Final Text (Steps, Tools, stop_reason)

Das hat bei uns echte Regressionen gefunden:

  • ein Tool‑Schema‑Rename ließ den Agent auf Validation Errors loopen
  • ein Retry‑Tweak hat Tool Calls verdoppelt (Kosten ~2× über Nacht)

Wenn du einen Run nicht deterministisch replayen kannst, wird Debugging zu Archäologie.

Build Order: „boring first“

Wenn du von null anfängst, bau in dieser Reihenfolge:

  1. tool wrapper (allowlist + timeouts + idempotency)
  2. budgets (steps/time) + stop reasons
  3. Audit Events (Tool Calls mit Args‑Hash)
  4. Kill Switch
  5. und erst dann: fancy Planning, Memory, Multi‑Agent‑Routing

Die meisten Teams machen’s andersrum, weil Demos „smart“ belohnen. Production belohnt „stoppt, wenn’s komisch wird“.

Realer Ausfall

Wir haben einmal einen „funktionierenden“ Agent ohne strukturierte Audit Events geshippt. Dann hat er in Production etwas Komisches gemacht.

Postmortem‑Timeline:

  • „es hat das Tool sehr oft aufgerufen“
  • „wir denken, es hat retried“
  • „wir können nicht sagen, welche Args genutzt wurden“

Das hat uns ~einen halben Tag Engineering‑Zeit gekostet – hauptsächlich Streit darüber, was eigentlich passiert ist.

Fix:

  • jeder Tool Call emittiert ein strukturiertes Event (Tool, Args‑Hash, Dauer, Status)
  • eine Request‑ID wird durch alles durchgereicht
  • der Kill Switch ist ein Click, kein Deploy

Abwägungen

  • Mehr Instrumentation = mehr Code.
  • Mehr Policy = mehr „agent refused“‑Fälle.
  • Es ist trotzdem billiger als blind zu debuggen.

Wann du keinen Agent‑Stack bauen solltest

Wenn das ein One‑Off‑Script ist, das einmal pro Woche intern läuft: übertreib’s nicht. Aber wenn es Prod‑Systeme oder echtes Geld berührt, brauchst du den Stack. Punkt.

Nicht sicher, ob das dein Fall ist?

Agent gestalten ->
⏱️ 10 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.