LLM’s beperken met gestructureerde output: Ollama, Qwen3 en Python of Go

Enkele manieren om gestructureerde output van Ollama te krijgen

Inhoud

Grote Taalmodellen (LLM’s) zijn krachtig, maar in productieomgevingen willen we zelden vrij tekst. In plaats daarvan willen we voorspelbare data: attributen, feiten of gestructureerde objecten die je in een applicatie kunt laden. Dat is Gestructureerde Output van LLM’s.

Schema-afdwinging vermindert de frequentie waarmee slechte logits leiden tot ongeldig JSON, maar temperatuur en straffen zijn nog steeds belangrijk voor ‘retry storms’; zie agentische inferentieparameters voor Qwen en Gemma wanneer je format-beperkingen combineert met agents.

Sommige tijd geleden heeft Ollama ondersteuning voor gestructureerde output geïntroduceerd (aan kondiging), waardoor het mogelijk is om de antwoorden van een model te beperken tot een JSON-schema. Dit opent de deur voor consistente data-extractiepijplijnen voor taken zoals het catalogiseren van LLM-functies, het benchmarken van modellen of het automatiseren van systeemintegratie.

eenden in een rij

In dit bericht behandelen we:

  • Wat gestructureerde output is en waarom het belangrijk is
  • Een eenvoudige manier om gestructureerde output van LLM’s te krijgen
  • Hoe de nieuwe functie van Ollama werkt
  • Voorbeelden van het extraheren van LLM-capaciteiten:

Wat is gestructureerde output?

Normaal gesproken genereren LLM’s vrije tekst:

“Model X ondersteunt redeneren met chain-of-thought, heeft een contextvenster van 200K en spreekt Engels, Chinees en Spaans.”

Dat is leesbaar, maar moeilijk te parsen.

In plaats daarvan vragen we met gestructureerde output om een strikt schema:

{
  "name": "Model X",
  "supports_thinking": true,
  "max_context_tokens": 200000,
  "languages": ["Engels", "Chinees", "Spaans"]
}

Dit JSON is eenvoudig te valideren, op te slaan in een database of aan een UI te voeden.


Eenvoudige manier om gestructureerde output van LLM te krijgen

LLM’s begrijpen soms wat het schema is en we kunnen de LLM vragen om output in JSON te retourneren met behulp van een specifiek schema. Het Qwen3-model van Alibaba is geoptimaliseerd voor redeneren en gestructureerde antwoorden. Je kunt het expliciet instrueren om in JSON te antwoorden.

Voorbeeld 1: Qwen3 gebruiken met ollama in Python, JSON met schema aanvragen

import json
import ollama

prompt = """
Je bent een gestructureerde data-extractor.
Retourneer alleen JSON.
Tekst: "Elon Musk is 53 en woont in Austin."
Schema: { "name": string, "age": int, "city": string }
"""

response = ollama.chat(model="qwen3", messages=[{"role": "user", "content": prompt}])
output = response['message']['content']

# Parse JSON
try:
    data = json.loads(output)
    print(data)
except Exception as e:
    print("Fout bij het parsen van JSON:", e)

Output:

{"name": "Elon Musk", "age": 53, "city": "Austin"}

Schema-validatie afdwingen met Pydantic

Om misvormde outputs te voorkomen, kun je valideren tegen een Pydantic-schema in Python.

from pydantic import BaseModel

class Person(BaseModel):
    name: str
    age: int
    city: str

# Stel dat 'output' de JSON-string van Qwen3 is
data = Person.model_validate_json(output)
print(data.name, data.age, data.city)

Dit zorgt ervoor dat de output voldoet aan de verwachte structuur.


Gestructureerde output van Ollama

Ollama laat je nu een schema doorgeven in de format-parameter. Het model is dan beperkt om alleen te antwoorden in JSON die voldoet aan het schema (documentatie).

In Python definieer je je schema meestal met Pydantic en laat je Ollama dat gebruiken als JSON-schema.


Voorbeeld 2: LLM-functiemedata extraheren

Stel dat je een tekstfragment hebt dat de vaardigheden van een LLM beschrijft:

“Qwen3 heeft sterke meertalige ondersteuning (Engels, Chinees, Frans, Spaans, Arabisch). Het staat redeneerstappen toe (chain-of-thought). Het contextvenster is 128K tokens.”

Je wilt gestructureerde data:

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 = """
Analyser de volgende beschrijving en retourneer de functies van het model in JSON.
Modelbeschrijving:
'Qwen3 heeft sterke meertalige ondersteuning (Engels, Chinees, Frans, Spaans, Arabisch).
Het staat redeneerstappen toe (chain-of-thought).
Het contextvenster is 128K tokens.'
"""

resp = chat(
    model="qwen3",
    messages=[{"role": "user", "content": prompt}],
    format=LLMFeatures.model_json_schema(),
    options={"temperature": 0},
)

print(resp.message.content)

Mogelijke output:

{
  "name": "Qwen3",
  "supports_thinking": true,
  "max_context_tokens": 128000,
  "languages": ["Engels", "Chinees", "Frans", "Spaans", "Arabisch"]
}

Voorbeeld 3: Meerdere modellen vergelijken

Voer beschrijvingen van meerdere modellen in en extraheer deze naar gestructureerde vorm:

from typing import List

class ModelComparison(BaseModel):
    models: List[LLMFeatures]

prompt = """
Extraher de functies van elk model naar JSON.

1. Llama 3.1 ondersteunt redeneren. Contextvenster is 128K. Talen: alleen Engels.
2. GPT-4 Turbo ondersteunt redeneren. Contextvenster is 128K. Talen: Engels, Japans.
3. Qwen3 ondersteunt redeneren. Contextvenster is 128K. Talen: Engels, Chinees, Frans, Spaans, Arabisch.
"""

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": ["Engels"]
    },
    {
      "name": "GPT-4 Turbo",
      "supports_thinking": true,
      "max_context_tokens": 128000,
      "languages": ["Engels", "Japans"]
    },
    {
      "name": "Qwen3",
      "supports_thinking": true,
      "max_context_tokens": 128000,
      "languages": ["Engels", "Chinees", "Frans", "Spaans", "Arabisch"]
    }
  ]
}

Dit maakt het triviaal om modellen te benchmarken, visualiseren of filteren op basis van hun functies.


Voorbeeld 4: Automatisch lacunes detecteren

Je kunt zelfs null-waarden toestaan wanneer een veld ontbreekt:

from typing import Optional

class FlexibleLLMFeatures(BaseModel):
    name: str
    supports_thinking: Optional[bool]
    max_context_tokens: Optional[int]
    languages: Optional[List[str]]

Dit zorgt ervoor dat je schema geldig blijft, zelfs als sommige informatie onbekend is.


Voordelen, valkuilen & beste praktijken

Het gebruik van gestructureerde output via Ollama (of elk ander systeem dat dit ondersteunt) biedt veel voordelen — maar heeft ook enkele valkuilen.

Voordelen

  • Sterkere garanties: Het model wordt gevraagd om te voldoen aan een JSON-schema in plaats van vrije tekst.
  • Eenvoudiger parsen: Je kunt direct json.loads gebruiken of valideren met Pydantic/Zod, in plaats van regex of heuristieken.
  • Schema-gebaseerde evolutie: Je kunt je schema versioneren, velden toevoegen (met standaardwaarden) en backward compatibility behouden.
  • Interoperabiliteit: Afdalingssystemen verwachten gestructureerde data.
  • Determinisme (beter bij lage temperatuur): Wanneer de temperatuur laag is (bijv. 0), is het model waarschijnlijker om strikt aan het schema te blijven. Ollama’s documentatie beveelt dit aan.

Valkuilen & Problemen

  • Schemamismatch: Het model kan nog steeds afwijken — bijv. een verplichte eigenschap missen, keys herschikken of extra velden toevoegen. Je hebt validatie nodig.
  • Complexe schema’s: Zeer diepe of recursieve JSON-schema’s kunnen het model verwarren of tot falen leiden.
  • Ambiguïteit in prompt: Als je prompt vaag is, kan het model velden of eenheden verkeerd raden.
  • Inconsistentie tussen modellen: Sommige modellen kunnen beter of slechter zijn in het naleven van gestructureerde beperkingen.
  • Tokenlimieten: Het schema zelf voegt tokenkosten toe aan de prompt of API-aanroep.

Beste praktijken & tips (geput uit Ollama’s blog + ervaring)

  • Gebruik Pydantic (Python) of Zod (JavaScript) om je schema’s te definiëren en JSON-schema’s automatisch te genereren. Dit voorkomt handmatige fouten.
  • Voeg altijd instructies toe zoals “antwoord alleen in JSON” of “voeg geen commentaar of extra tekst toe” aan je prompt.
  • Gebruik temperatuur = 0 (of zeer laag) om willekeur te minimaliseren en schema-naleving te maximaliseren. Ollama beveelt determinisme aan.
  • Valideer en overweeg een fallback (bijv. opnieuw proberen of opruimen) wanneer het parsen van JSON faalt of schema-validatie faalt.
  • Begin met een eenvoudiger schema en breid het geleidelijk uit. Maak het aanvankelijk niet te complex.
  • Voeg behulpzame maar beperkte foutinstructies toe: bijv. als het model een verplicht veld niet kan invullen, antwoord dan met null in plaats van het weg te laten (als je schema dit toestaat).

Go Voorbeeld 1: LLM-functies extraheren

Hier is een eenvoudig Go-programma dat Qwen3 vraagt om gestructureerde output over de functies van een 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 := `
  Analyseer de volgende beschrijving en retourneer de functies van het model in JSON.
  Beschrijving:
  "Qwen3 heeft sterke meertalige ondersteuning (Engels, Chinees, Frans, Spaans, Arabisch).
  Het staat redeneerstappen toe (chain-of-thought).
  Het contextvenster is 128K tokens."
  `

	// Definieer het JSON-schema voor gestructureerde output
	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"},
	}

	// Converteer schema naar JSON
	formatJSON, err := json.Marshal(formatSchema)
	if err != nil {
		log.Fatal("Fout bij het marshallen van het formatschema:", 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 {
		// Accumuleer content terwijl het streamt
		rawResponse += response.Response

		// Parse alleen wanneer de response compleet is
		if response.Done {
			if err := json.Unmarshal([]byte(rawResponse), &features); err != nil {
				return fmt.Errorf("JSON parse error: %v", err)
			}
		}
		return nil
	})
	if err != nil {
		log.Fatal(err)
	}

	fmt.Printf("Geparsed struct: %+v\n", features)
}

Om dit voorbeeld Go-programma te compileren en uit te voeren — laten we aannemen dat we dit main.go-bestand in een map ollama-struct hebben, We moeten binnen deze map uitvoeren:

# module initialiseren
go mod init ollama-struct
# alle afhankelijkheden ophalen
go mod tidy
# bouwen & uitvoeren
go build -o ollama-struct main.go
./ollama-struct

Voorbeeldoutput

Geparsed struct: {Name:Qwen3 SupportsThinking:true MaxContextTokens:128000 Languages:[Engels Chinees Frans Spaans Arabisch]}

Go Voorbeeld 2: Meerdere modellen vergelijken

Je kunt dit uitbreiden om een lijst van modellen voor vergelijking te extraheren.

  type ModelComparison struct {
		Models []LLMFeatures `json:"models"`
	}

	prompt = `
	Extraher de functies uit de volgende modelbeschrijvingen en retourneer als JSON:

	1. PaLM 2: Dit model heeft beperkte redeneermogelijkheden en richt zich op basis taalbegrip. Het ondersteunt een contextvenster van 8.000 tokens. Het ondersteunt voornamelijk alleen de Engelse taal.
	2. LLaMA 2: Dit model heeft matige redeneermogelijkheden en kan sommige logische taken aan. Het kan tot 4.000 tokens in zijn context verwerken. Het ondersteunt Engels, Spaans en Italiaans.
	3. Codex: Dit model heeft sterke redeneermogelijkheden specifiek voor programmeren en code-analyse. Het heeft een contextvenster van 16.000 tokens. Het ondersteunt Engels, Python, JavaScript en Java.

	 Retourneer een JSON-object met een "models"-array die alle modellen bevat.
	`

	// Definieer het JSON-schema voor modelvergelijking
	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"},
	}

	// Converteer schema naar JSON
	comparisonFormatJSON, err := json.Marshal(comparisonSchema)
	if err != nil {
		log.Fatal("Fout bij het marshallen van het vergelijkingsschema:", 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 {
		// Accumuleer content terwijl het streamt
		comparisonResponse += response.Response

		// Parse alleen wanneer de response compleet is
		if response.Done {
			if err := json.Unmarshal([]byte(comparisonResponse), &comp); err != nil {
				return fmt.Errorf("JSON parse error: %v", err)
			}
		}
		return nil
	})
	if err != nil {
		log.Fatal(err)
	}

	for _, m := range comp.Models {
		fmt.Printf("%s: Context=%d, Talen=%v\n", m.Name, m.MaxContextTokens, m.Languages)
	}

Voorbeeldoutput

PaLM 2: Context=8000, Talen=[Engels]
LLaMA 2: Context=4000, Talen=[Engels Spaans Italiaans]
Codex: Context=16000, Talen=[Engels Python JavaScript Java]

Overigens werkt qwen3:4b op deze voorbeelden goed, net als qwen3:8b.

Beste praktijken voor Go-ontwikkelaars

  • Stel de temperatuur op 0 voor maximale schema-naleving.
  • Valideer met json.Unmarshal en gebruik een fallback als het parsen faalt.
  • Houd schema’s simpel — diep geneste of recursieve JSON-structuren kunnen problemen veroorzaken.
  • Sta optionele velden toe (gebruik omitempty in Go-struct-tags) als je ontbrekende data verwacht.
  • Voeg retries toe als het model af en toe ongeldig JSON produceert.

Volledig voorbeeld - Een grafiek tekenen met LLM-specificaties (Stap-voor-stap: van gestructureerd JSON naar vergelijkingstabellen)

llm-chart

  1. Definieer een schema voor de data die je wilt

Gebruik Pydantic zodat je zowel (a) een JSON-schema voor Ollama kunt genereren als (b) het antwoord van het model kunt valideren.

from pydantic import BaseModel
from typing import List, Optional

class LLMFeatures(BaseModel):
    name: str
    supports_thinking: bool
    max_context_tokens: int
    languages: List[str]
  1. Vraag Ollama om alleen JSON in die vorm te retourneren

Geef het schema door in format= en zink de temperatuur voor determinisme.

from ollama import chat

prompt = """
Extraher de functies voor elk model. Retourneer alleen JSON dat aan het schema voldoet.
1) Qwen3 ondersteunt chain-of-thought; 128K context; Engels, Chinees, Frans, Spaans, Arabisch.
2) Llama 3.1 ondersteunt chain-of-thought; 128K context; Engels.
3) GPT-4 Turbo ondersteunt chain-of-thought; 128K context; Engels, Japans.
"""

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-lijst van LLMFeatures
  1. Valideren & normaliseren

Valideer altijd voordat je het in productie gebruikt.

from pydantic import TypeAdapter

adapter = TypeAdapter(list[LLMFeatures])
models = adapter.validate_json(raw_json)  # -> list[LLMFeatures]
  1. Bouw een vergelijkingstabel (pandas)

Zet je gevalideerde objecten om in een DataFrame die je kunt sorteren/filteren en exporteren.

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))

# Kolommen herschikken voor leesbaarheid
df = df[["name", "supports_thinking", "max_context_tokens", "languages_count", "languages"]]

# Opslaan als CSV voor verder gebruik
df.to_csv("llm_feature_comparison.csv", index=False)
  1. (Optioneel) Snelle visualisaties

Eenvoudige grafieken helpen je om verschillen tussen modellen snel te overzien.

import matplotlib.pyplot as plt

plt.figure()
plt.bar(df["name"], df["max_context_tokens"])
plt.title("Max Context Window per Model (tokens)")
plt.xlabel("Model")
plt.ylabel("Max Context Tokens")
plt.xticks(rotation=20, ha="right")
plt.tight_layout()
plt.savefig("max_context_window.png")

TL;DR

Met de nieuwe ondersteuning voor gestructureerde output van Ollama, kun je LLM’s niet alleen behandelen als chatbots, maar als data-extractiemotoren.

De bovenstaande voorbeelden toonden aan hoe je gestructureerde metadata over LLM-functies zoals ondersteuning voor redeneren, contextvenstergrootte en ondersteunde talen automatisch kunt extraheren — taken die anders breekbare parsing zouden vereisen.

Of je nu een LLM-modelcatalogus, een evaluatiedashboard of een AI-gestuurde onderzoeksassistent bouwt, gestructureerde outputs maken integratie soepel, betrouwbaar en productieklaar.

Abonneren

Ontvang nieuwe berichten over systemen, infrastructuur en AI-engineering.