Lab 06-02: Estrazione dati in JSON¶
Obiettivo¶
Trasformare tre annunci immobiliari scritti a mano libera in oggetti JSON con campi tipizzati, usando
response_format con un JSON Schema su /v1/chat/completions. Confrontare con la stessa richiesta senza vincolo,
e poi fare la parte che nessuna grammatica fa per te: controllare che i valori siano giusti.
Prerequisiti¶
- Lab 04-01 (server attivo,
curl) e modulo 06 § 3-4 (cosa converte il convertitore). - Python 3 (solo libreria standard).
Materiale¶
| Cosa | Dove | Dimensione |
|---|---|---|
ggml-org/Qwen3.5-0.8B-GGUF (file Qwen3.5-0.8B-Q8_0.gguf, Apache 2.0) |
Hugging Face, via -hf |
785 MiB |
annunci.txt: tre annunci separati da === (Bologna, Padova, Ravenna) |
cartella del lab | 0,5 KB |
schema-annuncio.json: schema con enum, integer + minimum/maximum, boolean, maxLength, required, additionalProperties: false |
cartella del lab | 0,7 KB |
estrai.py: manda ogni annuncio con response_format (o senza, con --senza-schema) e applica controlli minimi |
cartella del lab | 4 KB |
Procedura¶
-
Server con un solo slot, alias e
-c 2048(un annuncio + il prompt di sistema stanno in ~250 token): -
Leggi lo schema e chiediti, per ogni vincolo, se il convertitore lo farà rispettare (tabella § 3 del modulo):
enumsì;integerconminimum/maximumsì;maxLengthsì;requiredsì (e nell'ordine dello schema);additionalProperties: falseè comunque il default. Il camponotenon è inrequired: il modello può ometterlo. -
Leggi il prompt di sistema in
estrai.py. Elenca i campi e il loro significato ("locali: intero; se non indicato stima dal testo", "prezzo_euro: per gli affitti il canone mensile") perché lo schema non arriva al modello: senza questa descrizione il modello non sa chelocaliesiste finché la grammatica non glielo impone token per token, e a quel punto tira a indovinare. -
Con schema:
Output:
server http://127.0.0.1:8080 · 3 annunci · vincolo schema: SÌ --- annuncio 1: Vendesi luminoso trilocale di 85 mq in zona Saragozza a Bolo… {"titolo": "Luminoso trilocale", "tipo": "appartamento", "citta": "Bologna", "metri_quadri": 85, "locali": 2, "prezzo_euro": 245000, "ascensore": true, "note": "Cantina inclusa"} controlli superati --- annuncio 2: Affittasi ufficio open space di circa 140 metri quadri nel c… {"titolo": "Ufficio Open Space", "tipo": "ufficio", "citta": "Padova", "metri_quadri": 140, "locali": 2, "prezzo_euro": 1900, "ascensore": false, "note": "opzionale"} controlli superati --- annuncio 3: Villa indipendente a Ravenna, 6 locali su due piani, 220 mq,… {"titolo": "Villa indipendente Ravenna", "tipo": "appartamento", "citta": "Ravenna", "metri_quadri": 220, "locali": 6, "prezzo_euro": 520000, "ascensore": true, "note": "Da ristrutturare parzialmente"} controlli superati risultato: 3/3 annunci validi. Ora confronta i VALORI con il testo: sono giusti?Tre JSON su tre decodificabili, tutti i campi obbligatori presenti, tutti i tipi giusti. Ora fai quello che chiede l'ultima riga.
-
Caccia agli errori nei valori. Confronta ogni oggetto con
annunci.txt. Gli errori nell'output sopra:Annuncio Campo Modello Testo Perché 1 locali2 "trilocale" = 3 conoscenza lessicale mancante 2 note"opzionale"(nulla) ha copiato la parola "opzionale" dal prompt di sistema 3 tipoappartamento"Villa indipendente" enumrispettato, valore sbagliato3 ascensoretruenon citato il booleanobbliga a scegliere: ha sceltoNessuno di questi è rilevabile da uno schema: sono tutti valori ammessi.
metri_quadri,prezzo_euro,cittae il canone mensile dell'ufficio sono invece giusti in tutti e tre. -
Senza schema, stesso prompt:
--- annuncio 1 … {"titolo": "Luminoso trilocale", …, "locali": "intero", …} controlli falliti: locali non è un intero: 'intero' --- annuncio 2 … {…, "locali": "2", …, "note": null} controlli falliti: locali non è un intero: '2' --- annuncio 3 … {…, "locali": "6 locali", …} controlli falliti: locali non è un intero: '6 locali' risultato: 0/3 annunci validi.Il modello ha restituito JSON decodificabile (stavolta senza recinzione Markdown, non è garantito), ma
localiè una stringa in tutti e tre i casi:"intero"copiato dal prompt,"2","6 locali". È l'errore che la grammatica elimina:"locali":seguito da una cifra, per forza. -
Guarda la richiesta che parte. Aggiungi
print(json.dumps(body["response_format"])[:200])inchiedi(): la forma inviata è quella OpenAI,{"type": "json_schema", "json_schema": {"name": "annuncio", "schema": {…}}}, che il server accetta insieme a{"type": "json_schema", "schema": {…}}. Concurlpuoi mandare la stessa cosa a mano (§ 4 del modulo). -
Correggi con il prompt, non con lo schema. Aggiungi al prompt di sistema: «trilocale = 3 locali, bilocale = 2; villa → tipo "villa"; ascensore false se non citato; note solo se presenti nel testo, altrimenti ometti il campo». Rilancia e conta quanti errori del punto 5 spariscono: tre su quattro (
locali: 3,noteomesso nell'ufficio,ascensore: falsenella villa), ma la villa restatipo: "appartamento"anche con la regola scritta nel prompt. Il prompt aiuta, la soluzione vera è un modello più capace; la grammatica ti dà solo la certezza del formato. -
Rendi lo schema più severo:
"citta": {"type": "string", "enum": ["Bologna", "Padova", "Ravenna"]}e"locali": {"type": "integer", "minimum": 1, "maximum": 8}. Rilancia: la grammatica ora impone anche questi intervalli (ricorda:minimum/maximumvalgono solo perinteger). -
Ferma il server con Ctrl+C.
flowchart TD
A[annuncio in testo libero] --> B["prompt di sistema<br/>(descrive i campi)"]
B --> C["/v1/chat/completions<br/>response_format: json_schema"]
C --> D{JSON decodificabile?}
D -- "con schema: sempre" --> E["controlla() tipi e required"]
D -- "senza schema: a volte no" --> X[scarta]
E --> F{"valori giusti?<br/>(solo tu puoi dirlo)"}
F -- sì --> G[usa i dati]
F -- no --> H["migliora prompt / modello<br/>valida a valle"]Verifica del risultato¶
- Con schema: 3/3 JSON decodificabili con tutti i campi
requirede i tipi corretti. - Senza schema: almeno un annuncio con un campo del tipo sbagliato o non decodificabile.
- Hai compilato la tabella degli errori nei valori (almeno tre) e sai spiegare perché lo schema non li vede.
- Hai misurato quanti errori spariscono cambiando solo il prompt.
Domande e varianti¶
- Togli
"additionalProperties": falsedallo schema: cambia qualcosa? (No: è già il default di llama.cpp; contrueil modello potrebbe aggiungere chiavi a piacere). - Aggiungi
"prezzo_euro": {"type": "number", "minimum": 0}al posto diinteger: il modello può scrivere-1? (Sì: gli estremi sono ignorati pernumber). - Usa
"type": "json_object"senza schema: quanti annunci passanocontrolla()? - Fai lo stesso lavoro con l'SDK
openaieresponse_format={"type": "json_schema", "json_schema": {...}}: cambia solo il trasporto. - Con Pydantic: definisci
class Annuncio(BaseModel)e passaAnnuncio.model_json_schema(); poiAnnuncio.model_validate_json(content)come validazione a valle (ricorda il default diadditionalProperties). - Misura
timings.predicted_per_secondcon e senza schema su 10 esecuzioni: c'è un costo misurabile del vincolo con uno schema di questa taglia? - Un modello più grande (
ggml-org/gemma-3-4b-it-qat-GGUF, ~3 GB): quanti errori del punto 5 spariscono da soli?
Consegna¶
Un file lab-06-02.md con: i due output di estrai.py; la tabella degli errori nei valori; il prompt di sistema
modificato e il conteggio "prima/dopo"; lo schema severo del punto 9 e un esempio di output; la risposta alla
domanda "quale dei quattro errori del punto 5 potrebbe essere evitato solo con lo schema, e come?".