Model językowy zawsze zwraca tekst, który wygląda na poprawny. To podstępne: odpowiedź brzmi pewnie nawet wtedy, gdy pole jest zmyślone, liczba spoza zakresu, a klucz w JSON-ie nazwany inaczej niż wczoraj. Jeśli to wyjście trafia prosto do bazy, do faktury albo do innego systemu, jeden zniekształcony rekord potrafi zatrzymać cały proces. Poniżej opisujemy wzorzec walidacji, który stosujemy, żeby wyjście LLM dało się traktować jak dane, a nie jak nadzieję.
Dlaczego „wygląda dobrze” to za mało
#Surowy tekst z modelu ma trzy klasy wad, które trzeba rozdzielić, bo każda wymaga innej obrony. Pierwsza to wady kształtu: brakujący przecinek, ucięty JSON, dodatkowy komentarz przed nawiasem, pole jako string zamiast liczby. Druga to wady treści: poprawny strukturalnie obiekt, w którym wartość jest halucynacją — wymyślony numer umowy, data spoza zakresu, kategoria, której nie ma w słowniku. Trzecia to wyjścia niebezpieczne: próba wstrzyknięcia instrukcji, wyciek danych, treść, której nie wolno pokazać użytkownikowi.
Walidacja musi adresować wszystkie trzy. Sprawdzenie samego JSON-a nie wyłapie halucynacji; sprawdzenie reguł biznesowych nie zadziała, dopóki tekst się w ogóle nie sparsuje. Dlatego budujemy je warstwowo.
Wymuszanie kształtu: schemat jako kontrakt
#Punktem wyjścia jest JSON Schema — formalny opis tego, jak ma wyglądać wyjście: jakie pola, jakie typy, które wymagane, jakie dozwolone wartości (enum). Schemat pełni dwie role naraz: jest instrukcją dla modelu i kontraktem dla walidatora. Są dwie drogi, by go wymusić.
| Podejście | Jak działa | Mocne strony | Ograniczenia |
|---|---|---|---|
| Structured output natywny | Provider gwarantuje JSON zgodny ze schematem (constrained decoding) | Brak błędów kształtu po stronie dekodowania | Nie każdy model/host to wspiera; bywa wolniejszy dla złożonych schematów |
| Structured output prompt-based | Schemat w prompcie + walidacja po stronie aplikacji | Działa z każdym modelem, pełna kontrola | Model bywa rozwlekły lub odbiega od schematu — walidacja obowiązkowa |
W naszych wdrożeniach domyślnie stosujemy wariant prompt-based z walidacją: schemat w prompcie, a po odebraniu odpowiedzi twarde sprawdzenie biblioteką walidującą JSON Schema. Powód jest praktyczny — natywne wymuszanie ze sztywnym response_format na części hostów bywa wolne (sekundy, czasem timeouty) i niedostępne dla każdego modelu, a chcemy móc wybrać model najlepszy do zadania, nie najlepszy do trybu API. Jak dobierać model pod konkretne zadanie, opisujemy w tekście o wyborze modelu LLM.
Niezależnie od wariantu zasada jest ta sama: schemat waliduje strukturę, nie sens. Luźne ograniczenia długości (model pisze bogato), ścisłe typy i enumy tam, gdzie wartość trafia dalej do systemu.
Pętla walidacja + naprawa
#Pojedyncze wywołanie nie zawsze trafia w schemat — i to jest normalne, nie awaria. Dlatego po walidacji, jeśli się nie powiodła, robimy jedną kontrolowaną próbę naprawy: oddajemy modelowi jego własne wyjście razem z konkretnym komunikatem błędu walidatora („pole kwota musi być liczbą”, „brak wymaganego kategoria”) i prosimy o poprawkę. To zwykle wystarcza, bo model ma teraz precyzyjną informację, co było nie tak.
Pętla musi mieć twardy limit. W naszej praktyce sprawdza się jedna, najwyżej dwie próby naprawy — więcej rzadko cokolwiek zmienia, a mnoży koszt i opóźnienie. Po wyczerpaniu prób nie zgadujemy: wyjście idzie do obsługi błędu (kolejka, człowiek w pętli, wartość domyślna), nigdy do systemu docelowego. Schematycznie:
- Generuj → wyjście wg schematu w prompcie.
- Waliduj → parsuj JSON + sprawdź JSON Schema + reguły biznesowe.
- Napraw → przy błędzie oddaj modelowi komunikat walidatora; powtórz walidację (limit prób).
- Decyduj → sukces: przepuść; porażka po limicie: fail-closed.
Guardrails: treść i bezpieczeństwo
#Schemat pilnuje kształtu, ale nie powie, że numer zamówienia jest zmyślony albo że model próbuje wykonać wstrzykniętą instrukcję. To zadanie dla guardrails — warstwy reguł działającej po walidacji strukturalnej, a często też przed modelem (na wejściu). Trzy kontrole, które stosujemy najczęściej:
- Grounding wartości — wartości, które muszą istnieć w rzeczywistości (numer umowy, kategoria, ID klienta), porównujemy ze źródłem prawdy. Pole, które nie pasuje do żadnego rekordu, traktujemy jako halucynację i odrzucamy, nawet jeśli typ się zgadza.
- Kontrola zakresów i reguł — kwoty, daty, liczby muszą mieścić się w widełkach biznesowych. Cena spoza zakresu albo data w przeszłości dla terminu w przyszłości to sygnał błędu, nie dane.
- Bezpieczeństwo wyjścia — filtr przeciw wyciekowi danych, próbom injection i treściom, których nie wolno pokazać. To ten sam nurt myślenia, co przy bezpieczeństwie agentów AI — wyjście modelu to niezaufane dane, dopóki go nie sprawdzimy.
Guardrails wpinamy też tam, gdzie model klasyfikuje — wynik klasyfikatora musi należeć do zamkniętego zbioru etykiet, inaczej routing zgłoszeń wysyła sprawę w nieznane miejsce. Gdy wyjście zasila firmowy GPT na bazie wiedzy albo system RAG, ta sama dyscyplina chroni przed podaniem użytkownikowi pewnie brzmiącej, ale nieprawdziwej odpowiedzi.
Kiedy fail-closed, a kiedy fail-open
#Najważniejsza decyzja projektowa: co zrobić, gdy walidacja po naprawie wciąż się nie udaje. Fail-closed oznacza odmowę — system nie przepuszcza niepewnego wyjścia, tylko eskaluje do człowieka, zwraca błąd albo używa bezpiecznej wartości domyślnej. To domyślny tryb wszędzie, gdzie skutek jest nieodwracalny lub kosztowny: płatności, zmiany w bazie, treść wysyłana klientowi, decyzje z konsekwencjami prawnymi.
Fail-open (przepuść mimo niepewności) dopuszczamy tylko tam, gdzie błąd jest tani i odwracalny, a brak odpowiedzi gorszy niż odpowiedź niedoskonała — na przykład podpowiedź tagu, którą człowiek i tak weryfikuje. Reguła kciuka: jeśli nie potrafisz tanio cofnąć skutku błędnego wyjścia, projektuj fail-closed. Jak przejść z takim wzorcem od pilota do stabilnej produkcji, rozwijamy w tekście od pilota AI do produkcji.
FAQ
#Czy structured output sam załatwia walidację?
#Nie. Natywny structured output pilnuje, by wyjście było poprawnym JSON-em zgodnym ze schematem — to wada kształtu, jedna z trzech klas. Nie sprawdza, czy wartości są prawdziwe ani bezpieczne, więc walidacja treści i guardrails są nadal potrzebne. Traktujemy go jako fundament, nie jako komplet.
Prompt-based czy natywny structured output?
#Zależy od hosta i modelu. Natywny daje gwarancję kształtu, ale nie każdy model go wspiera i bywa wolniejszy dla złożonych schematów. W praktyce często wybieramy prompt-based z twardą walidacją po stronie aplikacji, bo działa z dowolnym modelem i zostawia kontrolę nad pętlą naprawy.
Ile razy próbować naprawiać wyjście?
#W naszej praktyce jedna, najwyżej dwie próby naprawy. Po przekazaniu modelowi konkretnego komunikatu błędu pierwsza poprawka zwykle wystarcza; kolejne rzadko pomagają, a mnożą koszt i opóźnienie. Po wyczerpaniu limitu wyjście idzie do obsługi błędu, nie do systemu.
Jak walidacja chroni przed halucynacjami?
#Sam schemat nie chroni — halucynacja może być strukturalnie poprawna. Chronią guardrails: porównanie wartości ze źródłem prawdy (grounding), kontrola zakresów i zamkniętych słowników. Jeśli pole nie pasuje do żadnego rzeczywistego rekordu, odrzucamy je niezależnie od tego, jak pewnie brzmi.
Czy walidacja bardzo spowalnia system?
#Walidacja strukturalna i reguły są tanie — to milisekundy. Realny koszt to ewentualna runda naprawy, czyli jedno dodatkowe wywołanie modelu, i tylko gdy pierwsze wyjście nie przeszło. W zamian zyskujesz przewidywalność, którą trudno przecenić, gdy wyjście zasila inne systemy.