Autor: Jakub Rusinowski · Ostatnia aktualizacja: 12 lipca 2026
Aby uzyskać niezawodny JSON z lokalnego LLM-a, przestań błagać go w prompcie i ogranicz dekoder: ustrukturyzowane wyjścia w Ollama zmuszają model do emitowania wyłącznie tokenów pasujących do Twojego
Aby uzyskać niezawodny JSON z lokalnego LLM-a, przestań błagać go w prompcie i ogranicz dekoder: ustrukturyzowane wyjścia w Ollama zmuszają model do emitowania wyłącznie tokenów pasujących do Twojego schematu, przez co zniekształcony JSON staje się mechanicznie niemożliwy. Ten poradnik omawia parametr format w Ollama, gramatyki GBNF w llama.cpp, tryb schematu w LM Studio oraz wzorzec „waliduj i ponów”, który czyni całość bezpieczną produkcyjnie.
Ostatnia aktualizacja: lipiec 2026
„Odpowiadaj WYŁĄCZNIE poprawnym JSON-em” to prośba, a nie ograniczenie. Model *próbkuje* każdy token, więc przy odpowiedniej liczbie wywołań prędzej czy później wyprodukuje zbędny przecinek, gadatliwy wstęp („Jasne! Oto Twój JSON:”), znaczniki markdown wokół obiektu albo urwany nawias na granicy kontekstu. Przy temperaturze większej od zera to nie jest błąd — to jest właśnie próbkowanie. Wszystko, co budujesz na wierzchu (parsery, agenci, potoki), umiera wtedy na JSONDecodeError.
Lekarstwem jest dekodowanie ograniczone gramatyką: na każdym kroku środowisko uruchomieniowe maskuje wszystkie tokeny, które naruszyłyby Twój schemat, a model może wybierać tylko spośród dozwolonych kontynuacji. Inżynieria promptów zmniejsza liczbę awarii; dekodowanie z ograniczeniami całkowicie eliminuje klasę błędów składniowych.
Najszybszy zysk — wymuszenie składniowo poprawnego JSON-a o dowolnym kształcie:
curl http://localhost:11434/api/chat -d '{
"model": "llama3.1",
"format": "json",
"stream": false,
"messages": [{"role": "user",
"content": "Extract name and email from: Reach Jan Kowalski at jan@example.com. Reply in JSON."}]
}'
Dwie zasady: zawsze napisz też w prompcie „odpowiedz w JSON” (inaczej niektóre modele w nieskończoność emitują białe znaki) i pamiętaj, że gwarantuje to jedynie *poprawny* JSON — a nie *Twój* JSON. Klucze wciąż mogą być dowolne. Do tego potrzebny jest schemat.
Od wersji Ollama 0.5 parametr format przyjmuje pełny schemat JSON — i to jest ustawienie, którego naprawdę chcesz w aplikacjach:
curl http://localhost:11434/api/chat -d '{
"model": "llama3.1",
"stream": false,
"messages": [{"role": "user", "content": "Extract: Reach Jan Kowalski at jan@example.com"}],
"format": {
"type": "object",
"properties": {
"name": {"type": "string"},
"email": {"type": "string"}
},
"required": ["name", "email"]
}
}'
W Pythonie pozwól Pydanticowi napisać schemat i zwalidować wynik — ta para stanowi rdzeń każdej niezawodnej integracji z lokalnym LLM-em:
from ollama import chat
from pydantic import BaseModel
class Contact(BaseModel):
name: str
email: str
resp = chat(
model="llama3.1",
messages=[{"role": "user", "content": "Extract: Reach Jan Kowalski at jan@example.com"}],
format=Contact.model_json_schema(),
options={"temperature": 0},
)
contact = Contact.model_validate_json(resp.message.content)
Zwróć uwagę na temperature: 0 — przy ekstrakcji i klasyfikacji zależy Ci na determinizmie, a to mierzalnie poprawia dokładność na poziomie pól w modelach 7–8B. Dokumentacja: ustrukturyzowane wyjścia w Ollama.
Obsługa schematów w Ollama jest zbudowana na gramatykach GBNF z llama.cpp — dialekcie BNF, który potrafi ograniczyć wyjście do *dowolnego* formalnego kształtu, nie tylko JSON-a (fragmenty SQL, pliki ini, etykiety klasyfikatora typu „jedna z N”):
# choice.gbnf - the model can output ONLY one of three labels
root ::= "positive" | "negative" | "neutral"
./llama-cli -m model.gguf --grammar-file choice.gbnf \
-p "Classify the sentiment: 'The delivery was late again.'"
Konkretnie dla JSON-a llama.cpp dostarcza plik json.gbnf oraz konwerter json_schema_to_grammar.py. Sięgaj po surowy GBNF, gdy i tak korzystasz z serwera llama.cpp albo LM Studio, albo gdy wyjście w ogóle nie jest JSON-em. Materiał źródłowy: gramatyki llama.cpp.
Lokalny serwer LM Studio przyjmuje parametr response_format z OpenAI, więc istniejący kod SDK OpenAI przenosi się wprost:
from openai import OpenAI
client = OpenAI(base_url="http://localhost:1234/v1", api_key="lm-studio")
resp = client.chat.completions.create(
model="qwen2.5-7b-instruct",
messages=[{"role": "user", "content": "Extract: Jan Kowalski, jan@example.com"}],
response_format={"type": "json_schema", "json_schema": {"name": "contact", "schema": {
"type": "object",
"properties": {"name": {"type": "string"}, "email": {"type": "string"}},
"required": ["name", "email"]}}},
)
Ta sama konstrukcja działa z vLLM, jeśli przejdziesz na serwowanie produkcyjne.
Wywoływanie narzędzi i funkcji to ustrukturyzowane wyjście w przebraniu: model emituje JSON z nazwą funkcji i jej argumentami. Ollama obsługuje parametr tools w modelach, które to potrafią (Llama 3.1+, Qwen 2.5+, Mistral). Zasady niezawodności są identyczne — małe modele wybierają *właściwe* narzędzie mniej pewnie, niż formatują samo wywołanie, więc utrzymuj małą liczbę narzędzi (3–5) i pisz ostre opisy. Jeśli budujesz na tym agentów, zobacz Budowanie autonomicznych agentów.
Dekodowanie z ograniczeniami zabija błędy składniowe; nie powstrzyma jednak modelu przed wstawieniem złej *treści* w poprawny kształt (puste ciągi, wyhalucynowane adresy e-mail). Kod produkcyjny opakowuje każde wywołanie walidacją:
def extract(text, retries=2):
for attempt in range(retries + 1):
resp = chat(model="llama3.1", format=Contact.model_json_schema(),
options={"temperature": 0},
messages=[{"role": "user", "content": f"Extract contact: {text}"}])
try:
c = Contact.model_validate_json(resp.message.content)
if "@" in c.email: # semantic check, not just shape
return c
except ValidationError:
pass # schema drift - retry
return None # explicit failure beats silent garbage
Trzy nawyki, które utrzymują współczynnik awarii blisko zera: waliduj *semantykę* (a nie tylko parsowalność), zawodź jawnie do None albo do kolejki zamiast zgadywać i loguj każdą ponowną próbę — rosnący współczynnik ponowień to sposób, w jaki zauważysz regresję modelu albo promptu.
| Model | Jakość ustrukturyzowanego wyjścia |
|---|---|
| Qwen 2.5 7B/14B | Najlepszy w klasie w trzymaniu się schematów; domyślny wybór |
| Llama 3.1 8B | Niezawodny przy dekodowaniu z ograniczeniami; mocne wywoływanie narzędzi |
| Mistral NeMo 12B | Dobry, bez ograniczeń bywa nadmiernie zagnieżdżający |
| Modele klasy 3B | W porządku *z* dekodowaniem ograniczonym; beznadziejne bez niego |
Przy aktywnych ograniczeniach gramatycznych nawet małe modele produkują nienaganną składnię — różnice przenoszą się na dokładność treści, a tam w zadaniach ekstrakcji prowadzi Qwen 2.5.
Bo polegasz wyłącznie na prompcie. Próbkowanie sprawia, że przy skali sporadyczne odchylenia od formatu są gwarantowane. Przejdź na dekodowanie z ograniczeniami — format w Ollama, response_format w LM Studio albo gramatyka GBNF w llama.cpp — a składniowo niepoprawne wyjście stanie się niemożliwe.
Silne ograniczenia mogą lekko obniżyć jakość odpowiedzi w zadaniach wymagających rozumowania — model nie może „myśleć na głos”, zanim zwiąże się strukturą. Przy ekstrakcji i klasyfikacji efekt jest pomijalny. Jeśli potrzebujesz rozumowania i struktury naraz, wykonaj dwa wywołania: najpierw pozwól modelowi swobodnie rozumować, a potem wyekstrahuj wynik do schematu.
Nieznacznie — maska gramatyczna dokłada narzut na każdy token, zwykle jednocyfrowy procent w środowiskach opartych na llama.cpp. Odzyskasz to wielokrotnie, nigdy więcej nie powtarzając nieudanych parsowań.
Tak, tokeny płyną jak zwykle — ale obiekt daje się sparsować dopiero, gdy jest kompletny. Praktyczne podejścia: waliduj po zakończeniu strumienia albo strumieniuj do interfejsu, podczas gdy Twój kod czeka na domykający nawias.