La prima biforcazione: hai avuto un 200 oppure no?
Davanti a «l’agente non funziona», la domanda che taglia il problema a metà è una sola: la Messages API ha risposto 200? Se no, il guasto è nel tuo livello di integrazione e la risposta te lo dice già. La API restituisce sempre l’errore come JSON con un oggetto error che ha type e message, più un request_id a livello superiore; lo stesso identificativo arriva anche nell’header request-id di ogni risposta, ed è il valore da mettere in log e nei ticket di supporto.
{
"type": "error",
"error": {
"type": "not_found_error",
"message": "The requested resource could not be found."
},
"request_id": "req_011CSHoEeqs5C35K2UUqR7Fy"
}
Il tipo dice già dove guardare. 400 invalid_request_error è quasi sempre codice tuo: schema del messaggio, blocchi fuori ordine, parametro non supportato dal modello. 401 authentication_error è la chiave; 403 permission_error sono i permessi dell’organizzazione o del workspace; 404 not_found_error è il path o un id di risorsa; 413 request_too_large è il limite di byte (32 MB su Messages, 256 MB su Batch, 500 MB su Files). 429 rate_limit_error, 500 api_error, 504 timeout_error e 529 overloaded_error non sono bug tuoi: sono capacità, e vanno gestiti con retry a backoff esponenziale rispettando retry-after. Gli SDK ufficiali lo fanno già, due volte per default, e ogni client espone un’opzione per il numero massimo di retry. Un 504 ricorrente è il sintomo classico di una richiesta lunga senza streaming: la documentazione consiglia lo streaming o la Batch API sopra i dieci minuti.
Un dettaglio che risparmia una notte: cattura le eccezioni tipizzate dell’SDK, dalla più specifica alla più generica, non fare match sulle stringhe dei messaggi. I messaggi cambiano, i tipi no.
Un 200 non significa che sia andata bene
Qui comincia il territorio dell’output del modello, e l’indicatore è stop_reason:
end_turn ha finito da solo
max_tokens ha toccato il tuo max_tokens: risposta troncata
stop_sequence ha emesso una delle tue stop_sequences
tool_use vuole un tool: esegui e restituisci il risultato
pause_turn un loop di server tool ha raggiunto il limite di iterazioni
refusal ha declinato
model_context_window_exceeded la generazione ha riempito la finestra di contesto
Il caso che confonde è refusal. Non è un errore HTTP: è un 200 con content vuoto e un oggetto stop_details che porta category ed explanation. Se il tuo codice controlla solo lo status e poi legge content[0].text, su un rifiuto va in eccezione e tu passi ore a cercare un bug di rete. Il rimedio documentato è ritentare la stessa richiesta su un altro modello Claude.
Anche max_tokens viene diagnosticato male di continuo: JSON tagliato a metà non è «il modello che sbaglia il formato», è il tuo tetto. Alzi max_tokens o continui la risposta. E ricorda che i token di ragionamento sono un sottoinsieme di max_tokens, quindi con il thinking attivo il tetto va tarato più alto.
L’anello dei tool è dove si rompe di più
Le tre famiglie di guasto sono distinte e vanno diagnosticate in modo diverso.
Errori di protocollo. Il messaggio tool_use ids were found without tool_result blocks immediately after è integrazione pura: devi restituire un tool_result per ogni blocco tool_use, e i tool_result devono venire per primi nell’array content del messaggio utente, con l’eventuale testo dopo. Stessa famiglia: il 400 sui blocchi thinking modificati, che significa che stai filtrando o ricostruendo i blocchi di pensiero invece di rimandarli indietro identici.
Errori di esecuzione del tuo tool. Non li nascondi: li rimandi come risultato con is_error.
{
"type": "tool_result",
"tool_use_id": "toolu_01A09q90qw90lq917835lq9",
"content": "ConnectionError: the weather service API is not available (HTTP 500)",
"is_error": true
}
Scrivi messaggi istruttivi: Rate limit exceeded. Retry after 60 seconds. dà al modello di che recuperare, failed no.
Errori di scelta. Se Claude chiama il tool sbagliato, la causa tipica è l’ambiguità delle descrizioni: differenziali su quando usarli, non solo su cosa fanno. Se inventa parametri o sbaglia i tipi, la leva è strict: true sulla definizione, oppure input_examples. E se sembra che il tuo prompt venga ignorato dentro un risultato di tool, non è un bug: le istruzioni infilate in un tool_result sono trattate come contenuto di terze parti potenzialmente non fidato. Spostale in un turno utente successivo e lascia nel risultato solo i dati.
Dove leggere la traccia
In streaming l’errore può arrivare dopo il 200, come evento error nel flusso — per esempio un overloaded_error che in una chiamata non-streaming sarebbe stato un 529. Un client che gestisce gli errori solo attorno alla chiamata HTTP li perde tutti. Nel flusso, message_delta porta lo stop_reason finale e il campo usage con conteggi cumulativi: sono i due numeri da loggare insieme al request_id per ricostruire a posteriori se la richiesta è stata rifiutata, troncata o semplicemente costosa.
Ultimo confine: se il confronto sugli input dei tool inizia a fallire dopo un cambio modello, non è una regressione del modello. L’escaping di Unicode e delle barre nella serializzazione JSON cambia fra versioni. Fai il parse con json.loads() o JSON.parse() e confronta strutture, mai stringhe serializzate.