Beperken van LLMs met gestructureerde uitvoer: Ollama, Qwen3 & Python of Go

Een paar manieren om gestructureerde uitvoer te krijgen van Ollama

Inhoud

Large Language Models (LLMs) zijn krachtig, maar in productie willen we zelden vrije tekst. In plaats daarvan willen we voorspelbare data: kenmerken, feiten of gestructureerde objecten die je kunt voeden in een app. Dat is LLM Structured Output.

Een tijdje geleden introduceerde Ollama ondersteuning voor gestructureerde uitvoer (aankondiging), waardoor het mogelijk werd om de reacties van een model te beperken tot overeenstemming met een JSON schema. Dit ontgrendelt consistente data-extractiepijplijnen voor taken zoals het catalogiseren van LLM-functies, het beoordelen van modellen of het automatiseren van systeemintegratie.

ducks in a row

In dit artikel bespreken we:

  • Wat gestructureerde uitvoer is en waarom het belangrijk is
  • Eenvoudige manier om gestructureerde uitvoer te verkrijgen van LLMs
  • Hoe Ollama’s nieuwe functie werkt
  • Voorbeelden van het extraheren van LLM-functies:

Wat is Gestructureerde Uitvoer?

Normaal gesproken genereren LLMs vrije tekst:

“Model X ondersteunt redeneren met een keten van gedachten, heeft een contextvenster van 200K en spreekt Engels, Chinees en Spaans.”

Dat is leesbaar, maar moeilijk te verwerken.

In plaats daarvan vragen we met gestructureerde uitvoer naar een strikte schema:

{
  "name": "Model X",
  "supports_thinking": true,
  "max_context_tokens": 200000,
  "languages": ["English", "Chinese", "Spanish"]
}

Deze JSON is gemakkelijk te valideren, op te slaan in een database of te voeden in een UI.


Eenvoudige manier om gestructureerde uitvoer te verkrijgen van LLMs

Soms begrijpen de LLMs wat het schema is en kunnen we het model expliciet vragen om uitvoer in JSON te geven met een bepaald schema. Het Qwen3-model van Alibaba is geoptimaliseerd voor redeneren en gestructureerde reacties. Je kunt het expliciet instructies geven om in JSON te reageren.

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

import json
import ollama

prompt = """
Je bent een gestructureerde data-extractor.
Geef 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)

Uitvoer:

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

Schema Validatie Aanpassen met Pydantic

Om vervormde uitvoer 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 'output' is de JSON-string van Qwen3
data = Person.model_validate_json(output)
print(data.name, data.age, data.city)

Dit zorgt ervoor dat de uitvoer overeenkomt met de verwachte structuur.


Ollama’s Gestructureerde Uitvoer

Ollama laat je nu een schema doorgeven in de format-parameter. Het model wordt dan beperkt tot het reageren in JSON die overeenkomt met het schema (docs).

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


Voorbeeld 2: Extraheer LLM-functie metadata

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

“Qwen3 heeft sterke multilingual ondersteuning (Engels, Chinees, Frans, Spaans, Arabisch). Het stelt redeneringsstappen (keten van gedachten) toe. 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 = """
Analyseer de volgende beschrijving en geef de functies van het model weer in JSON alleen.
Model beschrijving:
'Qwen3 heeft sterke multilingual ondersteuning (Engels, Chinees, Frans, Spaans, Arabisch).
Het stelt redeneringsstappen (keten van gedachten) toe.
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 uitvoer:

{
  "name": "Qwen3",
  "supports_thinking": true,
  "max_context_tokens": 128000,
  "languages": ["English", "Chinese", "French", "Spanish", "Arabic"]
}

Voorbeeld 3: Vergelijk meerdere modellen

Voer beschrijvingen van meerdere modellen in en extraheer ze in gestructureerde vorm:

from typing import List

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

prompt = """
Extraheer de functies van elk model in JSON.

1. Llama 3.1 ondersteunt redeneren. Contextvenster is 128K. Talen: Engels alleen.
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)

Uitvoer:

{
  "models": [
    {
      "name": "Llama 3.1",
      "supports_thinking": true,
      "max_context_tokens": 128000,
      "languages": ["English"]
    },
    {
      "name": "GPT-4 Turbo",
      "supports_thinking": true,
      "max_context_tokens": 128000,
      "languages": ["English", "Japanese"]
    },
    {
      "name": "Qwen3",
      "supports_thinking": true,
      "max_context_tokens": 128000,
      "languages": ["English", "Chinese", "French", "Spanish", "Arabic"]
    }
  ]
}

Dit maakt het eenvoudig om benchmarks, visualisaties of filters te maken op basis van de functies van modellen.


Voorbeeld 4: Detecteer automatisch leemtes

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 blijft geldig zelfs als enige informatie ontbreekt.


Voordelen, nadelen en beste praktijken

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

Voordelen

  • Sterkere garanties: Het model wordt gevraagd om overeen te komen met een JSON-schema in plaats van vrije tekst.
  • Eenvoudiger parsen: Je kunt direct json.loads of valideren met Pydantic / Zod, in plaats van regex of heuristieken.
  • Schema-gestuurde evolutie: Je kunt je schema versiebeheren, velden toevoegen (met standaardwaarden) en achterwaartse compatibiliteit behouden.
  • Interoperabiliteit: Afspraken met downstream-systemen 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 houden. Ollama’s documentatie raadt dit aan.

Nadelen en valkuilen

  • Schema-onovereenstemming: Het model kan nog steeds afwijken — bijvoorbeeld een vereist veld missen, sleutels in een andere volgorde plaatsen of extra velden toevoegen. Je hebt validatie nodig.
  • Complexe schema’s: Zeer diepe of recursieve JSON-schema’s kunnen het model verwarring opleveren of leiden tot fouten.
  • Vagheid in de prompt: Als je prompt onduidelijk is, kan het model velden of eenheden verkeerd raden.
  • Ongelijkheid 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 en tips (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 “reageer alleen in JSON” of “geen commentaar of extra tekst toevoegen” in je prompt.
  • Gebruik temperatuur = 0 (of zeer laag) om willekeur te minimaliseren en schema-aanpassing te maximaliseren. Ollama raadt determinisme aan.
  • Valideer en eventueel terugvallen (bijv. herproberen of opschonen) wanneer JSON-parsen of schema-validatie mislukt.
  • Start met een eenvoudiger schema en breid het geleidelijk uit. Compliceer het niet direct.
  • Voeg nuttige maar beperkte foutinstructies toe: bijvoorbeeld, als het model een vereist veld niet kan vullen, reageer dan met null in plaats van het te overslaan (als je schema dat toestaat).

Go Voorbeeld 1: Extraheer LLM-functies

Hieronder staat een eenvoudig Go programma dat Qwen3 vraagt om gestructureerde uitvoer 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 geef de functies van het model weer in JSON alleen.
  Beschrijving:
  "Qwen3 heeft sterke multilingual ondersteuning (Engels, Chinees, Frans, Spaans, Arabisch).
  Het stelt redeneringsstappen (keten van gedachten) toe.
  Het contextvenster is 128K tokens."
  `

	// Definieer het JSON-schema voor gestructureerde uitvoer
	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 converteren van het format schema:", 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 inhoud terwijl het streams
		rawResponse += response.Response

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

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

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

# initialiseren van module
go mod init ollama-struct
# trek alle afhankelijkheden
go mod tidy
# bouw & voer uit
go build -o ollama-struct main.go
./ollama-struct

Voorbeeld Uitvoer

Geparseerde struct: {Name:Qwen3 SupportsThinking:true MaxContextTokens:128000 Languages:[English Chinese French Spanish Arabic]}

Go Voorbeeld 2: Vergelijk meerdere modellen

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

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

	prompt = `
	Extraheer functies uit de volgende modelbeschrijvingen en geef ze als JSON weer:

	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 Engels.
	2. LLaMA 2: Dit model heeft gematigde redeneermogelijkheden en kan enkele logische taken verwerken. 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 codeanalyse. Het heeft een contextvenster van 16.000 tokens. Het ondersteunt Engels, Python, JavaScript en Java.

	Geeft 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 converteren van vergelijkingschema:", 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 inhoud terwijl het streams
		comparisonResponse += response.Response

		// Parse alleen wanneer de respons compleet is
		if response.Done {
			if err := json.Unmarshal([]byte(comparisonResponse), &comp); err != nil {
				return fmt.Errorf("JSON parse fout: %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)
	}

Voorbeeld Uitvoer

PaLM 2: Context=8000, Talen=[English]
LLaMA 2: Context=4000, Talen=[English Spanish Italian]
Codex: Context=16000, Talen=[English Python JavaScript Java]

Overigens werkt qwen3:4b goed in deze voorbeelden, net zoals qwen3:8b.

Beste Praktijken voor Go-ontwikkelaars

  • Stel temperatuur in op 0 voor maximale schema-aanpassing.
  • Valideer met json.Unmarshal en gebruik fallback als het parsen mislukt.
  • Houd schema’s eenvoudig — zeer ingewikkelde of recursieve JSON-structuren kunnen problemen veroorzaken.
  • Sta optionele velden toe (gebruik omitempty in Go struct tags) als je ontbrekende data verwacht.
  • Voeg herproeven toe als het model soms ongeldige JSON genereert.

Volledig voorbeeld - Teken een grafiek met LLM-specs (Stap-voor-stap: van gestructureerde JSON naar vergelijkings-tabellen)

llm-chart

  1. Definieer een schema voor de data die je wilt

Gebruik Pydantic zodat je zowel (a) een JSON-schema kunt genereren voor Ollama als (b) de reactie 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 zet de temperatuur lager voor determinisme.

from ollama import chat

prompt = """
Extraheer functies voor elk model. Geef alleen JSON weer die overeenkomt met het schema.
1) Qwen3 ondersteunt keten van gedachten; 128K context; Engels, Chinees, Frans, Spaans, Arabisch.
2) Llama 3.1 ondersteunt keten van gedachten; 128K context; Engels.
3) GPT-4 Turbo ondersteunt keten van gedachten; 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. Valideer & normaliseer

Valideer altijd voor gebruik in productie.

from pydantic import TypeAdapter

adapter = TypeAdapter(list[LLMFeatures])
models = adapter.validate_json(raw_json)  # -> list[LLMFeatures]
  1. Bouw een vergelijkings tabel (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))

# Herorden kolommen voor leesbaarheid
df = df[["name", "supports_thinking", "max_context_tokens", "languages_count", "languages"]]

# Opslaan als CSV voor verdere gebruik
df.to_csv("llm_feature_comparison.csv", index=False)
  1. (Optioneel) Snel visuele weergave

Eenvoudige grafieken helpen je snel visuele verschillen tussen modellen te herkennen.

import matplotlib.pyplot as plt

plt.figure()
plt.bar(df["name"], df["max_context_tokens"])
plt.title("Max Contextvenster 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 gestructureerde uitvoerondersteuning van Ollama, kun je LLMs niet alleen als chatbots beschouwen, maar ook als data-extractiemotoren.

De bovenstaande voorbeelden laten zien hoe je gestructureerde metadata automatisch kunt extraheren over LLM-functies zoals redeneringsondersteuning, contextvensteromvang en ondersteunde talen — taken die anders zouden vereisen bros parsing.

Of je nu een LLM-modellencatalogus, een evaluatie-dashboard of een AI-gestuurde onderzoeksassistent bouwt, maken gestructureerde uitvoer integratie glad, betrouwbaar en productie-klar.