AI
Perché il suo agente AI ha inviato quel messaggio due volte, e cosa richiedere prima del lancio
Gli agenti AI inviano due volte messaggi, email e ordini perché l'infrastruttura sottostante effettua retry di default. Ecco cosa i founder dovrebbero richiedere prima che un agente interagisca con clienti reali.
TL;DR
I messaggi duplicati non sono un bug dell'LLM, sono un bug di retry: webhook, code e chiamate al modello consegnano tutti almeno una volta (at-least-once), quindi ogni azione esterna compiuta da un agente ha bisogno di una idempotency key, di uno step di prenotazione (reservation) e di una tabella di deduplicazione. Richieda questi elementi prima che un agente invii qualcosa a un cliente reale.

Il suo agente AI ha inviato il messaggio due volte perché qualcosa sotto di esso ha effettuato un retry, non perché il modello ha "deciso" di farlo. Webhook, code, worker e client delle API degli LLM sono tutti impostati di default su consegna at-least-once, e un agente che chiama il mondo esterno senza una idempotency key prima o poi inviare due volte, prenoterà due volte o addebiterà due volte. La correzione è architetturale ed economica rispetto al danno, quindi appartiene ai requisiti, non a una patch post-lancio.
I retry sono la regola, non l'eccezione
I founder tendono a immaginare un agente come una singola conversazione: un cliente scrive, il modello pensa, una risposta parte. In produzione è una catena di passaggi, e quasi ogni passaggio si ripeterà sotto stress.
- Il webhook in entrata. Le piattaforme di messaggistica ri-inviano una notifica quando il suo endpoint è lento a rispondere o restituisce un errore. Se il suo handler aveva già iniziato a lavorare prima del timeout, adesso ha due copie dello stesso evento in corso.
- La coda. Le code standard come Amazon SQS sono esplicitamente documentate come at-least-once: un messaggio può essere consegnato più di una volta, e il consumer deve essere in grado di gestirlo (AWS docs). La maggior parte dei runtime di agenti gestiti si basa su qualcosa di equivalente.
- Il worker. Un crash, un deploy o un'uccisione per out-of-memory a metà task significano che il job riparte da zero. Tutto ciò che aveva già fatto, lo rifà.
- La chiamata al modello. Ogni SDK di LLM serio effettua retry in caso di rate limit ed errori transitori. Se il primo tentativo era in realtà riuscito lato server ma la risposta si è persa, paga due volte e, peggio, il suo codice potrebbe agire su entrambe.
- La chiamata al tool. L'agente chiama la sua funzione di invio messaggio o creazione ordine. Quella funzione parla con un'API di terze parti con i propri timeout. Un timeout non è un fallimento; è un'incognita.
Nessuno di questi è esotico. Sono il comportamento normale di un'infrastruttura normale. Un agente "affidabile al 99%" in una demo viene testato a un carico in cui nulla di tutto ciò si attiva. Al secondo mese, con qualche migliaio di conversazioni al giorno, si attiva tutto.
Perché l'LLM rende questo più difficile di un job classico
Un worker convenzionale che effettua retry almeno si comporta allo stesso modo la seconda volta. Un agente no. Se un worker ri-esegue uno step di pianificazione, il modello può scegliere un'azione leggermente diversa: il primo tentativo ha inviato "Il suo ordine parte martedì", il retry invia "Il suo ordine parte domani" perché la finestra di contesto ora include uno stato scritto a metà. Il cliente vede due messaggi sicuri e contraddittori dalla stessa azienda.
Questo è il motivo per cui le correzioni a livello di prompt non funzionano. Dire al modello "non inviare mai un messaggio due volte" presume che il modello sappia che il primo tentativo è avvenuto. Spesso non lo sa, perché il fallimento è avvenuto dopo che il modello ha restituito la risposta e prima che il suo codice registrasse il risultato. La deduplicazione deve stare sotto il modello, al confine dove il suo sistema tocca il mondo esterno. È lo stesso principio alla base dei guardrail deterministici trattati in reliability for AI features: il modello propone, il codice decide se quell'azione è ancora consentita.
Cosa significa davvero exactly-once per un agente
Non si può ottenere la consegna exactly-once dalla rete. Si può ottenere un effetto exactly-once rendendo idempotente ogni azione esterna. Tre elementi fanno il lavoro.
Una idempotency key per ogni azione prevista
Prima che l'agente invii qualcosa, il suo codice deriva una key da ciò che l'azione è, non dal tentativo. Per una risposta, questo è tipicamente l'ID del messaggio in entrata più lo step della conversazione. Per un ordine, l'ID del carrello più il cliente. Due retry della stessa intenzione producono la stessa key; due azioni genuinamente diverse no. L'API di Stripe è il design di riferimento qui: il client fornisce la key, il server memorizza il primo risultato e lo restituisce alle ripetizioni (Stripe docs). La funzione di invio del suo agente dovrebbe comportarsi allo stesso modo, sia che l'API downstream supporti le key nativamente, sia che lo imponga lei stesso.
Prenota, poi agisci, poi registra
Il fallimento che morde più forte è "inviato ma non registrato". L'agente chiama l'API di messaggistica, l'API accetta il messaggio, il suo processo muore prima di scrivere nel database, e il retry non vede nessun record e invia di nuovo. La correzione è prenotare prima: inserire la key in una tabella actions con stato pending prima di chiamare l'esterno. Un vincolo di unicità (unique constraint) sulla key fa in modo che un tentativo concorrente o ripetuto fallisca all'inserimento e si arresti (PostgreSQL constraints). Dopo che la chiamata esterna ha successo, aggiorni la riga a sent con l'ID messaggio del provider. Se trova una riga pending più vecchia del suo timeout, è un caso di riconciliazione, non un caso di reinvio: interroghi il provider tramite la key, oppure sospenda in attesa di un intervento umano.
Deduplichi gli eventi in entrata al confine (edge)
Gli handler dei webhook dovrebbero confermare rapidamente (acknowledge) e memorizzare l'ID dell'evento in una tabella di deduplicazione con lo stesso trucco del vincolo di unicità. Una consegna duplicata incappa nel vincolo e viene scartata prima che qualsiasi modello venga invocato. Questo fa anche risparmiare denaro: ogni duplicato che scarta al confine è una chiamata LLM che non paga mai, cosa che conta ai volumi di cui parliamo in what a WhatsApp sales agent costs.
Cosa abbiamo imparato dal nostro motore di outreach
Il nostro sistema di cold-outreach esegue lo scraping del sito di ogni prospect con un crawler self-hosted e un modello locale, poi redige un'email su misura per ogni azienda. All'inizio pensavamo che il problema difficile fosse la qualità della scrittura, e in parte lo era. Il pattern che ha vinto sia in termini di costo sia di qualità è stata un'unica chiamata al modello che estrae i fatti e redige l'email insieme, piuttosto che una catena multi-step. Un beneficio sottovalutato è emerso dopo: un'unica chiamata ha un solo output da rendere idempotente, quindi la promessa "una email per azienda" poteva essere applicata con una sola riga di prenotazione con key sul dominio dell'azienda. Ogni step aggiuntivo in una catena è un altro punto in cui un retry può scattare due volte, quindi accorpare la catena ha semplificato il discorso sull'affidabilità tanto quanto ha semplificato il conto. Ne abbiamo parlato lato redazione in single call vs agent chains.
Cosa richiedere prima che un agente interagisca con un cliente reale
Metta questi punti nello statement of work o nel contratto con il fornitore. Ognuno è una domanda sì/no con una risposta dimostrabile.
- Ogni azione in uscita ha una idempotency key nominata e la derivazione è documentata per ogni tipo di azione.
- Prenotazione prima dell'invio. Faccia mostrare la tabella, il vincolo di unicità e la macchina a stati. Chieda cosa succede a una riga bloccata in
pending. - Deduplicazione in entrata al webhook, con la finestra di conservazione (retention) dichiarata. Le piattaforme possono ri-consegnare ore dopo.
- Chiamate al modello retry-safe. I retry dell'SDK vanno bene, ma il codice circostante non deve trattare "nessuna risposta" come "non è successo nulla".
- Un chaos test in fase di accettazione. Uccida il worker a metà invio, ripeta lo stesso webhook tre volte, forzi un timeout sull'API di messaggistica. Zero duplicati devono raggiungere il numero di test. Se l'agenzia non può eseguire questo test, non ha costruito per questo scenario.
- Riconciliazione, non reinvio, come comportamento predefinito quando lo stato è incerto. Una coda visibile a un umano per i casi incerti batte sempre un retry automatico.
- Osservabilità sui duplicati. Un contatore per i colpi al vincolo (constraint hits) è il suo campanello d'allarme precoce che indica che l'infrastruttura sta effettuando retry più del previsto, molto prima che un cliente se ne lamenti. Lo abbini agli alert di monitoring missed agent runs.
Il discorso sui costi
Rendere un agente retry-safe non è una voce di costo rilevante. Per un tipico agente di vendita o supporto con tre o quattro tipi di azione, si aspetti qualche giorno di ingegnerizzazione focalizzata, in gran parte sulla progettazione delle key e del flusso di prenotazione, più un giorno per i chaos test. La versione costosa è quella in cui lo salta: addebiti doppi rimborsati, un numero di messaggistica segnalato per comportamento simil-spam, e la lenta erosione che avviene quando i clienti imparano che il suo agente non è del tutto affidabile. Se una proposta per un agente che invia messaggi reali non menziona l'idempotenza da nessuna parte, quella è la domanda da porre prima di firmare.
Se sta definendo l'ambito di un agente che agirà per conto della sua azienda e vuole i requisiti di affidabilità scritti nero su bianco prima che inizi la build, parliamone.
Domande frequenti
Perché il mio agente AI ha inviato lo stesso messaggio WhatsApp due volte?
Quasi sempre perché un webhook, una coda o un worker ha effettuato un retry dopo un timeout e l'agente non aveva una idempotency key legata all'azione prevista. Il modello ha fatto il suo lavoro; è l'infrastruttura (plumbing) che l'ha eseguito due volte.
Il prompt engineering può fermare le azioni duplicate?
No. Il prompt viene eseguito all'interno del retry. La deduplicazione deve stare al confine dove l'agente chiama il mondo esterno, nel codice e nel database, non nelle istruzioni al modello.
Quanto costa in più rendere un agente retry-safe?
Di solito qualche giorno di ingegnerizzazione per ogni tipo di azione esterna, dominato dalla progettazione della key e dello step di prenotazione. È molto più economico dei rimborsi, dei numeri bloccati e del danno alla fiducia causati dai duplicati.
È un problema anche sulle piattaforme di agenti gestite (managed)?
Sì. I runtime gestiti effettuano retry degli step falliti per suo conto, il che è utile, ma l'idempotenza delle sue azioni resta comunque sua responsabilità. Chieda al fornitore esattamente dove avviene la deduplicazione.
Come può aiutare Pykero
Servizi collegati a questo articolo.
State costruendo qualcosa di simile?
Pykero Agency progetta e rilascia prodotti web, mobile, SaaS e AI pronti per la produzione.
Parliamone


