Du ber modellen svara med JSON, får JSON, och skriver klart integrationen. Sedan kommer körningen där svaret inleds med en artig mening, avslutas med ett stycke förklaring eller lägger objektet i en kodruta med tre bakåtfnuttar – och parsern kastar. Att be om ett format är en instruktion i prompten. Att tvinga fram det är något annat: både Ollama och llama.cpp kan begränsa själva avkodningen så att bara tokens som kan leda till ett giltigt svar får väljas. Det är en inställning i den lokala runtimen, inte ett promptknep, och den går att slå på, mäta och prissätta.
Tre lager, tre olika sorters garanti
Håll isär var i kedjan formen bestäms. I prompten ber du om ett format; modellen kan följa instruktionen eller låta bli. I avkodningen tvingar runtimen fram formen genom att maskera bort tokens som skulle bryta mot schemat eller grammatiken. Efter svaret avvisar valideringen det som ändå är fel. De tre lagren löser olika problem, och det vanligaste misstaget är att låta lager ett stå ensamt och sedan ta en lyckad körning för ett bevis.
| Lager | Vad det gör | Vad det inte gör |
|---|---|---|
| Prompt | Talar om vilka fält som avses och vad de betyder | Garanterar ingenting om formen |
| Avkodning | Utesluter tokens som bryter formen medan svaret genereras | Säger inget om att innehållet är sant eller rimligt |
| Validering | Avvisar fel innehåll och styr en reparation eller ett omtag | Räddar inte ett svar som aldrig gick att tolka |
Den här artikeln äger lager två: hur du låser formen i den runtime som kör på din egen maskin och hur du mäter att låset håller. Lager tre – ett schema som avvisar fel innehåll, en reparationsloop med tak och loggning i en integration – hör hemma i Monkeybases genomgång av validering av strukturerade AI-svar. Valet av modell och variant ligger före båda och hör till modellnavet och kvantiseringsartikeln.
Sätt ett JSON-schema på Ollama-anropet
Ollamas parameter heter format och finns dokumenterad både för /api/generate och /api/chat. Den tar antingen strängen "json" eller ett fullständigt JSON-schema som objekt. Skillnaden är större än den ser ut: strängen tvingar fram syntaktiskt giltig JSON, medan schemaobjektet också binder vilka nycklar och typer som är tillåtna. Vill du ha dina fält och inte bara ett tolkbart svar är det schemat du ska skicka.
curl http://localhost:11434/api/generate -d '{
"model": "llama3.2",
"prompt": "Sammanfatta ordern. Svara enligt schemat.",
"stream": false,
"format": {
"type": "object",
"properties": {
"ordernummer": {"type": "string"},
"antal_rader": {"type": "integer"},
"bradskande": {"type": "boolean"}
},
"required": ["ordernummer", "antal_rader", "bradskande"]
},
"options": {"temperature": 0, "seed": 42}
}'
Två rekommendationer står i Ollamas egen dokumentation och är värda att följa. Den första är att också lägga schemat som text i prompten, för att förankra modellens svar – tvånget styr formen, men modellen behöver ändå veta vad fälten betyder. Den andra är att sänka temperaturen, förslagsvis till noll, för mer deterministiska svar. seed är dokumenterat som ett fält under options för reproducerbara svar och behövs för att mätningen längre ned ska bli jämförbar.
En avgränsning som lätt förbises: dokumentationen anger att Ollamas molntjänst för närvarande inte stöder strukturerad utdata. Det är alltså en funktion du får just genom att köra lokalt. Går anropet i stället via det OpenAI-kompatibla gränssnittet heter parametern response_format.
GBNF i llama.cpp när formen inte är JSON
Kör du llama.cpp direkt har du ett mer generellt verktyg. GBNF, som står för GGML BNF, är llama.cpps format för formella grammatiker som begränsar modellens utdata. Det är Backus-Naur-form utökad med regexliknande drag: * för noll eller fler, + för en eller fler, ? för valfri, {m} för exakt m gånger och {m,n} för ett intervall. Teckenklasser skrivs som [1-9] och negeras med ett inledande cirkumflex. Regeln root är ingången och beskriver hela svaret.
En grammatik skickas till llama-cli med flaggorna --grammar eller --grammar-file. Repot innehåller färdiga grammatiker, bland dem grammars/json.gbnf, vars root-regel helt enkelt är object och som därifrån bygger upp värden, arrayer, strängar och tal. Den fungerar som utgångspunkt när du vill ha giltig JSON men inte har något schema.
./llama-cli -m modell.gguf \
--grammar-file grammars/json.gbnf \
-p 'Sammanfatta ordern som JSON'
Har du redan ett JSON-schema finns tre vägar. Skriptet examples/json_schema_to_grammar.py konverterar schemat till en GBNF-fil i förväg, vilket är praktiskt när samma schema ska återanvändas och versionshanteras. Flaggan -j, alltså --json-schema, tar ett schema direkt på kommandoraden. I llama-server skickas schemat i kroppsfältet json_schema för completion-anropen och i response_format för /chat/completions.
Dokumentationen är uttrycklig: JSON-schemat används enbart för att begränsa modellens utdata och injiceras inte i prompten. Modellen ser alltså inte fältnamnen om du inte själv beskriver dem. En grammatik som tvingar fram tre fält utan att prompten förklarar vad de ska innehålla ger giltig JSON med gissat innehåll.
Mät andelen giltiga svar i stället för att lita på en körning
Frys det som ska vara lika mellan lägena: samma modell och variant, temperatur noll, samma kontextlängd och samma runtime-version. Bygg sedan en svit med minst tio olika indatafall som representerar det integrationen faktiskt ska ta emot. Spara en fast seed per fall och kör exakt samma fall med samma seed i varje läge. Upprepar du i stället samma prompt och samma seed tio gånger mäter du reproducerbarhet för ett enda fall, inte hur ofta formen håller över olika indata. Notera runtime-versionen – ett låst prov som jämförs över en uppgradering är bara meningsfullt om du vet vilken version det gällde. Mätdisciplinen är densamma som i mätguiden för kall och varm Ollama-start; det är bara ett annat utfall som räknas.
Kör därefter hela sviten och för två separata räknare. Den första räknar svar som över huvud taget går att tolka som JSON. Den andra räknar svar som dessutom uppfyller schemat: alla obligatoriska fält på plats och rätt typ i varje. Med "format": "json" kan den första räknaren stå på tio medan den andra står lägre, och det är precis den skillnaden som avgör om strängen räcker eller om hela schemat behöver skickas med.
| Kolumn i protokollet | Vad du noterar |
|---|---|
| Läge | Utan tvång, med strängen "json", eller med fullt schema |
| Testfall | Minst 10 olika indatafall; samma svit och seed per fall i varje läge |
| Tolkbara svar | Antal som parsern accepterar |
| Schemagiltiga svar | Antal med rätt fält och rätt typer |
| Runtime | Exakt version av Ollama eller llama.cpp |
Redovisa först det observerade utfallet, till exempel 10 av 10 schemagiltiga svar, utan decimalprecision som provet inte bär. Tio felfria testfall betyder inte hundra procent. Om testfallen rimligt representerar den trafik du bryr dig om och utfallen kan behandlas som oberoende försök ger tumregeln 3/n en ungefärlig övre 95-procentsgräns kring trettio procents felandel vid tio felfria försök och kring tio procent vid trettio. Tio omkörningar av samma prompt och seed uppfyller inte det villkoret. Ska beslutet bära en integration i drift är trettio representativa testfall en billig uppgradering av underlaget; ska det bara avgöra vilken av två inställningar som används kan tio vara ett första jämförelseprov.
Räkna på vad tvånget kostar
Begränsad avkodning är inte gratis: runtimen måste vid varje steg avgöra vilka tokens som fortfarande är tillåtna. Om det märks beror på modell, maskin och hur komplicerat schemat är, och därför ska siffran mätas lokalt i stället för hämtas ur en artikel. Ollama redovisar i det icke-strömmande svaret eval_count och eval_duration, där tidsfälten är dokumenterade i nanosekunder. Genereringstakten blir eval_count delat med eval_duration, multiplicerat med en miljard, uttryckt i tokens per sekund.
curl -s http://localhost:11434/api/generate -d @body.json \
| python -c "import json,sys; d=json.load(sys.stdin); print(d['eval_count'], d['eval_duration'])"
Kör samma prompt med och utan format och jämför medianen av minst fem par. Se upp med en fallgrop i tolkningen: ett tvingat svar är oftast kortare, eftersom den artiga inledningen och den avslutande förklaringen försvinner. Total genereringstid kan därför sjunka samtidigt som takten i tokens per sekund sjunker. Bestäm före mätningen vilken av de två siffrorna beslutet vilar på, och håll modellen laddad mellan anropen så att load_duration inte smyger in i jämförelsen – keep-alive-artikeln beskriver hur du kontrollerar det.
Fallgropar när schemat blir grammatik
Konverteringen från JSON-schema till GBNF täcker inte hela schemastandarden, och begränsningarna är dokumenterade. additionalProperties har som standard värdet falskt, alltså inga extra nycklar. minimum, exclusiveMinimum, maximum och exclusiveMaximum stöds tills vidare bara för heltal, inte för decimaltal. Nästlade $ref är trasiga, och $ref mot en fjärradress stöds inte i C++-versionen. Strängformaten uri och email saknas, liksom uniqueItems, contains, not och villkoren if, then och else. För upprepningar avråds mönstret med en rad valfria upprepningar efter varandra av prestandaskäl, till förmån för intervallformen x{0,N}.
Praktisk följd: ett schema som validerar utmärkt i din applikation kan ge en grammatik som tyst släpper igenom en regel du trodde var låst. Låt därför aldrig grammatiken vara den enda kontrollen av ett värdeintervall. Prova dessutom schemat i sin grammatikform, inte bara i sin valideringsform, och lägg schemafilen i samma versionshantering som resten av konfigurationen enligt driftguidens rutin.
Formen är inte innehållet
Ett tvingat svar är giltigt, inte sant. Ett fält som heter antal_rader och är deklarerat som heltal kommer att innehålla ett heltal – även när modellen saknade underlag för att räkna. Tvånget flyttar alltså felen från parsern till affärslogiken, vilket är en förbättring men inte en lösning. Det är därför lager tre finns: ett valideringssteg som prövar innehållet mot rimliga gränser, ett tak för hur många gånger ett svar får repareras och en logg som visar hur ofta det händer. Den delen bor hos Monkeybase. Här räcker det att formen är låst, att du vet hur ofta den håller och vad låset kostar.
Källor
- Ollama — strukturerad utdata:
formatsom sträng eller schema, schemat i prompten, temperatur noll och molnbegränsningen - Ollama generate-API —
format,options,seedoch tidsfälten i nanosekunder - Ollama chat-API —
formatoch dokumenteradeoptions-fält - llama.cpp GBNF-guide — syntax,
--grammar-file,--json-schema,json_schemai llama-server och kända begränsningar - llama.cpp
grammars/json.gbnf— färdig grammatik för giltig JSON
Källorna kontrollerades 25 aug 2026. Inga egna mätvärden anges: tio respektive trettio testfall och fem tidspar är AI-burkens provupplägg, inte utfästelser från Ollama eller llama.cpp. Kontrollera artikeln igen när Ollama ändrar dokumentationen för format, när llama.cpp ändrar GBNF-guiden eller konverteringens begränsningar, eller senast 25 nov 2026.