Warum KI-Agenten Nachrichten doppelt senden
Doppelte Nachrichten entstehen nicht durch eine Entscheidung des Modells, sondern durch Retries in Webhooks, Queues, Workern und LLM-API-Clients. Ohne Idempotency-Keys drohen doppelte Sends und Folgefehler, bevor ein Agent in Produktion geht.
Veröffentlicht am 23. Okt. • Zuerst erschienen auf pykero.com
Ihr KI-Agent hat die Nachricht zweimal versendet, weil etwas darunter einen erneuten Versuch gestartet hat, nicht weil das Modell es "entschieden" hat. Webhooks, Queues, Worker und LLM-API-Clients arbeiten standardmäßig alle mit At-Least-Once-Zustellung, und ein Agent, der mit der Außenwelt kommuniziert, ohne einen Idempotenzschlüssel zu verwenden, wird irgendwann Nachrichten doppelt senden, Buchungen doppelt vornehmen oder Abbuchungen doppelt ausführen. Die Lösung ist architektonischer Natur und günstig im Vergleich zum Schaden, deshalb gehört sie in die Anforderungen und nicht in einen Patch nach dem Launch.
Gründer stellen sich einen Agenten meist als ein einzelnes Gespräch vor: Ein Kunde schreibt, das Modell denkt nach, eine Antwort geht raus. Im Produktivbetrieb ist es eine Kette von Hops, und nahezu jeder Hop wiederholt sich unter Last.
Nichts davon ist exotisch. Es ist das normale Verhalten normaler Infrastruktur. Ein Agent, der in einer Demo "99 Prozent zuverlässig" ist, wird bei einer Last getestet, bei der none davon ausgelöst wird. Im zweiten Monat, bei einigen tausend Gesprächen pro Tag, tritt alles davon ein.
Ein konventioneller Worker, der einen Vorgang wiederholt, verhält sich beim zweiten Mal zumindest gleich. Ein Agent tut das nicht. Wenn ein Worker einen Planungsschritt erneut ausführt, kann das Modell eine leicht andere Aktion wählen: Der erste Versuch sandte "Ihre Bestellung wird Dienstag versandt", der Wiederholungsversuch sandt "Ihre Bestellung wird morgen versandt", weil das Kontextfenster inzwischen einen halbgeschriebenen Zustand enthält. Der Kunde sieht zwei selbstbewusste, widersprüchliche Nachrichten desselben Unternehmens.
Deshalb scheitern Lösungen auf Prompt-Ebene. Dem Modell zu sagen "sende nie eine Nachricht zweimal" setzt voraus, dass das Modell weiß, dass der erste Versuch stattfand. Oft weiß es das nicht, weil der Fehler auftrat, nachdem das Modell geantwortet hatte und bevor Ihr Code das Ergebnis gespeichert hat. Deduplizierung muss unterhalb des Modells angesiedelt sein, an der Grenze, an der Ihr System die Welt berührt. Das ist dasselbe Prinzip wie hinter den deterministischen Leitplanken, die wir in Reliability for AI Features behandeln: Das Modell schlägt vor, der Code entscheidet, ob die Aktion noch erlaubt ist.
Exakt-einmalige Zustellung können Sie aus dem Netzwerk nicht herausholen. Exakt-einmalige Wirkung erreichen Sie, indem Sie jede externe Aktion idempotent machen. Drei Bausteine erledigen die Arbeit.
Bevor der Agent etwas sendet, leitet Ihr Code einen Schlüssel aus der Art der Aktion ab, nicht aus dem Versuch. Bei einer Antwort ist das typischerweise die ID der eingehenden Nachricht plus der Gesprächsschritt. Bei einer Bestellung die Warenkorb-ID plus der Kunde. Zwei Wiederholungen derselben Absicht erzeugen denselben Schlüssel; zwei wirklich unterschiedliche Aktionen tun es nicht. Die API von Stripe ist hier das Referenzdesign: Der Client liefert den Schlüssel, der Server speichert das erste Ergebnis und gibt es bei Wiederholungen zurück (Stripe docs). Die Sendefunktion Ihres Agenten sollte sich genauso verhalten, egal ob die nachgelagerte API Schlüssel nativ unterstützt oder ob Sie es selbst durchsetzen.
Der Fehler, der am härtesten zuschlägt, ist "gesendet, aber nicht gespeichert". Der Agent ruft die Messaging-API auf, die API nimmt die Nachricht an, Ihr Prozess stirbt, bevor er in die Datenbank schreibt, und der Wiederholungsversuch findet keinen Eintrag und sendet erneut. Die Lösung besteht darin, zuerst zu reservieren: Fügen Sie den Schlüssel mit dem Status pending in eine Actions-Tabelle ein, bevor Sie nach außen rufen.
Eine Unique-Constraint auf dem Schlüssel bedeutet, dass ein gleichzeitiger oder wiederholter Versuch beim Einfügen scheitert und stoppt (PostgreSQL constraints). Nachdem der externe Aufruf erfolgreich war, aktualisieren Sie die Zeile auf sent mit der Nachrichten-ID des Anbieters. Wenn Sie eine pending-Zeile finden, die älter ist als Ihr Timeout, ist das ein Reconciliation-Fall, kein Resend-Fall: Fragen Sie den Anbieter anhand des Schlüssels ab, oder halten Sie für einen Menschen.
Webhook-Handler sollten schnell bestätigen und die Event-ID mit demselben Unique-Constraint-Trick in einer Dedupe-Tabelle speichern. Eine doppelte Zustellung stößt an die Constraint und wird verworfen, bevor irgendein Modell aufgerufen wird. Das spart auch Geld: Jedes Duplikat, das Sie am Rand verwerfen, ist ein LLM-Aufruf, für den Sie nie zahlen, was bei den Volumina relevant wird, über die wir in What a WhatsApp Sales Agent Costs sprechen.
Unser Cold-Outreach-System crawlt die Website jedes Interessenten mit einem selbst gehosteten Crawler und einem lokalen Modell und entwirft dann eine maßgeschneiderte E-Mail pro Unternehmen. Anfangs nahmen wir an, dass das schwierige Problem die Schreibqualität sei, und das war es zum Teil. Das Muster, das bei Kosten und Qualität gewann, war ein einzelner Modellaufruf, der Fakten extrahiert und die E-Mail gemeinsam entwirft, statt einer mehrstufigen Kette.
Ein unterschätzter Vorteil zeigte sich später: Ein einzelner Aufruf hat eine einzige Ausgabe, die idempotent gemacht werden muss, sodass das Versprechen "eine E-Mail pro Unternehmen" mit einer einzigen Reservierungszeile durchgesetzt werden konnte, die auf der Unternehmensdomäne basiert. Jeder zusätzliche Schritt in einer Kette ist ein weiterer Ort, an dem ein Wiederholungsversuch doppelt auslösen kann, sodass das Zusammenfassen der Kette die Zuverlässigkeitsgeschichte genauso vereinfachte wie die Rechnung. Wir haben die Entwurfsseite in Single Call vs Agent Chains aufgearbeitet.
Nehmen Sie diese Punkte in das Lastenheft oder den Lieferantenvertrag auf. Jeder ist eine Ja-oder-Nein-Frage mit einer demonstrierbaren Antwort.
Einen Agenten retry-sicher zu machen, ist kein großer Posten. Für einen typischen Vertriebs- oder Support-Agenten mit drei oder vier Aktionstypen erwarten Sie einige Tage konzentrierter Entwicklungsarbeit, den Großteil davon in der Gestaltung der Schlüssel und des Reservierungsablaufs, plus einen Tag für die Chaos-Tests. Die teure Version ist die, in der Sie es weglassen: erstattete Doppelabbuchungen, eine Messaging-Nummer, die wegen spamähnlichem Verhalten markiert wird, und die langsame Erosion, die einsetzt, wenn Kunden lernen, dass Ihrem Agenten nicht ganz zu trauen ist. Wenn ein Vorschlag für einen Agenten, der echte Nachrichten versendet, Idempotenz nirgends erwähnt, ist das die Frage, die Sie vor der Unterschrift stellen sollten.
Wenn Sie einen Agenten planen, der im Namen Ihres Unternehmens handeln soll, und möchten, dass die Zuverlässigkeitsanforderungen vor Baubeginn schriftlich festgehalten werden, sprechen wir darüber.
Zuerst veröffentlicht im Pykero-Blog.
Automatisch übersetzt. Das Original auf Englisch ist unten aufklappbar.
Original (Englisch) anzeigen
Posted on Oct 23 • Originally published at pykero.com
Your AI agent sent the message twice because something underneath it retried, not because the model "decided" to. Webhooks, queues, workers and LLM API clients all default to at-least-once delivery, and an agent that calls the outside world without an idempotency key will eventually double-send, double-book or double-charge. The fix is architectural and cheap compared to the damage, so it belongs in the requirements, not in a post-launch patch.
Founders tend to picture an agent as a single conversation: a customer writes, the model thinks, a reply goes out. In production it is a chain of hops, and nearly every hop will repeat itself under stress.
None of these is exotic. They are the normal behaviour of normal infrastructure. An agent that is "99% reliable" in a demo is being tested at a load where none of this fires. In month two, with a few thousand conversations a day, all of it fires.
A conventional worker that retries at least behaves the same way the second time. An agent does not. If a worker re-runs a planning step, the model may pick a slightly different action: the first attempt sent "Your order ships Tuesday", the retry sends "Your order ships tomorrow" because the context window now includes a half-written state. The customer sees two confident, contradictory messages from the same business.
This is why prompt-level fixes fail. Telling the model "never send a message twice" assumes the model knows the first attempt happened. It often does not, because the failure occurred after the model returned and before your code recorded the result. Deduplication has to live below the model, at the boundary where your system touches the world. This is the same principle behind the deterministic guardrails we cover in reliability for AI features: the model proposes, the code decides whether that action is still allowed.
You cannot get exactly-once delivery from the network. You can get exactly-once effect by making every external action idempotent. Three pieces do the work.
Before the agent sends anything, your code derives a key from what the action is, not from the attempt. For a reply, that is typically the inbound message ID plus the conversation step. For an order, the cart ID plus the customer. Two retries of the same intent produce the same key; two genuinely different actions do not. Stripe's API is the reference design here: the client supplies the key, the server stores the first result and returns it on repeats ( Stripe docs ). Your agent's send function should behave the same way, whether the downstream API supports keys natively or you enforce it yourself.
The failure that bites hardest is "sent but not recorded". The agent calls the messaging API, the API accepts the message, your process dies before writing to the database, and the retry sees no record and sends again. The fix is to reserve first: insert the key into an actions table with status pending before calling out. A unique constraint on the key means a concurrent or repeated attempt fails at the insert and stops ( PostgreSQL constraints ). After the external call succeeds, update the row to sent with the provider's message ID. If you find a pending row older than your timeout, that is a reconciliation case, not a resend case: query the provider by key, or hold for a human.
Webhook handlers should acknowledge fast and store the event ID in a dedupe table with the same unique-constraint trick. A duplicate delivery hits the constraint and is dropped before any model is invoked. This also saves money: every duplicate you drop at the edge is an LLM call you never pay for, which matters at the volumes we discuss in what a WhatsApp sales agent costs.
Our cold-outreach system scrapes each prospect's site with a self-hosted crawler and a local model, then drafts one tailored email per company. Early on we assumed the hard problem was writing quality, and it partly was. The pattern that won on both cost and quality was a single model call that extracts facts and drafts the email together, rather than a multi-step chain.
An underrated benefit showed up later: a single call has one output to make idempotent, so the "one email per company" promise could be enforced with one reservation row keyed on the company domain. Each extra step in a chain is another place where a retry can double-fire, so collapsing the chain simplified the reliability story as much as it simplified the bill. We wrote up the drafting side in single call vs agent chains.
Put these in the statement of work or the vendor contract. Each is a yes or no question with a demonstrable answer.
Making an agent retry-safe is not a large line item. For a typical sales or support agent with three or four action types, expect a few days of focused engineering, most of it in designing keys and the reservation flow, plus a day for the chaos tests. The expensive version is the one where you skip it: refunded double charges, a messaging number flagged for spam-like behaviour, and the slow erosion that happens when customers learn your agent is not quite trustworthy. If a proposal for an agent that sends real messages does not mention idempotency anywhere, that is the question to ask before signing.
If you are scoping an agent that will act on behalf of your business and want the reliability requirements written down before the build starts, let's talk.
Originally published on the Pykero blog.