Özgür Işık Damar
9 Min Lesezeit

Agentische KI ist meistens while(true) mit Vibes

Produktionserfahrungen mit autonomen Agenten in lang laufenden Schleifen, Fallback-Pattern, die wirklich funktionieren, und der Tag, an dem dein Agent 47-mal hintereinander einen Retry beschloss.

agentic-aiproductionengineering-lessons

Als ich meine erste Agent-Schleife in Produktion brachte, lief sie drei Stunden lang einwandfrei — und produzierte dann still und leise eine Rechnung von 312 USD, weil sie denselben Tool-Aufruf siebenundvierzig Mal wiederholte. Die Upstream-API hatte angefangen, ein höflich formuliertes 200 OK zurückzugeben, dessen Body "service temporarily unavailable" enthielt. Das Modell, hilfsbereit wie immer, hielt das für einen kurzen Schluckauf. Siebenundvierzig Mal in Folge.

Man lernt von Agenten viel über verteilte Systeme. Nichts davon ist neu — es sind dieselben Lektionen, die wir 2008 aus Cronjobs gelernt haben — aber das Modell bringt eine spezielle Art von Selbstvertrauen mit, die die Failure-Modes lauter macht.

Die naive Form

Jedes Agentic-AI-Tutorial beginnt gleich. Du schreibst eine Schleife:

async function agentLoop(task: string) {
  const messages = [{ role: "user", content: task }];
 
  while (true) {
    const response = await llm.complete({ messages, tools });
 
    if (response.stop_reason === "end_turn") {
      return response.content;
    }
 
    for (const toolCall of response.tool_calls) {
      const result = await runTool(toolCall);
      messages.push({ role: "tool", content: result });
    }
  }
}

In der Demo läuft das schön. Das Modell macht ein paar Runden, entscheidet, dass es fertig ist, kehrt zurück. Du gehst live.

In Produktion ist diese Schleife eine Geldkanone, die auf dein Konto zielt.

Wo es bricht

Eine kurze, nicht vollständige Liste, wie ich diese Schleife scheitern gesehen habe:

  • Das Modell entschied, ein Funktionsaufruf sei "fast richtig", probierte zwölf Varianten derselben Argumente, gab dann auf.
  • Ein Tool gab 500 zurück. Das Modell versuchte es erneut. Tool gab 500 zurück. Modell versuchte es erneut — mit denselben Argumenten. 91 Mal, bis der Budget-Alarm losging.
  • Das Modell traf auf ein Tool-Ergebnis, das es nicht verstand, halluzinierte ein zweites Tool, das nicht existierte, und versuchte sich zu retten, indem es das halluzinierte Tool aufrief.
  • Die Aufgabe des Nutzers war nach Schritt zwei eigentlich erledigt, aber das Modell rief weiterhin Such-Tools auf, "nur zur Bestätigung", bis es das 100k-Context-Limit erreichte und crashte.
  • In einem Multi-Agent-System begannen zwei Agenten, sich gegenseitig Tool-Aufrufe zu Ping-Pong-en. Sie waren dabei sehr höflich.

Das Kernproblem ist nicht die Schleife. Es ist, dass das Modell keine Meinung dazu hat, wie lange es laufen soll. Es läuft glücklich weiter bis zum Wärmetod des Universums — oder bis du es ihm in Rechnung stellst.

Was tatsächlich hilft

Drei Kontrollen, in der Reihenfolge, wie viel Schmerz sie mir erspart haben.

1. Budget — nicht Retry-Anzahl

Tool-Aufrufe zu zählen ist die falsche Einheit. Token-Verbrauch ist die richtige, denn das tut weh. Verfolge ihn innerhalb der Schleife und brich kurz:

type Budget = {
  maxInputTokens: number;
  maxOutputTokens: number;
  maxToolCalls: number;
  spentInput: number;
  spentOutput: number;
  toolCalls: number;
};
 
function overBudget(b: Budget): boolean {
  return (
    b.spentInput > b.maxInputTokens ||
    b.spentOutput > b.maxOutputTokens ||
    b.toolCalls > b.maxToolCalls
  );
}
 
async function agentLoop(task: string, budget: Budget) {
  const messages: Message[] = [{ role: "user", content: task }];
 
  while (!overBudget(budget)) {
    const response = await llm.complete({ messages, tools });
    budget.spentInput += response.usage.input_tokens;
    budget.spentOutput += response.usage.output_tokens;
 
    if (response.stop_reason === "end_turn") return response.content;
 
    for (const call of response.tool_calls) {
      budget.toolCalls += 1;
      const result = await runTool(call);
      messages.push({ role: "tool", content: result });
    }
  }
 
  // Kein Throw — strukturiertes Teilergebnis zurückgeben.
  return {
    status: "budget_exceeded",
    partial: messages.at(-1),
    spent: budget,
  };
}

Der billige Erkenntnisgewinn: Wenn das Budget aufgebraucht ist, wirf keine Exception. Gib ein strukturiertes Teilergebnis zurück. Eine Exception zerstört den Kontext des Aufrufers. Ein Teilergebnis mit status: "budget_exceeded" lässt den Aufrufer entscheiden — eskalieren, auf einen einfacheren Pfad wechseln, oder einfach loggen.

2. Tool-Aufrufe sind nicht idempotent. Mach sie idempotent.

Wenn das Modell ein Tool neu aufruft, sollen sich Seiteneffekte nicht vervielfachen. Wickel jedes Tool in einen Idempotency-Key, der aus den Argumenten abgeleitet ist, und cache das Ergebnis innerhalb der Schleife:

function toolKey(name: string, args: unknown): string {
  return `${name}:${stableStringify(args)}`;
}
 
const toolCache = new Map<string, ToolResult>();
 
async function runToolMemoized(call: ToolCall) {
  const key = toolKey(call.name, call.arguments);
  const hit = toolCache.get(key);
  if (hit) {
    return { ...hit, _cached: true };
  }
  const result = await runTool(call);
  toolCache.set(key, result);
  return result;
}

Ruft das Modell dasselbe Tool mit denselben Argumenten zweimal in einer Schleife auf, lieferst du das gecachte Ergebnis und kennzeichnest es. Das Modell folgert meist aus dem Hinweis, dass es etwas anderes probieren soll. Manchmal nicht. Dafür ist das Budget da.

3. Der Fallback ist ein echtes Produkt-Feature

Jede Agent-Schleife braucht eine finale Antwort, wenn das Budget aufgebraucht, die Geduld zu Ende oder ein Tool-Fehler nicht behebbar ist. Der Fallback ist keine Exception. Der Fallback ist eine bescheidene, strukturierte Antwort, die sagt, was versucht wurde, was gelernt wurde und was der Agent als nächsten Schritt vorschlägt.

type AgentResult =
  | { status: "ok"; answer: string; trace: TraceEntry[] }
  | { status: "partial"; bestGuess: string; missing: string[]; trace: TraceEntry[] }
  | { status: "failed"; reason: string; trace: TraceEntry[] };

Wenn dein Agent { status: "failed", reason: "habe mein Bestes gegeben" } zurückgibt, ist das kein Bug. Das ist die Schleife, die weiß, wann sie stoppen muss. Der schwere Teil von agentischer KI ist genau das: zu wissen, wann man aufhört, es zu versuchen. Das Modell wird es dir nicht sagen. Das musst du tun.

Trade-offs

Für einen Agenten mit fünf Runden, der eine einzelne Suche durchführt und zusammenfasst, brauchst du nichts davon. Du brauchst jede Zeile, sobald der Agent mehr als ein Tool aufruft, länger als eine Minute läuft oder pro Aufruf mehr als einen Cent kostet.

Das Pattern skaliert nach unten auf null — budget=Infinity ist ein No-Op — und nach oben auf Stunden Laufzeit. Die Kosten, es zu schreiben, sind klein. Die Kosten, es nicht zu schreiben, waren bei mir eine Rechnungsposition mit "API-Nutzung" und einer vierstelligen Zahl daneben.

Die Zusammenfassung, die dein Chef will

Agentische KI ist meistens eine while(true)-Schleife mit Vibes. Die Vibes sind der einfache Teil. Die Schleife auch. Der schwere Teil ist das Budget, die Idempotenz und der Fallback — drei Dinge, die jeder Backend-Engineer schon einmal gebaut hat, für Cronjobs, 2008.

Das Modell fügt verteilten Systemen nichts Neues hinzu. Es macht die Failures nur ausdrucksstärker.

// wenn du schon hier bist