Limitare gli LLM con Output Strutturati: Ollama, Qwen3 & Python o Go
Un paio di modi per ottenere un output strutturato da Ollama
Modelli di grandi dimensioni (LLMs) sono potenti, ma in produzione raramente desideriamo paragrafi liberi. Invece, vogliamo dati prevedibili: attributi, fatti o oggetti strutturati che possiamo alimentare in un’app. Questo è LLM Structured Output.
Alcuni mesi fa Ollama ha introdotto il supporto per l’output strutturato (annuncio), rendendo possibile limitare le risposte di un modello in modo che corrispondano a una schema JSON. Questo sblocca pipeline di estrazione dei dati coerenti per compiti come l’inventario delle funzionalità degli LLM, il benchmarking dei modelli o l’automazione dell’integrazione dei sistemi.
In questo post, parleremo di:
- Cosa è l’output strutturato e perché è importante
- Modo semplice per ottenere l’output strutturato dagli LLM
- Come funziona la nuova funzionalità di Ollama
- Esempi di estrazione delle capacità degli LLM:
Cosa è l’Output Strutturato?
Normalmente, gli LLM generano testo libero:
“Il modello X supporta il ragionamento con la catena di pensiero, ha una finestra di contesto di 200K e parla inglese, cinese e spagnolo.”
Questo è leggibile, ma difficile da analizzare.
Invece, con l’output strutturato chiediamo uno schema rigoroso:
{
"name": "Modello X",
"supports_thinking": true,
"max_context_tokens": 200000,
"languages": ["Inglese", "Cinese", "Spagnolo"]
}
Questo JSON è facile da validare, archiviare in un database o alimentare a un’interfaccia utente.
Modo semplice per ottenere l’Output Strutturato dagli LLM
A volte gli LLM comprendono cosa è lo schema e possiamo chiedere loro di restituire l’output in JSON utilizzando uno schema specifico. Il modello Qwen3 di Alibaba è ottimizzato per il ragionamento e le risposte strutturate. Puoi istruirlo esplicitamente a rispondere in JSON.
Esempio 1: Utilizzo di Qwen3 con ollama
in Python, richiedendo JSON con schema
import json
import ollama
prompt = """
Sei un estrattore di dati strutturati.
Restituisci solo JSON.
Testo: "Elon Musk ha 53 anni e vive ad Austin."
Schema: { "name": string, "age": int, "city": string }
"""
response = ollama.chat(model="qwen3", messages=[{"role": "user", "content": prompt}])
output = response['message']['content']
# Analisi JSON
try:
data = json.loads(output)
print(data)
except Exception as e:
print("Errore nell'analisi del JSON:", e)
Output:
{"name": "Elon Musk", "age": 53, "city": "Austin"}
Validazione dello Schema con Pydantic
Per evitare output non corretti, puoi validare contro uno schema Pydantic in Python.
from pydantic import BaseModel
class Persona(BaseModel):
name: str
age: int
city: str
# Supponiamo che 'output' sia la stringa JSON da Qwen3
data = Persona.model_validate_json(output)
print(data.name, data.age, data.city)
Questo garantisce che l’output corrisponda alla struttura prevista.
L’Output Strutturato di Ollama
Ollama ora ti permette di passare un schema nel parametro format
. Il modello è quindi vincolato a rispondere solo in JSON che corrisponde allo schema (docs).
In Python, normalmente definisci lo schema con Pydantic e lasci che Ollama lo utilizzi come schema JSON.
Esempio 2: Estrazione delle Funzionalità di un LLM
Supponiamo di avere un frammento di testo che descrive le capacità di un LLM:
“Qwen3 ha un forte supporto multilingue (inglese, cinese, francese, spagnolo, arabo). Permette passaggi di ragionamento (catena di pensiero). La finestra di contesto è di 128K token.”
Vuoi dati strutturati:
from pydantic import BaseModel
from typing import List
from ollama import chat
class LLMFeatures(BaseModel):
name: str
supports_thinking: bool
max_context_tokens: int
languages: List[str]
prompt = """
Analizza la seguente descrizione e restituisci le funzionalità del modello in JSON solo.
Descrizione del modello:
'Qwen3 ha un forte supporto multilingue (inglese, cinese, francese, spagnolo, arabo).
Permette passaggi di ragionamento (catena di pensiero).
La finestra di contesto è di 128K token.'
"""
resp = chat(
model="qwen3",
messages=[{"role": "user", "content": prompt}],
format=LLMFeatures.model_json_schema(),
options={"temperature": 0},
)
print(resp.message.content)
Output possibile:
{
"name": "Qwen3",
"supports_thinking": true,
"max_context_tokens": 128000,
"languages": ["Inglese", "Cinese", "Francese", "Spagnolo", "Arabo"]
}
Esempio 3: Confronto di Più Modelli
Inserisci descrizioni di più modelli e estrai in forma strutturata:
from typing import List
class ModelComparison(BaseModel):
models: List[LLMFeatures]
prompt = """
Estrai le funzionalità di ciascun modello in formato JSON.
1. Llama 3.1 supporta il ragionamento. Finestra di contesto di 128K. Lingue: inglese solo.
2. GPT-4 Turbo supporta il ragionamento. Finestra di contesto di 128K. Lingue: inglese, giapponese.
3. Qwen3 supporta il ragionamento. Finestra di contesto di 128K. Lingue: inglese, cinese, francese, spagnolo, arabo.
"""
resp = chat(
model="qwen3",
messages=[{"role": "user", "content": prompt}],
format=ModelComparison.model_json_schema(),
options={"temperature": 0},
)
print(resp.message.content)
Output:
{
"models": [
{
"name": "Llama 3.1",
"supports_thinking": true,
"max_context_tokens": 128000,
"languages": ["Inglese"]
},
{
"name": "GPT-4 Turbo",
"supports_thinking": true,
"max_context_tokens": 128000,
"languages": ["Inglese", "Giapponese"]
},
{
"name": "Qwen3",
"supports_thinking": true,
"max_context_tokens": 128000,
"languages": ["Inglese", "Cinese", "Francese", "Spagnolo", "Arabo"]
}
]
}
Questo rende semplice benchmarking, visualizzazione o filtraggio dei modelli in base alle loro funzionalità.
Esempio 4: Rilevamento Automatico di Gap
Puoi anche permettere valori null
quando un campo è mancante:
from typing import Optional
class FlexibleLLMFeatures(BaseModel):
name: str
supports_thinking: Optional[bool]
max_context_tokens: Optional[int]
languages: Optional[List[str]]
Questo garantisce che lo schema rimanga valido anche se alcune informazioni sono sconosciute.
Vantaggi, Avvertenze e Linee Guida
L’uso dell’output strutturato tramite Ollama (o qualsiasi sistema che lo supporta) offre molti vantaggi — ma anche alcune avvertenze.
Vantaggi
- Garanzie più forti: Il modello è chiesto di conformarsi a uno schema JSON invece che a testo libero.
- Parsing più semplice: Puoi direttamente
json.loads
o validare con Pydantic / Zod, invece che regex o euristiche. - Evoluzione basata su schema: Puoi versionare lo schema, aggiungere campi (con valori predefiniti) e mantenere la compatibilità all’indietro.
- Interoperabilità: I sistemi downstream aspettano dati strutturati.
- Determinismo (migliore con temperatura bassa): Quando la temperatura è bassa (es. 0), il modello è più propenso a rispettare rigidamente lo schema. Le documentazioni di Ollama raccomandano questo.
Avvertenze e Pericoli
- Mancata corrispondenza dello schema: Il modello potrebbe comunque deviare — ad esempio, mancare una proprietà richiesta, riordinare le chiavi o includere campi extra. Hai bisogno di validazione.
- Schema complessi: Schema JSON molto profondi o ricorsivi potrebbero confondere il modello o causare errori.
- Ambiguità nel prompt: Se il tuo prompt è vago, il modello potrebbe indovinare i campi o le unità in modo errato.
- Incoerenza tra i modelli: Alcuni modelli potrebbero essere migliori o peggiori nel rispettare i vincoli strutturati.
- Limiti dei token: Lo schema stesso aggiunge un costo in termini di token al prompt o alla chiamata API.
Linee Guida e Consigli (tratti dal blog di Ollama + esperienza)
- Usa Pydantic (Python) o Zod (JavaScript) per definire i tuoi schemi e generare automaticamente gli schemi JSON. Questo evita errori manuali.
- Includi sempre istruzioni come “rispondi solo in JSON” o “non includere commenti o testo extra” nel tuo prompt.
- Usa temperature = 0 (o molto bassa) per minimizzare la casualità e massimizzare la conformità allo schema. Ollama raccomanda il determinismo.
- Valida e potenzialmente fallback (es. riprova o pulizia) quando l’analisi JSON fallisce o la validazione dello schema fallisce.
- Inizia con uno schema più semplice, quindi estendilo gradualmente. Non complicare troppo all’inizio.
- Includi istruzioni aiuto ma vincolate: ad esempio, se il modello non può riempire un campo richiesto, rispondi con
null
invece di ometterlo (se lo schema lo permette).
Esempio Go 1: Estrazione delle Funzionalità degli LLM
Ecco un semplice programma Go che chiede a Qwen3 di restituire un output strutturato sulle funzionalità di un LLM.
package main
import (
"context"
"encoding/json"
"fmt"
"log"
"github.com/ollama/ollama/api"
)
type LLMFeatures struct {
Name string `json:"name"`
SupportsThinking bool `json:"supports_thinking"`
MaxContextTokens int `json:"max_context_tokens"`
Languages []string `json:"languages"`
}
func main() {
client, err := api.ClientFromEnvironment()
if err != nil {
log.Fatal(err)
}
prompt := `
Analizza la seguente descrizione e restituisci le funzionalità del modello in JSON solo.
Descrizione:
"Qwen3 ha un forte supporto multilingue (inglese, cinese, francese, spagnolo, arabo).
Permette passaggi di ragionamento (catena di pensiero).
La finestra di contesto è di 128K token."
`
// Definisci lo schema JSON per l'output strutturato
formatSchema := map[string]any{
"type": "object",
"properties": map[string]any{
"name": map[string]string{
"type": "string",
},
"supports_thinking": map[string]string{
"type": "boolean",
},
"max_context_tokens": map[string]string{
"type": "integer",
},
"languages": map[string]any{
"type": "array",
"items": map[string]string{
"type": "string",
},
},
},
"required": []string{"name", "supports_thinking", "max_context_tokens", "languages"},
}
// Converte lo schema in JSON
formatJSON, err := json.Marshal(formatSchema)
if err != nil {
log.Fatal("Fallimento nel marshalling dello schema di formato:", err)
}
req := &api.GenerateRequest{
Model: "qwen3:8b",
Prompt: prompt,
Format: formatJSON,
Options: map[string]any{"temperature": 0},
}
var features LLMFeatures
var rawResponse string
err = client.Generate(context.Background(), req, func(response api.GenerateResponse) error {
// Accumula il contenuto mentre scorre
rawResponse += response.Response
// Solo analizza quando la risposta è completa
if response.Done {
if err := json.Unmarshal([]byte(rawResponse), &features); err != nil {
return fmt.Errorf("errore di analisi JSON: %v", err)
}
}
return nil
})
if err != nil {
log.Fatal(err)
}
fmt.Printf("Struttura analizzata: %+v\n", features)
}
Per compilare e eseguire questo esempio di programma Go - supponiamo che abbiamo questo file main.go in una cartella ollama-struct
,
Dobbiamo eseguire all’interno di questa cartella:
# inizializza modulo
go mod init ollama-struct
# scarica tutte le dipendenze
go mod tidy
# compila ed esegui
go build -o ollama-struct main.go
./ollama-struct
Output Esempio
Struttura analizzata: {Name:Qwen3 SupportsThinking:true MaxContextTokens:128000 Languages:[Inglese Cinese Francese Spagnolo Arabo]}
Esempio Go 2: Confronto di Più Modelli
Puoi estendere questo per estrarre un elenco di modelli per il confronto.
type ModelComparison struct {
Models []LLMFeatures `json:"models"`
}
prompt = `
Estrai le funzionalità dalle seguenti descrizioni dei modelli e restituiscile in formato JSON:
1. PaLM 2: Questo modello ha capacità di ragionamento limitate e si concentra sull'intendimento linguistico di base. Supporta una finestra di contesto di 8.000 token. Supporta principalmente l'inglese.
2. LLaMA 2: Questo modello ha capacità di ragionamento moderate e può gestire alcuni compiti logici. Può elaborare fino a 4.000 token nel contesto. Supporta inglese, spagnolo e italiano.
3. Codex: Questo modello ha forti capacità di ragionamento specifiche per la programmazione e l'analisi del codice. Ha una finestra di contesto di 16.000 token. Supporta inglese, Python, JavaScript e Java.
Restituisci un oggetto JSON con un "array models" che contiene tutti i modelli.
`
// Definisci lo schema JSON per il confronto dei modelli
comparisonSchema := map[string]any{
"type": "object",
"properties": map[string]any{
"models": map[string]any{
"type": "array",
"items": map[string]any{
"type": "object",
"properties": map[string]any{
"name": map[string]string{
"type": "string",
},
"supports_thinking": map[string]string{
"type": "boolean",
},
"max_context_tokens": map[string]string{
"type": "integer",
},
"languages": map[string]any{
"type": "array",
"items": map[string]string{
"type": "string",
},
},
},
"required": []string{"name", "supports_thinking", "max_context_tokens", "languages"},
},
},
},
"required": []string{"models"},
}
// Converte lo schema in JSON
comparisonFormatJSON, err := json.Marshal(comparisonSchema)
if err != nil {
log.Fatal("Fallimento nel marshalling dello schema di confronto:", err)
}
req = &api.GenerateRequest{
Model: "qwen3:8b",
Prompt: prompt,
Format: comparisonFormatJSON,
Options: map[string]any{"temperature": 0},
}
var comp ModelComparison
var comparisonResponse string
err = client.Generate(context.Background(), req, func(response api.GenerateResponse) error {
// Accumula il contenuto mentre scorre
comparisonResponse += response.Response
// Solo analizza quando la risposta è completa
if response.Done {
if err := json.Unmarshal([]byte(comparisonResponse), &comp); err != nil {
return fmt.Errorf("errore di analisi JSON: %v", err)
}
}
return nil
})
if err != nil {
log.Fatal(err)
}
for _, m := range comp.Models {
fmt.Printf("%s: Contesto=%d, Lingue=%v\n", m.Name, m.MaxContextTokens, m.Languages)
}
Output Esempio
PaLM 2: Contesto=8000, Lingue=[Inglese]
LLaMA 2: Contesto=4000, Lingue=[Inglese Spagnolo Italiano]
Codex: Contesto=16000, Lingue=[Inglese Python JavaScript Java]
Per inciso, qwen3:4b funziona bene in questi esempi, così come qwen3:8b.
Linee Guida per gli sviluppatori Go
- Imposta la temperatura a 0 per la massima conformità allo schema.
- Valida con
json.Unmarshal
e fallback se l’analisi fallisce. - Mantieni gli schemi semplici — le strutture JSON profonde o ricorsive potrebbero causare problemi.
- Permetti campi opzionali (usa
omitempty
nei tag delle strutture Go) se si prevedono dati mancanti. - Aggiungi retry se il modello emette occasionalmente JSON non valido.
Esempio completo - Disegnare un grafico con le specifiche degli LLM (Passo passo: da JSON strutturato a tabelle di confronto)
- Definisci uno schema per i dati che desideri
Usa Pydantic in modo da poter (a) generare uno schema JSON per Ollama e (b) validare la risposta del modello.
from pydantic import BaseModel
from typing import List, Optional
class LLMFeatures(BaseModel):
name: str
supports_thinking: bool
max_context_tokens: int
languages: List[str]
- Chiedi a Ollama di restituire solo JSON in quel formato
Passa lo schema in format=
e abbassa la temperatura per il determinismo.
from ollama import chat
prompt = """
Estrai le funzionalità per ciascun modello. Restituisci solo JSON che corrisponda allo schema.
1) Qwen3 supporta la catena di pensiero; 128K contesto; inglese, cinese, francese, spagnolo, arabo.
2) Llama 3.1 supporta la catena di pensiero; 128K contesto; inglese.
3) GPT-4 Turbo supporta la catena di pensiero; 128K contesto; inglese, giapponese.
"""
resp = chat(
model="qwen3",
messages=[{"role": "user", "content": prompt}],
format={"type": "array", "items": LLMFeatures.model_json_schema()},
options={"temperature": 0}
)
raw_json = resp.message.content # JSON list of LLMFeatures
- Validazione e normalizzazione
Valida sempre prima di utilizzarlo in produzione.
from pydantic import TypeAdapter
adapter = TypeAdapter(list[LLMFeatures])
models = adapter.validate_json(raw_json) # -> list[LLMFeatures]
- Costruisci una tabella di confronto (pandas)
Trasforma gli oggetti validati in un DataFrame che puoi ordinare, filtrare e esportare.
import pandas as pd
df = pd.DataFrame([m.model_dump() for m in models])
df["languages_count"] = df["languages"].apply(len)
df["languages"] = df["languages"].apply(lambda xs: ", ".join(xs))
# Riordina le colonne per leggibilità
df = df[["name", "supports_thinking", "max_context_tokens", "languages_count", "languages"]]
# Salva come CSV per ulteriore utilizzo
df.to_csv("llm_feature_comparison.csv", index=False)
- (Opzionale) Visualizzazioni rapide
Grafici semplici ti aiutano a vedere rapidamente le differenze tra i modelli.
import matplotlib.pyplot as plt
plt.figure()
plt.bar(df["name"], df["max_context_tokens"])
plt.title("Finestra di contesto massima per modello (token)")
plt.xlabel("Modello")
plt.ylabel("Token di contesto massimi")
plt.xticks(rotation=20, ha="right")
plt.tight_layout()
plt.savefig("max_context_window.png")
TL;DR
Con il nuovo supporto per l’output strutturato di Ollama, puoi trattare gli LLM non solo come chatbot ma come motori di estrazione dati.
Gli esempi sopra mostrano come estrarre automaticamente metadati strutturati sulle funzionalità degli LLM come supporto al ragionamento, dimensione della finestra di contesto e lingue supportate — compiti che altrimenti richiederebbero un parsing fragile.
Che tu stia costruendo un catalogo di modelli LLM, un dashboard di valutazione o un assistente di ricerca AI, gli output strutturati rendono l’integrazione fluida, affidabile e pronta per la produzione.
Link utili
- https://ollama.com/blog/structured-outputs
- Ollama cheatsheet
- Python Cheatsheet
- AWS SAM + AWS SQS + Python PowerTools
- Golang Cheatsheet
- Confronto tra ORMs Go per PostgreSQL: GORM vs Ent vs Bun vs sqlc
- Riordinamento di documenti testuali con Ollama e modello Qwen3 Embedding - in Go
- Prestazioni di AWS lambda: JavaScript vs Python vs Golang