Strukturované výstupy z LLM v Pythonu: Průvodce Instructor, Pydantic a nativními API

Jak z jazykových modelů získat spolehlivá, typově bezpečná strukturovaná data? Praktický průvodce knihovnou Instructor, Pydantic validací a nativními API od OpenAI a Anthropic — s reálnými příklady v Pythonu.

Proč vůbec řešit strukturované výstupy z LLM

Jazykové modely generují text. To je jejich superSchopnost — a zároveň největší problém, jakmile s výstupem potřebujete pracovat v kódu. Představte si to: napíšete prompt „Extrahuj jméno, věk a e-mail z tohoto textu" a model vám vrátí něco takového:

Jméno: Jan Novák
Věk: 32
E-mail: [email protected]

Na první pohled to vypadá skvěle. Ale co s tím teď reálně uděláte? Budete parsovat řetězce regexem? Budete doufat, že model pokaždé vrátí přesně stejný formát?

Spoiler: nevrátí. Někdy si přidá úvodní větu typu „Zde jsou extrahovaná data:", jindy změní pořadí polí, občas nějaké pole úplně vynechá. A vy skončíte s křehkým parserem, který se vám rozpadne každý druhý dotaz. Tím si prošel asi každý, kdo zkoušel LLM výstupy zpracovávat programově.

Přesně tady přicházejí na scénu strukturované výstupy — mechanismus, díky kterému LLM vrátí odpověď v přesně definovaném formátu (typicky JSON), validovanou vůči schématu. Takovou odpověď můžete bezpečně deserializovat na typovaný objekt a pracovat s ní jako s normálními daty.

V roce 2026 jsou strukturované výstupy v podstatě základ každé produkční LLM aplikace. Ať už stavíte RAG pipeline, AI agenta s nástroji, nebo jednoduchý extraktor dat — bez spolehlivých strukturovaných výstupů se prostě neobejdete.

Tři přístupy ke strukturovaným výstupům

Než se vrhneme do kódu, pojďme si udělat rychlý přehled. V zásadě existují tři cesty, jak z LLM dostat strukturovaná data:

1. Prompt-based přístup (nespolehlivý)

Nejjednodušší varianta — do promptu napíšete „Odpověz ve formátu JSON" a doufáte v nejlepší. Překvapivě to funguje docela často, ale v produkci je to časovaná bomba. Model totiž může:

  • Obalit JSON do markdownového bloku ```json ... ```
  • Přidat vysvětlující text před nebo za JSON
  • Vynechat povinná pole
  • Použít špatné datové typy (string místo čísla)
  • Vrátit nevalidní JSON (chybějící čárka, nekončená závorka)

Na prototyp OK, do produkce rozhodně ne.

2. Nativní API poskytovatele (spolehlivý, ale vendor-locked)

OpenAI, Anthropic i Google dnes nabízejí nativní podporu strukturovaných výstupů přímo ve svých API. OpenAI má response_format: json_schema, Anthropic přidal strukturované výstupy s strict: true v únoru 2026. Tyhle přístupy jsou spolehlivé — model je na úrovni dekódování omezený produkovat pouze validní JSON odpovídající schématu. Nevýhoda? Každý poskytovatel má jiné API, jiný formát, jiná omezení. Takže pokud byste chtěli přejít jinam, přepisujete kód.

3. Knihovna Instructor (spolehlivý + univerzální)

A tady přichází hlavní hrdina tohohle článku. Instructor je Python knihovna s více než 3 miliony stažení měsíčně, která vám umožní definovat požadovaný výstup jako Pydantic model a automaticky se postará o validaci, retry logiku a kompatibilitu s 18+ poskytovateli LLM. Jeden kód, všichni poskytovatelé. Zní to moc dobře? Čtěte dál.

Pydantic — základ všeho

Než začneme s Instructor, musíme si říct pár slov o Pydantic. Pokud s Pythonem děláte profesionálně, pravděpodobně ho už znáte — je to nejpoužívanější knihovna pro validaci dat v Pythonu s více než 300 miliony stažení měsíčně. Stojí na něm FastAPI, LangChain, a teď vlastně i většina LLM nástrojů.

Pydantic vám umožňuje definovat datové modely pomocí typových anotací a automaticky validuje příchozí data. Tady je jednoduchý příklad:

from pydantic import BaseModel, Field
from typing import List, Optional

class Kontakt(BaseModel):
    """Kontaktní údaje extrahované z textu."""
    jmeno: str = Field(description="Celé jméno osoby")
    vek: int = Field(ge=0, le=150, description="Věk v letech")
    email: Optional[str] = Field(None, description="E-mailová adresa")
    telefon: Optional[str] = Field(None, description="Telefonní číslo")
    dovednosti: List[str] = Field(
        default_factory=list,
        description="Seznam odborných dovedností"
    )

# Pydantic automaticky validuje data
kontakt = Kontakt(
    jmeno="Jan Novák",
    vek=32,
    email="[email protected]",
    dovednosti=["Python", "Machine Learning"]
)
print(kontakt.model_json_schema())
# Vrátí JSON Schema, které lze předat LLM

A teď to klíčové pro práci s LLM: metoda model_json_schema() automaticky vygeneruje JSON Schema z vašeho modelu. Toto schéma se pak předá LLM jako definice požadovaného výstupu. A metoda model_validate_json() parsuje JSON řetězec zpět na typovaný objekt s plnou validací. Elegantní, ne?

Instructor — praktický průvodce

Instalace a základní nastavení

Instructor podporuje instalaci specifickou pro vašeho poskytovatele, což je fajn — nemusíte tahat závislosti, které nepotřebujete:

# Základní instalace (obsahuje podporu OpenAI)
pip install instructor

# Pro Anthropic Claude
pip install "instructor[anthropic]"

# Pro Google Gemini
pip install "instructor[google-genai]"

# Pro lokální modely přes Ollama
pip install "instructor[ollama]"

# Pro přístup k více poskytovatelům přes LiteLLM
pip install "instructor[litellm]"

První strukturovaný výstup

Pojďme rovnou na nejjednodušší možný příklad — extrakce strukturovaných dat z běžného textu:

import instructor
from pydantic import BaseModel, Field

class Uzivatel(BaseModel):
    jmeno: str = Field(description="Celé jméno")
    vek: int = Field(description="Věk v letech")

# Vytvoření klienta s from_provider
client = instructor.from_provider("openai/gpt-4o-mini")

uzivatel = client.chat.completions.create(
    response_model=Uzivatel,
    messages=[
        {
            "role": "user",
            "content": "Jan Novák je zkušený vývojář, je mu 32 let."
        }
    ],
)

print(uzivatel)
# Uzivatel(jmeno='Jan Novák', vek=32)
print(type(uzivatel))
# <class '__main__.Uzivatel'>

Všimněte si, co se tu děje: žádné parsování JSON, žádné regulární výrazy, žádné try-except bloky. Prostě definujete Pydantic model, předáte ho jako response_model a dostanete zpět typovaný Python objekt. Instructor se pod kapotou postará o všechno — konverzi modelu na JSON Schema, předání schématu, parsování odpovědi a validaci. Poprvé když jsem to viděl v akci, říkal jsem si, proč jsem předtím trávil hodiny psaním custom parserů.

Funkce from_provider — jeden kód pro všechny poskytovatele

Funkce from_provider() je asi ta nejsilnější vlastnost Instructor. Stačí změnit řetězec s identifikátorem poskytovatele a modelu a zbytek kódu zůstane beze změny:

import instructor
from pydantic import BaseModel

class Sentiment(BaseModel):
    text: str
    sentiment: str  # "pozitivní", "negativní", "neutrální"
    confidence: float

# OpenAI
client_openai = instructor.from_provider("openai/gpt-4o")

# Anthropic Claude
client_claude = instructor.from_provider("anthropic/claude-sonnet-4-6")

# Google Gemini
client_gemini = instructor.from_provider("google/gemini-2.5-pro")

# Lokální model přes Ollama
client_local = instructor.from_provider("ollama/llama3.2")

# Groq (ultra rychlé inference)
client_groq = instructor.from_provider("groq/llama-3.3-70b-versatile")

# Všichni klienti se volají stejně:
result = client_openai.chat.completions.create(
    response_model=Sentiment,
    messages=[{"role": "user", "content": "Tento produkt je naprosto skvělý!"}],
)
print(result)
# Sentiment(text='Tento produkt je naprosto skvělý!',
#           sentiment='pozitivní', confidence=0.95)

Tahle univerzálnost je prostě k nezaplacení. Váš kód pro extrakci dat zůstane stejný, bez ohledu na to, jestli zítra přejdete z OpenAI na Claude nebo si rozjedete lokální model. A věřte mi, v praxi se to hodí častěji, než byste čekali — třeba když vám poskytovatel zvedne ceny (a ano, to se stává).

Pokročilé vzory s Instructor

Vnořené modely a komplexní schémata

V reálných aplikacích budete potřebovat složitější struktury než jen jméno a věk. Pydantic a Instructor zvládají libovolně vnořené modely bez problémů:

import instructor
from pydantic import BaseModel, Field
from typing import List, Optional
from enum import Enum

class Priorita(str, Enum):
    VYSOKA = "vysoká"
    STREDNI = "střední"
    NIZKA = "nízká"

class Ukol(BaseModel):
    nazev: str = Field(description="Stručný název úkolu")
    popis: str = Field(description="Detailní popis, co je třeba udělat")
    priorita: Priorita
    odhadovane_hodiny: float = Field(
        ge=0.5, le=100,
        description="Odhadovaný čas v hodinách"
    )

class ProjektovyPlan(BaseModel):
    nazev_projektu: str
    cil: str = Field(description="Hlavní cíl projektu v 1-2 větách")
    ukoly: List[Ukol] = Field(
        min_length=3, max_length=10,
        description="Seznam úkolů k dokončení"
    )
    celkovy_odhad_hodin: float
    rizika: List[str] = Field(
        min_length=1,
        description="Potenciální rizika projektu"
    )

client = instructor.from_provider("openai/gpt-4o")

plan = client.chat.completions.create(
    response_model=ProjektovyPlan,
    messages=[
        {
            "role": "system",
            "content": "Jsi zkušený projektový manažer."
        },
        {
            "role": "user",
            "content": """Vytvoř projektový plán pro migraci
            monolitické Python aplikace na mikroslužby.
            Aplikace používá Flask, PostgreSQL a Redis."""
        }
    ],
)

print(f"Projekt: {plan.nazev_projektu}")
print(f"Cíl: {plan.cil}")
for ukol in plan.ukoly:
    print(f"  [{ukol.priorita.value}] {ukol.nazev} "
          f"({ukol.odhadovane_hodiny}h)")
print(f"Celkem: {plan.celkovy_odhad_hodin}h")
print(f"Rizika: {plan.rizika}")

Instructor validuje celou strukturu — včetně enum hodnot, minimální a maximální délky seznamů i rozsahů čísel. A co je fajn: pokud model vrátí nevalidní data, Instructor automaticky pošle chybu zpět modelu a nechá ho odpověď opravit. Žádné ruční ošetřování, prostě to funguje.

Validace s vlastní logikou — Pydantic validátory

Tady se teprve ukazuje skutečná síla celého přístupu. Když zkombinujete strukturované výstupy s vlastními validátory, dostanete něco opravdu mocného:

from pydantic import BaseModel, Field, field_validator
import instructor
import re

class ExtrakceEmailu(BaseModel):
    email: str = Field(description="Extrahovaná e-mailová adresa")
    domena: str = Field(description="Doména e-mailu")
    je_firemni: bool = Field(
        description="True pokud jde o firemní e-mail"
    )

    @field_validator("email")
    @classmethod
    def validuj_email(cls, v: str) -> str:
        pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
        if not re.match(pattern, v):
            raise ValueError(f"Neplatný formát e-mailu: {v}")
        return v.lower()

    @field_validator("domena")
    @classmethod
    def validuj_domenu(cls, v: str, info) -> str:
        if info.data.get("email") and not info.data["email"].endswith(f"@{v}"):
            raise ValueError(
                f"Doména {v} neodpovídá e-mailu {info.data['email']}"
            )
        return v

client = instructor.from_provider("openai/gpt-4o-mini")

vysledek = client.chat.completions.create(
    response_model=ExtrakceEmailu,
    messages=[
        {
            "role": "user",
            "content": "Kontaktujte mě na [email protected]"
        }
    ],
)
print(vysledek)
# ExtrakceEmailu(email='[email protected]',
#                domena='firma.cz', je_firemni=True)

Pokud model vrátí neplatný e-mail nebo doménu, která nesedí, Pydantic validátor vyhodí výjimku a Instructor automaticky pošle chybovou zprávu zpět modelu s žádostí o opravu. Celý retry cyklus je vestavěný a transparentní — vy se o něj nemusíte starat.

Streamování částečných výstupů

U delších odpovědí (a kdo by chtěl koukat na prázdnou obrazovku) chcete uživateli ukazovat výsledky průběžně. Instructor na to má elegantní řešení:

import instructor
from pydantic import BaseModel, Field
from typing import List

class ClanekAnalyza(BaseModel):
    nadpis: str
    shrnutí: str
    klicove_body: List[str]
    sentiment: str

client = instructor.from_provider("openai/gpt-4o")

# create_partial vrací generátor částečných objektů
for castecny in client.chat.completions.create_partial(
    response_model=ClanekAnalyza,
    messages=[
        {
            "role": "user",
            "content": "Analyzuj tento článek: [dlouhý text...]"
        }
    ],
):
    # Každá iterace přinese komplétnější objekt
    print(f"Nadpis: {castecny.nadpis}")
    if castecny.klicove_body:
        print(f"Body: {len(castecny.klicove_body)} nalezeno")
    print("---")

Nativní strukturované výstupy: OpenAI vs. Anthropic

Dobře, pojďme se podívat, jak to vypadá, když chcete strukturované výstupy řešit přímo přes API poskytovatelů — bez Instructor. Ať máte srovnání.

OpenAI — response_format s JSON Schema

OpenAI zavedlo strukturované výstupy v srpnu 2024 a v roce 2026 je to už standardní funkce pro modely GPT-4o a novější. V novém Responses API se to konfiguruje přes text.format:

from openai import OpenAI
from pydantic import BaseModel

client = OpenAI()

class KalendarnUdalost(BaseModel):
    nazev: str
    datum: str
    cas: str
    misto: str

# Nové Responses API (2026)
odpoved = client.responses.create(
    model="gpt-4o",
    input=[
        {
            "role": "user",
            "content": "Schůzka s klientem v pondělí 24. března "
                       "v 10:00 v kanceláři na Václavském náměstí."
        }
    ],
    text={
        "format": {
            "type": "json_schema",
            "name": "kalendar_udalost",
            "strict": True,
            "schema": KalendarnUdalost.model_json_schema()
        }
    }
)

import json
udalost = KalendarnUdalost.model_validate_json(
    odpoved.output_text
)
print(udalost)

S parametrem strict: True OpenAI garantuje 100% shodu se schématem na úrovni dekódování tokenů. Malá vada na kráse: při prvním použití nového schématu je vyšší latence (schéma se musí zkompilovat), ale další volání už jedou rychle.

Anthropic Claude — strukturované výstupy od února 2026

Anthropic přidal nativní podporu strukturovaných výstupů v únoru 2026 a nabízí dva způsoby — přes JSON schéma nebo přes tool_use. Oba fungují spolehlivě:

import anthropic
from pydantic import BaseModel

client = anthropic.Anthropic()

class AnalyzaTextu(BaseModel):
    tema: str
    jazyk: str
    pocet_slov: int
    klicova_slova: list[str]

# Způsob 1: Přes JSON schéma
zprava = client.messages.create(
    model="claude-sonnet-4-6-20250514",
    max_tokens=1024,
    messages=[
        {
            "role": "user",
            "content": "Analyzuj tento text: [text k analýze]"
        }
    ],
    response_format={
        "type": "json_schema",
        "json_schema": {
            "name": "analyza_textu",
            "strict": True,
            "schema": AnalyzaTextu.model_json_schema()
        }
    }
)

# Způsob 2: Přes tool_use se strict: true
zprava = client.messages.create(
    model="claude-sonnet-4-6-20250514",
    max_tokens=1024,
    messages=[
        {
            "role": "user",
            "content": "Analyzuj tento text: [text k analýze]"
        }
    ],
    tools=[
        {
            "name": "analyza",
            "description": "Vrátí analýzu textu",
            "input_schema": AnalyzaTextu.model_json_schema(),
            "strict": True
        }
    ],
    tool_choice={"type": "tool", "name": "analyza"}
)

Instructor vs. nativní API — kdy co použít

Tohle je ta otázka, kterou slyším pořád dokola: „Proč bych měl používat Instructor, když OpenAI i Claude mají nativní podporu?" Férová otázka, tak si to pojďme rozebrat:

KritériumNativní APIInstructor
PřenositelnostVendor lock-in18+ poskytovatelů, jeden kód
ValidacePouze schémaSchéma + Pydantic validátory
Retry logikaMusíte implementovat samiVestavěná, automatická
StreamováníRuční parsovánícreate_partial s typovanými objekty
Složitost kóduVíce boilerplateMinimální — definujte model, zavolejte API
VýkonMinimální overheadZanedbatelný overhead navíc
Async podporaZávisí na SDKPlná podpora async/await

Můj verdikt: Pokud pracujete s jediným poskytovatelem, nikdy neplánujete přecházet a nepotřebujete vlastní validátory — nativní API je naprosto v pohodě. Ve všech ostatních případech (a to je podle mé zkušenosti většina reálných projektů) sáhněte po Instructor. Overhead je zanedbatelný a dostanete výrazně čistší, přenosnější a robustnější kód.

Reálný případ: Extraktor dat z faktur

Dost teorie, pojďme na něco praktického. Tady je kompletní příklad systému pro extrakci strukturovaných dat z textu faktur — tohle je přesně typ úlohy, kde strukturované výstupy excelují:

import instructor
from pydantic import BaseModel, Field, field_validator
from typing import List, Optional
from decimal import Decimal
from datetime import date

class PolozkaFaktury(BaseModel):
    nazev: str = Field(description="Název položky nebo služby")
    mnozstvi: float = Field(ge=0, description="Počet kusů/hodin")
    jednotkova_cena: float = Field(ge=0, description="Cena za kus/hodinu v CZK")
    celkova_cena: float = Field(ge=0, description="Celková cena položky v CZK")

    @field_validator("celkova_cena")
    @classmethod
    def validuj_celkovou_cenu(cls, v, info):
        ocekavana = round(
            info.data.get("mnozstvi", 0) * info.data.get("jednotkova_cena", 0), 2
        )
        if abs(v - ocekavana) > 1:  # tolerance 1 CZK
            raise ValueError(
                f"Celková cena {v} neodpovídá "
                f"{info.data['mnozstvi']} × {info.data['jednotkova_cena']} = {ocekavana}"
            )
        return v

class Faktura(BaseModel):
    cislo_faktury: str = Field(description="Číslo faktury")
    datum_vystaveni: str = Field(description="Datum vystavení ve formátu YYYY-MM-DD")
    datum_splatnosti: str = Field(description="Datum splatnosti ve formátu YYYY-MM-DD")
    dodavatel: str = Field(description="Název dodavatele")
    odberatel: str = Field(description="Název odběratele")
    ico_dodavatele: Optional[str] = Field(None, description="IČO dodavatele")
    polozky: List[PolozkaFaktury] = Field(
        min_length=1, description="Seznam položek faktury"
    )
    castka_bez_dph: float = Field(ge=0, description="Celková částka bez DPH")
    sazba_dph: float = Field(description="Sazba DPH v procentech")
    castka_s_dph: float = Field(ge=0, description="Celková částka s DPH")

client = instructor.from_provider("openai/gpt-4o")

text_faktury = """
Faktura č. FV-2026-00142
Vystavena: 20. 3. 2026
Splatnost: 3. 4. 2026

Dodavatel: WebDev Solutions s.r.o., IČO: 12345678
Odběratel: Novák & Partners a.s.

1. Vývoj webové aplikace - 40 hodin × 1 500 Kč = 60 000 Kč
2. Grafický návrh UI - 12 hodin × 1 200 Kč = 14 400 Kč
3. Testování a QA - 8 hodin × 1 000 Kč = 8 000 Kč

Celkem bez DPH: 82 400 Kč
DPH 21%: 17 304 Kč
Celkem s DPH: 99 704 Kč
"""

faktura = client.chat.completions.create(
    response_model=Faktura,
    messages=[
        {
            "role": "system",
            "content": "Extrahuj strukturovaná data z textu faktury. "
                       "Buď přesný u číselných hodnot."
        },
        {"role": "user", "content": text_faktury}
    ],
)

print(f"Faktura: {faktura.cislo_faktury}")
print(f"Dodavatel: {faktura.dodavatel} (IČO: {faktura.ico_dodavatele})")
print(f"Položek: {len(faktura.polozky)}")
print(f"Celkem s DPH: {faktura.castka_s_dph} Kč")

Na tomhle příkladu je vidět, proč je kombinace Pydantic validátorů s LLM tak silná. Ten validátor validuj_celkovou_cenu kontroluje, jestli množství krát jednotková cena odpovídá celkové ceně. A LLM modely dělají aritmetické chyby častěji, než byste čekali — zvlášť u větších čísel. Díky Instructor se ale chyba automaticky vrátí modelu a ten odpověď opraví. Vy se o to nemusíte starat.

Asynchronní zpracování a dávkové operace

V produkci často potřebujete zpracovat desítky nebo stovky požadavků najednou. Sekvenční zpracování by trvalo věčnost, takže se hodí vědět, že Instructor plně podporuje async/await:

import instructor
import asyncio
from pydantic import BaseModel, Field
from typing import List

class ExtrakceEntit(BaseModel):
    osoby: List[str] = Field(description="Jména osob zmíněných v textu")
    organizace: List[str] = Field(description="Názvy organizací")
    mista: List[str] = Field(description="Zeměpisná místa")

async def extrahuj_entity(client, text: str) -> ExtrakceEntit:
    return await client.chat.completions.create(
        response_model=ExtrakceEntit,
        messages=[{"role": "user", "content": f"Extrahuj entity: {text}"}],
    )

async def main():
    # Async klient
    client = instructor.from_provider(
        "openai/gpt-4o-mini",
        async_client=True
    )

    texty = [
        "Premiér Fiala se sešel s prezidentem Pavlem na Hradě.",
        "Google a Microsoft investují do AI výzkumu v Brně.",
        "Konference PyCon CZ proběhne v Praze v červnu 2026.",
    ]

    # Paralelní zpracování
    vysledky = await asyncio.gather(
        *[extrahuj_entity(client, text) for text in texty]
    )

    for text, vysledek in zip(texty, vysledky):
        print(f"Text: {text[:50]}...")
        print(f"  Osoby: {vysledek.osoby}")
        print(f"  Organizace: {vysledek.organizace}")
        print(f"  Místa: {vysledek.mista}")
        print()

asyncio.run(main())

Díky asyncio.gather zpracujete všechny texty paralelně, což dramaticky zkrátí celkový čas. V praxi mluvíme o rozdílu typu 3 sekundy vs. 30 sekund u většího batche.

Integrace s existujícím ekosystémem

Instructor + RAG pipeline

Strukturované výstupy se skvěle hodí pro zpracování výsledků z RAG pipeline. Místo volného textu, kde se musíte dohadovat co je odkud, dostanete strukturovanou odpověď s citacemi zdrojů:

import instructor
from pydantic import BaseModel, Field
from typing import List

class Citace(BaseModel):
    text: str = Field(description="Citovaný úryvek ze zdrojového dokumentu")
    zdroj: str = Field(description="Název nebo ID zdrojového dokumentu")

class OdpovedSCitacemi(BaseModel):
    odpoved: str = Field(description="Odpověď na otázku uživatele")
    citace: List[Citace] = Field(
        min_length=1,
        description="Seznam citací podpírajících odpověď"
    )
    relevance_skore: float = Field(
        ge=0, le=1,
        description="Jak relevantní jsou nalezené dokumenty (0-1)"
    )

client = instructor.from_provider("openai/gpt-4o")

# Kontext z vektorové databáze (simulováno)
nalezene_dokumenty = """
[Dokument 1: Firemní politika] Dovolená činí 25 dní ročně...
[Dokument 2: HR příručka] Sick days jsou max 3 dny bez potvrzení...
"""

odpoved = client.chat.completions.create(
    response_model=OdpovedSCitacemi,
    messages=[
        {
            "role": "system",
            "content": f"Odpovídej na základě těchto dokumentů:\n"
                       f"{nalezene_dokumenty}"
        },
        {
            "role": "user",
            "content": "Kolik mám dní dovolené?"
        }
    ],
)

print(f"Odpověď: {odpoved.odpoved}")
print(f"Relevance: {odpoved.relevance_skore}")
for c in odpoved.citace:
    print(f"  Citace z '{c.zdroj}': {c.text}")

Nejčastější chyby a jak se jim vyhnout

Po pár měsících práce se strukturovanými výstupy v produkci (a pár bolestivých lekcích) jsem narazil na několik chyb, které se opakují znovu a znovu:

  • Příliš vágní popisy polí. Pole status: str bez Field(description="...") dává modelu příliš volný prostor. Buďte konkrétní — použijte Enum typy nebo alespoň jasně popište, jaké hodnoty očekáváte. Tenhle detail má překvapivě velký vliv na kvalitu výstupů.
  • Příliš hluboké vnořování. Model má viditelné potíže s více než 3-4 úrovněmi vnořených objektů. Radši tu strukturu zploštěte nebo rozdělte extrakci na víc kroků. Méně je tu opravdu více.
  • Ignorování retry logiky. Výchozí počet retries v Instructor je 1. Pro produkci to zvyšte na 2-3 pomocí parametru max_retries. Stojí vás to pár tokenů navíc, ale ušetříte si spoustu headaches.
  • Chybějící Field descriptions. Pydantic field descriptions se posílají modelu jako součást JSON Schema. Bez nich model háda, co pole znamenají, a výsledky jsou výrazně méně přesné. Tohle je asi ta nejčastější chyba, co vídám.
  • Neošetření refusals. Model může odmítnout odpovědět z bezpečnostních důvodů. Vždycky ošetřete případ, kdy odpověď neobsahuje platná data — jinak vám to jednou spadne v produkci v tu nejhorší chvíli.

Alternativy k Instructor

Instructor není samozřejmě jediná volba na trhu. Tady je stručný přehled alternativ, které stojí za zvážení:

  • PydanticAI — Oficiální agent framework od tvůrců Pydantic. Pokud stavíte AI agenta s nástroji, dependency injection a potřebujete vestavěnou observabilitu, tohle je cesta. Instructor je primárně pro extrakci dat, PydanticAI pro komplexnější agentní workflow.
  • Outlines — Knihovna pro constrained decoding u open-source modelů. Zajišťuje 100% validní výstup bez retries, protože omezuje model přímo při generování tokenů. Ideální pokud jedete na lokálních modelech a potřebujete vysoký throughput.
  • LangChain PydanticOutputParser — Pokud už jedete na LangChainu, nabízí integrovaný parser. Je méně robustní než Instructor, ale nepřidáváte další závislost do projektu, což někdy taky něco znamená.

Často kladené otázky (FAQ)

Jaký je rozdíl mezi JSON mode a strukturovanými výstupy?

JSON mode vám jenom garantuje, že model vrátí syntakticky validní JSON — ale vůbec negarantuje, že ten JSON bude odpovídat vašemu schématu. Může chybět pole, mít špatný typ, nebo obsahovat klíče navíc. Strukturované výstupy (Structured Outputs) navíc vynucují dodržení JSON Schema přímo na úrovni dekódování tokenů. Vždycky preferujte strukturované výstupy před pouhým JSON mode — rozdíl v spolehlivosti je obrovský.

Funguje Instructor s lokálními modely přes Ollama?

Ano, bez problémů. Nainstalujte pip install "instructor[ollama]" a použijte instructor.from_provider("ollama/llama3.2"). Ale počítejte s tím, že kvalita závisí na schopnostech konkrétního modelu — větší modely (70B+) dodržují schémata výrazně lépe než menší varianty. S 7B modelem občas narazíte na problémy u složitějších schémat.

Kolik stojí strukturované výstupy navíc oproti běžnému volání API?

Z hlediska ceny tokenů nic navíc — strukturované výstupy využívají stejný cenový model. Trochu narostou vstupní tokeny (JSON Schema se posílá jako součást promptu) a potenciálně se přidají další volání při retries. V praxi je overhead minimální — typicky 5-10 % více tokenů. Za tu spolehlivost to rozhodně stojí.

Mohu kombinovat strukturované výstupy s function calling?

Ano, a je to vlastně docela běžný vzor. Function calling slouží k tomu, aby model mohl volat vaše nástroje (databáze, API, výpočty), zatímco strukturované výstupy zajistí, že parametry těchto volání mají správný formát. U Anthropic Claude můžete u tool definitions nastavit strict: true a máte garantované dodržení schémat vstupních parametrů nástrojů.

Jak řeším situaci, kdy model odmítne odpovědět?

Modely mohou z bezpečnostních důvodů odmítnout generovat odpověď (tzv. refusal). OpenAI API vrací speciální pole refusal v odpovědi. Při práci s Instructor zachytíte výjimku nebo zkontrolujete, zda odpověď obsahuje platný objekt. V produkčním kódu vždycky implementujte fallback logiku pro případ odmítnutí — Murphy's law platí i tady a model odmítne přesně ten požadavek, u kterého to nejméně čekáte.

O Autorovi Editorial Team

Our team of expert writers and editors.