Structured Output z LLM w Pythonie – Pydantic, Instructor i natywne API krok po kroku

Praktyczny poradnik uzyskiwania strukturalnych odpowiedzi JSON z modeli LLM w Pythonie. Porównanie trzech podejść: natywne SDK (Claude, OpenAI), biblioteka Instructor i framework Pydantic AI – z działającymi przykładami kodu.

Czym jest Structured Output i dlaczego jest niezbędny w 2026 roku?

Duże modele językowe są z natury generatorami tekstu. Podajesz prompt, dostajesz ciąg tokenów -- czasem genialny, czasem halucynujący, ale zawsze niestrukturalny. Dla człowieka to nie problem: przeczytasz odpowiedź i wyciągniesz wnioski. Ale gdy Twoja aplikacja potrzebuje obiektu JSON z polami product_name, rating i sentiment, żeby zapisać go w bazie danych -- "prawie poprawny JSON" to dokładnie to samo co błąd. Żadnej litości.

W produkcyjnych systemach AI niezawodność parsowania odpowiedzi modelu to nie "nice to have", to fundament całej architektury. Jeśli Twój pipeline przetwarza tysiące dokumentów dziennie, nawet 1% błędów parsowania oznacza dziesiątki utraconych rekordów, alerty w monitoringu i nocne debugowanie. Sam przez to przechodziłem -- i nie polecam.

Możemy wyróżnić trzy poziomy dojrzałości w uzyskiwaniu strukturalnych odpowiedzi z LLM:

  1. Poziom 1: Prompt engineering -- prosisz model "zwróć JSON w takim formacie..." i masz nadzieję. Niezawodność: 80-95%. Wystarczające do prototypów, fatalne w produkcji.
  2. Poziom 2: Function calling / Tool Use -- model wybiera funkcję i generuje argumenty zgodne ze schematem. Niezawodność: 95-99%. Solidne rozwiązanie, ale model może technicznie odmówić użycia narzędzia.
  3. Poziom 3: Natywne Structured Output -- provider kompiluje Twój schemat JSON do gramatyki i ogranicza generowanie tokenów. Niezawodność: 100% zgodności ze schematem. To jest standard produkcyjny w 2026 roku.

Jeśli budujesz produkcyjną aplikację w 2026 roku, powinieneś być na Poziomie 3 -- albo przynajmniej na solidnym Poziomie 2 z automatycznymi retries. W ekosystemie Pythona głównym narzędziem do definiowania schematów jest Pydantic (v2), a w TypeScript -- Zod. Ten artykuł skupia się na Pythonie i pokaże Ci trzy podejścia: natywne SDK providerów, bibliotekę Instructor i framework Pydantic AI.

Natywne Structured Output w Claude API

Jak to działa pod maską

Claude API oferuje natywne Structured Output od listopada 2025 roku. Mechanizm działa tak: gdy wysyłasz zapytanie z parametrem output_format zawierającym schemat JSON, Anthropic kompiluje ten schemat do gramatyki formalnej (context-free grammar) i używa jej do ograniczenia generowania tokenów (constrained decoding). Model może generować tylko tokeny, które prowadzą do poprawnego JSON-a zgodnego z Twoim schematem. To nie jest walidacja post-hoc -- to gwarancja na poziomie samego generowania. Zasadnicza różnica.

Żeby korzystać z tej funkcji, potrzebujesz nagłówka beta: anthropic-beta: structured-outputs-2025-11-13. SDK w Pythonie dodaje go automatycznie, gdy użyjesz odpowiednich parametrów.

Obsługiwane modele: Claude Sonnet 4.5, Claude Opus 4.5, Claude Sonnet 4.6, Claude Opus 4.6 oraz Claude Haiku 4.5.

Ograniczenia, o których musisz wiedzieć:

  • Rekurencyjne schematy nie są obsługiwane (np. drzewo, gdzie węzeł zawiera listę węzłów tego samego typu)
  • additionalProperties musi być ustawione na false -- model nie może dodawać pól spoza schematu
  • Numeryczne ograniczenia w schemacie (np. minimum, maximum) nie są wymuszane przez gramatykę -- musisz walidować je po stronie Pydantic

Przykład: ekstrakcja danych z recenzji produktu

Zobaczmy pełny, działający przykład. Definiujemy model Pydantic reprezentujący recenzję produktu, konwertujemy go do schematu JSON i wysyłamy zapytanie do Claude API z parametrem output_format. Całość jest zaskakująco zwięzła.

import json
import anthropic
from pydantic import BaseModel, Field
from typing import Literal

# Definicja modelu danych recenzji produktu
class ProductReview(BaseModel):
    product_name: str = Field(description="Nazwa recenzowanego produktu")
    rating: int = Field(description="Ocena produktu w skali 1-5", ge=1, le=5)
    pros: list[str] = Field(description="Lista zalet produktu")
    cons: list[str] = Field(description="Lista wad produktu")
    summary: str = Field(description="Krotkie podsumowanie recenzji w 1-2 zdaniach")
    sentiment: Literal["pozytywny", "neutralny", "negatywny"] = Field(
        description="Ogolny wydzwiek recenzji"
    )

# Konwersja modelu Pydantic do schematu JSON
schema = ProductReview.model_json_schema()

# Inicjalizacja klienta Anthropic
client = anthropic.Anthropic()

# Tekst recenzji do analizy
review_text = """
Kupilem sluchawki SoundMax Pro 3 miesiac temu i mam mieszane uczucia.
Jakosc dzwieku jest fenomenalna -- basy sa glebkie, tony wysokie czyste.
ANC dziala swietnie w metrze. Bateria trzyma obiecane 30 godzin.
Niestety, po godzinie noszenia zaczynaja uciskac i bola uszy.
Etui ladujace jest ogromne i nie miesci sie w kieszeni.
Za 899 zl oczekiwalbym wiekszego komfortu.
"""

# Wyslanie zapytania z natywnym Structured Output
response = client.messages.create(
    model="claude-sonnet-4-5-20250514",
    max_tokens=1024,
    messages=[
        {
            "role": "user",
            "content": f"Przeanalizuj ponizsa recenzje produktu i wyodrebnij "
                       f"strukturalne dane:\n\n{review_text}"
        }
    ],
    # Parametr wymuszajacy strukturalne wyjscie
    extra_headers={"anthropic-beta": "structured-outputs-2025-11-13"},
    # Schemat JSON definiujacy format odpowiedzi
    output_format={
        "type": "json_schema",
        "json_schema": {
            "name": "product_review",
            "schema": schema,
            "strict": True
        }
    }
)

# Parsowanie odpowiedzi do obiektu Pydantic
raw_json = response.content[0].text
review = ProductReview.model_validate_json(raw_json)

# Wyswietlenie wynikow
print(f"Produkt: {review.product_name}")
print(f"Ocena: {review.rating}/5")
print(f"Sentyment: {review.sentiment}")
print(f"Zalety: {', '.join(review.pros)}")
print(f"Wady: {', '.join(review.cons)}")
print(f"Podsumowanie: {review.summary}")

Zwróć uwagę na kluczowy element: parametr output_format z typem json_schema i flagą strict: True. Dzięki temu odpowiedź zawsze będzie poprawnym JSON-em zgodnym z Twoim schematem. Nie ma potrzeby obsługi błędów parsowania -- one po prostu nie występują.

Structured Output w OpenAI API

response_format z JSON Schema

OpenAI oferuje analogiczny mechanizm przez parametr response_format. Wewnętrznie działa tak samo -- schemat jest kompilowany do gramatyki, a generowanie tokenów jest ograniczone (constrained decoding). OpenAI, podobnie jak Anthropic, gwarantuje 100% zgodności ze schematem.

Użyjmy tego samego modelu ProductReview, żeby łatwo porównać oba podejścia:

from openai import OpenAI
from pydantic import BaseModel, Field
from typing import Literal

# Ten sam model Pydantic co wyzej
class ProductReview(BaseModel):
    product_name: str = Field(description="Nazwa recenzowanego produktu")
    rating: int = Field(description="Ocena produktu w skali 1-5", ge=1, le=5)
    pros: list[str] = Field(description="Lista zalet produktu")
    cons: list[str] = Field(description="Lista wad produktu")
    summary: str = Field(description="Krotkie podsumowanie recenzji w 1-2 zdaniach")
    sentiment: Literal["pozytywny", "neutralny", "negatywny"] = Field(
        description="Ogolny wydzwiek recenzji"
    )

client = OpenAI()

review_text = """
Kupilem sluchawki SoundMax Pro 3 miesiac temu i mam mieszane uczucia.
Jakosc dzwieku jest fenomenalna -- basy sa glebkie, tony wysokie czyste.
ANC dziala swietnie w metrze. Bateria trzyma obiecane 30 godzin.
Niestety, po godzinie noszenia zaczynaja uciskac i bola uszy.
Etui ladujace jest ogromne i nie miesci sie w kieszeni.
Za 899 zl oczekiwalbym wiekszego komfortu.
"""

# Metoda parse() automatycznie obsluguje Pydantic
completion = client.beta.chat.completions.parse(
    model="gpt-4o",
    messages=[
        {
            "role": "system",
            "content": "Jestes ekspertem od analizy recenzji produktow."
        },
        {
            "role": "user",
            "content": f"Przeanalizuj recenzje i wyodrebnij dane:\n\n{review_text}"
        }
    ],
    # Bezposrednie przekazanie modelu Pydantic
    response_format=ProductReview,
)

# Obiekt Pydantic jest juz zparsowany i zwalidowany
review = completion.choices[0].message.parsed

print(f"Produkt: {review.product_name}")
print(f"Ocena: {review.rating}/5")
print(f"Sentyment: {review.sentiment}")

Zauważ, jak elegancko OpenAI SDK integruje się z Pydantic -- metoda client.beta.chat.completions.parse() przyjmuje klasę Pydantic bezpośrednio jako response_format i zwraca już zparsowany obiekt w message.parsed. Nie musisz ręcznie konwertować schematu ani parsować JSON-a. Miłe, prawda?

Biblioteka Instructor -- jedno API dla 15+ providerów

Instalacja i konfiguracja

Co jeśli używasz Claude dziś, ale jutro chcesz przetestować GPT-4o albo Gemini? Albo jeśli potrzebujesz automatycznych retries, kiedy walidacja Pydantic się nie powiedzie? Tutaj wchodzi Instructor -- biblioteka, która dodaje warstwę abstrakcji nad API różnych providerów i ujednolica sposób uzyskiwania strukturalnych odpowiedzi. Szczerze mówiąc, to jeden z tych narzędzi, które po pierwszym użyciu trudno sobie wyobrazić bez nich.

# Instalacja Instructor (aktualna wersja: 1.14.5, styczen 2026)
pip install instructor

Kluczowa metoda to from_provider(), która automatycznie wykrywa klienta providera i konfiguruje odpowiedni adapter:

import instructor
import anthropic
from openai import OpenAI

# Klient dla Anthropic
anthropic_client = instructor.from_provider(anthropic.Anthropic())

# Klient dla OpenAI
openai_client = instructor.from_provider(OpenAI())

# Ten sam interfejs dla obu providerow!

Przykład z Claude (Anthropic)

Zobaczmy pełny przykład z Instructor i Claude, włącznie z automatycznymi retries i walidatorami Pydantic:

import instructor
import anthropic
from pydantic import BaseModel, Field, field_validator
from typing import Literal

# Model z niestandardowym walidatorem
class ProductReview(BaseModel):
    product_name: str = Field(description="Nazwa recenzowanego produktu")
    rating: int = Field(description="Ocena produktu w skali 1-5")
    pros: list[str] = Field(description="Lista zalet produktu", min_length=1)
    cons: list[str] = Field(description="Lista wad produktu")
    summary: str = Field(description="Podsumowanie w 1-2 zdaniach")
    sentiment: Literal["pozytywny", "neutralny", "negatywny"]

    # Walidator Pydantic -- jesli model zwroci ocene poza zakresem,
    # Instructor automatycznie ponowi zapytanie z informacja o bledzie
    @field_validator("rating")
    @classmethod
    def validate_rating(cls, v: int) -> int:
        if not 1 <= v <= 5:
            raise ValueError(f"Ocena musi byc miedzy 1 a 5, otrzymano: {v}")
        return v

    @field_validator("summary")
    @classmethod
    def validate_summary(cls, v: str) -> str:
        if len(v) > 200:
            raise ValueError("Podsumowanie nie moze przekraczac 200 znakow")
        return v

# Inicjalizacja klienta Instructor z Anthropic
client = instructor.from_provider(anthropic.Anthropic())

review_text = """
Kupilem sluchawki SoundMax Pro 3 miesiac temu i mam mieszane uczucia.
Jakosc dzwieku jest fenomenalna. ANC dziala swietnie.
Niestety, po godzinie noszenia uciskaja i bola uszy.
"""

# Wyslanie zapytania -- Instructor automatycznie:
# 1. Konwertuje model Pydantic do schematu
# 2. Parsuje odpowiedz do obiektu Pydantic
# 3. Jesli walidacja sie nie powiedzie, ponawia zapytanie (max 3 razy)
review = client.chat.completions.create(
    model="claude-sonnet-4-5-20250514",
    max_tokens=1024,
    max_retries=3,  # Automatyczne ponowienie przy bledzie walidacji
    messages=[
        {
            "role": "user",
            "content": f"Przeanalizuj recenzje:\n\n{review_text}"
        }
    ],
    response_model=ProductReview,  # Model Pydantic jako docelowy typ
)

# Obiekt jest juz zwalidowany -- mozesz go bezpiecznie uzywac
print(f"Produkt: {review.product_name}")
print(f"Ocena: {review.rating}/5")
print(f"Sentyment: {review.sentiment}")

Najważniejsza różnica w porównaniu z natywnym SDK to parametr max_retries=3. Gdy walidator Pydantic odrzuci odpowiedź (np. ocena poza zakresem 1-5), Instructor automatycznie ponawia zapytanie, dołączając do promptu informację o błędzie walidacji. Model dostaje drugą szansę -- i zazwyczaj poprawia się już za pierwszym razem.

Przykład z OpenAI

Przejście na innego providera to zmiana dosłownie dwóch linijek:

from openai import OpenAI
import instructor

# Jedyna roznica: inny klient bazowy i nazwa modelu
client = instructor.from_provider(OpenAI())

review = client.chat.completions.create(
    model="gpt-4o",
    max_retries=3,
    messages=[
        {
            "role": "user",
            "content": f"Przeanalizuj recenzje:\n\n{review_text}"
        }
    ],
    response_model=ProductReview,  # Ten sam model Pydantic!
)

print(f"Produkt: {review.product_name}")

Ten sam ProductReview, ten sam interfejs, ten sam kod walidacji. Zmieniłeś dostawcę modelu bez dotykania logiki biznesowej. To właśnie jest wartość dobrej abstrakcji.

Walidacja semantyczna z Instructor

Instructor oferuje też coś naprawdę wyjątkowego: walidację semantyczną za pomocą LLM. Standardowe walidatory Pydantic sprawdzają format i zakresy, ale nie rozumieją znaczenia danych. llm_validator używa osobnego wywołania LLM, żeby ocenić jakość treści:

from instructor import llm_validator
from pydantic import BaseModel, field_validator

class ArticleSummary(BaseModel):
    title: str
    summary: str
    key_points: list[str]

    # Walidator semantyczny -- LLM sprawdza, czy podsumowanie
    # jest rzeczywiscie zwiazane z trescia artykulu
    @field_validator("summary")
    @classmethod
    def validate_summary_quality(cls, v: str) -> str:
        # llm_validator wywoluje LLM, zeby ocenic jakosc tresci
        validator = llm_validator(
            statement="Podsumowanie powinno byc zwiezle, "
                      "obiektywne i zawierac kluczowe informacje. "
                      "Nie powinno zawierac opinii autora.",
            allow_override=False,
        )
        return validator(v)

To potężne narzędzie -- ale pamiętaj o kosztach. Każda walidacja semantyczna to dodatkowe wywołanie API. Stosuj ją selektywnie, tam gdzie sama kontrola formatu po prostu nie wystarcza.

Pydantic AI -- framework agentowy z wbudowanym Structured Output

Czym jest Pydantic AI

Pydantic AI (aktualna wersja: 1.67.0, marzec 2026) to framework agentowy stworzony przez zespół Pydantic. Jeśli znasz FastAPI, poczujesz się jak w domu -- twórcy opisują go jako "FastAPI feeling for GenAI". Framework jest model-agnostic (obsługuje 15+ providerów), ma wbudowaną obserwowalność przez Logfire i natywne wsparcie dla typowanego wyjścia.

Kluczowa różnica między Pydantic AI a Instructorem: Pydantic AI to pełny framework agentowy. Nie tylko wyciąga strukturalne dane -- obsługuje narzędzia (tools), zależności (dependencies), grafy agentów i streamingowe strukturalne odpowiedzi. Jeśli budujesz agenta, który ma podejmować decyzje i używać narzędzi, Pydantic AI jest naturalnym wyborem.

Agent z typowanym wyjściem

Zobaczmy, jak stworzyć agenta z typowanym wyjściem. Pydantic AI wewnętrznie używa tool calling, żeby uzyskać strukturalne dane -- definiujesz output_type, a framework zajmuje się resztą:

import asyncio
from pydantic import BaseModel, Field
from typing import Literal
from pydantic_ai import Agent

# Model wyjsciowy agenta
class ProductReview(BaseModel):
    product_name: str = Field(description="Nazwa produktu")
    rating: int = Field(description="Ocena 1-5", ge=1, le=5)
    pros: list[str] = Field(description="Zalety")
    cons: list[str] = Field(description="Wady")
    summary: str = Field(description="Podsumowanie")
    sentiment: Literal["pozytywny", "neutralny", "negatywny"]

# Tworzenie agenta z typowanym wyjsciem
agent = Agent(
    model="anthropic:claude-sonnet-4-5-20250514",
    output_type=ProductReview,  # Agent zawsze zwroci ten typ
    system_prompt=(
        "Jestes ekspertem od analizy recenzji produktow. "
        "Analizujesz recenzje i wyodrebniasz strukturalne dane."
    ),
)

# Uruchomienie agenta
async def analyze_review():
    review_text = """
    Kupilem sluchawki SoundMax Pro 3 miesiac temu.
    Jakosc dzwieku jest fenomenalna. ANC swietne.
    Niestety, uciskaja po godzinie. Etui za duze.
    """
    result = await agent.run(
        f"Przeanalizuj te recenzje:\n\n{review_text}"
    )
    # result.output jest typowanym obiektem ProductReview
    review = result.output
    print(f"Produkt: {review.product_name}")
    print(f"Ocena: {review.rating}/5")
    print(f"Sentyment: {review.sentiment}")
    print(f"Koszt (tokeny): {result.usage()}")

asyncio.run(analyze_review())

Pydantic AI obsługuje też streamowane strukturalne wyjście, co jest przydatne w aplikacjach webowych, gdzie chcesz pokazywać wyniki użytkownikowi w czasie rzeczywistym:

import asyncio
from pydantic_ai import Agent

# Streamowane strukturalne wyjscie
async def stream_review():
    async with agent.run_stream(
        "Przeanalizuj recenzje: Swietny telefon, bateria 2 dni, aparat slaby."
    ) as stream:
        # Mozesz obserwowac czesciowe wyniki w trakcie generowania
        async for partial in stream.stream_output():
            print(f"Czesciowy wynik: {partial}")

        # Pelny wynik po zakonczeniu streamu
        result = await stream.get_output()
        print(f"Finalny wynik: {result}")

asyncio.run(stream_review())

Porównanie podejść -- kiedy użyć czego?

Każde z trzech podejść ma swoje miejsce w ekosystemie. Oto szczegółowe porównanie:

Cecha Natywne SDK Instructor Pydantic AI
Wsparcie wielu providerów Nie -- jeden provider na SDK Tak -- 15+ providerów Tak -- 15+ providerów
Automatyczne retries Nie -- musisz zaimplementować Tak -- wbudowane z eskalacją Tak -- konfiguracja w agencie
Wsparcie agentów Nie Nie -- tylko ekstrakcja Tak -- pełny framework
Streaming Tak (surowy JSON) Tak (częściowe obiekty) Tak (częściowe obiekty)
Obserwowalność Brak wbudowanej Podstawowa (logi retries) Pełna (Logfire)
Złożoność wdrożenia Niska Niska Średnia
Najlepszy przypadek użycia Prosta ekstrakcja, jeden provider Ekstrakcja danych, wiele providerów Agenci, złożone workflow

Krótkie podsumowanie: jeśli masz prosty pipeline ekstrakcji z jednym providerem -- użyj natywnego SDK. Jeśli potrzebujesz elastyczności wielu providerów i solidnych retries -- sięgnij po Instructor. A jeśli budujesz agentowy system z narzędziami, zależnościami i obserwowalnością -- Pydantic AI jest Twoim frameworkiem. Warto też dodać, że te podejścia nie są wzajemnie wykluczające: wiele zespołów używa Instructora do prostych zadań ekstrakcji i Pydantic AI do złożonych workflow agentowych w ramach tego samego projektu. To całkowicie sensowna kombinacja.

Wzorce produkcyjne i najlepsze praktyki

Retry z eskalacją promptu

Automatyczne retries w Instructor to dopiero początek. W produkcyjnych systemach warto zastosować eskalację promptu -- jeśli pierwsza próba się nie powiodła, kolejna dostaje bardziej szczegółowe instrukcje. Prościej, niż brzmi:

import instructor
import anthropic
from pydantic import BaseModel, Field
from tenacity import retry, stop_after_attempt, wait_exponential

class ExtractedData(BaseModel):
    entity: str = Field(description="Nazwa glownej encji")
    category: str = Field(description="Kategoria encji")
    confidence: float = Field(description="Pewnosc ekstrakcji 0.0-1.0", ge=0.0, le=1.0)

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

# Prompty o rosnacym poziomie szczegolowosci
PROMPTS = [
    # Poziom 1: zwiezly prompt
    "Wyodrebnij glowna encje z tekstu: {text}",
    # Poziom 2: szczegolowszy prompt po pierwszym niepowodzeniu
    (
        "Wyodrebnij glowna encje z tekstu ponizej. "
        "Encja to nazwa wlasna (firma, osoba, produkt). "
        "Kategoria to jedna z: osoba, firma, produkt, miejsce. "
        "Pewnosc od 0.0 do 1.0.\n\nTekst: {text}"
    ),
    # Poziom 3: maksymalnie szczegolowy prompt
    (
        "WAZNE: Musisz zwrocic dokladnie jeden obiekt JSON.\n"
        "Pole 'entity': nazwa wlasna znaleziona w tekscie (string).\n"
        "Pole 'category': dokladnie jedna z wartosci: osoba, firma, produkt, miejsce.\n"
        "Pole 'confidence': liczba zmiennoprzecinkowa od 0.0 do 1.0.\n\n"
        "Tekst do analizy: {text}"
    ),
]

def extract_with_escalation(text: str) -> ExtractedData:
    """Probuj ekstrakcji z coraz bardziej szczegolowym promptem."""
    last_error = None
    for level, prompt_template in enumerate(PROMPTS):
        try:
            result = client.chat.completions.create(
                model="claude-sonnet-4-5-20250514",
                max_tokens=512,
                max_retries=2,  # 2 retries na kazdym poziomie
                messages=[
                    {"role": "user", "content": prompt_template.format(text=text)}
                ],
                response_model=ExtractedData,
            )
            return result
        except Exception as e:
            last_error = e
            print(f"Poziom {level + 1} nie powiodl sie: {e}")
            continue

    raise RuntimeError(f"Ekstrakcja nie powiodla sie na zadnym poziomie: {last_error}")

Ten wzorzec daje Ci 6 prób na uzyskanie poprawnego wyniku (3 poziomy × 2 retries każdy) z coraz precyzyjniejszymi instrukcjami. W praktyce ponad 99.9% zapytań kończy się sukcesem już na poziomie 1 lub 2.

Zagnieżdżone modele Pydantic

Rzeczywiste dane rzadko są płaskie. Oto jak obsłużyć zagnieżdżone struktury -- na przykładzie zamówienia z listą produktów:

from pydantic import BaseModel, Field
from typing import Literal
import instructor
import anthropic

# Model zagniezdony: pozycja zamowienia
class OrderItem(BaseModel):
    product_name: str = Field(description="Nazwa produktu")
    quantity: int = Field(description="Ilosc sztuk", ge=1)
    unit_price: float = Field(description="Cena jednostkowa w PLN", ge=0)
    total_price: float = Field(description="Cena calkowita (ilosc * cena)")

# Model nadrzedny: zamowienie
class Order(BaseModel):
    order_id: str = Field(description="Numer zamowienia")
    customer_name: str = Field(description="Imie i nazwisko klienta")
    items: list[OrderItem] = Field(description="Lista pozycji zamowienia", min_length=1)
    total_amount: float = Field(description="Laczna kwota zamowienia w PLN")
    payment_method: Literal["karta", "przelew", "blik", "gotowka"]
    notes: str | None = Field(default=None, description="Dodatkowe uwagi")

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

# Ekstrakcja zamowienia z tekstu w jezyk naturalnym
email_text = """
Czesc, chcialbym zamowic:
- 3 sztuki klawiatury MechPro X1 po 459 zl kazda
- 1 monitor UltraWide 34" za 2899 zl
- 2 podkladki pod mysz GlidePad za 89 zl sztuka

Platnosc BLIK-iem. Prosze o fakture na Jan Kowalski.
Numer zamowienia: ZAM-2026-001.
"""

order = client.chat.completions.create(
    model="claude-sonnet-4-5-20250514",
    max_tokens=1024,
    messages=[
        {"role": "user", "content": f"Wyodrebnij dane zamowienia:\n\n{email_text}"}
    ],
    response_model=Order,
)

# Wyswietlenie zagniezdzonej struktury
print(f"Zamowienie: {order.order_id}")
print(f"Klient: {order.customer_name}")
print(f"Metoda platnosci: {order.payment_method}")
for item in order.items:
    print(f"  - {item.product_name}: {item.quantity}x {item.unit_price} PLN "
          f"= {item.total_price} PLN")
print(f"RAZEM: {order.total_amount} PLN")

Zwróć uwagę, że Order zawiera listę OrderItem -- Pydantic i wszystkie trzy podejścia (natywne SDK, Instructor, Pydantic AI) radzą sobie z takimi zagnieżdżonymi strukturami bez żadnych problemów. Możesz zagnieżdżać modele na wiele poziomów, dopóki nie tworzysz rekurencyjnych schematów (co dotyczy natywnego Structured Output Claude API).

Cache'owanie schematów w Claude API

Claude API automatycznie cache'uje skompilowane schematy przez 24 godziny. Oznacza to, że pierwsze zapytanie z danym schematem może być nieco wolniejsze (kompilacja gramatyki), ale kolejne są już szybsze. Ten cache działa na poziomie konta i schematu -- jeśli dwóch użytkowników Twojej aplikacji używa tego samego schematu, obaj korzystają z cache'a.

Możesz to połączyć z prompt caching, który cache'uje dłuższe prompty systemowe. Kombinacja obu technik może przynieść nawet 90% redukcji kosztów w scenariuszach, gdzie wielokrotnie przetwarzasz dokumenty tym samym schematem i promptem. Jeśli interesujesz się optymalizacją kosztów API, to naturalne uzupełnienie artykułu o Tool Use w Claude API, który znajdziesz na naszym blogu.

Najczęściej zadawane pytania (FAQ)

Czy Structured Output działa z modelami open source (Ollama, vLLM)?

Tak. Nie masz natywnego constrained decoding jak w Claude czy GPT-4o, ale Instructor obsługuje Ollama, a Pydantic AI dodał natywne wsparcie dla Structured Output z Ollama w wersji 1.67.0. W przypadku vLLM możesz użyć outlines -- biblioteki do constrained decoding dla modeli Hugging Face, która oferuje 100% gwarancję schematu na poziomie lokalnym.

Jaka jest różnica między JSON mode a Structured Output?

JSON mode (dostępny np. w OpenAI jako response_format: {"type": "json_object"}) gwarantuje, że odpowiedź będzie poprawnym JSON-em, ale nie gwarantuje zgodności z Twoim schematem. Model może zwrócić {"foo": "bar"} zamiast oczekiwanego {"product_name": "...", "rating": 5}. Structured Output gwarantuje zarówno poprawność JSON-a, jak i zgodność ze schematem. W produkcji zawsze wybieraj Structured Output.

Czy mogę łączyć Structured Output z Tool Use?

Tak. W Claude API możesz w jednym zapytaniu używać narzędzi (Tool Use) i jednocześnie wymuszać strukturalny format odpowiedzi JSON. To przydatne w scenariuszach, gdzie agent najpierw wywołuje narzędzia (np. wyszukiwanie, baza danych), a następnie zwraca wynik w ściśle określonym formacie. Więcej o Tool Use znajdziesz w naszym dedykowanym artykule na blogu.

Jak obsłużyć błędy walidacji w produkcji?

Rekomendowane podejście to wielopoziomowa strategia: (1) użyj Instructor z max_retries=3, który automatycznie ponawia zapytanie z informacją o błędzie, (2) zaimplementuj eskalację promptu (wzorzec opisany wyżej), (3) po wyczerpaniu prób zaloguj błąd z pełnym kontekstem i przekieruj do kolejki ręcznej inspekcji. W praktyce z natywnym Structured Output (Claude, OpenAI) błędy walidacji formatu nie występują -- constrained decoding gwarantuje zgodność ze schematem. Błędy mogą wystąpić tylko na poziomie walidatorów Pydantic (np. ocena poza zakresem), gdzie Instructor automatycznie je obsługuje.

Ile kosztuje Structured Output w porównaniu do zwykłego API call?

Koszty tokenów są identyczne -- płacisz za tokeny wejściowe i wyjściowe według standardowego cennika modelu. Jedyny narzut to minimalne opóźnienie na przetwarzanie schematu (kompilacja gramatyki), które wynosi kilka-kilkanaście milisekund przy pierwszym użyciu i jest eliminowane przez cache'owanie przy kolejnych zapytaniach. Schemat JSON dodaje też niewielką liczbę tokenów wejściowych -- ale to zazwyczaj pomijalna część całkowitego kosztu.

O Autorze Editorial Team

Our team of expert writers and editors.