De meeste teams lopen tegen hetzelfde probleem aan, ergens in de derde week van hun werk met LLM's: het model antwoordt bijna correct. De JSON bevat een extra commentaar. Of de afsluitende accolade ontbreekt. Of — en dit is het verraderlijkste — het model geeft geldige JSON terug, maar met het veld status ingesteld op "ok" in plaats van "approved", wat uw downstream logica niet herkent. De applicatie crasht vier uur later, wanneer dat specifieke stuk code eindelijk wordt aangeroepen. In productie.
Het probleem zit niet in onbetrouwbare LLM's. Het probleem is dat tekstopmaak en semantische correctheid twee verschillende dingen zijn, en de meeste implementaties verwarren ze. Dit artikel legt uit waar de grens ligt, welke hulpmiddelen er bestaan voor syntactische correctheid en hoe u een validatielaag bouwt die de rest opvangt.
Waarom "geef JSON terug" in een prompt niet volstaat
Wanneer u "Respond in JSON format" in een prompt schrijft, geeft u het model een instructie — geen garantie. Autoregressieve taalmodellen genereren token voor token op basis van kansen: elk volgend token is het resultaat van een kansverdeling geconditioneerd op alles wat eraan voorafging. Het model "ziet" geen "openingsaccolade die ik moet sluiten". Het ziet kansen op volgende tokens.
Hieruit vloeien drie categorieën problemen voort:
- Syntaxisfout — ongeldige JSON, overbodige tekst voor of na de JSON, markdown-blokken `
`json ...`` rondom de uitvoer, besturingstekens, unicode-escape-fouten - Schemafout — geldige JSON, maar een verplicht veld ontbreekt, een veld heeft het verkeerde gegevenstype (string in plaats van int), een enum-waarde valt buiten de toegestane lijst, een geneste structuur is afgevlakt
- Semantische fout — geldige JSON, correct schema, maar de inhoud klopt niet (verkeerde classificatie, verzonnen waarde, logische tegenstrijdigheid tussen velden)
Prompt engineering kan de eerste categorie gedeeltelijk onderdrukken. Voor de tweede en derde is het op zichzelf onvoldoende.
JSON mode vs. constrained decoding — een verschil dat ertoe doet
Wanneer providers over "JSON mode" spreken, bedoelen ze doorgaans één van twee dingen die technisch sterk van elkaar verschillen.
JSON mode (token-level bias): Via een API-parameter wordt de kans op tokens die tot de JSON-grammatica behoren verhoogd, en tokens die de geldigheid zouden breken worden onderdrukt. Het resultaat is bijna altijd syntactisch geldige JSON, maar het model bepaalt de structuur nog steeds zelf. U kunt niet garanderen dat het resultaat het veld invoice_number bevat — alleen dat het parseerbaar is.
Constrained decoding / grammar-based generation: Het model genereert uitsluitend tokens die op een gegeven moment zijn toegestaan volgens een formele grammatica (zoals JSON Schema, RegEx of een contextvrije grammatica). De implementatie gebruikt doorgaans een eindige-toestandsmachine (FSM — finite state machine) of een vergelijkbare structuur die de parsingstatus bijhoudt en bij elke stap de tokenkeuze beperkt tot wat is toegestaan. Het resultaat garandeert overeenstemming met de grammatica — inclusief het concrete schema met verplichte velden en gegevenstypen.
Het verschil in de praktijk: JSON mode levert u een parseerbaar resultaat, grammar-based generation levert u een resultaat dat overeenkomt met uw Pydantic- of zod-schema.
XGrammar: de huidige standaard voor constrained decoding
Voor de meeste productie-serving-frameworks — vLLM, SGLang, TensorRT-LLM — is XGrammar sinds begin 2026 de standaard backend voor constrained decoding. Vergeleken met de oudere aanpak (de bibliotheek Outlines, die de FSM-aanpak als eerste introduceerde maar problemen had met compilatietijden bij complexe schema's) behaalt XGrammar een overhead van minder dan 40 microseconden per token — praktisch geen impact op de latentie. Als u een moderne self-hosted serving stack gebruikt, is constrained decoding dus beschikbaar zonder extra configuratie.
Voor cloud-API's (OpenAI, Anthropic, Google) is constrained decoding doorgaans niet rechtstreeks beschikbaar — u heeft JSON mode of een response_format-parameter met een schema, die vergelijkbaar werkt maar waarvan de implementatie aan de kant van de provider ligt.
Meer over self-hosted serving: vLLM vs SGLang vs Ollama — wanneer wat te gebruiken.
Schemadefinitie: Pydantic en zod als enige bron van waarheid
Als constrained decoding afhankelijk is van een formeel schema, moet u dat schema ergens definiëren en onderhouden. In de praktijk zijn er twee dominante aanpakken:
Python — Pydantic v2:
from pydantic import BaseModel, Field
from enum import Enum
class RiskLevel(str, Enum):
low = "low"
medium = "medium"
high = "high"
class SupplierAssessment(BaseModel):
supplier_name: str
risk_level: RiskLevel
score: int = Field(ge=0, le=100)
flags: list[str]
summary: str = Field(max_length=300)Een Pydantic-model wordt direct geserialiseerd naar JSON Schema, die de meeste LLM-frameworks accepteren. Validatie bij het parsen (model.model_validate(json_data)) onthult schemafouten met een exacte locatie.
TypeScript / Node.js — zod:
import { z } from "zod";
const SupplierAssessment = z.object({
supplier_name: z.string(),
risk_level: z.enum(["low", "medium", "high"]),
score: z.number().int().min(0).max(100),
flags: z.array(z.string()),
summary: z.string().max(300),
});Het kernprincipe: het schema is de enige bron van waarheid. Definieer het niet één keer in de prompt en nog eens in de code — ze zullen uit elkaar lopen. Genereer de schemabeschrijving voor de prompt dynamisch vanuit de Pydantic- of zod-definitie zelf.
Validatie en retry — de productielaag
Noch constrained decoding, noch JSON mode beschermt tegen semantische fouten. En bij cloud-API's kunnen er ondanks JSON mode sporadisch syntactische afwijkingen optreden (netwerkonderbrekingen, afkapping bij max_tokens). Een productie-pipeline heeft daarom een validatielaag met retry-logica nodig.
Basispatroon:
import json
from pydantic import ValidationError
async def call_with_validation(prompt: str, schema: type[BaseModel], max_retries: int = 3):
for attempt in range(max_retries):
raw = await llm_call(prompt)
try:
data = json.loads(raw)
return schema.model_validate(data)
except (json.JSONDecodeError, ValidationError) as e:
if attempt == max_retries - 1:
raise
# Vrátiť chybu do promptu pre opravu
prompt = repair_prompt(prompt, raw, str(e))
raise RuntimeError("Max retries exceeded")Enkele praktische aandachtspunten:
- Retry met fout in context: Voeg bij de volgende poging de exacte foutmelding van de validator toe aan de prompt. Het model corrigeert de fout doorgaans wanneer het weet wáár de fout zit — niet wanneer het alleen "probeer het opnieuw" te horen krijgt.
- Exponential backoff: Bij API rate limits of een instabiel netwerk.
- Beperk het aantal retries: In de praktijk volstaan 2–3 pogingen. Als het model na drie pogingen nog steeds ongeldige uitvoer geeft, zit het probleem in uw schema of prompt, niet in toeval.
- Log elke mislukte poging: Zonder log weet u niet hoe vaak het voorkomt of waarom.
De betrouwbaarheid van tool calling — een nauw verwant probleem — komt aan bod in het artikel Tool calling betrouwbaar in productie.
Waarom zelfs hoge betrouwbaarheid niet volstaat en wat u eraan kunt doen
De betrouwbaarheid van een "Respond in JSON"-prompt zonder verdere infrastructuur is sterk afhankelijk van het model, de complexiteit van het schema en de prompt — en zakt bij veeleisendere schema's regelmatig onder het niveau dat in productie bruikbaar is. JSON mode (token bias) verbetert de situatie aanzienlijk. Grammar-based generation + Pydantic-validatie + retry-laag tilt de betrouwbaarheid naar een niveau waarop de meeste teams syntactische fouten niet meer hoeven op te lossen en zich kunnen concentreren op semantische correctheid.
Voor veel use-cases klinkt dat goed. Maar stelt u zich een pipeline voor die duizend keer per dag draait: bij 99% betrouwbaarheid heeft u tien mislukkingen per dag. Als elke mislukking een handmatige ingreep van een operator of een fout verwerkt document betekent, worden die tien gevallen per dag snel een probleem.
Praktische maatregelen naast retry:
- Eenvoudige schema's: Elk niveau van nesting en elk nieuw veld verlaagt de betrouwbaarheid. Als u een complex schema nodig heeft, splits het dan op in meerdere sequentiële aanroepen met eenvoudigere uitvoer.
- Enum in plaats van vrije tekst: Vervang waar mogelijk vrije tekst door een vaste lijst met waarden. Het model moet kiezen uit
["approved", "rejected", "pending"], niet zelf een formaat verzinnen. - Lage temperaturen:
temperature=0of dicht bij nul voor extractietaken. Creativiteit is hier niet gewenst. - Expliciete voorbeelden in de prompt: Few-shot-voorbeelden van geldige JSON-uitvoer zijn nog steeds een van de meest effectieve trucs.
Semantische correctheid: de grens waar technologie tekortschiet
Constrained decoding garandeert dat de uitvoer syntactisch en schema-conform is. Het garandeert niet dat de inhoud correct is.
Voorbeelden van semantische fouten die door alle technische lagen heen glippen:
- Classificatie van een factuur als
"approved"hoewel het bedrag de goedgekeurde limiet overschrijdt — omdat het model niet is bijgetraind op uw interne regels - Extractie van de verkeerde datum uit een document (verwisseld veld)
- Sentiment
"positive"voor een tekst die ironisch kritisch is - Waarde
score: 87die niet uit de tekst voortvloeit maar wordt gegenereerd als een "redelijk getal"
Deze fouten lost u anders op: evals, tests op een representatieve steekproef van uitvoer, en waar het risico hoog is — een human-in-the-loop-laag. Hoe u de kwaliteit van LLM-uitvoer systematisch meet, inclusief de semantische dimensie, beschrijven we in het artikel Hoe u de kwaliteit van een LLM-applicatie meet (evals).
Structured outputs via API-providers vs. self-hosted
Als u cloud-API's gebruikt, heeft u de volgende opties:
- OpenAI: Parameter
response_formatmettype: "json_schema"en een inline JSON Schema-definitie. Vanaf een bepaalde API-versie wordt schema-conformiteit op grammaticaniveau gegarandeerd (niet alleen token bias). - Anthropic Claude: Native gestructureerde uitvoer via de parameter
output_format— constrained decoding rechtstreeks op tokenniveau, garandeert overeenstemming met JSON Schema. Ook strict tool use beschikbaar met een nauwkeurig gedefinieerd invoerschema. - Google Gemini:
responseMimeType: "application/json"+ parameterresponseSchema.
Alle drie aanpakken werken goed voor typische use-cases. Praktische verschillen ontstaan bij:
- Zeer complexe schema's (diepe nesting,
oneOf/anyOf) — cloud-API's kunnen beperkingen hebben op ondersteunde JSON Schema-functies - Hoge throughput — bij duizenden requests per minuut is self-hosted
vLLM+XGrammareconomisch voordeliger, zie de vergelijking in vLLM vs SGLang vs Ollama - Gevoelige data — als invoerdocumenten PII of bedrijfsgeheimen bevatten, brengt een cloud-API data-egress-risico met zich mee; self-hosted on-prem elimineert dat risico
Voor gereguleerde sectoren (gezondheidszorg, financiën, juridisch) is de on-prem route vrijwel altijd relevant — meer hierover in Lokale LLM vs. cloud: wanneer wat zinvol is.
Gestructureerde uitvoer in RAG- en agentpipelines
Structured outputs zijn niet alleen voor extractietaken. In elke pipeline waarbij een LLM een tussenresultaat genereert dat door een volgend component wordt geconsumeerd, is een gestructureerd formaat een voorwaarde voor betrouwbaarheid.
In een RAG-pipeline heeft u doorgaans gestructureerde uitvoer nodig voor:
- Query planning — het model beslist welke bronnen bevraagd worden, met welk filter, hoeveel resultaten
- Reranking-beslissing — bij agentic RAG beoordeelt het model de relevantie van een chunk en geeft een score terug
- Citaties — waar in het brondocument het antwoord staat (chunk-ID, offset, betrouwbaarheid)
- Einduitvoer voor downstream — wanneer de RAG-pipeline door een ander systeem wordt geconsumeerd in plaats van alleen tekst te tonen
In een agentpipeline geldt hetzelfde voor elke tool-aanroep: de agent moet een gestructureerde actiekeuze teruggeven (tool name + parameters), zodat de orchestrator weet wat hij moet uitvoeren. Een uitvalsvorm van de structuur betekent hier niet alleen een parsefout — het kan de uitvoering van de verkeerde actie tot gevolg hebben.
Praktische ervaring: in een pipeline met meer dan drie tools loont het om elke tool-invoer te valideren via een Pydantic-model in plaats van een vrij dict. Fouten worden dan aan de ingangsgrens van de tool ontdekt, niet diep in de logica ervan.
Monitoring en drift in productie
Gestructureerde uitvoer heeft de neiging goed te werken na de deployment en geleidelijk te verslechteren — wanneer het model aan de kant van de provider wordt bijgewerkt (cloud-API's vernieuwen modellen stilzwijgend), wanneer de verdeling van de invoerdata verandert, of wanneer het schema nieuwe velden krijgt zonder dat de prompt wordt bijgewerkt.
Minimale monitoring die we aanbevelen:
- Parse-foutpercentage: Welk percentage van de aanroepen eindigt na de eerste poging met een validatiefout? Als dit getal stijgt, is er iets veranderd.
- Retry-percentage: Welk percentage van de aanroepen had meer dan één poging nodig? Boven de 5% is een signaal.
- Field-level null/default-percentage: Als het veld
invoice_numberin 30% van de gevallen null is, bevatten de invoerdocumenten het óf niet, óf het model extraheert het niet meer. - Schema-versietracking: Elke schemawijziging is een deployment-event — log de schemaversie bij elke aanroep, zodat u kwaliteitsveranderingen kunt correleren.
Zonder deze monitoring wordt degradatie pas ontdekt wanneer het downstream systeem in productie uitvalt — niet wanneer het begint.
Veelgestelde vragen
Wat is het verschil tussen JSON mode en structured outputs?
JSON mode (token-level bias) garandeert alleen syntactische geldigheid — het resultaat is parseerbare JSON, maar zonder garantie voor een concrete structuur. Structured outputs (grammar-based / schema-constrained) garanderen overeenstemming met een gedefinieerd schema, inclusief verplichte velden en gegevenstypen. Voor productie-pipelines heeft u de tweede aanpak nodig.
Heeft constrained decoding invloed op de kwaliteit of snelheid van het model?
Bij moderne implementaties (XGrammar) is de overhead minder dan 40 microseconden per token — in de praktijk niet meetbaar. De antwoordkwaliteit kan licht afwijken, omdat het model niet vrij kan formuleren — bij brede schema's met lange tekstvelden is dat verwaarloosbaar; bij zeer beperkende schema's (bijvoorbeeld een kleine enum voor een antwoord in natuurlijke taal) kan de uitvoer minder natuurlijk zijn.
Kan ik structured outputs gebruiken met een lokaal draaiend model?
Ja, met vLLM of SGLang is XGrammar out-of-the-box beschikbaar. Ollama ondersteunt JSON mode, maar geen grammar-based constrained decoding in volledige omvang — voor productie-pipelines met een nauwkeurig schema is vLLM de betere keuze. Meer hierover in vLLM vs SGLang vs Ollama.
Wat te doen als het model ondanks het juiste formaat toch onjuiste waarden blijft teruggeven?
Dit is een semantische, geen syntactische fout — constrained decoding lost dit niet op. Oplossingen: few-shot-voorbeelden met correct/incorrect-patronen, expliciete regels in de systeemprompt, fine-tuning op een domeinspecifieke dataset, of een human-in-the-loop-laag voor kritieke beslissingen.
Hoe definieert u een schema zodat het model geen fouten maakt in gegevenstypen?
Wees expliciet: gebruik in plaats van score: number liever score: integer, minimum: 0, maximum: 100. In plaats van date: string gebruikt u date: string, format: date (ISO 8601). Definieer enum-waarden altijd — een vrije string is een uitnodiging voor variaties. Elke precisering in het schema verkleint de kans op een fout.
*MP Industrial Solutions helpt bedrijven bij het ontwerpen en implementeren van productie-LLM-pipelines — van uitvoervalidatie tot monitoring in een echte omgeving. Als u de betrouwbaarheid van gestructureerde uitvoer in uw systeem wilt verbeteren, kijken we graag samen met u naar de concrete architectuur.*
