Większość zespołów natrafia na ten sam problem mniej więcej w trzecim tygodniu pracy z LLM: model odpowiada prawie poprawnie. JSON ma dodatkowy komentarz. Albo brakuje zamykającego nawiasu klamrowego. Albo — i to jest podstępne — zwrócił prawidłowy JSON, ale z polem status ustawionym na "ok" zamiast "approved", czego Państwa logika downstream nie rozróżnia. Aplikacja pada cztery godziny później, kiedy ta konkretna gałąź kodu w końcu zostaje wywołana. W środowisku produkcyjnym.
Problem nie polega na tym, że LLM są zawodne. Problem polega na tym, że formatowanie tekstu i poprawność semantyczna to dwie różne rzeczy, a większość wdrożeń miesza je ze sobą. Ten artykuł wyjaśnia, gdzie przebiega między nimi granica, jakie narzędzia istnieją do zapewnienia poprawności składniowej i jak zbudować warstwę walidacji, która wychwytuje resztę.
Dlaczego „zwróć JSON" w prompcie nie wystarcza
Gdy do promptu wpisują Państwo "Respond in JSON format", dają Państwo modelowi instrukcję, nie gwarancję. Autoregresywne modele językowe generują token po tokenie według prawdopodobieństwa — każdy kolejny token jest wynikiem rozkładu warunkowego na wszystko, co poprzedzało. Model nie widzi „otwartego nawiasu, który muszę zamknąć". Widzi prawdopodobieństwa następnych tokenów.
Z tego wynikają trzy kategorie problemów:
- Błąd składniowy — nieprawidłowy JSON, nadmiarowy tekst przed/po, bloki markdown `
`json ...`` owijające wynik, znaki kontrolne, błędy ucieczki unicode - Błąd schematu — prawidłowy JSON, ale brakuje wymaganego pola, pole ma nieprawidłowy typ danych (string zamiast int), wartość enum poza dozwolonym zbiorem, zagnieżdżona struktura jest spłaszczona
- Błąd semantyczny — prawidłowy JSON, właściwy schemat, ale treść jest błędna (nieprawidłowa klasyfikacja, wymyślona wartość, logiczna niespójność między polami)
Prompt engineering potrafi częściowo stłumić pierwszą kategorię. Na drugą i trzecią sam w sobie nie wystarcza.
JSON mode a constrained decoding — różnica, która ma znaczenie
Gdy dostawcy mówią o „JSON mode", zazwyczaj mają na myśli jedną z dwóch rzeczy, które są technicznie bardzo odmienne.
JSON mode (biasowanie na poziomie tokenów): Parametrem API zwiększa się prawdopodobieństwo tokenów należących do gramatyki JSON i tłumi tokeny, które naruszałyby poprawność. Wynik jest niemal zawsze składniowo prawidłowym JSON-em, ale model nadal sam decyduje o strukturze. Nie można zagwarantować, że wynik będzie zawierał pole invoice_number — tylko to, że będzie parsowalny.
Constrained decoding / generowanie oparte na gramatyce: Model generuje wyłącznie tokeny dozwolone w danym stanie przez formalną gramatykę (na przykład JSON Schema, RegEx, gramatykę bezkontekstową). Implementacja typowo używa automatu skończonego (FSM — finite state machine) lub podobnej struktury, która śledzi stan parsowania i w każdym kroku ogranicza wybór tokenów do dozwolonych. Wynik gwarantuje zgodność z gramatyką — włącznie z konkretnym schematem zawierającym wymagane pola i typy danych.
Różnica w praktyce: JSON mode daje parsowalny wynik, generowanie oparte na gramatyce daje wynik zgodny z Państwa schematem Pydantic lub zod.
XGrammar: aktualny standard constrained decoding
Dla większości produkcyjnych frameworków serving — vLLM, SGLang, TensorRT-LLM — od początku 2026 roku domyślnym backendem dla dekodowania z ograniczeniami jest XGrammar. W porównaniu ze starszym podejściem (biblioteka Outlines, która pioniersko wprowadziła podejście FSM, ale miała problemy z czasami kompilacji przy złożonych schematach) XGrammar osiąga narzut poniżej 40 mikrosekund na token — praktycznie zerowy wpływ na latencję. Jeśli zatem używają Państwo nowoczesnego stosu self-hosted serving, constrained decoding jest dostępne bez dodatkowej konfiguracji.
Dla chmurowych API (OpenAI, Anthropic, Google) constrained decoding zazwyczaj nie jest wystawiane bezpośrednio — mają Państwo JSON mode lub parametr „response format" ze schematem, który działa podobnie, ale implementacja leży po stronie dostawcy.
Więcej o self-hosted serving: vLLM vs SGLang vs Ollama — kiedy co stosować.
Definicja schematu: Pydantic i zod jako jedyne źródło prawdy
Skoro constrained decoding zależy od formalnego schematu, muszą go Państwo gdzieś zdefiniować i utrzymywać. W praktyce dominują dwie ścieżki:
Python — Pydantic v2:
from pydantic import BaseModel, Field
from enum import Enum
class RiskLevel(str, Enum):
low = "low"
medium = "medium"
high = "high"
class SupplierAssessment(BaseModel):
supplier_name: str
risk_level: RiskLevel
score: int = Field(ge=0, le=100)
flags: list[str]
summary: str = Field(max_length=300)Model Pydantic jest bezpośrednio serializowany do JSON Schema, którą akceptuje większość frameworków LLM. Walidacja przy parsowaniu (model.model_validate(json_data)) wykrywa błędy schematu z dokładną lokalizacją.
TypeScript / Node.js — zod:
import { z } from "zod";
const SupplierAssessment = z.object({
supplier_name: z.string(),
risk_level: z.enum(["low", "medium", "high"]),
score: z.number().int().min(0).max(100),
flags: z.array(z.string()),
summary: z.string().max(300),
});Kluczowa zasada: schemat jest jedynym źródłem prawdy. Nie definiować go raz w prompcie i raz w kodzie — w końcu się rozejdą. Opis schematu do promptu należy generować dynamicznie z samej definicji Pydantic lub zod.
Walidacja i retry — warstwa produkcyjna
Ani constrained decoding, ani JSON mode nie chronią przed błędami semantycznymi. A w chmurowych API mogą sporadycznie występować anomalie składniowe mimo JSON mode (przerwy sieciowe, obcięcie przy max_tokens). Produkcyjny pipeline potrzebuje zatem warstwy walidacji z logiką ponownych prób.
Podstawowy wzorzec:
import json
from pydantic import ValidationError
async def call_with_validation(prompt: str, schema: type[BaseModel], max_retries: int = 3):
for attempt in range(max_retries):
raw = await llm_call(prompt)
try:
data = json.loads(raw)
return schema.model_validate(data)
except (json.JSONDecodeError, ValidationError) as e:
if attempt == max_retries - 1:
raise
# Zwróć błąd do promptu w celu naprawy
prompt = repair_prompt(prompt, raw, str(e))
raise RuntimeError("Max retries exceeded")Kilka praktycznych wskazówek:
- Retry z błędem w kontekście: Przy kolejnej próbie należy umieścić w prompcie dokładny komunikat błędu z walidatora. Model zazwyczaj poprawia błąd, gdy wie, gdzie on jest — nie gdy otrzymuje jedynie „spróbuj ponownie".
- Exponential backoff: Przy limitach szybkości API lub niestabilnej sieci.
- Limit prób: W praktyce wystarczą 2–3 próby. Jeśli model po trzech próbach nadal zwraca nieprawidłowy wynik, problem leży w Państwa schemacie lub prompcie, a nie w losowości.
- Logowanie każdej nieudanej próby: Bez logu nie wiadomo, jak często to się zdarza ani dlaczego.
Niezawodność tool callingu — ściśle powiązany problem — omawiamy w artykule Tool calling niezawodnie w środowisku produkcyjnym.
Dlaczego nawet wysoka niezawodność nie wystarcza i co z tym zrobić
Niezawodność promptu "Respond in JSON" bez dodatkowej infrastruktury silnie zależy od modelu, złożoności schematu i promptu — a przy bardziej wymagających schematach regularnie spada poniżej poziomu użytecznego w produkcji. JSON mode (biasowanie tokenów) znacznie poprawia sytuację. Generowanie oparte na gramatyce + walidacja Pydantic + warstwa retry podnosi niezawodność do poziomu, na którym większość zespołów przestaje zajmować się błędami składniowymi i może skupić się na poprawności semantycznej.
Dla wielu przypadków użycia brzmi to dobrze. Ale proszę wyobrazić sobie pipeline, który działa tysiąc razy dziennie: przy niezawodności 99% mają Państwo 10 niepowodzeń dziennie. Jeśli każde niepowodzenie oznacza ręczną interwencję operatora lub błędnie przetworzony dokument, te 10 przypadków dziennie szybko staje się problemem.
Praktyczne środki zaradcze poza retry:
- Proste schematy: Każdy poziom zagnieżdżenia i każde nowe pole obniża niezawodność. Jeśli potrzebny jest złożony schemat, należy podzielić go na wiele sekwencyjnych wywołań z prostszymi wynikami.
- Enum zamiast wolnego tekstu: Tam gdzie to możliwe, należy zastąpić wolny tekst stałą listą wartości. Model musi wybrać z
["approved", "rejected", "pending"], a nie wymyślić format samodzielnie. - Niskie temperatury:
temperature=0lub bliskie zeru dla zadań ekstrakcji. Kreatywność nie jest tu pożądana. - Explicite przykłady w prompcie: Próbki
few-shotprawidłowego wyniku JSON to wciąż jeden z najskuteczniejszych trików.
Poprawność semantyczna: granica, gdzie technologia nie wystarcza
Constrained decoding gwarantuje, że wynik jest składniowo i schematycznie poprawny. Nie gwarantuje, że jest poprawny treściowo.
Przykłady błędów semantycznych, które przechodzą przez wszystkie warstwy techniczne:
- Klasyfikacja faktury jako
"approved"mimo że kwota przekracza zatwierdzony limit — ponieważ model nie był douczony na wewnętrznych regułach firmy - Ekstrakcja błędnej daty z dokumentu (zamienione pole)
- Sentiment
"positive"dla tekstu, który jest ironicznie krytyczny - Wartość
score: 87, która nie wynika z tekstu, lecz jest generowana jako „rozsądna liczba"
Te błędy rozwiązuje się inaczej: evals, testy na reprezentatywnej próbce wyników i — tam gdzie ryzyko jest wysokie — warstwa human-in-the-loop. Jak systematycznie mierzyć jakość wyników LLM, w tym wymiar semantyczny, opisujemy w artykule Jak zmierzyć jakość aplikacji LLM (evals).
Structured outputs przez dostawców API a self-hosted
Korzystając z chmurowych API, mają Państwo do dyspozycji:
- OpenAI: Parametr
response_formatztype: "json_schema"i inline definicją JSON Schema. Od określonej wersji API gwarantuje zgodność ze schematem na poziomie gramatyki (nie tylko biasowanie tokenów). - Anthropic Claude: Natywne ustrukturyzowane wyjścia przez parametr
output_format— dekodowanie z ograniczeniami bezpośrednio na poziomie tokenów, gwarantuje zgodność z JSON Schema. Dostępne jest również strict tool use z precyzyjnie zdefiniowanym schematem wejściowym. - Google Gemini:
responseMimeType: "application/json"+ parametrresponseSchema.
Wszystkie trzy podejścia działają dobrze dla typowych przypadków użycia. Praktyczna różnica ujawnia się przy:
- Bardzo złożonych schematach (głębokie zagnieżdżenie,
oneOf/anyOf) — chmurowe API mogą mieć ograniczenia co do obsługiwanych funkcji JSON Schema - Wysokiej przepustowości — przy tysiącach requestów na minutę self-hosted
vLLM+XGrammarjest ekonomicznie korzystniejszy, zob. porównanie w vLLM vs SGLang vs Ollama - Wrażliwych danych — jeśli dokumenty wejściowe zawierają dane osobowe lub tajemnice handlowe, chmurowe API generuje ryzyko wycieku danych; self-hosted on-prem je eliminuje
Dla regulowanych branż (ochrona zdrowia, finanse, prawo) ścieżka on-prem jest niemal zawsze właściwa — więcej na ten temat w Lokalne LLM vs chmura: kiedy co ma sens.
Ustrukturyzowany wynik w pipeline RAG i agentowym
Structured outputs to nie tylko zadania ekstrakcji. W każdym pipeline, gdzie LLM generuje wynik pośredni konsumowany przez kolejny komponent, ustrukturyzowany format jest warunkiem niezawodności.
W pipeline RAG ustrukturyzowany wynik jest zazwyczaj potrzebny przy:
- Query planning — model decyduje, które źródła odpytać, z jakim filtrem, ile wyników
- Decyzja rerankingu — w agentic RAG model ocenia trafność fragmentu i zwraca wynik
- Cytaty — gdzie w źródłowym dokumencie znajduje się odpowiedź (ID fragmentu, offset, poziom pewności)
- Finalny wynik dla downstream — gdy pipeline RAG jest konsumowany przez inny system, a nie tylko wyświetla tekst
W pipeline agentowym obowiązuje to samo dla każdego wywołania narzędzia — agent musi zwrócić ustrukturyzowany wybór akcji (nazwa narzędzia + parametry), aby orkiestrator wiedział, co wykonać. Brak struktury nie oznacza tu tylko błędu parsowania — może oznaczać wykonanie błędnej akcji.
Praktyczne doświadczenie: w pipeline z więcej niż trzema narzędziami warto walidować każde wejście narzędzia przez model Pydantic, a nie tylko swobodny dict. Błędy są wtedy wykrywane na granicy wejścia do narzędzia, a nie głęboko w jego logice.
Monitoring i dryf w środowisku produkcyjnym
Ustrukturyzowany wynik ma tendencję do dobrego działania po wdrożeniu i stopniowego pogarszania się — gdy model zmienia się po stronie dostawcy (chmurowe API po cichu aktualizują model), gdy zmienia się rozkład danych wejściowych lub gdy schemat rośnie o nowe pola bez aktualizacji promptu.
Minimum monitoringu, które rekomendujemy:
- Wskaźnik błędów parsowania: Jaki procent wywołań kończy się błędem walidacji po pierwszej próbie? Jeśli ta liczba rośnie, coś się zmieniło.
- Wskaźnik retry: Jaki procent wywołań wymagał więcej niż jednej próby? Powyżej 5% to sygnał.
- Wskaźnik null/default na poziomie pola: Jeśli pole
invoice_numberjest null w 30% przypadków, albo wejścia go nie zawierają, albo model przestaje je ekstrahować. - Śledzenie wersji schematu: Każda zmiana schematu to zdarzenie deploy — należy logować wersję schematu przy każdym wywołaniu, aby móc korelować zmiany jakości.
Bez tego monitoringu degradacja wychodzi na jaw dopiero gdy system downstream zawiedzie w produkcji — nie wtedy, gdy nastąpi.
Najczęstsze pytania
Jaka jest różnica między JSON mode a structured outputs?
JSON mode (biasowanie tokenów) gwarantuje tylko poprawność składniową — wynik będzie parsowalnym JSON-em, ale bez gwarancji konkretnej struktury. Structured outputs (oparte na gramatyce / z ograniczeniami schematu) gwarantują zgodność z zdefiniowanym schematem, włącznie z wymaganymi polami i typami danych. Do produkcyjnych pipeline'ów potrzebne jest drugie podejście.
Czy constrained decoding wpływa na jakość lub szybkość modelu?
Przy nowoczesnych implementacjach (XGrammar) narzut wynosi poniżej 40 mikrosekund na token — w praktyce niemierzalny. Jakość odpowiedzi może nieznacznie się różnić, ponieważ model nie może swobodnie formułować — przy wąskich schematach z długimi polami tekstowymi jest to zaniedbywalne; przy bardzo restrykcyjnych schematach (np. mały enum na odpowiedź w języku naturalnym) wynik może być mniej naturalny.
Czy mogę używać structured outputs z modelem działającym lokalnie?
Tak, z vLLM lub SGLang XGrammar jest dostępny out-of-the-box. Ollama obsługuje JSON mode, ale nie constrained decoding oparty na gramatyce w pełnym zakresie — do produkcyjnych pipeline'ów z precyzyjnym schematem vLLM jest lepszym wyborem. Więcej w vLLM vs SGLang vs Ollama.
Co zrobić, gdy model nadal zwraca błędne wartości mimo prawidłowego formatu?
To jest błąd semantyczny, nie składniowy — constrained decoding go nie rozwiąże. Rozwiązania: przykłady few-shot z poprawnymi/niepoprawnym wzorcami, explicite reguły w prompcie systemowym, fine-tuning na próbce domenowej lub warstwa human-in-the-loop dla decyzji krytycznych.
Jak zdefiniować schemat, aby model nie mylił się w typach danych?
Należy być precyzyjnym: zamiast score: number używać score: integer, minimum: 0, maximum: 100. Zamiast date: string używać date: string, format: date (ISO 8601). Wartości enum zawsze definiować — wolny string to zaproszenie do wariantów. Każde uszczegółowienie w schemacie zmniejsza prawdopodobieństwo błędu.
*MP Industrial Solutions pomaga firmom projektować i wdrażać produkcyjne pipeline'y LLM — od walidacji wyników po monitoring w rzeczywistym środowisku. Jeśli zajmują się Państwo niezawodnością ustrukturyzowanych wyników w swoim systemie, chętnie przyjrzymy się razem konkretnej architekturze.*
