Strukturierte JSON-Ausgaben aus lokalen LLMs: Grammar-Constraints und JSON-Mode für zuverlässige Automatisierungen
Lokale LLMs liefern ohne Vorgaben oft unzuverlässige Ausgaben. Mit Grammar-Sampling in llama.cpp und dem JSON-Schema-Mode von Ollama erzwingen Sie exakt strukturierte JSON-Antworten für PowerShell-Skripte, n8n-Workflows und Python, ohne manuelle Nachbearbeitung.
Werbung Mit * markierte Links sind Werbelinks: Bei einem Kauf erhalten wir eine Provision, der Preis bleibt gleich. Als Amazon-Partner verdiene ich an qualifizierten Verkäufen. Mehr dazu

Lokale LLMs übernehmen in vielen KMU Aufgaben wie Zusammenfassung, Datenextraktion und Klassifizierung auf eigener Hardware. Ohne Vorgaben liefern sie jedoch freien Text, der nur manchmal wie JSON aussieht und sich nicht zuverlässig maschinell weiterverarbeiten lässt. Diese Anleitung zeigt zwei Wege zu strukturierten Ausgaben: Grammar-Sampling in llama.cpp und den JSON-Schema-Mode von Ollama, jeweils einsetzbar in PowerShell-Skripten, n8n-Workflows und Python.
Voraussetzungen
- Hardware: mindestens 8 GB RAM für CPU-Inferenz; für GPU-Inferenz 8 bis 16 GB VRAM (NVIDIA CUDA oder AMD ROCm)
- llama.cpp als Binary-Release von github.com/ggml-org/llama.cpp/releases oder selbst kompiliert
- Ollama von ollama.com (Windows, macOS oder Linux, nativ oder per Docker)
- GGUF-Modell, z. B. Llama 3.1 8B Q4_K_M
- Python 3.9+ mit
pip install ollama pydantic - PowerShell 7+ (Windows 10/11 bringt nur Windows PowerShell 5.1 mit, PowerShell 7 separat installieren)
- curl für API-Tests, optional n8n
| Modell | Größe | Quantisierung | VRAM (ca.) | Kontext | Lizenz |
|---|---|---|---|---|---|
| Llama 3.1 8B | 8B | Q4_K_M | 5–6 GB | 128k | Llama 3 Community |
| Llama 3.2 3B | 3B | Q4_K_M | 2–3 GB | 128k | Llama 3 Community |
| Qwen2.5 7B | 7B | Q4_K_M | ~5 GB | 32k | Apache 2.0 |
| Qwen3 8B | 8B | Q4_K_M | 5–6 GB | 128k | Apache 2.0 |
| Gemma 4 4B | 4B | Q4_K_M | 3–4 GB | 128k | Gemma ToS |
| Mistral-Nemo 12B | 12B | Q4_K_M | 8–9 GB | 128k | Apache 2.0 |
Schritt 1: Das Prinzip verstehen: wie Grammar-Constraints wirklich funktionieren
Der Unterschied zu Prompt-Anweisungen wie „Antworte nur als JSON“ liegt darin, wo die Einschränkung greift. Beim Grammar-Sampling prüft llama.cpp nach jedem Token, welche Folge-Token zur Grammatik passen, und schließt alle anderen aus. Das Schema wird nicht in den Prompt eingefügt; es wirkt nur im Sampler.
Ollama nutzt dasselbe Prinzip (Constrained Decoding). Der Parameter format akzeptiert "json" (gültiges JSON, Struktur frei) oder ein JSON-Schema-Objekt (feste Struktur).
Weil das Modell das Schema nicht sieht, beschreiben Sie im Prompt, welche Daten Sie erwarten. Der Sampler sorgt für die Form, der Prompt für den Inhalt. Die Ollama-Dokumentation empfiehlt, das Schema zusätzlich als Text in den Prompt zu schreiben.
Sie können für Ihren Anwendungsfall benennen, welche Felder Pflicht sind und welchen Typ sie haben; daraus entsteht in den nächsten Schritten das Schema.
Schritt 2: llama.cpp-Server starten und mit JSON-Schema abfragen
Laden Sie das aktuelle Binary-Release herunter und starten Sie den Server. Er lauscht standardmäßig auf 127.0.0.1:8080 und ist OpenAI-API-kompatibel.
# CPU
./llama-server -m models/llama-3.1-8b-q4_k_m.gguf -c 4096
# Mit GPU-Offload (35 Layer auf der GPU)
./llama-server -m models/llama-3.1-8b-q4_k_m.gguf -c 4096 -ngl 35Für einen schnellen Test ohne Server übergeben Sie das Schema mit -j:
./llama-cli -m models/llama-3.1-8b-q4_k_m.gguf \
-j '{"type":"object","properties":{"name":{"type":"string"},"age":{"type":"integer"}},"required":["name","age"]}' \
-p "Extrahiere Name und Alter aus: Max Mustermann, 34 Jahre"Am Server senden Sie das Schema im Feld json_schema an /completion:
curl http://localhost:8080/completion \
-H "Content-Type: application/json" \
-d '{
"prompt": "Extrahiere Name und Alter aus: Max Mustermann, 34 Jahre",
"json_schema": {
"type": "object",
"properties": { "name": {"type": "string"}, "age": {"type": "integer"} },
"required": ["name", "age"]
},
"stream": false
}'OpenAI-kompatible Clients nutzen /v1/chat/completions mit response_format:
curl http://localhost:8080/v1/chat/completions \
-H "Content-Type: application/json" \
-H "Authorization: Bearer no-key" \
-d '{
"model": "local-model",
"response_format": {
"type": "json_schema",
"schema": {
"type": "object",
"properties": {
"name": {"title": "Name", "type": "string"},
"date": {"title": "Date", "type": "string"},
"participants": {"title": "Participants", "type": "array", "items": {"type": "string"}}
},
"required": ["name", "date", "participants"]
}
},
"messages": [
{"role": "user", "content": "Extrahiere Meeting-Daten: Projektmeeting am 15.06.2026 mit Anna und Bob"}
],
"stream": false
}'Die Antwort enthält unter content ein gültiges JSON-Objekt mit genau den definierten Feldern, ohne Freitext davor oder danach.
Schritt 3: GBNF-Grammatiken: wenn JSON-Schema nicht reicht
GBNF (GGML BNF) erweitert die Backus-Naur-Form und legt das Ausgabeformat vollständig fest, auch für CSV, Markdown-Tabellen oder eigene Protokolle. JSON-Schemas wandelt llama.cpp intern in GBNF um; für andere Formate schreiben Sie die Grammatik selbst.
# grammars/person.gbnf
root ::= object
object ::= "{" ws name-pair "," ws age-pair "}" ws
name-pair ::= "\"name\"" ws ":" ws string
age-pair ::= "\"age\"" ws ":" ws number
string ::= "\"" [a-zA-Z ]+ "\""
number ::= [0-9]+
ws ::= [ \t\n]*Die Datei übergeben Sie mit --grammar-file:
./llama-cli -m models/llama-3.1-8b-q4_k_m.gguf \
--grammar-file grammars/person.gbnf \
-p "Erstelle ein Personen-Objekt für Max Mustermann, 34 Jahre"Ein vorhandenes JSON-Schema als Datei übergeben Sie direkt mit -jf:
./llama-cli -m models/llama-3.1-8b-q4_k_m.gguf -jf schema.json -p "Extrahiere Name und Alter aus: Max Mustermann, 34 Jahre"Unterstützt werden unter anderem string, integer, object, array, boolean, null, minLength/maxLength, minimum/maximum, required, minItems/maxItems. Nicht unterstützt: uniqueItems, contains, not, Conditionals und entfernte $ref.
Die Ausgabe von llama-cli entspricht der Grammatik, z. B. {"name": "Max Mustermann", "age": 34}. Meldet llama.cpp einen Parse-Fehler, stimmt die GBNF-Syntax nicht.
Schritt 4: Ollama JSON-Schema-Mode einrichten
Ollama braucht kein separates Binary. Nach dem Start (ollama serve oder als Dienst) nimmt es Anfragen auf Port 11434 an. Verwenden Sie für Automatisierungen nicht format: "json", das nur gültiges JSON erzwingt, sondern das vollständige Schema-Objekt:
curl http://localhost:11434/api/chat \
-H "Content-Type: application/json" \
-d '{
"model": "llama3.1",
"messages": [{"role": "user", "content": "Extrahiere Daten: Max Mustermann, 34 Jahre, Berlin"}],
"format": {
"type": "object",
"properties": { "name": {"type": "string"}, "age": {"type": "integer"}, "city": {"type": "string"} },
"required": ["name", "age", "city"]
},
"stream": false
}'message.content enthält ein JSON-Objekt mit den drei Pflichtfeldern; jq '.message.content | fromjson' gibt es ohne Fehler aus.
Schritt 5: Python-Integration mit Pydantic
Pydantic erzeugt das Schema und validiert die Antwort. Field(description=...) landet im Schema und hilft dem Modell, die Felder richtig zu füllen.
from ollama import chat
from pydantic import BaseModel, Field
class Person(BaseModel):
name: str = Field(description="Vollständiger Name der Person")
age: int = Field(description="Alter in Jahren")
city: str = Field(description="Wohnort")
response = chat(
model="llama3.1",
messages=[{"role": "user", "content": "Extrahiere: Max Mustermann, 34 Jahre, Berlin"}],
format=Person.model_json_schema(),
options={"temperature": 0},
)
person = Person.model_validate_json(response.message.content)
print(f"{person.name}, {person.age} Jahre, {person.city}")Das Skript gibt „Max Mustermann, 34 Jahre, Berlin“ aus. Bei Typfehlern wirft model_validate_json() einen ValidationError.
Schritt 6: PowerShell-Integration für Windows-Admins
Invoke-RestMethod gibt das geparste JSON als PSCustomObject zurück. ConvertTo-Json -Depth 10 ist für verschachtelte Schemas nötig, weil PowerShell sonst ab Tiefe 2 abschneidet.
$schema = @{
type = "object"
properties = @{
name = @{ type = "string" }
age = @{ type = "integer" }
city = @{ type = "string" }
}
required = @("name", "age", "city")
}
$body = @{
model = "llama3.1"
messages = @(@{ role = "user"; content = "Extrahiere: Max Mustermann, 34 Jahre, Berlin" })
format = $schema
stream = $false
} | ConvertTo-Json -Depth 10 -Compress
$response = Invoke-RestMethod -Uri "http://localhost:11434/api/chat" `
-Method Post -ContentType "application/json; charset=utf-8" -Body $body
$data = $response.message.content | ConvertFrom-Json
Write-Host "Name: $($data.name), Alter: $($data.age), Stadt: $($data.city)"Die Ausgabe lautet „Name: Max Mustermann, Alter: 34, Stadt: Berlin“; $data lässt sich ohne String-Parsing weiterverarbeiten.
Schritt 7: n8n-Workflow-Integration
In n8n binden Sie Ollama oder den llama.cpp-Server über den Node HTTP Request ein: Method POST, URL http://localhost:11434/api/chat (läuft n8n in Docker, statt localhost den Namen des Ollama-Containers oder host.docker.internal), Body als JSON:
{
"model": "llama3.1",
"messages": [{"role": "user", "content": "{{ $json.input_text }}"}],
"format": {
"type": "object",
"properties": { "result": {"type": "string"}, "confidence": {"type": "number"} },
"required": ["result", "confidence"]
},
"stream": false
}Im nächsten Node erreichen Sie die Antwort über {{ $json.message.content }} (Ollama-API) bzw. {{ $json.choices[0].message.content }} (OpenAI-kompatibel). Die Verwechslung beider Pfade ist der häufigste Fehler. Mehr dazu in n8n mit Docker: KI-Workflows und Automatisierung self-hosted.
Im Testlauf des HTTP-Request-Nodes zeigt die Ausgabe message.content mit den Feldern result und confidence.
Methoden im Vergleich
| Methode | Tool | Endpunkt / Flag | Struktur garantiert | Modell sieht Schema | Geeignet für |
|---|---|---|---|---|---|
| GBNF-Grammatik (manuell) | llama.cpp | --grammar / --grammar-file | Ja (Sampler) | Nein | Vollständige Kontrolle, Nicht-JSON |
| JSON-Schema zu GBNF | llama.cpp CLI | --json / -j | Ja (Sampler) | Nein | JSON-Extraktion, Skripte |
| json_schema im Server | llama.cpp-Server | POST /completion | Ja | Nein | REST-Integration, n8n |
| response_format (OpenAI) | llama.cpp-Server | POST /v1/chat/completions | Ja | Nein | OpenAI-kompatible Clients |
| format: "json" | Ollama | /api/generate, /api/chat | Nur valides JSON | Nein | Einfache JSON-Rückgaben |
| format: {schema} | Ollama | /api/generate, /api/chat | Ja (nach Schema) | Nein | Automatisierungen, PowerShell |
| Pydantic + Ollama | Ollama + Python | ollama.chat() | Ja + validiert | Nein | Python-Workflows |
Troubleshooting / Typische Fehler
stream nicht auf false gesetzt
Ohne "stream": false liefern Ollama und llama.cpp NDJSON-Streams (zeilenweise Token-Pakete); Invoke-RestMethod und einfache curl-Auswertungen scheitern dann.
format: "json" statt format: {schema-objekt}
format: "json" garantiert nur gültiges JSON, nicht die gewünschten Felder. Übergeben Sie das Schema-Objekt mit required.
Schema fehlt im Prompt-Kontext
Das Modell sieht das Schema nicht. Ohne erklärenden Prompt fehlt der Kontext für Feldnamen und Bedeutung; beschreiben Sie die erwarteten Daten im Prompt.
required-Array fehlt
Ohne required sind alle Felder optional, das Modell kann Felder weglassen, und Folgecode bricht mit KeyError ab. Führen Sie alle Pflichtfelder auf.
Performance-Falle bei GBNF-Wiederholungen
Mehrere optionale Elemente hintereinander (x? x? x?) machen das Sampling extrem langsam. Schreiben Sie stattdessen x{0,N}.
Nicht unterstützte JSON-Schema-Features
uniqueItems, contains, not, Conditionals und entfernte $ref funktionieren in llama.cpp nicht. Prüfen Sie Schemas vorher auf diese Merkmale.
Kontextlänge unterschätzt
Setzen Sie bei langen Dokumenten -c (llama.cpp) bzw. num_ctx (Ollama) ausreichend hoch. Überschreitet die Eingabe den Kontext, wird sie still abgeschnitten.
n8n: falscher Antwortpfad
Ollama-API: message.content; OpenAI-kompatibel: choices[0].message.content. Der falsche Pfad führt zu leeren Feldern.
Häufige Fragen
Funktioniert Grammar-Sampling mit jedem GGUF-Modell?
Ja. Grammar-Sampling wirkt im Sampler und ist unabhängig von Modell und Fine-Tuning.
Was ist der Unterschied zwischen format: "json" und format: {schema} in Ollama?
format: "json" erzwingt nur gültiges JSON; Felder können fehlen oder variieren. Ein Schema-Objekt erzwingt die definierte Struktur mit allen required-Feldern.
Muss ich Pydantic verwenden oder reicht reines JSON-Schema?
Pydantic ist optional und verbindet Schema-Erzeugung und Validierung. Alternativ übergeben Sie ein Python-dict mit dem Schema als format.
Kann ich llama.cpp-Server und Ollama gleichzeitig in n8n nutzen?
Ja. llama.cpp lauscht auf Port 8080, Ollama auf 11434; beide bieten OpenAI-kompatible APIs. In n8n konfigurieren Sie je eine Base-URL und einen Platzhalter-API-Key wie no-key.
Wie groß muss das Modell für zuverlässige strukturierte Ausgaben sein?
Ab etwa 7 bis 8 Milliarden Parametern (Q4_K_M) sind einfache Extraktionen zuverlässig. Modelle mit 3 Milliarden Parametern genügen für einfache Schemas, scheitern aber öfter an verschachtelten Strukturen.
Wirkt das JSON-Schema auch als Ausgabe-Validierung?
Nein. Constrained Decoding steuert nur die Erzeugung. Validieren Sie im Produktivbetrieb zusätzlich mit Pydantic oder einem JSON-Schema-Validator.
Unterstützt Cloud-Ollama (ollama.com) Structured Outputs?
Nein. Laut Ollama-Dokumentation unterstützt die Cloud-Variante Structured Outputs derzeit nicht; die Funktion steht im selbst betriebenen Ollama zur Verfügung.
Fazit
Grammar-Sampling in llama.cpp und der JSON-Schema-Mode von Ollama ersetzen unzuverlässige Freitext-Ausgaben durch feste Strukturen, weil sie im Sampler greifen statt im Prompt. PowerShell-Skripte nutzen sie über Invoke-RestMethod, n8n über den HTTP-Request-Node, Python über Pydantic mit zusätzlicher Validierung. Für die Modellwahl hilft Ollama-Modelle 2026 richtig auswählen, für komplette RAG-Pipelines RAG produktiv betreiben.
Weiterführende Anleitungen und Quellen
- Ollama-Modelle 2026 richtig auswählen: VRAM, Quantisierung und Modellvergleich für KMU
- RAG produktiv betreiben: Chunking, Hybrid-Search, Reranking und Antwortqualität messen
- n8n mit Docker: KI-Workflows und Automatisierung self-hosted
- Lokales RAG-System mit Qdrant und Embeddings selbst bauen
- llama.cpp Grammars README (offiziell)
- Ollama Blog: Structured Outputs (offiziell)
- Ollama Docs: Structured Outputs
- llama.cpp Server README (offiziell)
- Ollama API Dokumentation (offiziell)


