DEV Community

Cover image for Fehlerbehandlung für KI-Agenten: Retry, Timeout, Backoff und Circuit-Breaker-Muster
Emre Demir
Emre Demir

Posted on • Originally published at apidog.com

Fehlerbehandlung für KI-Agenten: Retry, Timeout, Backoff und Circuit-Breaker-Muster

Robuste Fehlerbehandlung für KI-Agenten: Retries, Timeouts, Circuit Breaker und Idempotenz

Ihr Agent ruft eine API auf, erhält eine 429 Too Many Requests und versucht es sofort erneut. Es folgt die nächste 429, dann der nächste Retry. Ohne Begrenzung, Backoff und serverseitige Vorgaben entsteht eine Schleife, die einen bereits gedrosselten Dienst weiter belastet, bis der Lauf fehlschlägt oder unnötige Kosten verursacht. Diese Art naiver Fehlerbehandlung ist ein häufiges Thema im Anthropic-SDK-Diskussionsforum.

Apidog noch heute ausprobieren

Robuste Agenten unterscheiden sich nicht primär durch das Modell, sondern durch ihr Verhalten bei langsamen, gedrosselten oder fehlerhaften Tool-Aufrufen. Dieser Artikel zeigt vier grundlegende Muster:

  1. Retries mit exponentiellem Backoff und Jitter
  2. Timeouts pro Aufruf und pro Agentenlauf
  3. Circuit Breaker für ausgefallene Abhängigkeiten
  4. Idempotenzschlüssel für sichere schreibende Retries

Anschließend testen Sie diese Pfade mit einem Mock, statt Fehler erst in Produktion zu entdecken. Für den breiteren Kontext lesen Sie auch warum KI-Agenten in der Produktion ausfallen.

Sie können Wiederherstellung nicht gegen eine fehlerfreie API testen

In der Entwicklung funktioniert eine Abhängigkeit oft zuverlässig: Aufrufe sind erfolgreich, die Demo läuft, und der Recovery-Code bleibt unberührt. Das erste Mal, dass Backoff, Timeout oder Circuit Breaker ausgeführt werden, ist dann im echten Ausfall mit echten Nutzern.

Testen Sie Fehlerpfade deshalb absichtlich:

  1. Erstellen Sie einen Mock für die externe API.
  2. Konfigurieren Sie Antworten wie 429, 500, Timeouts oder fehlerhafte Bodies.
  3. Richten Sie das Tool Ihres Agenten auf die Mock-URL.
  4. Prüfen Sie Wartezeiten, Retry-Anzahl, Header und Abbruchverhalten.

Mit Apidog können Sie einen Mock bereitstellen und Antwortsequenzen skripten. So wird ein Produktionsfehler zu einem reproduzierbaren Testfall.

Retries mit exponentiellem Backoff und Jitter

Ein sofortiger Retry kann bei einem kurzen Netzwerkfehler helfen. Bei einem überlasteten Dienst verschärft er jedoch das Problem: Viele Clients schlagen gleichzeitig fehl und versuchen gleichzeitig erneut.

Verwenden Sie stattdessen:

  • Exponentielles Backoff: Wartezeiten wachsen beispielsweise von 1 auf 2, 4 und 8 Sekunden.
  • Jitter: Ein zufälliger Anteil verhindert, dass alle Clients synchron erneut senden.
  • Maximale Verzögerung: Verhindert minutenlange Wartezeiten.
  • Maximale Retry-Anzahl: Beendet dauerhafte Fehler kontrolliert.

Ein einfaches Python-Beispiel:

import random
import time
import requests

MAX_RETRIES = 4
MAX_DELAY_SECONDS = 16

RETRYABLE_STATUS_CODES = {429, 500, 502, 503, 504}


def request_with_retry(method: str, url: str, **kwargs):
    for attempt in range(MAX_RETRIES + 1):
        try:
            response = requests.request(method, url, timeout=(3, 15), **kwargs)

            if response.status_code not in RETRYABLE_STATUS_CODES:
                response.raise_for_status()
                return response

            if attempt == MAX_RETRIES:
                response.raise_for_status()

        except requests.Timeout:
            if attempt == MAX_RETRIES:
                raise

        except requests.ConnectionError:
            if attempt == MAX_RETRIES:
                raise

        base_delay = min(2 ** attempt, MAX_DELAY_SECONDS)
        jitter = random.uniform(0, base_delay * 0.25)
        time.sleep(base_delay + jitter)

    raise RuntimeError("Unerreichbarer Code")
Enter fullscreen mode Exit fullscreen mode

Wiederholen Sie nicht jeden Fehler blind. Typische Kandidaten für Retries sind:

  • Verbindungsfehler
  • Timeouts
  • 429 Too Many Requests
  • temporäre Serverfehler wie 500, 502, 503 und 504

Fehler wie 400 Bad Request, 401 Unauthorized oder Validierungsfehler sollten normalerweise nicht erneut versucht werden.

Das Anthropic SDK übernimmt einen Teil dieser Logik für eigene API-Aufrufe. Externe APIs, die Ihre Agent-Tools verwenden, müssen Sie jedoch selbst absichern. Besonders bei Zahlungen oder anderen kritischen Aktionen ist das entscheidend; mehr dazu finden Sie in der Übersicht zur Wiederholungslogik für hochriskante APIs.

Legen Sie für jeden Aufruf ein Timeout fest

Retries helfen nur, wenn ein Aufruf auch tatsächlich fehlschlägt. Eine Anfrage kann jedoch eine Verbindung öffnen und dann dauerhaft hängen. Ohne Timeout blockiert der Tool-Aufruf den Agentenlauf, verbraucht Zeit und möglicherweise Token-Budget.

Definieren Sie mindestens drei Budgets:

Budget Zweck
Verbindungs-Timeout Maximale Zeit für den Verbindungsaufbau
Lese-Timeout Maximale Zeit bis zur Antwort
Gesamtbudget des Agenten Obergrenze für den gesamten Lauf

Beispiel mit requests:

response = requests.get(
    "https://api.example.com/search",
    timeout=(3, 15),  # 3 s Connect, 15 s Read
)
Enter fullscreen mode Exit fullscreen mode

Für einen Agentenlauf können Sie zusätzlich ein Gesamtbudget verfolgen:

import time

RUN_BUDGET_SECONDS = 45
started_at = time.monotonic()

def ensure_budget_remaining():
    elapsed = time.monotonic() - started_at
    if elapsed >= RUN_BUDGET_SECONDS:
        raise TimeoutError("Gesamtzeitbudget des Agentenlaufs überschritten")
Enter fullscreen mode Exit fullscreen mode

Wählen Sie Werte anhand realer Latenzdaten. Ein praktikabler Ausgangspunkt ist ein Timeout oberhalb des p99-Werts der Abhängigkeit plus Puffer. Zu kurze Werte brechen legitime Anfragen ab; zu lange Werte lassen Agenten bei defekten Diensten hängen.

Streaming-Antworten benötigen ein separates Budget. Eine lange, aber fortlaufende Ausgabe ist nicht dasselbe wie ein blockierter Socket.

Circuit Breaker: Bei Ausfällen schnell fehlschlagen

Backoff hilft bei kurzzeitiger Überlastung. Wenn eine Abhängigkeit vollständig ausgefallen ist, sollten Sie nicht bei jeder neuen Agentenanfrage mehrere langsame Timeouts abwarten.

Ein Circuit Breaker hat drei Zustände:

  • Closed: Anfragen werden normal gesendet, Fehler werden gezählt.
  • Open: Nach zu vielen Fehlern werden neue Anfragen sofort abgelehnt.
  • Half-open: Nach einer Abkühlphase darf eine Probe-Anfrage durch. Bei Erfolg schließt der Breaker, bei Fehler öffnet er wieder.

Vereinfachtes Beispiel:

import time


class CircuitBreaker:
    def __init__(self, failure_threshold=5, reset_timeout=30):
        self.failure_threshold = failure_threshold
        self.reset_timeout = reset_timeout
        self.failures = 0
        self.opened_at = None

    def allow_request(self):
        if self.opened_at is None:
            return True

        if time.monotonic() - self.opened_at >= self.reset_timeout:
            return True  # Half-open: eine Probe zulassen

        return False

    def record_success(self):
        self.failures = 0
        self.opened_at = None

    def record_failure(self):
        self.failures += 1
        if self.failures >= self.failure_threshold:
            self.opened_at = time.monotonic()
Enter fullscreen mode Exit fullscreen mode

Wichtig: Verwenden Sie einen Circuit Breaker pro Abhängigkeit. Eine ausgefallene Such-API darf nicht verhindern, dass der Agent eine weiterhin funktionierende Abrechnungs- oder Datenbank-API nutzt.

Retries mit Idempotenzschlüsseln sicher machen

Ein Retry ist nicht automatisch sicher.

Beispiel: Ihr Agent sendet POST /charge. Der Server verarbeitet die Zahlung, aber die Antwort geht auf dem Rückweg verloren. Der Agent sieht nur einen Fehler und wiederholt den Request. Ohne Schutz kann die Zahlung doppelt ausgeführt werden.

Ein Idempotenzschlüssel löst dieses Problem:

  1. Der Client erzeugt für eine logische Aktion eine eindeutige ID.
  2. Er sendet diese ID im Header Idempotency-Key.
  3. Der Server speichert das Ergebnis beim ersten Request.
  4. Kommt derselbe Schlüssel erneut, liefert der Server das gespeicherte Ergebnis zurück, statt die Aktion erneut auszuführen.
import uuid
import requests

idempotency_key = str(uuid.uuid4())

response = requests.post(
    "https://api.example.com/charge",
    json={"amount": 4999, "currency": "EUR"},
    headers={
        "Idempotency-Key": idempotency_key,
    },
    timeout=(3, 15),
)
Enter fullscreen mode Exit fullscreen mode

Der Schlüssel muss für alle Retries derselben logischen Aktion stabil bleiben:

# Richtig: einmal vor der Retry-Schleife erzeugen
idempotency_key = str(uuid.uuid4())

for attempt in range(3):
    send_charge(idempotency_key=idempotency_key)
Enter fullscreen mode Exit fullscreen mode
# Falsch: jeder Versuch erhält einen neuen Schlüssel
for attempt in range(3):
    send_charge(idempotency_key=str(uuid.uuid4()))
Enter fullscreen mode Exit fullscreen mode

Verwenden Sie Idempotenzschlüssel für alle zustandsändernden Aktionen:

  • Zahlungen und Abbuchungen
  • Bestellungen
  • E-Mails und Nachrichten
  • Erstellen oder Ändern von Datensätzen
  • Provisionierung und Deployments

Der Leitfaden zu Idempotenzschlüsseln behandelt Generierung und serverseitige Verarbeitung ausführlicher.

Ratenbegrenzungen und die RateLimitError-Schleife überleben

Eine 429 ist nicht nur ein Fehler, sondern oft eine konkrete Anweisung. Viele APIs senden den Header Retry-After, der angibt, wie lange Sie warten sollen.

Beispiel:

HTTP/1.1 429 Too Many Requests
Retry-After: 30
Enter fullscreen mode Exit fullscreen mode

Warten Sie in diesem Fall mindestens 30 Sekunden. Ein Retry nach zwei Sekunden produziert wahrscheinlich nur die nächste 429.

from email.utils import parsedate_to_datetime
from datetime import datetime, timezone


def parse_retry_after(value: str) -> float:
    try:
        return max(0, float(value))
    except ValueError:
        retry_at = parsedate_to_datetime(value)
        now = datetime.now(timezone.utc)
        return max(0, (retry_at - now).total_seconds())
Enter fullscreen mode Exit fullscreen mode

Integrieren Sie die Servervorgabe in Ihre Retry-Logik:

if response.status_code == 429:
    retry_after = response.headers.get("Retry-After")

    if retry_after:
        delay = parse_retry_after(retry_after)
    else:
        delay = min(2 ** attempt, MAX_DELAY_SECONDS) + random.uniform(0, 1)

    time.sleep(delay)
    continue
Enter fullscreen mode Exit fullscreen mode

Eine Antwort bei überschrittener Ratenbegrenzung sollte daher diese Reihenfolge auslösen:

  1. Retry-After lesen.
  2. Mindestens so lange warten.
  3. Falls der Header fehlt: exponentielles Backoff mit Jitter verwenden.
  4. Nach dem Retry-Limit sauber fehlschlagen.

Ein separater SDK-Thread beschreibt dieselbe Fehlerklasse: Zu frühe Wiederholungen machen eine bestehende Drosselung schlimmer.

Zusätzlich zur Wiederherstellung brauchen Sie proaktive Taktung. Wenn ein Anbieter ein festes Anfragelimit pro Minute vorgibt, kann ein Token-Bucket helfen, die Grenze gar nicht erst zu erreichen.

So testen Sie den Wiederherstellungspfad

Testen Sie nicht nur den Happy Path. Erzwingen Sie die Fehler, die eine gesunde API in der lokalen Entwicklung selten liefert.

Szenario 1: 429, dann 500, dann Erfolg

Skripten Sie für einen Mock-Endpunkt diese Reihenfolge:

  1. 429 Too Many Requests mit Retry-After: 2
  2. 500 Internal Server Error
  3. 200 OK mit gültigem Response-Body

Prüfen Sie danach:

  • Der Agent wartete nach der 429 mindestens zwei Sekunden.
  • Der Retry nach dem 500 nutzte Backoff.
  • Der dritte Aufruf war erfolgreich.
  • Das konfigurierte Retry-Limit wurde nicht überschritten.

Szenario 2: Permanenter Fehler

Lassen Sie den Mock bei jedem Aufruf fehlschlagen.

Prüfen Sie:

  • Der Agent beendet die Retry-Schleife am Limit.
  • Der Fehler ist für Aufrufer verständlich.
  • Der Lauf wartet nicht unendlich.
  • Die Anzahl der Requests entspricht Ihrer Konfiguration.

Szenario 3: Circuit Breaker

Lassen Sie mehrere aufeinanderfolgende Requests mit 500 oder Timeout fehlschlagen.

Prüfen Sie:

  • Nach Erreichen des Fehlerlimits öffnet der Circuit Breaker.
  • Weitere Requests schlagen schnell fehl.
  • Nach dem Abkühlfenster wird genau eine Probe-Anfrage zugelassen.
  • Ein Erfolg schließt den Breaker wieder.

Szenario 4: Idempotenz bei verlorener Antwort

Skripten Sie einen mutierenden Request so:

  1. Der Mock akzeptiert POST /charge.
  2. Der Mock verarbeitet die Aktion, verwirft aber die Antwort.
  3. Der Agent führt einen Retry aus.
  4. Der Mock erhält den Request erneut.

Prüfen Sie:

  • Beide Requests haben denselben Idempotency-Key.
  • Der Mock zählt nur eine logische Aktion.
  • Der zweite Request erhält das Ergebnis der ersten Aktion.

Ein neuer Schlüssel beim Retry oder zwei ausgeführte Aktionen zeigen einen Fehler, bevor ein Kunde doppelt belastet wird. Das End-to-End-Vorgehen finden Sie auch im Leitfaden zum Testen von Agenten, die Ihre APIs aufrufen.

Die Checkliste zur Fehlerbehebung

Vor dem Produktionsstart sollte Ihr Agent diese Punkte erfüllen:

  • Jeder ausgehende Aufruf hat ein Verbindungs-Timeout, ein Lese-Timeout und ein Gesamt-Laufbudget.
  • Retries verwenden exponentielles Backoff mit Jitter.
  • Verzögerung und Retry-Anzahl sind begrenzt.
  • 429-Antworten respektieren Retry-After.
  • Bei fehlendem Retry-After greift Backoff als Fallback.
  • Ein Circuit Breaker schützt jede Abhängigkeit separat.
  • Zustandsändernde Requests verwenden stabile Idempotenzschlüssel.
  • Der Abbruchpfad liefert einen klaren Fehler statt einer Endlosschleife.
  • Jeder Fehlerpfad wird gegen einen Mock getestet.

Wenn diese Punkte erfüllt sind, erholt sich Ihr Agent bewusst und reproduzierbar – nicht nur zufällig in einer grünen Demo.

Wo Apidog passt – und wo nicht

Apidog zur Simulation und Prüfung von API-Fehlerfällen

Apidog kann in diesem Workflow drei Aufgaben übernehmen:

  1. Abhängigkeiten mocken: Ersetzen Sie Live-Dienste durch steuerbare Mock-Endpunkte.
  2. Fehlersequenzen skripten: Simulieren Sie 429 mit Retry-After, 500, Timeouts und fehlerhafte Response-Bodies.
  3. Requests validieren: Prüfen Sie Header, Request-Form und Aufrufanzahl – etwa, ob ein Idempotency-Key vorhanden und über Retries hinweg stabil ist.

Das Tool ersetzt nicht Ihre Retry-, Timeout- oder Circuit-Breaker-Implementierung. Es hilft Ihnen aber dabei, die Fehler zu erzeugen, gegen die Ihr Agent bestehen muss, und das resultierende Verhalten zu überprüfen.

Häufig gestellte Fragen

Übernimmt das Anthropic SDK Retries für mich?

Für eigene API-Aufrufe übernimmt das SDK Teile der Retry-Logik, einschließlich exponentiellem Backoff und der Berücksichtigung von Retry-After. Die Retry-Obergrenze wird über eine max_retries-Option konfiguriert.

Das gilt nicht automatisch für andere APIs, die Ihre Agent-Tools aufrufen. Diese Abhängigkeiten benötigen dieselben Muster in Ihrem eigenen Code.

Wann benötige ich einen Idempotenzschlüssel?

Bei jedem Request, der Zustand erstellt oder verändert: Zahlungen, Bestellungen, versendete Nachrichten oder neue Datensätze.

Reine Lesezugriffe können normalerweise sicher erneut versucht werden. Für schreibende Aktionen gilt: Erzeugen Sie den Schlüssel einmal pro logischer Aktion und behalten Sie ihn über alle Retries hinweg bei.

Proben Sie diese Woche einen Fehler

Sie müssen nicht alle Muster gleichzeitig implementieren. Beginnen Sie mit dem Fehler, der für Ihren Agenten den größten Schaden verursachen würde – oft eine 429-Schleife oder ein nicht-idempotenter Zahlungs-Retry.

Skripten Sie dafür einen Mock:

  • Geben Sie eine 429 mit Retry-After zurück.
  • Verwerfen Sie eine Antwort nach einer mutierenden Aktion.
  • Prüfen Sie, wie lange der Agent wartet.
  • Prüfen Sie, ob er denselben Idempotenzschlüssel erneut sendet.
  • Prüfen Sie, ob er nach dem Retry-Limit sauber abbricht.

Wenn Sie einen kontrollierten Backoff und genau eine logische Aktion sehen, haben Sie einen belastbaren Grund, Ihrem Agenten auch außerhalb der Demo zu vertrauen.

Top comments (0)