Model obiecał JSON, parser zwrócił null. Pięć przebiegów i trzy przyczyny
Odpowiedź zaczyna się poprawnie, a parser zwraca pustkę. Trzy przyczyny naraz i rozwiązanie, które przeszło za pierwszym podejściem.

👁 113 przeczytań
- Model zwraca null z json_decode z trzech przyczyn: ucięcie na limicie tokenów, surowe znaki nowej linii w łańcuchu oraz cudzysłowy w HTML zamykające łańcuch przedwcześnie.
- Parser dwustopniowy - najpierw json_decode, potem wyciąganie pól znak po znaku - pozwolił odzyskać dane z uciętej odpowiedzi i wyprodukować tekst na 1228 słów za pierwszym podejściem.
- Jeśli model uparcie ignoruje instrukcję formatu w prompcie, przyczyną może być warstwa transportowa, która narzuca tryb JSON na poziomie API - tak jak funkcja opakowująca zapytanie w opisanym przypadku.
Prosisz model o JSON, model obiecuje JSON, a `json_decode` zwraca `null`. Trafiłem na to w tym tygodniu przy automacie, który pisze teksty - i przy okazji zrozumiałem, dlaczego cała kategoria nowych narzędzi powstaje właśnie wokół tego problemu.
Wniosek w jednym akapicie
Model generujący tekst nigdy nie da gwarancji struktury - może ją tylko obiecać. Tryb JSON i structured outputs bardzo podnoszą skuteczność, ale nadal wymuszają format na systemie, który pod spodem produkuje kolejne tokeny. Przy krótkich odpowiedziach to wystarcza. Przy długich - takich, gdzie w polu siedzi cały artykuł HTML - zaczyna się sypać. Trzy rzeczy, które realnie pomagają: tolerancyjny parser, krótsze pola i rezygnacja z jednego wielkiego obiektu.
Co dokładnie się psuje
Mój przypadek: automat prosi o obiekt z czterema polami, gdzie ostatnie zawiera artykuł na 1200 słów w HTML. Model zwracał odpowiedź, która zaczynała się poprawnie - w logu widziałem czysty początek JSON-a z tytułem i opisem. A parser zwracał pustkę.
Przyczyny są trzy i występują razem:
- Ucięcie na limicie tokenów. Długi artykuł nie mieści się w limicie, odpowiedź kończy się w połowie łańcucha, JSON nie ma domknięcia. Treść jest kompletna, struktura nie.
- Surowe znaki nowej linii w łańcuchu. W poprawnym JSON-ie muszą być zapisane jako
n. Model wstawia prawdziwe. - Cudzysłowy w HTML.
class="pw-lead"w środku łańcucha zamyka go przedwcześnie, jeśli nie zostanie poprawnie zabezpieczone.
Ślepy zaułek, w który wszedłem
Skoro JSON się łamie, pomyślałem, żeby go porzucić i poprosić o format ze znacznikami: ===TYTUL===, ===HTML=== i tak dalej. Napisałem parser, wdrożyłem, uruchomiłem. Model dalej zwracał JSON.
Powód okazał się banalny i wart zapamiętania: funkcja, przez którą szło zapytanie, wymuszała tryb JSON na poziomie API. Model nie miał wyboru - moja instrukcja o znacznikach była ignorowana, bo format narzucała warstwa niżej. Straciłem na tym trzy przebiegi, zanim sprawdziłem, co robi sama funkcja, zamiast poprawiać prompt.
Wniosek: gdy model uporczywie ignoruje instrukcję formatu, sprawdź najpierw, czy coś w warstwie transportowej nie narzuca mu innego.
Co zadziałało
Parser dwustopniowy. Najpierw zwykły json_decode. Gdy ten padnie - wyciąganie pól znak po znaku, z obsługą sekwencji n, t i uXXXX, kończone na pierwszym niezabezpieczonym cudzysłowie.
Kluczowa właściwość: taki parser odzyskuje treść z odpowiedzi uciętej. Jeśli JSON urwał się po polu z artykułem, wszystkie wcześniejsze pola są kompletne i da się je odczytać. Zwykły dekoder odrzuca całość.
| json_decode | Parser tolerancyjny | |
|---|---|---|
| Poprawny JSON | działa | działa |
| Ucięty na limicie | null | odzyskuje kompletne pola |
| Surowe znaki nowej linii | null | odzyskuje |
| Śmieci przed obiektem | null | odzyskuje |
Po tej zmianie ten sam automat przeszedł za pierwszym podejściem i wyprodukował tekst na 1228 słów.
Cztery rzeczy, które robię teraz zawsze
- Loguję koniec odpowiedzi, nie początek. Początek zawsze wygląda dobrze. Dopiero ostatnie 100 znaków mówi, czy odpowiedź jest ucięta.
- Długa treść nie jedzie w JSON-ie. Jeśli pole ma mieć więcej niż kilkaset słów, lepiej osobne wywołanie albo format bez ucieczek.
- Waliduję zawartość, nie tylko format. Że JSON się sparsował, nie znaczy, że treść ma sens - u mnie osobny test sprawdza, czy w tekście są wszystkie liczby, które miały być.
- Trzy próby, potem wpis do logu. Po trzech nieudanych model nie „naprawi się” za czwartym razem. Lepiej odnotować porażkę niż opublikować śmieć.
Dlaczego powstaje osobna kategoria narzędzi
Nie przepłacaj za te subskrypcje
Prowadzę sklep z rocznymi dostępami do narzędzi AI - te same konta, o których piszę wyżej, tylko taniej niż w cenniku producenta.
Ten problem jest na tyle powszechny, że zaczęły powstawać modele, które w ogóle nie generują tekstu. Zamiast prosić model o JSON i go parsować, zadajesz typowane pytanie i dostajesz wartość z rozkładem prawdopodobieństwa - bez etapu tekstowego, więc bez etapu parsowania.
Opisałem jeden taki przypadek osobno: TypeSafe i model Jev, gdzie API ma trzy typy pytań i zwraca decyzje zamiast łańcuchów. To jest opinia, nie rekomendacja - nie testowałem tego jeszcze na własnym zadaniu. Ale kierunek jest logiczny: jeśli wynik ma konsumować kod, etap pośredni w postaci tekstu jest kosztem, nie funkcją.
Kiedy który sposób
| Sytuacja | Co wybrać |
|---|---|
| Krótka odpowiedź, kilka pól | Tryb JSON wystarczy |
| Długa treść w polu | Osobne wywołanie na treść plus tolerancyjny parser |
| Klasyfikacja, ocena, wybór z listy | Model zwracający typ zamiast tekstu |
| Tysiące drobnych rozstrzygnięć | Model decyzyjny - tam koszt i czas robią różnicę |
Najczęstsze pytania
Dlaczego model zwraca niepoprawny JSON, skoro prosiłem o poprawny?
Bo pod spodem generuje kolejne tokeny, a nie strukturę. Tryb JSON podnosi skuteczność, ale nie daje gwarancji - zwłaszcza gdy odpowiedź zostanie ucięta na limicie.
Czy structured outputs rozwiązują problem?
Bardzo pomagają przy krótkich i średnich odpowiedziach. Przy długiej treści w jednym polu nadal zostaje ryzyko ucięcia.
Jak sprawdzić, czy odpowiedź była ucięta?
Zaloguj jej ostatnie 100 znaków i długość. Ucięta kończy się w środku zdania albo łańcucha, bez domknięcia.
Model ignoruje mój format. Co robić?
Sprawdź, czy warstwa transportowa nie wymusza innego. U mnie funkcja opakowująca narzucała tryb JSON na poziomie API - instrukcja w promptcie nie miała szans.
Skąd to wiem
Wszystkie przypadki pochodzą z budowy automatu redakcyjnego na tym serwisie we wrześniu 2026. Pięć nieudanych przebiegów, trzy różne przyczyny, każda zdiagnozowana z logów. Parser dwustopniowy działa na produkcji - pierwszy tekst po poprawce przeszedł za pierwszym podejściem i miał 1228 słów.
Cały tydzień w AI, w jednym mailu
Wybrane premiery, narzędzia i analizy. Raz w tygodniu, prosto do skrzynki.
Zapisz się za darmo →Najczęstsze pytania
Dlaczego model zwraca niepoprawny JSON, skoro prosiłem o format JSON?
Model generuje kolejne tokeny, a nie gotową strukturę, więc nie może dać gwarancji poprawności - tryb JSON tylko podnosi skuteczność. Szczególnie przy długich odpowiedziach, jak artykuł na 1200 słów w jednym polu, ryzyko ucięcia na limicie tokenów sprawia, że obiekt nie ma domknięcia i parser odrzuca całość.
Czy tryb structured outputs rozwiązuje problem z parsowaniem odpowiedzi modelu?
Structured outputs bardzo pomagają przy krótkich i średnich odpowiedziach. Przy długiej treści w jednym polu nadal zostaje ryzyko ucięcia na limicie tokenów, które niszczy strukturę.
Jak sprawdzić, czy odpowiedź modelu została ucięta na limicie tokenów?
Wystarczy zalogować ostatnie 100 znaków odpowiedzi i jej długość. Ucięta odpowiedź kończy się w środku zdania albo łańcucha, bez domknięcia.
Dlaczego model ignoruje moje instrukcje dotyczące formatu odpowiedzi?
Przyczyną może być warstwa transportowa, która narzuca inny format na poziomie API - wtedy instrukcja w prompcie nie ma żadnego wpływu. W opisanym przypadku funkcja opakowująca zapytanie wymuszała tryb JSON, przez co instrukcja o znacznikach tekstowych była ignorowana, co kosztowało trzy dodatkowe przebiegi diagnostyczne.
