DEV Community

Cover image for KI-Agent-Idempotenz: Doppelte Abrechnungen durch Wiederholungen stoppen
Emre Demir
Emre Demir

Posted on Originally published at apidog.com

KI-Agent-Idempotenz: Doppelte Abrechnungen durch Wiederholungen stoppen

Ihr Agent hat den Zahlungs-Endpunkt aufgerufen. Die Anfrage wurde verarbeitet, die Abbuchung erfolgte, und dann kam es auf dem Rückweg zu einem Timeout der Antwort. Der Agent sah keinen 200-Status und wiederholte den Aufruf. Dem Kunden wurde zweimal etwas berechnet, obwohl in den Protokollen kein offensichtlicher Fehler erscheint.

Apidog noch heute testen

Das ist ein typischer Agenten-Fehlermodus: Wiederholungen machen Integrationen robuster, erhöhen bei nicht-idempotenten Schreibvorgängen aber das Risiko doppelter Effekte. Die Lösung ist Idempotenz: Mehrere identische Anfragen müssen denselben Serverzustand erzeugen wie eine einzelne Anfrage.

Dieser Beitrag zeigt, wie Sie Idempotenz für Agenten implementieren, testen und nachvollziehbar machen. Doppelte Schreibvorgänge sind oft die eigentliche Ursache hinter Meldungen wie „Der Agent hat es zweimal getan“ – ein Thema, das auch der Beitrag Warum KI-Agenten in der Produktion versagen behandelt.

Idempotenz für Agenten

Warum Agenten besonders anfällig für Duplikate sind

Agenten erzeugen häufiger doppelte Schreibvorgänge als Menschen:

  1. Aggressive Retries: Frameworks wiederholen Netzwerkfehler standardmäßig. Backoff und Circuit Breaker verbessern die Verfügbarkeit, erhöhen aber auch die Zahl der Anfragen an Ihren Server. Mehr dazu im Leitfaden zur Fehlerbehebung bei Agenten.
  2. Timeouts sind mehrdeutig: Ein 504 kann bedeuten, dass der Schreibvorgang nie stattfand – oder dass er erfolgreich war und nur die Antwort verloren ging.
  3. Aufgaben-Neustarts: Scheitert Schritt vier eines mehrstufigen Agentenablaufs, kann ein naiver Neustart auch Schritt eins erneut ausführen und beispielsweise eine zweite Bestellung erstellen.

Das Problem sind also nicht fehlerhafte Anfragen. Agenten senden häufig korrekte Anfragen mehrfach.

Was Idempotenz garantiert

Eine Operation ist idempotent, wenn ihre wiederholte Ausführung denselben Effekt hat wie eine einmalige Ausführung.

GET, PUT und DELETE sind laut RFC 9110 idempotent. POST ist es nicht – deshalb sind besonders riskante Vorgänge meist POST-Aufrufe:

  • Bestellung erstellen
  • Zahlung auslösen
  • Nachricht senden
  • Überweisung starten

Wichtig:

  • Idempotent bedeutet nicht sicher. DELETE ist idempotent, aber destruktiv.
  • Idempotent bedeutet nicht zwingend identische HTTP-Antworten. Entscheidend ist, dass der Serverzustand unverändert bleibt: eine Abbuchung, eine Bestellung, eine E-Mail.

Für gefährliche Agenten-Tools brauchen Sie Idempotenz zusätzlich zu restriktiven Berechtigungen, etwa nach dem Prinzip der API-Schlüssel mit geringsten Berechtigungen für Agenten.

POST mit Idempotenzschlüsseln absichern

Das Standardmuster ist ein clientseitig erzeugter Schlüssel im Header. Der Server speichert Schlüssel, Anfrage-Fingerabdruck und Ergebnis. Wiederholt ein Client dieselbe Anfrage mit demselben Schlüssel, gibt der Server das gespeicherte Ergebnis zurück, statt die Arbeit erneut auszuführen.

Stripe hat dieses Muster populär gemacht. Außerdem gibt es einen IETF-Entwurf für das Header-Feld Idempotency-Key.

POST /v1/payments HTTP/1.1
Host: api.yourservice.com
[REDACTED CREDENTIAL] [REDACTED]...
Idempotency-Key: 9f2b7c14-6d3a-4b18-9d55-1e2a7c0b4f31
Content-Type: application/json

{
  "amount": 4900,
  "currency": "usd",
  "customer_id": "cus_8812",
  "description": "Pro plan, August"
}
Enter fullscreen mode Exit fullscreen mode

Der Schlüssel hat nur eine Bedeutung: Dies ist dieselbe logische Operation.

Schlüssel so erzeugen, dass Agenten sie wiederverwenden

Der häufigste Implementierungsfehler: Der Tool-Wrapper generiert bei jedem HTTP-Versuch eine neue UUID. Dann unterscheiden sich Retries technisch voneinander, und Idempotenz greift nicht.

Generieren Sie den Schlüssel, wenn der Agent die Aktion entscheidet – nicht bei jedem Request-Versuch.

import uuid

class PaymentTool:
    def __init__(self, client):
        self.client = client
        self._keys = {}

    def charge(self, task_id, step_id, amount, customer_id):
        # Ein Schlüssel pro (Aufgabe, Schritt).
        # Retries desselben Schritts verwenden denselben Schlüssel.
        op = f"{task_id}:{step_id}"
        if op not in self._keys:
            self._keys[op] = str(uuid.uuid4())

        return self.client.post(
            "/v1/payments",
            headers={"Idempotency-Key": self._keys[op]},
            json={"amount": amount, "customer_id": customer_id},
        )
Enter fullscreen mode Exit fullscreen mode

Für Prozessneustarts ist ein deterministischer Schlüssel besser:

import hashlib

def idempotency_key(task_id: str, step_id: str, payload: dict) -> str:
    raw = f"{task_id}|{step_id}|{sorted(payload.items())}"
    return hashlib.sha256(raw.encode()).hexdigest()[:32]
Enter fullscreen mode Exit fullscreen mode

Leiten Sie den Schlüssel aus Task-ID und Schritt-ID ab – niemals aus Zeitstempeln oder Zufallswerten, die pro Retry neu entstehen. Startet der Agent eine neue Aufgabe, entsteht mit der neuen Task-ID auch bewusst ein neuer Schlüssel.

Was der Server speichern muss

Eine robuste Implementierung braucht vier Regeln:

  1. Schlüssel vor der Arbeit beanspruchen. Legen Sie den Eintrag per Unique Constraint an, bevor der Vorgang ausgeführt wird.
  2. Payload-Konflikte ablehnen. Existiert derselbe Schlüssel mit anderem Anfrage-Fingerabdruck, antworten Sie mit 422 Unprocessable Entity.
  3. Gleichzeitige Versuche stoppen. Läuft der erste Request noch, antworten Sie mit 409 Conflict, damit der Client zurückweicht.
  4. Ergebnis wiedergeben. Speichern Sie Statuscode und Response-Body nach Abschluss und liefern Sie sie bei späteren Treffern erneut aus.
CREATE TABLE idempotency_records (
  key             TEXT PRIMARY KEY,
  request_hash    TEXT NOT NULL,
  state           TEXT NOT NULL,      -- in_Bearbeitung | abgeschlossen
  response_status INT,
  response_body   JSONB,
  created_at      TIMESTAMPTZ NOT NULL DEFAULT now(),
  expires_at      TIMESTAMPTZ NOT NULL
);
Enter fullscreen mode Exit fullscreen mode

Setzen Sie ein Ablaufdatum. 24 Stunden decken praktisch jedes Retry-Fenster ab und verhindern, dass die Tabelle unbegrenzt wächst. Auch Stripe verwendet dieses Zeitfenster.

Testablauf für wiederholte Anfragen

Beweisen, dass der zweite Aufruf nichts ändert

Eine identische 200-Antwort beweist keine Idempotenz. Zwei separate, erfolgreiche Abbuchungen können beide 200 zurückgeben.

Testen Sie deshalb den Zustand:

  • Der zweite Response-Body enthält dieselbe Ressourcen-ID wie der erste.
  • Ein nachfolgender GET auf die Sammlung liefert genau einen Datensatz.
  • Zähler, Salden oder andere Seiteneffekte wurden genau einmal geändert.

In Apidog können Sie dafür ein gespeichertes Testszenario erstellen:

  1. Senden Sie einen POST mit festem Idempotency-Key.
  2. Wiederholen Sie exakt denselben Request.
  3. Prüfen Sie, ob beide Antworten dieselbe Ressourcen-ID enthalten.
  4. Listen Sie die Ressource auf und prüfen Sie, ob genau ein Datensatz existiert.

Speichern Sie dieses Szenario und führen Sie es in der CI aus. Das ergänzt API-Vertragstests, wie im API-Vertragstest-Leitfaden beschrieben.

Testen Sie außerdem diese zwei Fehlerfälle:

  • Gleicher Schlüssel, anderer Body: Erwartet wird 422, nicht stiller Erfolg.
  • Gleichzeitige Duplikate: Senden Sie beide Requests parallel. Genau einer darf die Arbeit ausführen.

Wenn die echte Zahlungs-API noch nicht verfügbar ist, verwenden Sie einen Mock mit denselben Schlüssel-Semantiken. Warum das für Agenten wichtig ist, erklärt Warum Agenten Mock-APIs statt der Produktion verwenden sollten.

Wenn die externe API keine Schlüssel unterstützt

Wenn Sie die Ziel-API nicht kontrollieren und sie keine Idempotenzschlüssel anbietet, gehen Sie in dieser Reihenfolge vor:

1. Operation von Natur aus idempotent machen

Verwenden Sie einen clientseitig bestimmten Ressourcenpfad:

PUT /orders/{client_order_id}
Enter fullscreen mode Exit fullscreen mode

PUT ist per Konstruktion idempotent und benötigt keine separate Schlüssel-Tabelle.

2. Vor dem Schreiben prüfen

Suchen Sie vor dem Erstellen nach einem bestehenden Datensatz mit demselben natürlichen Schlüssel. Das hilft gegen viele Timeout-Fälle, löst aber keine Race Condition zwischen Prüfung und Schreiben.

3. Downstream deduplizieren

Bei Nachrichten oder Events: Fügen Sie eine stabile Nachrichten-ID hinzu und lassen Sie Consumer Wiederholungen verwerfen. Das ist ein zentrales Muster für zuverlässige ereignisgesteuerte Systeme und zuverlässige Webhooks.

4. Menschliche Freigabe verlangen

Ist eine Aktion irreversibel und nicht idempotent machbar, setzen Sie ein Approval Gate davor. Für hochriskante Agenten-Aktionen ist das die richtige Schutzmaßnahme, wie im Beitrag zu KI-Agenten-Schutzmaßnahmen beschrieben.

Ausführungen nachvollziehbar machen

Idempotenz verhindert Duplikate, beantwortet aber nicht automatisch die Incident-Frage: Welcher Lauf hat den Datensatz erstellt?

Protokollieren Sie mindestens:

  • Task-ID
  • Schritt-ID
  • Idempotenzschlüssel
  • Request-Hash
  • Ergebnisstatus
  • erzeugte Ressourcen-ID

Bei verwalteten Laufzeitumgebungen sollte diese Zuordnung Teil der Plattform sein. In Sharkly ist beispielsweise jeder Lauf mit seiner Task, dem Ausführungsstatus und dem Ergebnis verknüpft. Dadurch lässt sich ein wiederholter Schreibvorgang auf einen konkreten Lauf zurückführen.

Checkliste vor dem Versand

  • [ ] Jedes nicht-idempotente Agenten-Tool verlangt einen Idempotenzschlüssel.
  • [ ] Schlüssel stammen aus Aufgabe und Schritt, nicht aus einem einzelnen Request-Versuch.
  • [ ] Der Server beansprucht den Schlüssel vor der Arbeit.
  • [ ] Derselbe Schlüssel mit anderer Payload führt zu einem Fehler.
  • [ ] Gleichzeitige Duplikate werden durch eine Datenbank-Constraint behandelt.
  • [ ] Ein gespeicherter CI-Test beweist, dass der zweite Aufruf keinen weiteren Effekt erzeugt.
  • [ ] Schlüssel laufen ab, und abgelaufene Datensätze werden bereinigt.

Idempotenz erlaubt aggressivere Wiederholungen, ohne Agenten gefährlicher zu machen. Das ist der eigentliche Gewinn: Ihr Agent wird widerstandsfähiger, ohne doppelte Zahlungen, Bestellungen oder Nachrichten zu erzeugen.

Häufig gestellte Fragen

Brauche ich Idempotenzschlüssel für schreibgeschützte Tools?

Nein. GET ist bereits idempotent und sicher. Schlüssel gehören zu Aufrufen, die Zustand erstellen, belasten, senden oder ändern.

Wo wird der Schlüssel erzeugt?

Im Tool-Wrapper, basierend auf Task- und Schritt-Identifikatoren. Lassen Sie das Modell den Schlüssel nicht selbst generieren: Bei Retries entstehen neue Werte, und taskübergreifende Kollisionen sind möglich.

Welchen Statuscode soll ein Retry zurückgeben?

Geben Sie den gespeicherten Status des ursprünglichen Aufrufs zurück. Hat der erste POST mit 201 geantwortet, sollte auch der Retry 201 mit demselben Body erhalten. Ein optionaler Header wie Idempotent-Replay: true erleichtert das Debugging.

Wie lange sollten Schlüssel gespeichert werden?

24 Stunden reichen für nahezu jedes Retry-Fenster. Danach behandeln Sie einen erneuten Request als neue Operation.

Ersetzt Idempotenz Transaktionen?

Nein. Idempotenz verhindert doppelte Effekte durch wiederholte Requests. Transaktionen machen einen einzelnen Request atomar. Verwenden Sie beides und schreiben Sie Schlüsselbeanspruchung und Geschäftsoperation nach Möglichkeit in derselben Transaktion.

Wie teste ich das ohne echten Zahlungsanbieter?

Richten Sie den Agenten auf einen Mock, der Schlüssel-Semantik einschließlich 422 bei abweichender Payload implementiert. Mit Apidog herunterladen können Sie Mock und Wiederholungstest im selben Projekt verwalten.

Top comments (0)