Niezawodny JSON z lokalnych LLM-ów: poradnik o ustrukturyzowanym wyjściu

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

W tym przewodniku

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

Dlaczego lokalne modele psują Twój JSON

„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.

Poziom 1 — Tryb JSON w Ollama (jedna linia)

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.

Poziom 2 — Ustrukturyzowane wyjścia w Ollama (JSON Schema)

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.

Poziom 3 — Gramatyki GBNF (bezpośrednio w llama.cpp)

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.

LM Studio i endpointy zgodne z OpenAI

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.

A co z wywoływaniem funkcji?

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.

Wzorzec produkcyjny: waliduj, ponawiaj, miej plan awaryjny

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.

Które lokalne modele najlepiej radzą sobie z ustrukturyzowanym wyjściem?

ModelJakość ustrukturyzowanego wyjścia
Qwen 2.5 7B/14BNajlepszy w klasie w trzymaniu się schematów; domyślny wybór
Llama 3.1 8BNiezawodny przy dekodowaniu z ograniczeniami; mocne wywoływanie narzędzi
Mistral NeMo 12BDobry, bez ograniczeń bywa nadmiernie zagnieżdżający
Modele klasy 3BW 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.

Najczęściej zadawane pytania

Dlaczego mój lokalny LLM ciągle produkuje niepoprawny JSON?

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.

Czy tryb JSON ogłupia model?

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.

Czy dekodowanie z ograniczeniami spowalnia wnioskowanie?

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ń.

Czy mogę strumieniować ustrukturyzowane wyjście?

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.

Powiązane poradniki

← Wszystkie przewodniki | Sprawdź zgodność GPU