Tabella dei Contenuti
Questo articolo ti insegnerà a strutturare la risposta di un LLM come GPT-4 oppure llama3 usando delle librerie di validazione in Python.
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.
Useremo diverse librerie, come Pydantic e Instructor. Il contenuto proposto sarà valido sia per modelli closed-source come GPT di OpenAI o Anthropic che per modelli open source come Llama3.
- cosa è e come definire uno modello dati
- come fare in modo che il tuo LLM rispetti il formato di output attraverso regole di validazione
- come usare le librerie Instructor e Pydantic
Buona lettura!
Perché occorre un output strutturato?
Certamente gli LLM come GPT-4 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.
Partendo da una particolare versione di GPT-3.5, OpenAI ha aggiunto il parametro response_format
nella sua API di completions - questo permette all'utente di definire diverse chiavi, come json_object
, per guidare il modello verso una risposta più consona al prompt inserito.
Ecco un esempio
from openai import OpenAI
client = OpenAI()
response = client.chat.completions.create(
model="gpt-3.5-turbo-0125",
response_format={ "type": "json_object" },
messages=[
{"role": "system", "content": "You are a helpful assistant designed to output JSON."},
{"role": "user", "content": "Who won the world series in 2020?"}
]
)
print(response.choices[0].message.content)
>>> "content": "{\"winner\": \"Los Angeles Dodgers\"}"`
Tale logica però non sempre funziona. Infatti OpenAI, nella sua documentazione, suggerisce di scrivere la parola "JSON" nel prompt proprio per guidare GPT nella generazione dello stesso. È un suggerimento talmente importante che siamo obbligati a scrivere farlo nel momento in cui usiamo response_format={ "type": "json_object" }
.
Perché è difficile per gli LLM produrre output JSON coerenti?
Questo perché gli LLM sono di fatto delle macchine che restituiscono il prossimo token con più probabilità di seguire il precedente dato un prompt in input. Di fatto, è difficile incontrare in "natura" questo pattern a meno che il modello non sia stato espressamente guidato in fase di training a vedere e comprendere questi formati.
La modalità JSON dei più recenti LLM non garantisce che l'output corrisponda a uno schema specifico, ma solo che sia valido e venga analizzato senza errori.
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-4 oppure Llama3, come accennato.
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, Instructor, 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
- Instructor: è di fatto un wrapper intorno a Pydantic specializzato per lavorare con gli LLM ed è la libreria che permetterà di creare le logiche di validazione
- 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
pip install pydantic instructor 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 Pydnatic 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 Instructor 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 Instructor
Qui creermo la logica principale della validazione e strutturazione del JSON grazie ad Instructor. Utilizza una interfaccia simile a quella che fornisce OpenAI per chiamare GPT via API.
Per prima cosa useremo OpenAI in una funzione chiamata query_gpt
che ci permette di parametrizzare il nostro prompt
from openai import OpenAI
import instructor
def query_gpt(prompt: str) -> list:
client = instructor.from_openai(OpenAI(api_key="..."))
resp = client.chat.completions.create(
model="gpt-3.5-turbo",
response_model=SoccerDataset,
messages=[
{"role": "system", "content": system_prompt},
{"role": "user", "content": prompt},
],
)
return resp.model_dump_json(indent=4)
Ricordiamoci di passare la nostra chiave API di OpenAI al client appena creato. Useremo GPT-3.5-Turbo, passando come response_model
proprio SoccerDataset
. Ovviamente sarebbe possibile usare "gpt-4o" se volessimo usare il modello più potente al momento della scrittura di questo articolo.
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
import instructor
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) -> list:
client = instructor.from_openai(OpenAI())
resp = client.chat.completions.create(
model="gpt-3.5-turbo",
response_model=SoccerDataset,
messages=[
{"role": "system", "content": system_prompt},
{"role": "user", "content": prompt},
],
)
return resp.model_dump_json(indent=4)
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-3.5-Turbo ha seguito alla perfezione il nostro prompt e Instructor 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 modelli open source con Instructor
Abbiamo visto come usare GPT in Instructor per avere un output JSON strutturato. Vediamo ora come usare ollama per usare modelli open source come llama3.
Utilizza il comando
ollama pull llama3
per scaricarlo!Creiamo una nuova funzione chiamata query_llama
.
def query_llama(prompt: str) -> list:
client = instructor.from_openai(
OpenAI(
base_url="http://localhost:11434/v1",
api_key="ollama", # valore richiesto, ma non influente
),
mode=instructor.Mode.JSON,
)
resp = client.chat.completions.create(
model="llama3",
messages=[
{
"role": "system",
"content": system_prompt
},
{
"role": "user",
"content": prompt
}
],
response_model=SoccerDataset,
)
return resp.model_dump_json(indent=4)
Ci sono delle differenze con il codice di GPT. Vediamole.
- ollama viene chiamato attraverso la stessa interfaccia di GPT, cambiando però il puntatore del url di base (
base_url
) e la chiave API, che è richiesta ma non serve per il corretto funzionamento (non chiedetemi perché) - bisogna esplicitare la modalità JSON attraverso il parametro
mode
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, Instructor 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