Tabella dei Contenuti
Questo articolo ti insegnerà a strutturare la risposta di un LLM come GPT-5 oppure modelli open source usando Pydantic per validare il tuo output.
Si tratta di un tema molto rilevante data l'affidabilità non sempre elevatissima dei sistemi anche più commerciali in giro, come proprio GPT. Infatti, l'esigenza di poter estrarre informazioni strutturate in formato JSON, ad esempio, si rivela fondamentale per compiti di data mining, dove dal formato non strutturato (come un testo libero) si vanno ad estrarre informazioni puntuali.
Il contenuto proposto sarà valido sia per modelli closed-source come GPT di OpenAI o Anthropic che per modelli open source.
- cosa è e come definire uno modello dati
- come fare in modo che il tuo LLM rispetti il formato di output attraverso regole di validazione
Perché occorre un output strutturato?
Certamente gli LLM come GPT-5 riescono a fornire enorme valore anche senza strutturare la loro risposta secondo uno schema. Resta importante però, soprattutto per i programmatori e coloro che lavorano con i dati, che un possibile schema di risposta possa essere rispettato se è quella la volontà dell'utente.
Ma perché non chiedere al LLM di strutturare l'output direttamente in prompt?
Ad esempio
Dato in input una città italiana, fornisci in output un JSON con questo formato
{
"città": [citta],
"cap": [cap],
"regione": [regione]
}
...
>> Input: "Rovereto"
Nulla di male.
Tale logica però non sempre funziona e manca di validazione. Come possiamo ad esser certi che il modello risponda nel formato giusto (ad esempio che il CAP sia sempre un numero, e che la regione sia sempre una regione d'Italia) e seguendo regole chiare?
Chiedere all LLM quindi non garantisce che l'output corrisponda a uno schema specifico.
Resta quindi importante poter validare quello che c'è dentro a questi output e sollevare eccezioni ed errori qualora non fossero consistenti con il nostro modello dati.
Caso d'uso
In questo articolo vedremo l'esempio di estrarre informazioni in JSON partendo da una semplice domanda ad un LLM come GPT-5.
La domanda può essere qualunque, ma chiederemo al modello domande relative ai vincitori dei mondiali di calcio nel tempo.
In particolare vogliamo estrarre
- Data della finale
- Nazione ospite del torneo
- Squadra vincitore
- Top marcatori
Non ci preoccuperemo di validare l'esattezza dei dati, ma solo di adattare la risposta testuale del LLM allo schema che vedremo ora.
Nell'articolo vedremo questo esempio e forse ne esploreremo anche degli altri.
Le dipendenze
Vediamo ora le dipendenze da installare per eseguire questo tutorial.
Ovviamente, dando per scontato che abbiamo già un ambiente di sviluppo attivo, andiamo ad installare Pydantic, client OpenAI e ollama.
- Pydantic: è la libreria di validazione e definizione modelli dati più famosa e usata dalla community grazie alla sua facilità d'uso, efficienza e rilevanza nella data science
- OpenAI: il famoso client per interrogare GPT e gli altri modelli di OpenAI
- ollama: interfaccia molto conveniente agli LLM open source come llama3
Nel nostro ambiente di sviluppo, lanciamo il comando per iniziare
uv add pydantic openai ollama
Poiché vogliamo anche provare i modelli open source, il prossimo step è installare ollama a livello di sistema. Puoi imparare come installare ed usare ollama leggendo questo articolo dedicato

Ora possiamo concentrarci allo sviluppo.
Definizione di un modello dati
Un modello dati è uno schema logico da seguire per strutturare dati. Si usano in moltissimi contesti, dalla definizione di tabelle in database alla validazione di dati input.
Ho già affrontato un po' il discorso di modelli dati usando Pydantic nella data science e machine learning nel post di seguito 👇

Iniziamo creando i modelli dati Pydantic
from pydantic import BaseModel, Field
from typing import List
import datetime
class SoccerData(BaseModel):
date_of_final: datetime.date = Field(..., description="Date of the final event")
hosting_country: str = Field(..., description="The nation hosting the tournament")
winner: str = Field(..., description="The soccer team that won the final cup")
top_scorers: list = Field(
..., description="A list of the top 3 scorers of the tournament"
)
class SoccerDataset(BaseModel):
reports: List[SoccerData] = []
In questo script stiamo importando la classe BaseModel
e Field
da Pydantic e usandole per creare un modello dati. Stiamo di fatto costruendo la struttura che deve avere il nostro risultato finale.
Pydantic richiede che dichiariamo il tipo di dato che entra nel modello. Abbiamo datetime.date
che, ad esempio, forza il campo date
ad essere una data e non una stringa. Allo stesso tempo, il campo top_scorers
dovrà essere per forza una lista, altrimenti Pydantic ritornerà un errore di validazione.
Infine, creiamo un modello dati che raccoglie molteplici istanze del modello SoccerData
. Questo è chiamato SoccerDataset
e verrà usato da OpenAI o ollama per validare la presenza di più report, non di uno solo.
Creazione del prompt di sistema
Molto semplicemente, scriveremo in inglese quello che modello deve fare sottolineando l'intento e la struttura del risultato fornendo degli esempi.
system_prompt = """You are an expert sports journalist. You'll be asked to create a small report on who won the soccer world cups in specific years.\
You'll report the date of the tournament's final, the top 3 scorers of the entire tournament, the winning team, and the nation hosting the tournament.
Return a JSON object with the following fields: date_of_final, hosting_country, winner, top_scorers.\
If multiple years are inputted, separate the reports with a comma.\
Here's an example
[
{
"date_of_final": "1966",
"hosting_country": "England",
"winner": "England",
"top_scorers": ["Player A", "Player B", "Player C"]
},
{
"date_of_final": ...
"hosting_country": ...
"winner": ...
"top_scorers": ...
},
]
Here's the years you'll need to report on:
"""
Questo prompt verrà usato come system prompt e ci permetterà semplicemente di passare gli anni di interesse separati da una virgola.
Creazione del codice per output strutturati
Per prima cosa useremo OpenAI in una funzione chiamata query_gpt
che ci permette di parametrizzare il nostro prompt
def query_gpt(prompt: str) -> SoccerDataset:
completion = client.chat.completions.parse(
model="gpt-4o-2024-08-06",
messages=[
{"role": "system", "content": system_prompt},
{"role": "user", "content": prompt},
],
response_format=SoccerDataset,
)
return completion.choices[0].message.parsed
Ricordiamoci di passare la nostra chiave API di OpenAI al client appena creato oppure di creare un file .env
nella cartella di progetto e di caricarla al suo interno nel formato OPENAI_API_KEY=sk-...
.
Useremo GPT-4o, passando come response_format
proprio SoccerDataset
.
SoccerData
, ma SoccerDataset
.Se usassimo il primo, l'LLM restituirebbe sempre e solo un singolo risultato.
Mettiamo tutto insieme e lanciamo il software passando come content nel prompt dell'utente gli anni 2010, 2014 e 2018 come input dalla quale vogliamo generare il report strutturato.
from openai import OpenAI
from typing import List
from pydantic import BaseModel, Field
import datetime
class SoccerData(BaseModel):
date_of_final: datetime.date = Field(..., description="Date of the final event")
hosting_country: str = Field(..., description="The nation hosting the tournament")
winner: str = Field(..., description="The soccer team that won the final cup")
top_scorers: list = Field(
..., description="A list of the top 3 scorers of the tournament"
)
class SoccerDataset(BaseModel):
reports: List[SoccerData] = []
system_prompt = """You are an expert sports journalist. You'll be asked to create a small report on who won the soccer world cups in specific years.\
You'll report the date of the tournament's final, the top 3 scorers of the entire tournament, the winning team, and the nation hosting the tournament.
Return a JSON object with the following fields: date_of_final, hosting_country, winner, top_scorers.\
If the query is invalid, return an empty report.\
If multiple years are inputted, separate the reports with a comma.\
Here's an example
[
{
"date_of_final": "1966",
"hosting_country": "England",
"winner": "England",
"top_scorers": ["Player A", "Player B", "Player C"]
},
{
"date_of_final": ...
"hosting_country": ...
"winner": ...
"top_scorers": ...
},
]
Here's the years you'll need to report on:
"""
def query_gpt(prompt: str) -> SoccerDataset:
completion = client.chat.completions.parse(
model="gpt-4o-2024-08-06",
messages=[
{"role": "system", "content": system_prompt},
{"role": "user", "content": prompt},
],
response_format=SoccerDataset,
)
return completion.choices[0].message.parsed
if __name__ == "__main__":
resp = query_gpt("2010, 2014, 2018")
print(resp)
Il risultato è il seguente
{
"reports": [
{
"date_of_final": "2010-07-11",
"hosting_country": "South Africa",
"winner": "Spain",
"top_scorers": [
"Thomas Müller",
"David Villa",
"Wesley Sneijder"
]
},
{
"date_of_final": "2014-07-13",
"hosting_country": "Brazil",
"winner": "Germany",
"top_scorers": [
"James Rodríguez",
"Thomas Müller",
"Neymar"
]
},
{
"date_of_final": "2018-07-15",
"hosting_country": "Russia",
"winner": "France",
"top_scorers": [
"Harry Kane",
"Antoine Griezmann",
"Romelu Lukaku"
]
}
]
}
Fantastico. GPT-4o ha seguito il nostro prompt e Pydantic ha validato i campi creando una struttura coerente col modello dati. Infatti, l'output non è una stringa, come tipicamente restituirebbe un LLM come GPT, ma una lista di dizionari Python.
Proviamo ora ad inserire un input che non ha senso.
if __name__ == "__main__":
print(query_gpt("ciao, come stai?"))
>>>
{
"reports": []
}
LLM restituisce correttamente un report vuoto, perché così gli abbiamo chiesto di gestire le query invalide via system prompt.
Usare output strutturato con modelli open source
Vediamo ora come usare ollama per usare modelli open source come llama3.
Utilizza il comando
ollama pull llama3
per scaricarlo!Prenderemo spunto dal blog post dedicato di Ollama.

Creiamo una nuova funzione chiamata query_llama
.
from ollama import chat
def query_llama(prompt: str) -> SoccerDataset:
response = chat(
messages=[
{
"role": "system",
"content": system_prompt
},
{
"role": "user",
"content": prompt
}
],
model='llama3.1',
format=SoccerDataset.model_json_schema(),
)
return SoccerDataset.model_validate_json(response.message.content)
Eseguiamo la nuova funzione
if __name__ == "__main__":
print(query_llama("2010, 2014, 2018"))
Ecco i risultati
{
"reports": [
{
"date_of_final": "2010-07-11",
"hosting_country": "South Africa",
"winner": "Spain",
"top_scorers": [
"Thomas Müller",
"Wolfram Toloi",
"Landon Donovan"
]
},
{
"date_of_final": "2014-07-13",
"hosting_country": "Brazil",
"winner": "Germany",
"top_scorers": [
"James Rodríguez",
"Miroslav Klose",
"Thomas Müller"
]
},
{
"date_of_final": "2018-07-15",
"hosting_country": "Russia",
"winner": "France",
"top_scorers": [
"Harry Kane",
"Kylian Mbappé",
"Antoine Griezmann"
]
}
]
}
Abbiamo una lista con JSON corretti! Tutto questo in locale con llama3.
Come ho accennato prima, la validazione avviene per la struttura, non per il contenuto. Infatti, il contenuto è diverso da quello generato da GPT.
Vediamo come i marcatori siano diversi. Forse è possibile avere la lista corretta andando ad iterare sul prompt, specificando bene quali siano i marcatori che vogliamo ricevere.
Conclusione
Abbiamo visto come usare Pydantic e ollama per guidare l'output di un LLM ad un formato strutturato, come il JSON.
Ricorda che il modello viene proprio guidato in questo processo, e quindi non è deterministico. Ci saranno casi in cui il JSON non verrà rispettato proprio per la natura non deterministica degli LLM.
Commenti dalla community