Costringere la forma alla fonte
Se il tuo codice deve consumare JSON, la prima mossa non è un parser tollerante: è vincolare la generazione. Con gli structured outputs passi uno schema e la risposta viene campionata sotto quel vincolo, quindi è garantita valida rispetto allo schema — niente errori di parsing, niente ritentativi per campi mancanti o tipi sbagliati.
{
"output_config": {
"format": {
"type": "json_schema",
"schema": {
"type": "object",
"properties": { "invoice_number": { "type": "string" } },
"required": ["invoice_number"],
"additionalProperties": false
}
}
}
}
Lo stesso meccanismo esiste sui tool: strict: true nella definizione garantisce che il campo input del blocco tool_use segua l’input_schema e che il name sia un tool esistente. È la differenza fra ricevere 2 e ricevere "due" in un campo dichiarato intero.
Tre cose da sapere prima di adottarlo. Il sottoinsieme di JSON Schema è limitato: additionalProperties deve valere false, gli enum accettano solo primitivi, e restano fuori schemi ricorsivi, $ref esterni e i vincoli numerici e di lunghezza come minimum, maximum, minLength. Una feature non supportata non degrada in silenzio, torna un 400 con il dettaglio. Quindi le regole di dominio — un importo positivo, un codice di dodici caratteri — restano validazione tua, dopo il parsing. Poi c’è un costo: la grammatica va compilata, la prima richiesta paga latenza in più e il risultato resta in cache per 24 ore dall’ultimo uso; cambiare output_config.format invalida la prompt cache della conversazione. Infine, se lo schema è molto grande o max_tokens è molto basso, il modello può non avere token sufficienti per produrre una risposta valida e rifiuta la richiesta anziché emettere JSON rotto.
Il primo controllo non è il parser, è stop_reason
Il difetto più comune del codice di consumo è leggere content[0].text e andare avanti. Prima si guarda perché la generazione si è fermata.
Con max_tokens o model_context_window_exceeded la risposta è troncata, e va trattata come incompleta: se è testo, segnalalo a chi legge; se l’ultimo blocco è un tool_use interrotto a metà, non provare a interpretarlo, rilancia con un max_tokens più alto. Con pause_turn il ciclo di un server tool ha esaurito le sue iterazioni: si continua rimandando indietro il contenuto dell’assistente. Con refusal sei davanti al caso che spezza più integrazioni, perché arriva come HTTP 200: un monitoraggio costruito sul tasso di errore non lo vede. Il rifiuto porta un oggetto stop_details con categoria e spiegazione, ma quei due campi possono essere null, quindi rama su stop_reason (o su stop_details.type) e tieni un messaggio tuo per l’utente. Rimandare la stessa richiesta allo stesso modello di solito produce un altro rifiuto: azzera il contesto o ritenta su un modello di fallback. E se lavori con i batch, un rifiuto ti torna come risultato riuscito, non come risultato errato.
Un ultimo caso da manuale: risposta vuota con end_turn, tipicamente dopo dei tool result. La documentazione dice esplicitamente di non ritentare, ma di aggiungere un nuovo messaggio user che chiede di continuare.
Lo schema garantisce la forma, non i fatti
Qui sta il punto che il decoding vincolato non risolve. Un oggetto conforme allo schema può essere completamente sbagliato, e un modello che sbaglia scrive con la stessa sicurezza di uno che ha ragione. Per questo la sicurezza espressa dal testo non è un segnale, e un campo confidence nello schema non misura niente: è un numero generato sotto lo stesso vincolo di tutto il resto.
Quello che funziona è rendere rappresentabile il dubbio e verificabile l’affermazione. Dai a Claude il permesso esplicito di dire che non sa: sull’output strutturato significa prevedere nell’enum un valore tipo prove insufficienti, e non solo le etichette che ti fanno comodo. Sui documenti lunghi, fagli estrarre prima le citazioni testuali rilevanti e poi rispondere basandosi solo su quelle. Chiedi la verifica come passo separato: per ogni affermazione trova una citazione che la sostenga, e se non la trova ritira l’affermazione. Vincola l’uso della conoscenza generale quando la risposta deve venire solo dai documenti forniti. E se il caso è critico, la stessa richiesta ripetuta più volte è un rilevatore economico: le incoerenze fra le esecuzioni segnalano dove il modello sta inventando. La documentazione è netta sul limite di tutto questo: riduce le allucinazioni, non le elimina.
Consumare senza fidarsi
Chiudi il cerchio a valle. L’oggetto che hai parsato è un input al tuo sistema, non una decisione già presa: applica le tue regole di dominio, confronta gli identificativi con una allow-list invece di usarli così come arrivano, e conserva la risposta grezza insieme a quella normalizzata, perché quando qualcosa andrà storto la differenza fra le due è l’unica traccia che avrai.