1. Perché in ChatGPT App gli errori sono la norma, non un’emergenza
Nella lezione precedente abbiamo parlato di come spezzare un compito in passaggi e costruire un workflow multi‑step in ChatGPT App. Ora aggiungiamo a questo schema la realtà: errori, timeout e interruzioni da parte dell’utente.
Nel web classico la logica spesso ruota attorno al «percorso felice», e gli errori sono percepiti come qualcosa di raro e critico — la pagina rossa 500 e simili. Nelle ChatGPT App la situazione è diversa: lavorate in un sistema distribuito con LLM, API esterni, MCP, widget e, in più, con un utente che può chiudere la scheda in qualsiasi momento. Errori e interruzioni sono routine quotidiana.
Ci sono alcune caratteristiche che complicano la vita:
- In primo luogo, la LLM è non deterministica. Anche con lo stesso prompt può prendere una decisione leggermente diversa: chiamare un altro tool, cambiare parametri o decidere di «chiedere chiarimenti».
- In secondo luogo, vincoli di rete e infrastruttura. Le chiamate agli strumenti (tool‑call) da ChatGPT hanno timeout (di solito decine di secondi), così come il vostro backend Next.js/Vercel. Se un’API esterna è lenta, tutto può interrompersi a metà.
- In terzo luogo, c’è il fattore UX: l’utente si distrae, chiude la chat, torna dopo un giorno, e voi non potete tenere aperta una transazione in database per tutto questo tempo.
Da qui il punto chiave della lezione:
Un workflow tollerante ai guasti = uno scenario in cui assumiamo in anticipo che qualsiasi passaggio possa fallire e definiamo esplicitamente cosa accade in quel caso.
L’errore non è solo un motivo per mostrare un messaggio all’utente, ma anche un segnale per il modello, che può cambiare strategia, proporre un rollback, provare un altro strumento o concludere lo scenario in modo accorto.
2. Il panorama degli errori nel workflow: quali sono
Per gestire correttamente i guasti, bisogna innanzitutto imparare a distinguerli. In un’applicazione LLM basata su ChatGPT Apps si incontrano tipicamente diverse classi di errori.
Errori tecnici. Tutta la classica casistica dei sistemi distribuiti: timeout di rete, 5xx dalle vostre API o da quelle esterne, crash del server MCP, bug nel codice dell’handler di uno strumento. Per esempio, in GiftGenius il vostro MCP‑tool search_products chiama il catalogo e quello risponde con 503 Service Unavailable. È un candidato per un retry automatico.
Errori logici (del modello). Qui rientrano i rifiuti del modello (ha deciso che la richiesta viola una policy), le allucinazioni o, per esempio, un JSON rotto nella risposta di uno strumento. Il modello può generare argomenti non validi per una tool‑call, e la vostra validazione JSON li rifiuta. Nella maggior parte dei casi è un errore di input, non di infrastruttura.
Errori di business. Riguardano il significato: prodotto esaurito, budget dell’utente troppo basso per i filtri scelti, codice promo non valido, prenotazione scaduta. In GiftGenius è la situazione «su 500 candidati nessuno soddisfa i vincoli richiesti». Qui il retry aiuta di rado: bisogna modificare i parametri o spiegare all’utente che il vincolo non è realistico.
Interruzioni UX. È l’utente a interrompere lo scenario: chiude ChatGPT, preme «Indietro» nel widget, annulla un’azione, cambia risposta al passaggio precedente. Anche questo va considerato un flusso normale, non un errore. È importante saper ripristinare e fare rollback dello stato in tali casi, di cui parleremo a breve.
Un caso problematico al confine tra errori logici e tecnici sono i cicli infiniti dell’agente: il modello riceve un errore, pensa «mm, provo ancora», di nuovo errore, e così fino a esaurire contesto o budget. Proteggersi da questo comportamento è una parte importante del design degli errori.
3. Strategie di base: retry, fail‑fast, rollback, coinvolgimento dell’utente
Qualsiasi errore può essere visto come un punto di diramazione: o proviamo a ripetere il passaggio, oppure facciamo rollback, oppure coinvolgiamo l’utente. E, cosa importante, queste strategie si combinano.
Per guasti tecnici e temporanei (rete instabile, API che risponde 503) ha senso fare un retry limitato con backoff. Per errori logici e di business («il validatore rifiuta il budget», «i prodotti sono finiti») ripetere è inutile: serve fail‑fast e chiedere all’utente di modificare l’input o i parametri.
Per operazioni che hanno già modificato qualcosa nel mondo esterno (ordine creato, prenotazione effettuata), serve il rollback — o sotto forma di «passo indietro» in UI/contesto, oppure come azioni compensative reali (annullamento ordine, rimborso).
Infine, ci sono casi che richiedono necessariamente la partecipazione dell’utente: per esempio, se il sistema di pagamento rifiuta con «carta respinta dalla banca», non potete sistemare tutto automaticamente. Il modello deve spiegare correttamente cosa è accaduto e proporre alternative: provare un’altra carta, ridurre l’importo o rinunciare all’acquisto.
Per un workflow affidabile è molto utile, per ogni passaggio, elencare esplicitamente quali tipi di errori sono possibili e cosa fate in ciascun caso — retry automatico, rollback, richiesta all’utente o solo log e chiusura del ramo.
4. Retry e backoff: quando e come farli
Partiamo dalla reazione più naturale dello sviluppatore: «Ok, proviamo ancora una volta». L’idea è giusta, ma, come sempre, il diavolo è nei dettagli.
Quali errori si possono ritentare
Una buona euristica dalla pratica delle integrazioni suona così: gli errori di rete e i 5xx si possono riprovare con una pausa, mentre i 4xx — probabilmente no.
Cioè, se avete ricevuto 503, 504 o semplicemente non avete ricevuto risposta da un’API esterna, ripetere la richiesta con un breve ritardo ha senso. Se invece il server ha risposto con 400 Bad Request o 422 Unprocessable Entity, con ogni probabilità il problema è nei dati, e ripetere con gli stessi parametri non cambierà nulla.
Piccola utility callWithRetry in TypeScript
Scriviamo una piccola utility per il livello MCP o backend, riutilizzabile nei tool:
type RetryOptions = {
maxRetries: number;
baseDelayMs: number;
};
async function callWithRetry<T>(
fn: () => Promise<T>,
{ maxRetries, baseDelayMs }: RetryOptions
): Promise<T> {
let attempt = 0;
// non ci servono cicli infiniti
while (true) {
try {
return await fn();
} catch (err: any) {
attempt++;
const status = err?.status ?? err?.response?.status;
// non facciamo retry sui 4xx
const isClientError = typeof status === "number" && status >= 400 && status < 500;
if (attempt > maxRetries || isClientError) {
throw err;
}
const delay = Math.min(baseDelayMs * 2 ** (attempt - 1), 10_000);
// piccola pausa per evitare l'effetto "thundering herd" sull'API
const jitter = Math.random() * 200;
await new Promise((r) => setTimeout(r, delay + jitter));
}
}
}
Questa funzione:
- ripete la chiamata a fn per un numero limitato di volte;
- applica un backoff esponenziale con un piccolo rumore casuale (jitter), per evitare l’effetto «mandria» in caso di retry simultanei;
- interrompe i retry sui 4xx.
È utile, ad esempio, all’interno di un MCP‑tool che interroga un catalogo prodotti o una vostra API interna di raccomandazioni.
Dove eseguire i retry
Un errore frequente è provare a ripetere tutte le richieste in fila, anche a livelli che non controllate. Nell’ecosistema ChatGPT avete vari punti in cui fare retry:
- all’interno del vostro backend/MCP (come abbiamo fatto in callWithRetry);
- all’interno di un worker in background/coda (nei moduli futuri parleremo di job queue e DLQ);
- a volte — nello stesso widget, quando si tratta di una richiesta leggera «aggiorna elenco» senza effetti collaterali.
È importante non duplicare la logica: se il vostro job worker fa già tre retry con backoff, non ha senso aggiungerne altri cinque nel widget. E, ovviamente, non fate mai while(true) { try ... } — è un modo sicuro per farvi un DDoS da soli.
5. Idempotenza dei passaggi: protezione dai duplicati
I retry creano un secondo problema: come evitare di eseguire due volte la stessa azione. Nel mondo LLM è particolarmente rilevante: il modello può chiamare per errore lo stesso strumento più volte, ChatGPT può ripetere una tool‑call dopo un timeout, l’utente può premere «Regenerate», e poi UI o agente possono aggiungere un’altra chiamata a modo loro.
L’idea dell’idempotenza è semplice: un passaggio è idempotente se la sua riesecuzione con gli stessi dati di input non produce ulteriori effetti collaterali. Richiedere un product feed — ok, ricalcolare le raccomandazioni — ok, ma addebitare due volte o creare un secondo ordine con gli stessi dati — assolutamente no.
Idempotency key in ChatGPT App
Pattern classico: per ogni passaggio logico con effetti collaterali generate una idempotency_key (di solito un UUID), la passate dal modello allo strumento MCP e lì conservate la mappatura «chiave → risultato». Se lo strumento viene chiamato una seconda volta con la stessa chiave, non ripete l’azione, ma restituisce il risultato già salvato.
Nel nostro GiftGenius c’è il passaggio create_order. Immaginate che l’utente prema «Paga», il modello chiami lo strumento, il pagamento vada a buon fine, ma la risposta si perda da qualche parte. Il modello o la piattaforma decidono di ripetere la chiamata e, se non abbiamo idempotenza, otterremo un ordine duplicato o un doppio addebito.
Esempio semplice di strumento idempotente in TypeScript
Creiamo un handler MCP semplificato per lo strumento create_order con chiave di idempotenza. Per semplicità usiamo una Map in‑memory; nella realtà sarà un DB o una cache.
type CreateOrderInput = {
userId: string;
items: Array<{ sku: string; qty: number }>;
idempotencyKey: string;
};
type CreateOrderResult = { orderId: string; status: "created" };
const idempotencyStore = new Map<
string,
{ paramsHash: string; result: CreateOrderResult }
>();
export async function createOrderTool(input: CreateOrderInput): Promise<CreateOrderResult> {
const { idempotencyKey, ...rest } = input;
const paramsHash = JSON.stringify(rest);
const existing = idempotencyStore.get(idempotencyKey);
if (existing) {
// se la chiave è già stata usata, verifichiamo che i parametri coincidano
if (existing.paramsHash !== paramsHash) {
throw new Error("Idempotency key reuse with different params");
}
return existing.result;
}
// qui effettuiamo la reale creazione dell'ordine e il pagamento
const result: CreateOrderResult = {
orderId: "order_" + Math.random().toString(36).slice(2),
status: "created",
};
idempotencyStore.set(idempotencyKey, { paramsHash, result });
return result;
}
Qui noi:
- richiediamo idempotencyKey nei dati di input dello strumento;
- memorizziamo insieme ad essa l’hash dei parametri (qui per semplicità JSON.stringify);
- al secondo invio con la stessa chiave ma dati diversi — lo consideriamo un errore;
- al secondo invio con gli stessi dati — restituiamo semplicemente il risultato precedente.
In un progetto reale conviene:
- memorizzare le chiavi in un DB con TTL (per evitare una crescita illimitata della tabella);
- loggare idempotency_key e includerla nei campi _meta dei messaggi MCP, per un tracciamento comodo tramite Inspector e dashboard.
6. Rollback dei passaggi e pattern Saga
L’idempotenza protegge dai duplicati, ma non risolve un altro caso: cosa fare se un passaggio a metà scenario fallisce.
Nell’e‑commerce è un problema classico: avete già creato l’ordine e riservato la merce a magazzino, ma qualcosa va storto al momento del pagamento. Non potete semplicemente «dimenticarvene» — bisogna annullare lo stato precedente.
Rollback logico vs tecnico
Nel workflow di ChatGPT ci sono due livelli di rollback.
Rollback logico — tornare al passaggio precedente dello scenario e correggere il contesto. Per esempio, al passaggio «pagamento» si verifica un errore e decidete di tornare a «scelta del metodo di pagamento» o persino a «scelta del regalo». Allora è importante:
- aggiornare il WorkflowContext sul backend (passaggio corrente, parametri selezionati);
- informare il modello del cambio di passaggio tramite tool‑call/ToolOutput, così che «dimentichi» il ramo precedente e adatti il comportamento successivo;
- aggiornare la UI del widget, affinché passaggi e pulsanti corrispondano al nuovo stato.
Rollback tecnico — livello business: annullamento delle entità create, compensazione degli effetti esterni. Per esempio: annullare l’ordine, rimuovere la riserva a magazzino, avviare il rimborso. Questo è il pattern Saga: per ogni passaggio «pericoloso» progettate in anticipo un’azione compensativa.
Schema forward/compensate per GiftGenius
Per un checkout semplificato di GiftGenius possiamo disegnare la seguente sequenza:
flowchart TD A[Passo 1: create_order] --> B[Passo 2: reserve_items] B --> C[Passo 3: charge_card] C -->|successo| D[Stato: completed] C -->|errore| E[Compensazione: cancel_reservation] E --> F[Compensazione: cancel_order] F --> G[Stato: failed + messaggio per l'utente]
A ogni azione che modifica il mondo esterno (creazione ordine, riserva, pagamento) corrisponde un’azione compensativa (annullamento ordine, rimozione riserva, rimborso). Non sono sempre simmetriche né sempre possibili uno a uno, ma il principio generale è questo.
Mini‑esempio con compensazione nel codice
Vediamo un piccolo frammento di codice che esegue questi passaggi:
async function completeCheckout(ctx: { userId: string }) {
const order = await createOrderInDb(ctx.userId);
try {
await reserveItems(order.id);
await chargeCard(order.id);
return { orderId: order.id, status: "paid" as const };
} catch (err) {
// azioni di compensazione
await safeCancelReservation(order.id);
await safeCancelOrder(order.id);
throw err;
}
}
Qui:
- createOrderInDb, reserveItems, chargeCard — passaggi forward;
- safeCancelReservation e safeCancelOrder — passaggi compensativi, che a loro volta devono essere idempotenti (se proviamo ad annullare qualcosa già annullato, non succede nulla di grave).
Notate che in caso di errore non lo nascondiamo, ma lo rilanciamo. Il modello (tramite ToolOutput) deve ricevere un messaggio d’errore comprensibile e poi spiegarlo in forma umana all’utente, proponendo il passo successivo.
7. Rollback dei passaggi e sincronizzazione dello stato: come evitare il disallineamento
C’è un tipo particolare di «errore» facile da sottovalutare: il disallineamento dello stato tra UI, backend e modello.
Scenario tipico:
- L’utente passa attraverso i passaggi 1 → 2 → 3.
- Al passaggio 3 qualcosa va storto, l’utente preme il pulsante «Indietro» nel widget.
- Il widget riporta onestamente il proprio stato locale al passaggio 2.
- Ma il modello «ricorda» che eravamo al passaggio 3 e avevamo già provato a pagare. Nel messaggio successivo continua a parlare di pagamento, mentre l’utente vede la schermata di scelta del regalo.
Per evitarlo, è utile introdurre un evento esplicito di rollback del passaggio. Lo invia il widget al MCP/modello — come chiamata di uno strumento o come ToolOutput.
Per esempio, potete creare un tool semplice user_navigated_to_step, che fissa il passaggio corrente e il suo stato:
type NavigateInput = {
workflowId: string;
stepId: string;
};
export async function userNavigatedToStep(input: NavigateInput) {
await workflowRepo.setCurrentStep(input.workflowId, input.stepId);
return {
message: `User moved to step ${input.stepId}`,
};
}
Il widget, quando si preme «Indietro», chiama questo tool; il modello ne vede il risultato nella cronologia delle tool‑call e capisce che ora bisogna proseguire il dialogo a partire dal nuovo passaggio.
Dal lato UI, un handler sarà circa così:
async function handleBackClick() {
const { workflowId, prevStepId } = widgetState;
await window.openai.tools.call("user_navigated_to_step", {
workflowId,
stepId: prevStepId,
});
setWidgetState((s) => ({ ...s, currentStepId: prevStepId }));
}
Punto importante: è il backend/agente la fonte di verità sul passaggio corrente, e il modello lo vede tramite i tool. Così, anche ripristinando una sessione più tardi, potete sincronizzare correttamente il contesto.
8. UX degli errori: cosa vede l’utente e cosa vede il modello
Abbiamo già imparato a gestire tecnicamente gli errori (retry, rollback, idempotenza, sincronizzazione dello stato). Resta da fare in modo che il tutto risulti adeguato sia per l’utente sia per il modello.
Anche un retry e un rollback perfetti non bastano se la UX degli errori è «come nei vecchi servlet Java»: testo rosso, stack trace e un enigmatico «Unexpected error».
Per una ChatGPT App ci sono due destinatari dei messaggi d’errore:
- l’utente, che deve capire cosa è successo e cosa può fare dopo;
- il modello, che deve ricevere abbastanza informazioni strutturate per decidere: ripetere, cambiare parametri, proporre alternative o chiudere lo scenario.
Buone pratiche:
- a livello di MCP/strumenti restituire un errore strutturato con codice, tipo, flag retryable e breve testo tecnico;
- dare al modello proprio questa struttura (per esempio in result.structuredContent), non un chilometro di stack trace;
- in UI mostrare all’utente un messaggio umano, breve e chiaro.
Ecco un mini‑esempio della struttura d’errore restituita da uno strumento:
type ToolError = {
code: string; // e.g. "PAYMENT_TIMEOUT"
message: string; // breve descrizione tecnica
retryable: boolean; // se è possibile riprovare
};
throw {
isError: true,
error: <ToolError>{
code: "PAYMENT_TIMEOUT",
message: "Payment provider did not respond in time",
retryable: true,
},
};
Il modello vede retryable: true e può provare un altro strumento o proporre all’utente di riprovare.
Dal lato widget mappate semplicemente questi codici in testi comprensibili per l’utente:
function ErrorBanner({ code }: { code: string }) {
const text =
code === "PAYMENT_TIMEOUT"
? "Il servizio di pagamento non ha risposto in tempo. Riprova tra un minuto."
: "Qualcosa è andato storto. Per favore, riprova.";
return <div className="error-banner">{text}</div>;
}
E un altro punto importante: non mostrate all’utente stack trace, token, segreti. È brutto e insicuro. Le informazioni tecniche loggatele da voi, all’utente date un messaggio breve e sicuro.
Insight
Nei sistemi LLM come ChatGPT le chiamate agli strumenti non corrette sono la norma, non l’eccezione. Il modello genera regolarmente argomenti che non passano la validazione: tipi scambiati, campi mancanti, valori errati, strutture rotte. Non è un errore nel senso ingegneristico tradizionale — è parte della natura di un modello stocastico, e a questa natura va adattata tutta l’interfaccia degli errori.
Idea chiave: il messaggio d’errore non è un segnale “si è rotto”, ma un’istruzione per correggere il tentativo successivo. Il suo pubblico principale è il modello stesso. Se il messaggio è strutturato e contiene indicazioni precise, il modello può correggere automaticamente i parametri e ripetere la chiamata correttamente. È proprio il principio su cui si basano le tecniche di Tool‑Reflection: un feedback corretto migliora l’azione successiva dell’agente senza intervento umano.
Consiglio di attenersi a questi requisiti per il formato degli errori:
- il messaggio deve indicare il campo specifico che non ha superato la validazione — senza generalizzazioni tipo “Invalid parameters”;
- è importante descrivere esplicitamente il formato atteso o i valori ammessi, così che il modello possa scegliere quelli corretti;
- il messaggio deve essere breve, formale e strutturato: campi come error_type, field, expected o allowed_values aiutano molto il modello;
- dove possibile conviene fornire un esempio minimo di input corretto — spesso aumenta la precisione di recupero del modello.
Il feedback d’errore ideale per il modello contiene due fatti: cosa è andato storto e l’istruzione su come correggerlo.
9. Log e metriche degli errori del workflow
Anche con una UX degli errori curata, per capire cosa si rompe davvero non bastano i messaggi all’utente. Servono log strutturati e metriche per passaggio.
Set minimo utile quando si logga ogni passaggio del workflow:
- user_id o almeno session_id;
- workflow_id e step_id;
- stato del passaggio (success, failed, retry, rolled_back);
- error_code (se presente);
- idempotency_key e correlation_id, se il passaggio è collegato a chiamate esterne.
In MCP e Agents ci sono campi _meta; è comodo metterci idempotency_key e correlation_id, così da vederli sia nei log sia in Inspector.
Esempio semplice di logging in Node.js/TypeScript (potete usare console, oppure winston/pino):
function logStepFailure(params: {
userId?: string;
workflowId: string;
stepId: string;
errorCode: string;
idempotencyKey?: string;
}) {
console.error(
JSON.stringify({
level: "error",
event: "workflow_step_failed",
...params,
timestamp: new Date().toISOString(),
})
);
}
Questi log sono facili da parsare, costruirci dashboard e calcolare:
- la conversione tra passaggi;
- i tipi di errore più frequenti;
- la quota di passaggi conclusi con retry vs fallimento definitivo.
Non ogni errore deve generare un alert in produzione. Quelli critici — crash dell’MCP, timeout sistematici, fallimenti di massa su un certo passaggio — sì, vanno nel monitoraggio. Ma «nessun risultato nella ricerca dei regali» — è un evento di business, non un incidente.
10. Sviluppiamo GiftGenius: un passo di checkout resiliente
Ora mettiamo insieme tutto: retry, idempotenza, Saga, sincronizzazione dello stato, UX degli errori e logging — sull’esempio di un passaggio nella nostra app didattica GiftGenius: il checkout.
Cosa c’è già
Fin qui abbiamo già:
- un workflow multi‑step: raccolta informazioni → generazione idee → scelta del regalo → checkout;
- tool gating configurato: al passaggio di checkout è disponibile solo il set di strumenti di commerce (create_order, get_payment_methods ecc.);
- un WorkflowContext che memorizza regalo scelto, budget, userId e passaggio corrente.
Cosa aggiungeremo in questa lezione
Per il passaggio di checkout inseriremo:
- idempotency_key per lo strumento create_order;
- retry per errori temporanei del provider di pagamento;
- compensazione per operazioni parzialmente riuscite;
- una UX degli errori corretta nel widget.
Generazione della chiave di idempotenza nel widget al clic su «Paga»:
import { v4 as uuid } from "uuid";
async function handlePayClick() {
const idempotencyKey = uuid();
setWidgetState((s) => ({ ...s, idempotencyKey }));
await window.openai.tools.call("create_order", {
userId: widgetState.userId,
items: [/* ... */],
idempotencyKey,
});
}
Dal lato dello strumento create_order — l’handler idempotente che abbiamo scritto sopra: memorizza la chiave e il risultato e, in caso di ripetizione, non crea un nuovo ordine.
Il codice che interagisce con l’API di pagamento può essere avvolto in callWithRetry, per provare più volte ad addebitare in caso di problemi di rete. E non dimenticate di aggiungere il flag retryable: true all’errore, così che il modello capisca che si può proporre di riprovare.
Se dopo la creazione dell’ordine e l’addebito andati a buon fine qualcosa si rompe (per esempio, un webhook esterno non arriva in tempo), lo logghiamo con correlation_id e workflow_id e poi:
- proviamo un retry in background (nel modulo futuro su code e eventi);
- oppure marchiamo esplicitamente il passaggio come failed, invochiamo le azioni compensative e spieghiamo all’utente cosa è successo.
11. Errori tipici nella progettazione di workflow resilienti
Errore n. 1: «Facciamo retry di tutto finché non funziona».
Ripetere automaticamente qualsiasi passaggio fino alla vittoria — un modo sicuro per crearvi un inferno locale. Gli errori di rete e i 5xx si possono riprovare con backoff e limite di tentativi. Ma i 4xx, gli errori di business e i fallimenti logici del modello vanno risolti con i dati o spiegati all’utente. Altrimenti otterrete comportamenti instabili, conti strani e log inquinati.
Errore n. 2: Assenza di idempotenza dove ci sono soldi e ordini.
Se uno strumento come create_order o charge_card non è idempotente, qualsiasi chiamata ripetuta (per timeout, Regenerate, bug nell’agente) può portare a duplicati. Negli scenari LLM i retry sono molto più frequenti che nel classico frontend REST, quindi la idempotency_key non è «un bel di più», ma un requisito obbligatorio per passaggi di pagamento e altri critici.
Errore n. 3: Mancanza di azioni compensative (assenza di Saga).
Avete creato l’ordine, riservato la merce, ma al pagamento tutto è caduto e avete solo mostrato all’utente «qualcosa è andato storto». Il risultato è un sistema pieno di semi‑ordini, riserve pendenti, «code» finanziarie. Per ogni passaggio che cambia il mondo esterno, pensate a cosa farete se fallisce il passaggio successivo: annullare, rimborsare, segnare come «expired» ecc.
Errore n. 4: Lasciare che l’agente entri in un ciclo infinito di retry.
Se non limitate il numero di tentativi (per esempio con maxRetries negli helper o con max_iterations nella logica dell’agente) e non marcate gli errori come retryable: false laddove i retry sono inutili, il modello può andare in loop: «Provo ancora… ancora…». Questo brucia token, tempo e nervi.
Errore n. 5: Disallineamento dello stato tra UI e modello durante il rollback.
Spesso gli sviluppatori implementano il pulsante «Indietro» solo in UI, dimenticando di sincronizzare il passaggio con backend e modello. Di conseguenza l’utente vede il passaggio 2, mentre il modello continua a «vivere» al passaggio 3 e propone cose strane. Soluzione — eventi espliciti come user_navigated_to_step e aggiornamento del WorkflowContext a ogni transizione.
Errore n. 6: Messaggi tecnici per l’utente e assenza di log per gli sviluppatori.
L’utente riceve «Error: ECONNRESET at TcpSocket.onEnd…», e voi — zero informazioni su quale passaggio e per quale workflow_id sia fallito. Approccio corretto: per l’utente — testo breve, chiaro e un suggerimento sul da farsi; per lo sviluppatore — log strutturato con workflow_id, step_id, error_code, idempotency_key e correlation_id.
Errore n. 7: Assenza di una strategia per gli alert.
O si manda in alert tutto, incluso «nessun regalo adatto per il tuo filtro molto stretto», oppure non si manda in alert nulla, incluso il vero crash dell’MCP. Cercate di separare i guasti di sistema critici (servizio giù, timeout di massa, perdita dei webhook) dagli eventi di business attesi. I primi vanno in monitoraggio e on‑call, i secondi — si contano in analytics.
GO TO FULL VERSION