Un’estrazione strutturata in produzione fallisce in tre modi che sembrano lo stesso errore e non lo sono: la rete cade, la risposta si tronca, oppure il JSON è perfettamente valido e il valore dentro è sbagliato. Progettare la qualità dell’estrazione vuol dire tenere questi tre anelli separati, perché hanno backoff diversi, costi diversi e soglie di rinuncia diverse.
Lo schema è garantito, il contenuto no
Gli structured outputs vincolano il campionamento a una grammatica costruita dal tuo JSON Schema. La documentazione è netta sui benefici: «Always valid: No more JSON.parse() errors», «Type safe: Guaranteed field types and required fields», «Reliable: No retries needed for schema violations». La stessa pipeline alimenta lo strict tool use, dove strict: true porta con sé la promessa «No need to validate and retry tool calls».
Il punto architetturale è che cosa resta fuori. Il sottoinsieme di JSON Schema supportato esclude i vincoli numerici (minimum, maximum, multipleOf), quelli sulle stringhe (minLength, maxLength), i vincoli sugli array oltre minItems 0 o 1, i riferimenti esterni e gli schemi ricorsivi; se usi una feature non supportata «you’ll receive a 400 error with details». Un IBAN di diciannove caratteri passa, una data del 2019 su una fattura del 2026 passa, un totale che non è la somma delle righe passa. La grammatica ti ha tolto il retry sintattico e ti ha lasciato intero quello semantico: è lì che va spostato il budget.
C’è anche un costo di primo colpo: «there is additional latency while the grammar compiles», e le grammatiche compilate restano in cache 24 ore dall’ultimo uso. Su un lotto con un solo schema è irrilevante; su un servizio che compila uno schema per tenant lo senti a ogni risveglio.
Tre anelli, tre cause
1. trasporto 429 / 500 / 529 backoff esponenziale, retry-after
2. protocollo stop_reason max_tokens, refusal, context window
3. semantica la tua validazione re-prompt con l'errore specifico
Il primo anello non lo scrivi tu: «The official SDKs automatically retry transient failures (such as connection errors, rate limits, and 5xx server errors) with exponential backoff, twice by default, honoring the retry-after header when present». Riscriverlo a mano è il modo più veloce per raddoppiare i tentativi.
Il secondo è quello che si dimentica. Con stop_reason uguale a max_tokens la risposta è troncata e va alzato il limite o continuata; con model_context_window_exceeded la doc dice di trattarla come troncata; con refusal la risposta è un HTTP 200 regolare, non un errore, e l’indicazione è «Read stop_details and retry on a fallback model». Un parser che guarda solo il corpo e mai stop_reason scrive record incompleti come se il documento non contenesse quei campi.
Il feedback deve dire che cosa non torna
Il terzo anello è il pattern evaluator-optimizer: «one LLM call generates a response while another provides evaluation and feedback in a loop». Anthropic lo raccomanda con una condizione precisa, «particularly effective when we have clear evaluation criteria, and when iterative refinement provides measurable value». Se il criterio non lo sai scrivere in codice, il secondo passaggio riscrive e basta, e paghi due volte per la stessa incertezza.
Un dettaglio che rompe in silenzio: non mettere la correzione dentro il tool_result. Il modello «is trained to treat instructions inside tool results as potentially untrusted third-party content». L’istruzione va in un turno user dopo il blocco di risultato, o in un system message a metà conversazione, lasciando nel risultato solo i dati.
Ancorare l’estrazione al documento
Per l’estrazione da documenti la validazione più economica è quella meccanica. Attivando le citazioni sul blocco documento ogni affermazione torna con il testo di origine e un indice.
{
"type": "document",
"source": { "type": "text", "media_type": "text/plain", "data": "..." },
"citations": { "enabled": true }
}
La garanzia dichiarata è forte: «citations are guaranteed to contain valid pointers to the provided documents». Il campo cited_text inoltre «does not count toward output tokens», e non conta nemmeno come input quando lo rimandi indietro nei turni successivi, quindi il controllo costa poco: cerchi la stringa citata nel documento sorgente e sai se il campo è ancorato o inventato. Testo e PDF vengono spezzati in frasi; se ti serve un’altra granularità passi ai custom content documents.
A monte, per documenti lunghi la guida sulle allucinazioni chiede di far estrarre prima le citazioni testuali e solo dopo ragionarci sopra, e di dare al modello il permesso esplicito di ammettere che non sa. Un campo dichiarato vuoto vale più di un campo inventato: il primo lo instradi, il secondo lo scopri in produzione.
Quando smettere di riprovare
Fissa il tetto dei tentativi semantici prima di scrivere il loop: due, quasi sempre. Oltre quella soglia conviene cambiare forma e non insistere in serie. La verifica best-of-N («Run Claude through the same prompt multiple times and compare the outputs. Inconsistencies across outputs could indicate hallucinations») costa quanto tre retry ma è parallela, quindi non moltiplica la latenza, e produce un segnale che il retry seriale non dà mai: il disaccordo fra istanze. I record che restano contesi vanno in quarantena con il loro request_id — ogni risposta ne porta uno nell’header request-id — non nel database di destinazione.