Instructor, ustrukturyzowane odpowiedzi z modeli
Instructor opakowuje klienta modelu językowego i dodaje do wywołania jeden parametr, response_model. Zamiast łańcucha znaków dostajesz zwalidowany obiekt Pydantic, a gdy walidacja nie przejdzie, biblioteka wysyła zapytanie ponownie razem z treścią błędu. Wersja dla Pythona to 1.15.4 z 28 czerwca 2026 roku, na licencji MIT.
Co Instructor właściwie robi
Mechanizm jest prosty i dobrze, żeby taki pozostał, bo od tego zależy przewidywalność biblioteki. Podajesz klasę dziedziczącą po BaseModel. Instructor zamienia ją na JSON Schema, wstawia ten schemat do definicji narzędzia albo do pola response_format, wysyła zapytanie, odbiera argumenty wywołania narzędzia i przepuszcza je przez model_validate_json. Jeśli Pydantic zgłosi ValidationError, biblioteka dokleja do rozmowy odpowiedź modelu oraz nową wiadomość użytkownika o treści Validation Error found: wraz z tekstem wyjątku i zdaniem Recall the function correctly, fix the errors, po czym powtarza wywołanie. Po wyczerpaniu prób leci InstructorRetryException.
To wszystko. Instructor nie jest ramą do budowy agentów, nie prowadzi rejestru narzędzi, nie zarządza pamięcią rozmowy i nie ma nic wspólnego z wyszukiwaniem semantycznym. Jeśli szukasz warstwy orkiestrującej z pętlą agenta i wstrzykiwaniem zależności, bliżej Ci do Pydantic AI. Jeśli interesuje Cię optymalizacja samych podpowiedzi, patrz na DSPy. Instructor celowo siedzi niżej i robi jedną rzecz.
Warstwa jest cienka, ale zależności już nie. Pakiet instructor 1.15.4 ciągnie za sobą dziesięć bibliotek: openai w zakresie od 2.0.0 do 3.0.0, pydantic od 2.8.0, pydantic-core, tenacity od 8.2.3, jiter, jinja2, docstring-parser, requests, rich oraz typer, plus aiohttp od 3.9.1. Znaczy to tyle, że nawet gdy pracujesz wyłącznie z Anthropic, pakiet OpenAI i tak wyląduje w Twoim środowisku. Obsługę pozostałych dostawców dodają rozszerzenia, na przykład instructor[anthropic] albo instructor[google-genai]. Wymagany Python to co najmniej 3.9 i mniej niż 4.0.
Trzy pakiety o tej samej nazwie
To najczęstsze potknięcie przy pierwszej instalacji i powód, dla którego ta sekcja stoi tak wysoko. W rejestrach istnieją trzy byty o zbliżonej nazwie i tylko jeden z nich jest tym, o czym mówi dokumentacja.
| Pakiet | Rejestr | Wersja | Licencja | Ostatnia zmiana | Czy to Instructor |
|---|---|---|---|---|---|
instructor | PyPI | 1.15.4 | MIT | 28 czerwca 2026 | tak, to ten projekt |
instructor | npm | 1.0.0 | ISC | 19 czerwca 2022 | nie, zajęta nazwa |
@instructor-ai/instructor | npm | 1.7.0 | MIT | 27 stycznia 2025 | tak, ale port dla TypeScriptu |
Pakiet instructor w npm nie ma nic wspólnego z tym projektem i nigdy nie miał. Rozpakowana paczka zawiera dokładnie dwa pliki: package.json o wielkości 450 bajtów oraz README.md o wielkości 222 bajtów. Nie ma tam ani jednej linii kodu, nie ma pliku licencyjnego, a pole repository wskazuje na github.com/npm/deprecate-holder. W pliku README stoi wprost, że pakiet jest wycofany i npm trzyma nazwę, żeby ktoś jej nie przejął w złym celu. Konto rejestru założono w lutym 2016 roku, czyli siedem lat przed powstaniem Instructora. Polecenie npm install instructor daje więc pustą wydmuszkę bez ostrzeżenia, że pomyliłeś pakiet.
Wersja dla TypeScriptu nazywa się @instructor-ai/instructor i to ona jest właściwym odpowiednikiem. Tu problem jest inny i poważniejszy w skutkach: wersja 1.7.0 została opublikowana 27 stycznia 2025 roku i od tamtej pory nic się nie zmieniło. Gałąź główna repozytorium 567-labs/instructor-js wciąż ma w package.json numer 1.7.0, ostatni znacznik wydania to v1.7.0, a repozytorium ma około 802 gwiazdki wobec 13 761 dla wersji Pythona. To nie jest projekt aktywnie utrzymywany, tylko taki, który stanął ponad półtora roku temu. Jeżeli piszesz w TypeScripcie i potrzebujesz czegoś, co nadąża za zmianami u dostawców, potraktuj ten pakiet jako ryzyko, a nie jako równorzędny wariant.
Licencja sprawdzona w trzech miejscach
Deklaracja licencji w rejestrze bywa czymś innym niż zawartość opublikowanej paczki, więc sprawdziłem oba pakiety w trzech niezależnych punktach.
| Źródło | Wersja Pythona | Wersja TypeScriptu |
|---|---|---|
| Plik LICENSE w repozytorium | MIT, Copyright (c) 2023 Jason Liu | MIT, Copyright (c) 2024 Jason Liu |
| Pole license w rejestrze | MIT w metadanych PyPI | MIT w metadanych npm |
| Zawartość opublikowanej paczki | LICENSE w katalogu dist-info, 195 plików .py | LICENSE w katalogu głównym, 9 plików |
Wynik jest zgodny w obu przypadkach i to rzadka przyjemność. Koło instructor-1.15.4-py3-none-any.whl waży 252 522 bajty, zawiera 195 plików .py oraz plik licencyjny w instructor-1.15.4.dist-info/licenses/LICENSE, a nagłówek License-File w metadanych na niego wskazuje. Paczka @instructor-ai/instructor 1.7.0 ma 9 plików o łącznej wielkości 110 938 bajtów po rozpakowaniu, w tym LICENSE z tekstem MIT i skompilowany katalog dist. Nigdzie nie ma rozjazdu między deklaracją a zawartością, nie ma dodatkowego pliku o innej licencji ani pustej paczki podszywającej się pod działający kod.
Jedna rzecz zaskakuje przy pobieraniu źródeł. Archiwum instructor-1.15.4.tar.gz w PyPI waży 70 049 678 bajtów, czyli mniej więcej 278 razy więcej niż koło. Do archiwum trafia repozytorium razem z dokumentacją i obrazkami. Instalacja przez pip pobiera koło i nie zauważysz różnicy, ale narzędzie budujące ze źródeł albo lustro rejestru już tak.
Płatnego wariantu nie ma. Instructor to biblioteka bez usługi po drugiej stronie, więc nie ma cennika, planu darmowego ani limitów. Koszty pochodzą wyłącznie od dostawcy modelu, a Instructor dokłada do nich narzut, o którym mówi sekcja o ponawianiu. Projekt utrzymuje w praktyce wąska grupa osób wokół autora, co przy 96 wydaniach w PyPI daje wysokie tempo, ale też typowe ryzyko projektu otwartego bez umowy wsparcia.
Dostawcy i tryby pracy
Dostawcę wybierasz albo funkcją dedykowaną, albo jednym łańcuchem znaków. Funkcja from_provider przyjmuje wartość w formacie dostawca/model i rozpoznaje 23 aliasy: openai, anthropic, google, generative-ai, gemini, vertexai, azure_openai, bedrock, mistral, cohere, groq, cerebras, fireworks, together, anyscale, databricks, deepseek, perplexity, openrouter, litellm, ollama, writer oraz xai.
import instructor
from pydantic import BaseModel, Field
class Invoice(BaseModel):
number: str = Field(description="Numer faktury z nagłówka")
net_total: float
currency: str
client = instructor.from_provider("openai/gpt-4o-mini")
invoice = client.chat.completions.create(
response_model=Invoice,
messages=[{"role": "user", "content": raw_text}],
max_retries=2,
)
print(invoice.net_total, invoice.currency)Sygnatura from_provider to model, async_client=False, cache=None oraz mode=None, a reszta argumentów wędruje do konstruktora klienta dostawcy. Ustawienie async_client=True zwraca AsyncInstructor zamiast Instructor. Parametr cache przyjmuje adapter, na przykład AutoCache(maxsize=1000) albo RedisCache, i przechodzi dalej przez kwargs do implementacji dostawcy.
Tryb rozstrzyga, jak schemat trafia do zapytania. Enum Mode ma wartości takie jak Mode.TOOLS o wartości tool_call, Mode.JSON o wartości json_mode, Mode.JSON_SCHEMA, Mode.MD_JSON o wartości markdown_json_mode, Mode.PARALLEL_TOOLS oraz Mode.RESPONSES_TOOLS dla nowego interfejsu OpenAI. Do tego dochodzą warianty dla konkretnych dostawców, między innymi Mode.ANTHROPIC_TOOLS, Mode.ANTHROPIC_JSON, Mode.GEMINI_TOOLS, Mode.GENAI_JSON, Mode.COHERE_TOOLS i Mode.BEDROCK_TOOLS.
from openai import OpenAI
import instructor
# tool calling, domyślny i zwykle najskuteczniejszy
tools = instructor.from_openai(OpenAI(), mode=instructor.Mode.TOOLS)
# JSON mode, gdy model nie ma wywołań narzędzi
plain_json = instructor.from_openai(OpenAI(), mode=instructor.Mode.JSON)
# schemat wymuszony po stronie dostawcy
strict = instructor.from_openai(OpenAI(), mode=instructor.Mode.JSON_SCHEMA)
# ostatnia deska ratunku: JSON w bloku markdown
markdown = instructor.from_openai(OpenAI(), mode=instructor.Mode.MD_JSON)Dla dostawcy openai biblioteka uznaje za wspierane sześć trybów: TOOLS, JSON, JSON_SCHEMA, MD_JSON, PARALLEL_TOOLS i RESPONSES_TOOLS. Trzy tryby są mapowane po cichu na inne, bo zostały wycofane: Mode.FUNCTIONS przechodzi na TOOLS z ostrzeżeniem o wycofaniu, Mode.TOOLS_STRICT również na TOOLS, a Mode.JSON_O1 na JSON_SCHEMA. Jeśli ustawisz jeden ze starych trybów i zdziwisz się, że zachowanie nie odpowiada nazwie, to jest właśnie powód. Różnica praktyczna między trybami sprowadza się do tego, że TOOLS i JSON_SCHEMA dają schemat egzekwowany przez dostawcę, a JSON i MD_JSON opierają się na tym, że model posłucha instrukcji, więc mają wyraźnie wyższy odsetek ponowień.
Ponawianie, walidacja i haki
Parametr max_retries przyjmuje liczbę całkowitą albo instancję Retrying z biblioteki tenacity. Przy liczbie całkowitej Instructor buduje warunek zatrzymania jako stop_after_attempt(max(max_retries, 0) + 1), czyli max_retries liczy ponowienia po pierwszej próbie. Ustawienie max_retries=3 daje w najgorszym razie cztery wywołania płatnego interfejsu, a nie trzy.
Uwaga na dwa różne wartości domyślne. Metoda Instructor.create oraz client.chat.completions.create mają w sygnaturze max_retries=3. Funkcja opakowana bezpośrednio przez instructor.patch ma w kodzie max_retries: int | Retrying = 1. Ta sama nazwa parametru, inna liczba wywołań w rachunku, zależnie od tego, którym wejściem wszedłeś.
from pydantic import BaseModel, field_validator
from instructor.core.exceptions import InstructorRetryException
class Ticket(BaseModel):
title: str
priority: str
@field_validator("priority")
@classmethod
def known_priority(cls, value: str) -> str:
allowed = {"low", "medium", "high"}
if value not in allowed:
raise ValueError(f"priority musi byc jednym z {sorted(allowed)}")
return value
try:
ticket = client.chat.completions.create(
response_model=Ticket,
messages=[{"role": "user", "content": report}],
max_retries=2,
context={"tenant": "acme"},
strict=True,
)
except InstructorRetryException as err:
print(err.n_attempts, err.total_usage)
print(err.last_completion)
print(err.create_kwargs)Wyjątek InstructorRetryException niesie komplet informacji do diagnozy: last_completion z ostatnią nieudaną odpowiedzią, n_attempts z liczbą prób, total_usage z sumarycznym zużyciem tokenów, create_kwargs z parametrami wywołania oraz failed_attempts z listą szczegółów każdej nieudanej próby. Pole messages nadal istnieje, ale w kodzie jest opisane jako przestarzałe na rzecz create_kwargs.
Do obserwowania przebiegu służą haki. Metody client.on, client.off i client.clear przyjmują jedną z pięciu nazw zdarzeń: completion:kwargs, completion:response, completion:error, completion:last_attempt oraz parse:error. Podpięcie licznika pod parse:error to najprostszy sposób zmierzenia, ile naprawdę kosztują Cię ponowienia, zanim rachunek u dostawcy zrobi to za Ciebie. Jeśli potrzebujesz pełnego panelu z rejestrem wywołań, ten strumień zdarzeń łatwo przekierować do narzędzia obserwacyjnego albo do bramki w rodzaju LiteLLM czy OpenRouter.
Biblioteka ma też walidator wykorzystujący model. Funkcja llm_validator przyjmuje statement, client, allow_override=False, model="gpt-3.5-turbo" oraz temperature=0. Wartość domyślna pola model jest wyraźnie przestarzała, więc podawaj ją jawnie zamiast polegać na domyślnej.
Strumieniowanie i modele pomocnicze
Klient udostępnia cztery warianty tworzenia odpowiedzi. Zwykły create zwraca gotowy obiekt. create_with_completion zwraca krotkę z obiektem i surową odpowiedzią dostawcy, co przydaje się do odczytania zużycia tokenów. create_iterable zwraca generator obiektów, gdy prosisz o listę i chcesz przetwarzać elementy w miarę napływania. create_partial zwraca generator kolejnych, coraz pełniejszych wersji tego samego obiektu.
from instructor import Partial
# kolejne przyblizenia jednego obiektu
for draft in client.chat.completions.create_partial(
response_model=Invoice,
messages=[{"role": "user", "content": raw_text}],
max_retries=1,
):
render(draft)
# elementy listy w miare ich powstawania
for row in client.chat.completions.create_iterable(
response_model=Invoice,
messages=[{"role": "user", "content": batch_text}],
):
save(row)
# obiekt razem z surowa odpowiedzia dostawcy
invoice, completion = client.chat.completions.create_with_completion(
response_model=Invoice,
messages=[{"role": "user", "content": raw_text}],
)
print(completion.usage.total_tokens)Częściowe obiekty mają jedno ograniczenie, o którym łatwo zapomnieć: dopóki strumień się nie skończy, walidacja nie jest kompletna, bo pola po prostu jeszcze nie istnieją. Strumieniowanie nadaje się do pokazywania postępu w interfejsie, nie do podejmowania decyzji na podstawie niedokończonych danych.
Poza tym w module instructor.dsl siedzi kilka gotowych wzorców. Partial[Model] opisuje typ częściowy. Maybe(Model) tworzy model z polami result, error i message, przy czym error ma wartość domyślną False, a __bool__ sprawdza, czy result nie jest None. To sposób na model, który może uczciwie powiedzieć, że w tekście nie było czego wyciągnąć, zamiast zmyślać. IterableModel, CitationMixin, ListResponse i ResponseList uzupełniają zestaw.
Instructor a alternatywy
| Cecha | Instructor | Pydantic AI | DSPy | Guardrails AI | Natywne structured outputs |
|---|---|---|---|---|---|
| Zakres | tylko schemat odpowiedzi | pełna rama agentowa | optymalizacja podpowiedzi | walidacja i poprawki | funkcja dostawcy |
| Schemat | Pydantic | Pydantic | sygnatury | własne walidatory | JSON Schema |
| Ponawianie przy błędzie | wbudowane | wbudowane | zależnie od modułu | wbudowane | brak |
| Zmiana dostawcy | 23 aliasy | wielu dostawców | wielu dostawców | wielu dostawców | brak, wiąże z jednym |
| Wersja | 1.15.4 dla Pythona, 1.7.0 dla TypeScriptu | zobacz osobny artykuł | zobacz osobny artykuł | zobacz osobny artykuł | nie dotyczy |
| Licencja | MIT | MIT | MIT | Apache 2.0 | nie dotyczy |
Wybór rozstrzyga jedno pytanie: czy potrzebujesz czegoś więcej niż zwalidowanego obiektu. Jeśli nie, Instructor jest najmniejszym rozwiązaniem, jakie da się do tego użyć, a jego kod czyta się w jeden wieczór. Jeśli budujesz agenta z narzędziami, historią i punktami kontrolnymi, cieńsza warstwa szybko przestanie wystarczać. Jeśli zależy Ci wyłącznie na wymuszeniu kształtu i pracujesz z jednym dostawcą, natywne structured outputs u tego dostawcy załatwią sprawę bez dodatkowej zależności, kosztem przywiązania do jednego interfejsu. Dla walidacji treści, a nie kształtu, sięgnij po Guardrails AI.
Osobno stoi Outlines, bo rozwiązuje ten sam problem od drugiej strony. Instructor prosi model o wynik i ponawia, gdy walidacja nie przejdzie, więc działa z każdym dostawcą, ale za każdą nieudaną próbę płacisz. Outlines maskuje tokeny w trakcie generowania, więc model nie może wyprodukować czegoś niezgodnego ze schematem i ponowienia znikają. Cena jest twarda: wymuszanie działa tylko dla trzech silników trzymających wagi w Twoim procesie, a przy OpenAI czy Anthropic biblioteka przekazuje schemat dostawcy i nie daje gwarancji większej niż ta, którą dostawca ma sam z siebie.
Typowe błędy
Pierwszy to npm install instructor zamiast npm install @instructor-ai/instructor. Instalacja przejdzie bez błędu, a Ty dostaniesz wycofaną wydmuszkę npm bez linii kodu. Sprawdź nazwę w package.json, zanim zaczniesz szukać przyczyny w swoim kodzie.
Drugi to traktowanie portu dla TypeScriptu jako równorzędnego. Pakiet @instructor-ai/instructor 1.7.0 przypina zod-stream do dokładnej wersji 3.0.0, a ta wersja deklaruje zależności równorzędne zod w zakresie ^3.23.3 oraz openai dokładnie w wersji 4.47.1. Tymczasem w npm zod ma dziś 4.4.3, a openai ma 7.5.0. Sam Instructor deklaruje openai w zakresie >=4.58.0, co przeczy przypiętej zależności wewnętrznej. Praktycznie znaczy to, że port dla TypeScriptu zamraża Cię przy Zod w wersji 3 i przy pokoleniu klienta OpenAI sprzed dwóch lat.
Trzeci to złe rozumienie max_retries. Wartość 3 to trzy ponowienia po pierwszym wywołaniu, więc cztery zapytania do dostawcy. Przy modelu rozumującym i dużym schemacie potrafi to potroić rachunek za jedno żądanie użytkownika, zwłaszcza w trybie MD_JSON, gdzie odsetek nieudanych walidacji jest najwyższy.
Czwarty to walidatory, które nie mówią modelowi nic użytecznego. Treść wyjątku z Pydantica trafia dosłownie do kolejnego zapytania, więc komunikat nieprawidłowa wartość marnuje ponowienie. Napisz w wyjątku, jaka wartość jest oczekiwana, a druga próba zwykle wystarcza.
Piąty to zapominanie o openai jako zależności twardej. Nawet instalacja instructor[anthropic] zaciąga klienta OpenAI, bo pakiet podstawowy go wymaga. Przy audycie zależności albo obrazie kontenera trzymanym na diecie to widoczna pozycja. Rozszerzenie dla Anthropic przypina do tego anthropic w dokładnej wersji 0.93.0, a nie w zakresie, co potrafi zderzyć się z inną biblioteką w tym samym środowisku.
Szósty to strict=True w trybie, który nie ma pojęcia o trybie ścisłym. Parametr istnieje w sygnaturze create niezależnie od wybranego trybu, ale jego znaczenie zależy od dostawcy, a przy MD_JSON nie ma go czym wymusić po drugiej stronie.
Siódmy to poleganie na wartości domyślnej model w llm_validator. Wpisany tam gpt-3.5-turbo jest reliktem i albo zwróci słaby wynik, albo w ogóle przestanie być dostępny u dostawcy.
FAQ
Który pakiet zainstalować w projekcie Pythona?
pip install instructor z PyPI, wersja 1.15.4. To ten projekt, na licencji MIT, z repozytorium 567-labs/instructor. Adres instructor-ai/instructor przekierowuje do tego samego miejsca, więc oba odnośniki w dokumentacji są poprawne.
Czy wersja dla TypeScriptu jest utrzymywana?
W praktyce nie. Ostatnia publikacja @instructor-ai/instructor to 1.7.0 z 27 stycznia 2025 roku, gałąź główna repozytorium ma ten sam numer wersji i nie ma nowszego znacznika wydania. Kod działa, ale nie nadąża za zmianami w zod i w kliencie OpenAI.
Ile dodatkowych zapytań kosztuje ponawianie?
Tyle, ile ustawisz w max_retries, licząc po pierwszej próbie. Domyślne 3 w client.chat.completions.create oznacza maksymalnie cztery wywołania. Rzeczywisty koszt zmierzysz, podpinając licznik pod zdarzenie parse:error.
Kiedy wybrać JSON mode zamiast tool calling?
Gdy model albo dostawca nie obsługuje wywołań narzędzi, albo gdy potrzebujesz Mode.JSON_SCHEMA z wymuszeniem po stronie dostawcy. W pozostałych przypadkach Mode.TOOLS daje mniej nieudanych walidacji, bo schemat jest egzekwowany, a nie sugerowany.
Czy Instructor zastępuje ramę agentową?
Nie. Nie ma pętli agenta, rejestru narzędzi ani zarządzania stanem rozmowy. Zwraca zwalidowany obiekt i na tym kończy swoją rolę, a resztę składasz sam albo bierzesz gotowe z innej warstwy.
Czy da się używać Instructora z modelem uruchomionym lokalnie?
Tak, przez alias ollama w from_provider albo przez klienta zgodnego z interfejsem OpenAI ze zmienionym base_url. Ograniczeniem jest wtedy tryb: mniejsze modele lokalne często nie radzą sobie z wywołaniami narzędzi, więc zostaje Mode.JSON lub Mode.MD_JSON i wyższy odsetek ponowień.
Dokumentację wersji Pythona znajdziesz na python.useinstructor.com, kod źródłowy w repozytorium 567-labs/instructor, a paczkę w rejestrze PyPI.