Strukturirani izlazi iz LLM-ova u 2026: Praktični vodič za Pydantic, Instructor i strict mode

Kako u 2026. dobiti pouzdane strukturirane izlaze iz LLM-ova: OpenAI strict mode, Pydantic sheme, Instructor biblioteka, Anthropic Tool Use, validacija i produkcijski obrasci s radnim Python primjerima.

Strukturirani LLM izlazi 2026: Pydantic vodič

Da budem iskren — ako vaša AI aplikacija u produkciji još uvijek parsira odgovore LLM-a regularnim izrazima, gubite novac, vrijeme i, što je najgore, povjerenje korisnika. U 2026. više ne postoji opravdanje za "nadati se" da je model vratio ispravan JSON. Strukturirani izlazi (engl. structured outputs) postali su produkcijski standard koji jamči — ne statistički, nego matematički — da odgovor LLM-a odgovara unaprijed definiranoj shemi.

U ovom vodiču pokrivamo sve što stvarno trebate znati o strukturiranim izlazima u 2026: kako rade pod haubom (constrained decoding i FSM-ovi), kako koristiti OpenAI strict mode s Pydanticom, kako izgleda usporedba s Anthropicovim Tool Use pristupom, zašto je biblioteka Instructor postala de facto standard, te — možda i najvažnije — kako dizajnirati sheme koje povećavaju, a ne smanjuju pouzdanost.

Što su strukturirani izlazi i zašto su važni

Strukturirani izlazi su mehanizam koji garantira da odgovor LLM-a odgovara unaprijed definiranoj JSON shemi. I tu je ključ: ne samo da je riječ o valjanom JSON-u, nego o shema-valjanom JSON-u — s točnim poljima, tipovima i ograničenjima koja ste vi specificirali.

Razlika između JSON Mode-a i Structured Outputs-a kritična je za produkciju:

  • JSON Mode (legacy): jamči samo sintaktički ispravan JSON. Polje koje ste očekivali kao integer može biti string, polja mogu biti potpuno izostavljena, model može izmisliti dodatna polja. Lijepo, ali nepouzdano.
  • Structured Outputs (strict mode): koristi constrained decoding. Pri svakom koraku generiranja tokena, samo tokeni koji vode kroz valjani put kroz konačni automat (FSM) imaju pozitivnu vjerojatnost. Nevaljali tokeni dobivaju logits postavljen na minus beskonačno. Drugim riječima, model fizički ne može generirati izlaz koji ne odgovara shemi.

U praksi to znači razliku između 2-3% stope grešaka (s JSON Mode-om i regex parsiranjem) i 0% strukturalnih grešaka (sa strict mode-om). Za sustav koji obrađuje 100.000 zahtjeva dnevno, to je 2-3 tisuće propalih poziva na koje više ne morate odgovarati retry logikom. Drugim riječima, manje pejdžera u 3 ujutro.

Stanje pružatelja u 2026

U 2026. ekosustav je konvergirao — sva tri velika pružatelja podržavaju nativne strukturirane izlaze, ali sa značajno različitim API-jima:

  • OpenAI: response_format s json_schema i strict: true. Najzreliji API, sa SDK helperima poput client.beta.chat.completions.parse() koji prihvaća Pydantic klasu izravno.
  • Anthropic Claude: strukturirani izlazi se ostvaruju kroz Tool Use. Definirate "alat" čija je shema vaš ciljani objekt, a model vraća tool_use blok s validiranim argumentima.
  • Google Gemini: parametar responseSchema u konfiguraciji generiranja, prima JSON shemu izravno.

Pydantic kao temelj definicije sheme

Pydantic je u Pythonu de facto standard za definiranje shema strukturiranih izlaza, i razlog je očit: Pydantic klase su istovremeno schema definition, type contract i runtime validator. Tri stvari u jednom — to je rijetkost u softveru koji vrijedi.

Druga prednost je da svi veliki SDK-jevi danas izravno prihvaćaju Pydantic klase i interno generiraju JSON Schemu, pa ne morate ručno održavati paralelne strukture.

Osnovni primjer:

from pydantic import BaseModel, Field
from typing import Literal
from enum import Enum

class Sentiment(str, Enum):
    POSITIVE = "positive"
    NEGATIVE = "negative"
    NEUTRAL = "neutral"

class ReviewAnalysis(BaseModel):
    reasoning: str = Field(
        description="Korak po korak analiza recenzije prije zaključka"
    )
    sentiment: Sentiment = Field(
        description="Ukupni sentiment recenzije"
    )
    confidence: float = Field(
        ge=0.0, le=1.0,
        description="Pouzdanost klasifikacije (0-1)"
    )
    key_phrases: list[str] = Field(
        max_length=5,
        description="Do 5 ključnih fraza iz recenzije"
    )

Tri ključne stvari koje obavezno morate uočiti:

  1. Opisi nisu samo dokumentacija — oni postaju dio JSON Sheme i izravno utječu na ono što model generira. Smatrajte ih prompt engineeringom unutar sheme.
  2. Enum-i ograničavaju kategorije — bez njih model svaki put izmišlja nove vrijednosti, što je iznenađujuće zabavno dok ne pošaljete tu vrijednost u bazu.
  3. Polje reasoning dolazi prvo. LLM-ovi generiraju lijevo-na-desno, pa stavljanjem rezoniranja prije odgovora ugrađujete chain-of-thought direktno u shemu.

OpenAI strict mode s Pydanticom: praktični primjer

S najnovijim OpenAI Python SDK-om, koristiti strukturirane izlaze je gotovo trivijalno:

from openai import OpenAI

client = OpenAI()

response = client.chat.completions.parse(
    model="gpt-4o-2024-08-06",
    messages=[
        {"role": "system", "content": "Ti si analitičar recenzija proizvoda."},
        {"role": "user", "content": "Telefon je solidan, ali baterija traje samo 4 sata."}
    ],
    response_format=ReviewAnalysis,
)

result: ReviewAnalysis = response.choices[0].message.parsed
print(result.sentiment)        # Sentiment.NEGATIVE
print(result.confidence)        # 0.85
print(result.key_phrases)       # ['solidan telefon', 'baterija 4 sata', ...]

Što SDK zapravo radi pod haubom:

  1. Pretvara Pydantic klasu u JSON Schema-u, automatski postavlja additionalProperties: false i sva polja u required.
  2. Šalje shemu uz strict: true.
  3. OpenAI server kompajlira shemu u FSM (jednom — pa se kešira po hash-u).
  4. Tijekom generiranja, samo tokeni koji vode kroz valjani put imaju nenultu vjerojatnost.
  5. Nakon generiranja, SDK parsira JSON i instancira Pydantic objekt — koji još jednom validira semantiku.

Česte zamke s Pydanticom u strict mode-u

Strict mode ne podržava sve što Pydantic dopušta. Ovo je popis stvari na kojima ćete se vjerojatno spotaknuti barem jednom:

  • Optional[X] u korijenu modela ne radi — sva polja u strict mode-u moraju biti required. Zaobilazak: koristite X | None i postavite default na None, ali polje uvijek navedite u required.
  • Union[str, int, float] (anyOf) nije podržan — sužavajte tipove.
  • Plain dict bez specificiranog tipa ključeva i vrijednosti će pasti — koristite dict[str, str] ili definirajte ugniježđeni model.
  • Maksimalno 5 razina ugnježđivanja i 100 svojstava ukupno (što zvuči kao puno, ali stigne se brže nego što mislite).

Anthropic Claude: strukturirani izlazi kroz Tool Use

Anthropic ne nudi izravan response_format parametar. Umjesto toga, svaki "alat" ima ulaznu shemu, pa definiranjem alata čija je shema vaš ciljani objekt prisiljavate Claude da pozove taj alat s validiranim argumentima. Trik je elegantan kad ga shvatite.

import anthropic
import json

client = anthropic.Anthropic()

review_tool = {
    "name": "submit_review_analysis",
    "description": "Pošalji strukturiranu analizu recenzije",
    "input_schema": ReviewAnalysis.model_json_schema()
}

response = client.messages.create(
    model="claude-sonnet-4-6",
    max_tokens=1024,
    tools=[review_tool],
    tool_choice={"type": "tool", "name": "submit_review_analysis"},
    messages=[
        {"role": "user", "content": "Telefon je solidan, ali baterija traje samo 4 sata."}
    ]
)

tool_use = next(b for b in response.content if b.type == "tool_use")
result = ReviewAnalysis(**tool_use.input)
print(result.sentiment)

Ključ je tool_choice={"type": "tool", "name": "..."} — to prisiljava Claude da pozove točno taj alat. Bez toga, model može odlučiti odgovoriti tekstualno, što ne želite kad gradite produkcijski pipeline.

Petlja validacije i ponovnog pokušaja

Strukturirani izlazi sprečavaju strukturalne greške, ali ne i semantičke. Model i dalje može vratiti sintaktički ispravan JSON s besmislenim vrijednostima — primjerice confidence: 1.5 kad ste rekli da mora biti između 0 i 1, ili datum prije početka projekta.

I tu na scenu stupaju Pydantic validatori:

from pydantic import BaseModel, field_validator
from datetime import date

class ProjectMilestone(BaseModel):
    project_start: date
    milestone_date: date
    description: str

    @field_validator("milestone_date")
    @classmethod
    def must_be_after_start(cls, v, info):
        start = info.data.get("project_start")
        if start and v < start:
            raise ValueError(
                f"milestone_date {v} mora biti nakon project_start {start}"
            )
        return v

Kada validacija padne, vraćate poruku o grešci modelu i tražite ispravak. U praksi, ovaj retry pattern u produkciji povećava end-to-end pouzdanost s ~92% na preko 99%:

from pydantic import ValidationError

def extract_with_retry(messages, schema, max_retries=2):
    for attempt in range(max_retries + 1):
        response = client.chat.completions.parse(
            model="gpt-4o-2024-08-06",
            messages=messages,
            response_format=schema,
        )
        try:
            return response.choices[0].message.parsed
        except ValidationError as e:
            if attempt == max_retries:
                raise
            messages.append({"role": "assistant", "content": response.choices[0].message.content})
            messages.append({
                "role": "user",
                "content": f"Validacija je pala s greskom: {e}. Ispravi i pokusaj ponovno."
            })
    raise RuntimeError("nedohvatljivo")

Instructor: standard za strukturirane izlaze u 2026

Instructor (preko 11 tisuća zvjezdica na GitHubu, oko 3 milijuna preuzimanja mjesečno) je biblioteka koja "patcha" standardne SDK-jeve i dodaje im response_model argument.

Glavne prednosti? Automatski retry s feedbackom validacije, jedinstveni API kroz 15+ pružatelja, te potpora za streaming Pydantic objekata. Iskreno, kad sam prvi put vidio koliko je trivijalno prebaciti se s OpenAI-ja na Anthropic, pomislio sam da je previše dobro da bi bilo istinito. Nije.

import instructor
from openai import OpenAI

client = instructor.from_openai(OpenAI())

result = client.chat.completions.create(
    model="gpt-4o-2024-08-06",
    response_model=ReviewAnalysis,
    max_retries=3,
    messages=[
        {"role": "user", "content": "Telefon je solidan, ali baterija traje samo 4 sata."}
    ]
)
print(result.sentiment)

Promjena pružatelja je trivijalna — samo zamijenite klijent:

import instructor
import anthropic

client = instructor.from_anthropic(anthropic.Anthropic())

result = client.messages.create(
    model="claude-sonnet-4-6",
    response_model=ReviewAnalysis,
    max_retries=3,
    max_tokens=1024,
    messages=[
        {"role": "user", "content": "Telefon je solidan, ali baterija traje samo 4 sata."}
    ]
)

Streaming strukturiranih objekata

Instructor podržava parcijalno streamanje Pydantic objekata — kako tokeni stižu, dobivate parcijalne instance koje se postupno popunjavaju. Korisno za UI gdje korisnik vidi rezultate u realnom vremenu (i, da budem iskren, izgleda poprilično spektakularno na demo prezentacijama):

from instructor import Partial

stream = client.chat.completions.create(
    model="gpt-4o-2024-08-06",
    response_model=Partial[ReviewAnalysis],
    stream=True,
    messages=[{"role": "user", "content": review_text}]
)

for partial in stream:
    print(partial.model_dump())

Najbolje prakse dizajna shema

1. Rezoniranje prvo

Stavite polje reasoning ili analysis prije konačnog odgovora. LLM-ovi generiraju lijevo-na-desno; ako prvo dolazi category, model bira kategoriju i tek onda izmišlja opravdanje. Ako prvo dolazi reasoning, model prvo razmišlja, a tek onda zaključuje. To je chain-of-thought ugrađen u shemu, bez dodatnih API poziva. Jeftino i moćno.

2. Opisi su prompt

Polje description u Pydanticu završava u JSON Schemi koja se šalje modelu. Iskoristite to:

class Invoice(BaseModel):
    total: float = Field(
        description=(
            "Ukupni iznos racuna u eurima, bez PDV-a. "
            "Ako je iznos u drugoj valuti, konvertiraj koristeci tecaj iz teksta."
        )
    )

3. Enum-i, ne stringovi

Nikada ne tražite "kategoriju" kao slobodni string. Model će izmisliti nove kategorije svaki put, garantirano. Koristite Enum ili Literal:

from typing import Literal

class Ticket(BaseModel):
    priority: Literal["low", "medium", "high", "critical"]
    department: Literal["billing", "technical", "sales", "other"]

4. Ograničite ugnježđivanje

Maksimalno 2-3 razine. Duboko ugniježđene sheme povećavaju stopu grešaka i usporavaju kompilaciju FSM-a na strani pružatelja. Nema potrebe za drvetom dubine 6 — to nije Backend-ova osvjeta nad Frontend developerima.

5. Razdvojite ekstrakciju i kategorizaciju

Umjesto jedne velike sheme s 30 polja, podijelite u dvije faze: prvo izvucite sirove podatke, zatim ih klasificirajte zasebnim pozivom. Manje šanse za halucinaciju, jeftinije retry-eve, lakše debugiranje. Probao sam oba pristupa na ekstrakciji ugovora — dvofazni je pobijedio za otprilike 8 postotnih bodova točnosti.

Produkcijski obrasci

Promatranje i metrike

U produkciji morate pratiti barem ovo:

  • Stopu validacijskih grešaka po shemi — porast signalizira degradaciju modela ili promjenu distribucije ulaza.
  • Broj retry-a po pozivu — više od 1 prosječno znači da shema ne odgovara stvarnim podacima.
  • Latenciju — strukturirani izlazi dodaju 50-200 ms na prvi token zbog kompilacije FSM-a (kešira se nakon prvog poziva).
  • Trošak po pozivu — opisi i validatori su besplatni, ali prevelike sheme troše izlazne tokene.

Refusals kao prvoklasna greška

OpenAI može vratiti refusal objekt umjesto JSON-a kada model odbije odgovoriti zbog sigurnosnih razloga. Provjerite to prije parsiranja — inače ćete loviti čudne bugove tjednima:

message = response.choices[0].message
if message.refusal:
    log_refusal(message.refusal)
    raise ContentPolicyError(message.refusal)
result = message.parsed

Verzioniranje shema

Sheme su API ugovor s podacima. Promjena imena polja ili tipa je breaking change, koliko god to izgledalo nedužno. Verzionirajte sheme i čuvajte stare verzije za backfill historijskih podataka:

class ReviewAnalysisV1(BaseModel):
    sentiment: Sentiment
    confidence: float

class ReviewAnalysisV2(BaseModel):
    reasoning: str
    sentiment: Sentiment
    confidence: float
    key_phrases: list[str]

Strukturirani izlazi u agentskim tijekovima

U LangGraph i sličnim okvirima, strukturirani izlazi su kritični za pouzdane tranzicije stanja. Bez stroge sheme između čvorova, jedan nedostajući ključ može srušiti lanac od deset agenata. (Da, prošao sam to. Ne preporučam.) Strukturirani izlazi eliminiraju cijelu klasu bugova gdje agenti generiraju sintaktički ispravno, ali semantički nevaljano stanje.

PydanticAI (od istog tima koji održava Pydantic) ide korak dalje — tretira agente kao Pydantic aplikacije sa sigurnim tipovima na ulazu i izlazu, dependency injectionom i nativnom potporom za strict mode.

from pydantic_ai import Agent

agent = Agent(
    "openai:gpt-4o-2024-08-06",
    result_type=ReviewAnalysis,
    system_prompt="Ti si analiticar recenzija proizvoda."
)

result = agent.run_sync("Telefon je solidan, ali baterija traje samo 4 sata.")
print(result.data.sentiment)

Kontrolni popis za produkciju u 2026

  • Koristite strict mode (OpenAI) ili Tool Use s tool_choice (Anthropic) — nikada plain JSON Mode.
  • Definirajte sheme u Pydanticu (Python) ili Zod (TypeScript). Tip je dokument.
  • Stavite reasoning polje prije odlučujućih polja.
  • Koristite Literal i Enum za sve kategorije s fiksnim skupom vrijednosti.
  • Dodajte semantičke validatore (field_validator, model_validator).
  • Implementirajte retry s feedbackom validacijskih grešaka — 2-3 pokušaja podiže pouzdanost preko 99%.
  • Tretirajte refusals kao posebnu klasu grešaka.
  • Pratite stopu validacijskih grešaka po shemi kao SLO metriku.
  • Verzionirajte sheme; nikada ne mijenjajte postojeću u backwards-incompatible smjeru.

Često postavljana pitanja (FAQ)

Smanjuje li strict mode kvalitetu odgovora?

Mjerenja iz 2026. pokazuju da strict mode ne degradira kvalitetu na zadacima koji su prirodno strukturirani (ekstrakcija, klasifikacija, generiranje strukturiranih artefakata). Na zadacima slobodnog rezoniranja može biti blagi pad ako shema nije dobro dizajnirana — primjerice ako prisilite kratak summary string bez prethodnog reasoning polja. Rješenje je dizajn sheme, ne odustajanje od strict mode-a.

Kada koristiti Instructor, a kada nativni SDK?

Ako koristite jedan pružatelj i sheme su jednostavne, nativni SDK parse() je dovoljan i ima manje ovisnosti. Prijeđite na Instructor kada trebate automatske retry-eve na validacijske greške, kada radite s više pružatelja, ili kada želite parcijalno streamanje Pydantic objekata. Pravilo je jednostavno: započnite s nativnim SDK-om, pređite na Instructor kada je kompleksnost opravdana.

Rade li strukturirani izlazi s lokalnim modelima?

Da, kroz biblioteke kao što su Outlines, XGrammar i llama.cpp grammar mode. One implementiraju constrained decoding na klijentskoj strani — svaki token koji ne vodi kroz valjani put kroz gramatiku ima maskiranu vjerojatnost. Performanse su nešto sporije od cloud rješenja jer se FSM kompajlira lokalno, ali pouzdanost je ista.

Što je s podrškom za nullable polja u strict mode-u?

OpenAI strict mode zahtijeva da sva polja budu u required popisu. Da biste imali "opcionalno" polje, morate eksplicitno dopustiti null kao tip: field: str | None = Field(default=None). Pydantic će ovo prevesti u {"type": ["string", "null"]}, a polje ostaje u required. Model može vratiti null kada nema vrijednosti, što semantički odgovara opcionalnosti.

Koliko košta strukturirani izlaz u odnosu na običan?

Tokenski trošak je gotovo identičan — JSON Schema se ne broji u inputu jer se kešira po hash-u. Mali dodatni izlazni trošak dolazi od JSON ključeva i interpunkcije (oko 10-15% više izlaznih tokena nego ekvivalent u prostom tekstu). U usporedbi s troškom retry-a kod neispravnog izlaza (cijeli novi poziv), strukturirani izlazi u praksi smanjuju ukupni trošak za 20-40%. Win-win, kako se to kaže.

Article changelog (1)
  • — SEO meta refreshed (title and description updated)
Editorial Team
O Autoru Editorial Team

Our team of expert writers and editors.