Finché un modello IA scrive per una persona, un po’ di disordine nella risposta non è un problema. Ma appena vuoi usarlo dentro un programma, per estrarre dati da un documento, riempire un database o farlo interagire con i tuoi sistemi, il testo libero diventa un guaio. Ti servono due cose: risposte in un formato che il codice possa leggere senza sorprese, e un modo per far chiedere al modello i dati che non ha. La prima si chiama output strutturato, la seconda function calling, che Anthropic chiama tool use.
In questo articolo ti spiego come funzionano entrambe e ti mostro cosa è successo quando le ho provate: 40 estrazioni di dati da un annuncio vero e un piccolo assistente che interroga l’API del tracker dei modelli IA di questo blog.
Il problema: “rispondi solo in JSON” non basta
Il modo più ingenuo per avere dati strutturati è chiederli a parole: “rispondi solo con un oggetto JSON con queste chiavi”. Funziona quasi sempre. Il problema è il quasi.
Per verificarlo ho preso l’annuncio di Claude Sonnet 5.5, uscito il 28 settembre, e ho chiesto di estrarre otto campi: nome, provider, data di rilascio, context window, output massimo, prezzo di input, prezzo di output e se i pesi sono scaricabili. Dieci volte per ogni configurazione. Il risultato:
| Configurazione | JSON leggibile dal codice | Valori giusti | Token input / output |
|---|---|---|---|
| Claude Haiku 4.5, JSON chiesto a parole | 0 su 10 | giusti, ma dentro un blocco “`json | 272 / 107 |
| Claude Haiku 4.5, con schema | 10 su 10 | 10 su 10 | 544 / 84 |
| GPT-6 Luna, JSON chiesto a parole | 10 su 10 | 10 su 10 | 226 / 64 |
| GPT-6 Luna, con schema | 10 su 10 | 10 su 10 | 261 / 68 |
La prima riga è istruttiva. Haiku ha estratto i dati giusti, ma li ha racchiusi tra tre apici inversi e la parola json, come si fa quando si scrive per una persona: lo si vede già dalla prima risposta. Un json_decode in PHP o un JSON.parse in JavaScript rifiutano una risposta così, e infatti il mio controllo le ha scartate tutte. Risultato: zero su dieci. GPT-6 Luna invece, con la stessa richiesta a parole, ha risposto sempre con JSON pulito. Ma è un’abitudine del modello, non una garanzia: cambi modello o prompt e il comportamento può cambiare.
Output strutturato: il formato diventa una garanzia
Con l’output strutturato non chiedi più il formato a parole: passi all’API uno schema JSON che descrive campi e tipi, e il provider vincola la generazione in modo che il modello non possa produrre altro. Non è il modello che “si impegna” a rispettarlo: i token che violerebbero lo schema semplicemente non possono essere scelti. Anthropic lo scrive chiaramente nella documentazione sugli output strutturati: risposta sempre valida e campi obbligatori sempre presenti.
Su Claude si passa lo schema nel parametro output_config:
"output_config": {
"format": {
"type": "json_schema",
"schema": {
"type": "object",
"properties": {
"nome": {"type": "string"},
"prezzo_in": {"type": "number"},
"pesi_aperti": {"type": "boolean"}
},
"required": ["nome", "prezzo_in", "pesi_aperti"],
"additionalProperties": false
}
}
}
Su OpenAI il concetto è identico, con il parametro response_format di tipo json_schema e strict a true, come spiega la guida di OpenAI. Attenzione a non confonderlo con la vecchia “JSON mode”, che garantisce solo che la risposta sia JSON valido, non che abbia i campi che ti servono.
Quello che lo schema costa
La garanzia non è gratis, e i numeri della tabella lo mostrano. Su Haiku lo schema ha raddoppiato i token di input, da 272 a 544: le regole del formato entrano nel prompt anche se non le vedi. In compenso l’output è sceso da 107 a 84 token, perché sparisce tutta la formattazione inutile. Su Luna la differenza è stata piccola, da 226 a 261.
C’è poi un costo di tempo, ma solo la prima volta. La prima chiamata di Haiku con lo schema ha impiegato 3,05 secondi, le nove successive tra 1,1 e 1,4. È il tempo per “compilare” lo schema in regole di generazione: Anthropic tiene il risultato in cache per 24 ore dall’ultimo uso, quindi paghi l’attesa solo quando cambi schema.
Lo schema garantisce il formato, non la verità
È la cosa da non dimenticare. Se un campo è obbligatorio e nel testo quel dato non c’è, il modello deve comunque metterci qualcosa, e quel qualcosa può essere inventato. La soluzione è prevedere il caso nello schema: rendere il campo annullabile, con un tipo che ammette anche null, e dire nel prompt di usare null quando il dato manca. E ricordati i limiti dello schema: su Claude, per esempio, non sono supportati vincoli come valori minimi e massimi o lunghezze delle stringhe, quindi i controlli di sensatezza restano nel tuo codice.
Function calling: il modello chiede, il tuo codice fa
Il function calling risolve un altro problema: il modello non sa quello che non gli dici, e non può andare a prenderlo da solo. Con gli strumenti gli descrivi delle funzioni che il tuo codice sa eseguire, e lui decide quando usarle. Il giro è sempre questo:
- mandi la domanda insieme alla descrizione degli strumenti: nome, a cosa servono, quali parametri accettano;
- il modello, invece di rispondere, restituisce una richiesta: “chiama questo strumento con questi parametri”;
- il tuo codice esegue la funzione vera;
- rimandi al modello il risultato;
- il modello risponde, oppure chiede un altro strumento, e il giro ricomincia.
Il punto chiave è il terzo: il modello non esegue mai niente. Propone una chiamata, e sei tu a decidere se e come farla. È questo che rende il function calling gestibile anche dal punto di vista della sicurezza.
L’esperimento: un assistente che interroga il tracker
Il tracker dei modelli di questo blog ha un’API pubblica in JSON, quindi era lo strumento perfetto. Ho dato a Claude Haiku 4.5 una sola funzione, cerca_modello, che riceve un nome, interroga il tracker e restituisce prezzi, context window e data di rilascio dei modelli trovati. In PHP, la definizione (con la descrizione un po’ accorciata) è questa:
$strumento = array(
'name' => 'cerca_modello',
'description' => 'Cerca un modello nel tracker e restituisce prezzi, '
. 'context window e data di rilascio. Usalo per ogni domanda '
. 'su prezzi o caratteristiche: non rispondere a memoria.',
'strict' => true,
'input_schema' => array(
'type' => 'object',
'properties' => array(
'nome' => array( 'type' => 'string' ),
),
'required' => array( 'nome' ),
'additionalProperties' => false,
),
);
Poi ho fatto la domanda: “Quanto costa Claude Sonnet 5.5 rispetto a GPT-6 Sol, e quale dei due legge più testo in una volta?”. Ecco cosa è successo.
| Turno | Cosa ha fatto il modello | Token input / output | Tempo |
|---|---|---|---|
| 1 | Ha chiesto due chiamate insieme: cerca_modello("Claude Sonnet 5.5") e cerca_modello("GPT-6 Sol") | 700 / 104 | 17,8 s |
| 2 | Ha ricevuto i due risultati e ha scritto la risposta | 1.044 / 304 | 4,0 s |
La risposta finale era corretta sui punti chiesti: stesso prezzo, $2 e $10 per milione di token, e GPT-6 Sol con una finestra leggermente più grande, 1.050.000 token contro 1.000.000. Il modello ha anche capito da solo che gli servivano due ricerche e le ha chieste nello stesso turno, risparmiando un giro. Ma dietro ci sono tre cose che vale la pena sapere.
Gli strumenti costano anche quando non li usi
Con l’endpoint di conteggio di Anthropic ho misurato la stessa domanda con e senza lo strumento: 42 token senza, 700 con. Quando passi degli strumenti, l’API aggiunge un prompt di sistema nascosto che su Haiku 4.5 vale 496 token (286 su Opus 5.5, 354 su Sonnet 5), più la descrizione di ogni strumento. E il conto si ripete a ogni giro: tra i due turni, in tutto, 1.744 token di input per una domanda che da sola ne pesava 42. Se hai tanti strumenti con descrizioni lunghe, qui la cache fa una grande differenza, perché le definizioni viaggiano identiche in ogni chiamata.
La prima chiamata può essere lentissima
Quei 17,8 secondi del primo turno mi hanno insospettito, quindi ho rifatto la prova: 1,67 secondi. Con strict attivo, lo schema dello strumento viene compilato come per l’output strutturato, e la prima volta si aspetta. In produzione conviene fare una chiamata di “riscaldamento” dopo ogni rilascio che cambia gli strumenti, invece di far pagare l’attesa al primo utente.
Il modello ragiona solo su quello che gli restituisci
Questa è la lezione più importante. Nella risposta, Haiku ha aggiunto di sua iniziativa che Sonnet 5.5 ha “il vantaggio della lettura dalla cache al 10% del prezzo”. Peccato che ce l’abbia anche GPT-6 Sol. Il modello non ha sbagliato a ragionare: nella scheda di Sonnet 5.5 che il tracker gli ha restituito la cache era citata, in quella di GPT-6 Sol no. Ha tratto la conclusione più logica da dati incompleti. Con il function calling, la qualità della risposta dipende tanto dal modello quanto da quello che i tuoi strumenti restituiscono.
Come scrivere strumenti che funzionano
- Nella descrizione, di’ quando usarlo. Non solo cosa fa. Il mio “usalo per ogni domanda su prezzi: non rispondere a memoria” serve a evitare che il modello risponda con dati vecchi presi dal suo addestramento.
- Pochi strumenti, nomi chiari. Ogni strumento in più è testo in più a ogni chiamata e una scelta in più per il modello.
- Usa
strict, ma valida comunque. Lo schema garantisce che i parametri abbiano la forma giusta, non che abbiano senso: controllali prima di eseguire qualsiasi cosa. - Restituisci dati completi e compatti. Tutti i campi che servono a rispondere, nessuno di quelli inutili. Se non trovi niente, dillo con una frase chiara, come il mio “Nessun modello trovato con questo nome”: il modello la capisce e si comporta di conseguenza.
- Metti un tetto ai giri. Nel mio esperimento il ciclo si ferma dopo cinque turni in ogni caso. Senza un limite, un modello che continua a chiedere strumenti è un ciclo infinito che paghi a token.
- Chiedi conferma per le azioni vere. Leggere dati è una cosa; mandare una mail, cancellare un record o fare un pagamento è un’altra. Per queste, fai confermare a una persona.
- Tratta i risultati come input non fidato. Se uno strumento legge pagine web o documenti scritti da altri, dentro possono esserci istruzioni pensate per il modello. Vale quello che dico sul prompt injection nell’articolo su quanto costa davvero un chatbot AI.
Un’ultima nota per chi usa i modelli Claude più recenti: su Opus 5.5, Sonnet 5.5 e Fable 5.1 non si può più obbligare il modello a usare uno strumento specifico con tool_choice. La documentazione suggerisce di usare al suo posto gli strumenti strict o l’output strutturato.
Quale usare, e quando
- Output strutturato quando i dati ce li hai già e vuoi tirarli fuori in ordine: estrarre campi da mail, fatture o annunci, classificare ticket, trasformare testo libero in record per un database.
- Function calling quando il modello deve andare a prendere qualcosa o fare qualcosa: cercare in un catalogo, leggere lo stato di un ordine, interrogare un’API.
- Tutti e due insieme quando il modello deve prima raccogliere dati con gli strumenti e poi restituirli in un formato preciso al tuo codice.
In sintesi
Chiedere il JSON a parole funziona finché non funziona: nel mio test, con un modello, zero risposte leggibili su dieci. L’output strutturato trasforma il formato in una garanzia, al prezzo di qualche token in più e di una prima chiamata lenta. Il function calling permette al modello di chiedere i dati che non ha, ma ogni strumento costa token a ogni giro e la risposta vale quanto i dati che gli restituisci. Usati bene, sono il passo che trasforma un chatbot in un pezzo del tuo software.
Il quadro generale della serie è nella guida alle API di intelligenza artificiale. Il prossimo articolo tira le fila di tutto: come scegliere il modello giusto per un progetto reale.


Lascia un commento