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:
- 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.
- 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.
- 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)
additionalPropertiesmusi być ustawione nafalse-- 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.