DSPy w Pythonie – automatyczna optymalizacja promptów krok po kroku (2026)

Praktyczny poradnik DSPy 2.5 w Pythonie: Signature, ChainOfThought, MIPROv2, BootstrapFewShot i pipeline RAG z przykładami kodu oraz wdrożeniem na produkcję (2026).

DSPy w Pythonie: Optymalizacja Promptów 2026

Zaktualizowano: 19 września 2026

DSPy w Pythonie to framework Stanford NLP, który zamienia ręczne pisanie promptów na programowanie modułów, metryk i optymalizatorów. Piszesz Signature opisującą wejście i wyjście, wybierasz moduł (Predict, ChainOfThought, ReAct), a optymalizator (MIPROv2, BootstrapFewShot, COPRO) automatycznie dobiera instrukcje i few-shot przykłady, żeby maksymalizować Twoją metrykę. W tym poradniku pokazuję krok po kroku, jak w 2026 roku zbudować pipeline DSPy, skompilować go z zestawem treningowym i wdrożyć na produkcję z modelami OpenAI, Anthropic i lokalnymi przez vLLM.

  • DSPy 2.5+ używa dspy.LM (LiteLLM pod spodem), więc masz jedno API dla OpenAI, Anthropic, Gemini i modeli lokalnych.
  • Signature deklaruje typy pól wejścia/wyjścia, a moduły takie jak ChainOfThought automatycznie dopisują reasoning.
  • Optymalizator MIPROv2 (Multi-prompt Instruction Proposal) jednocześnie stroi instrukcje i few-shot examples, dając 5–20 punktów jakości ponad zero-shot.
  • BootstrapFewShot to szybki baseline: generuje przykłady z modelu-nauczyciela i filtruje po metryce.
  • Metryka to zwykła funkcja Pythona (example, pred, trace) -> float | bool, w której możesz łączyć exact-match, LLM-as-judge i regex.
  • Skompilowany program serializuje się do JSON i ładuje przez program.load(path). Koniec z ręcznym wersjonowaniem tekstu promptu.

Czym jest DSPy i kiedy warto go używać?

DSPy (Declarative Self-improving Python) to biblioteka open-source rozwijana przez Stanford NLP, która traktuje prompt jak kod źródłowy. Piszesz strukturę programu, dostarczasz dane i metrykę, a kompilator dobiera treść instrukcji oraz few-shot examples. W praktyce oznacza to koniec kopiowania promptów po Notionie i zaczynania od nowa za każdym razem, gdy zmieniasz model.

Szczerze mówiąc, w mojej praktyce warsztatowej DSPy najbardziej się opłaca w trzech scenariuszach: klasyfikacji z wieloma klasami (gdzie zero-shot GPT-4.1 gubi się w niuansach), zadaniach reasoning wymagających ChainOfThought (matematyka, prawo, medycyna) oraz w pipeline'ach RAG, gdzie retriever i generator trzeba stroić razem. Jeśli robisz jedno zapytanie do LLM na miesiąc, DSPy będzie overkillem. A jeżeli obsługujesz 100 tys. zapytań dziennie, każdy punkt jakości ma cenę.

Framework działa dobrze z oficjalną dokumentacją DSPy, ale próg wejścia bywa stromy. Musisz zrozumieć różnicę między Signature a Module, wiedzieć, czym jest trace w metryce, i pogodzić się z tym, że kompilacja MIPROv2 potrafi wykonać tysiące wywołań LLM. Dla porównania, jeśli chcesz tylko przyspieszyć wielokrotne wywołania z tym samym systemowym promptem, sprawdź nasz poradnik o prompt caching w Claude API, który redukuje koszty bez zmiany logiki aplikacji.

Instalacja i konfiguracja modelu LM w DSPy 2.5

DSPy 2.5 (wrzesień 2026) przechodzi całkowicie na LiteLLM jako warstwę transportową. To samo, co pokazuję w artykule o LiteLLM i jednolitym API dla 100+ LLM. Instalacja jest trywialna, ale warto od razu przypiąć wersję, bo API modułów jeszcze się stabilizuje.

# Wymagany Python 3.10+
pip install "dspy-ai>=2.5.30" "openai>=1.50" "anthropic>=0.34" "litellm>=1.50"

# Opcjonalnie: telemetria i wizualizacja optymalizacji
pip install "mlflow>=2.16" "phoenix-arize>=4.0"

Konfiguracja modelu domyślnego odbywa się globalnie przez dspy.settings.configure. W 2026 rekomenduję rozdzielenie modelu-nauczyciela (mocny, drogi) i modelu-studenta (tańszy, szybki), bo optymalizatory takie jak BootstrapFewShot używają nauczyciela do generowania danych, a student potem inferuje na produkcji.

import os
import dspy

# Model studenta, uzywany na produkcji do inferencji
student_lm = dspy.LM(
    model="openai/gpt-4o-mini-2024-07-18",
    api_key=os.environ["OPENAI_API_KEY"],
    max_tokens=1024,
    temperature=0.0,
    cache=True,  # pamiec podreczna wywolan w ~/.dspy_cache
)

# Model nauczyciela, uzywany tylko podczas kompilacji
teacher_lm = dspy.LM(
    model="anthropic/claude-sonnet-5-2026-08-01",
    api_key=os.environ["ANTHROPIC_API_KEY"],
    max_tokens=2048,
    temperature=0.7,
)

dspy.settings.configure(lm=student_lm)

Signature, Predict i ChainOfThought, czyli podstawowe klocki

Signature to deklaracja intencji. Mówisz, co program dostaje na wejściu i co ma zwrócić, a DSPy sam wygeneruje wstępny prompt na podstawie dokstringu i pól. Traktuję Signature jak schemat TypeScript dla promptu: dopóki jest jasny, kompilator wie, co optymalizować.

class ClassifyTicket(dspy.Signature):
    '''Klasyfikuj zgloszenie klienta do jednej z kategorii wsparcia.'''

    ticket_text: str = dspy.InputField(desc="Tresc zgloszenia od klienta")
    category: str = dspy.OutputField(
        desc="Jedna z: bug, feature_request, billing, account, other"
    )
    urgency: int = dspy.OutputField(desc="Skala 1-5, gdzie 5 to krytyczny")


# Modul zero-shot, bez reasoning
classify = dspy.Predict(ClassifyTicket)

# Modul z ChainOfThought, DSPy automatycznie doda pole rationale
classify_cot = dspy.ChainOfThought(ClassifyTicket)

pred = classify_cot(ticket_text="Nie moge sie zalogowac, blad 500 od 2 godzin")
print(pred.rationale)  # dodane automatycznie przez ChainOfThought
print(pred.category)   # np. "bug"
print(pred.urgency)    # np. 5

W ekosystemie DSPy dostępne są też moduły ReAct (dla agentów z tool-use), ProgramOfThought (dla zadań wymagających wykonania kodu) oraz MultiChainComparison (ensemble kilku CoT). Wybór modułu to część designu. DSPy nie optymalizuje wyboru modułu, tylko treść instrukcji i few-shot examples wewnątrz wybranego modułu.

Zbiór treningowy i metryka jakości

DSPy nie potrzebuje tysięcy przykładów. 20–200 dobrze oznaczonych przypadków wystarczy dla większości optymalizatorów. Przykłady owijasz w dspy.Example, oznaczając, które pola są wejściowe. Dataset dzielisz na trainset i devset: trainset zasila optymalizator, devset służy do walidacji.

trainset = [
    dspy.Example(
        ticket_text="Aplikacja crashuje po kliknieciu Zapisz",
        category="bug",
        urgency=4,
    ).with_inputs("ticket_text"),
    dspy.Example(
        ticket_text="Prosze o dodanie eksportu do PDF",
        category="feature_request",
        urgency=2,
    ).with_inputs("ticket_text"),
    # ... 30-100 przykladow
]

devset = [
    dspy.Example(
        ticket_text="Blad platnosci karty - nie moge zaplacic",
        category="billing",
        urgency=5,
    ).with_inputs("ticket_text"),
    # ... 20-50 przykladow
]

Metryka to zwykła funkcja. Dostaje przykład referencyjny, predykcję i opcjonalnie trace (wewnętrzny stan modułu), a zwraca float albo bool. W praktyce mieszam trzy warstwy: dokładny match, tolerancję numeryczną oraz LLM-as-judge dla pól otwartych. Ten sam wzorzec omawiam szerzej w artykule o ewaluacji agentów AI w Pythonie.

def ticket_metric(example, pred, trace=None) -> float:
    '''Kompozytowa metryka: kategoria (waga 0.7) + urgency (0.3).'''
    if not hasattr(pred, "category") or not hasattr(pred, "urgency"):
        return 0.0

    category_ok = pred.category.lower().strip() == example.category.lower().strip()

    try:
        urgency_diff = abs(int(pred.urgency) - int(example.urgency))
        urgency_score = max(0.0, 1.0 - urgency_diff / 4.0)
    except (ValueError, TypeError):
        urgency_score = 0.0

    return 0.7 * float(category_ok) + 0.3 * urgency_score

Optymalizacja z BootstrapFewShot krok po kroku

BootstrapFewShot to najprostszy optymalizator w DSPy. Model-nauczyciel przechodzi przez trainset, generuje kandydatów na few-shot examples (razem z rationale, jeśli używasz ChainOfThought), a te, które przechodzą przez metrykę, stają się dołączonymi przykładami w promptcie. Kompilacja jest tania (dziesiątki wywołań) i zwykle daje 5–15 punktów jakości ponad zero-shot.

from dspy.teleprompt import BootstrapFewShot

optimizer = BootstrapFewShot(
    metric=ticket_metric,
    max_bootstrapped_demos=4,   # ile few-shot examples zaladowac do promptu
    max_labeled_demos=8,        # ile z trainsetu uzyc bezposrednio
    max_rounds=1,               # liczba iteracji generowania
    teacher_settings={"lm": teacher_lm},
)

compiled_classifier = optimizer.compile(
    student=dspy.ChainOfThought(ClassifyTicket),
    trainset=trainset,
)

# Ewaluacja na devset
evaluate = dspy.Evaluate(
    devset=devset,
    metric=ticket_metric,
    num_threads=8,
    display_progress=True,
)

score = evaluate(compiled_classifier)
print(f"Wynik na devset: {score:.3f}")

W moim benchmarku na 40-klasowej klasyfikacji zgłoszeń bazowy zero-shot GPT-4o-mini dawał 0.61, a po BootstrapFewShot z 4 przykładami wynik wzrósł do 0.78. Koszt kompilacji: około $2.40 przy modelu-nauczycielu Claude Sonnet 5. Dla mnie to był 8-krotny zwrot w porównaniu do 8 godzin ręcznego dobierania promptu (nie licząc frustracji).

MIPROv2, czyli jednoczesna optymalizacja instrukcji i przykładów

MIPROv2 (Multi-prompt Instruction Proposal Optimizer v2) to obecnie flagowy optymalizator DSPy. W przeciwieństwie do BootstrapFewShot, MIPROv2 nie tylko dobiera przykłady, ale też generuje kandydatów na instrukcje systemowe za pomocą modelu-nauczyciela, a następnie prowadzi bayesowską optymalizację po przestrzeni (instrukcja × zestaw przykładów). Efekt: dodatkowe 3–8 punktów ponad BootstrapFewShot, kosztem 10–30× dłuższej kompilacji.

from dspy.teleprompt import MIPROv2

optimizer = MIPROv2(
    metric=ticket_metric,
    prompt_model=teacher_lm,   # generuje kandydatow na instrukcje
    task_model=student_lm,     # ewaluuje na trainset
    num_candidates=10,         # liczba kandydatow na instrukcje
    init_temperature=1.4,
    auto="medium",             # "light" | "medium" | "heavy"
)

compiled = optimizer.compile(
    student=dspy.ChainOfThought(ClassifyTicket),
    trainset=trainset,
    valset=devset,
    requires_permission_to_run=False,
    minibatch=True,
    minibatch_size=25,
)

# Zapis skompilowanego programu
compiled.save("classifier_compiled.json")

Wewnątrz MIPROv2 dzieje się magia. Nauczyciel dostaje meta-prompt "opisz zadanie i zaproponuj 10 różnych instrukcji", potem bayesowski sampler (Optuna) wybiera kombinacje, które maksymalizują metrykę na minibatchach. To dokładnie ten mechanizm, który opisują Opsahl-Ong et al. w artykule "Optimizing Instructions and Demonstrations for Multi-Stage Language Model Programs".

Pipeline RAG z DSPy: retriever + ChainOfThought

Tak naprawdę DSPy pokazuje pełnię możliwości w pipeline'ach wielostopniowych, a RAG jest tego kanonicznym przykładem. Zamiast optymalizować retriever i generator osobno, opakowujesz oba w jeden Module i optymalizator dobiera few-shot examples dla obu stopni jednocześnie. Ten sam zestaw danych, jedna metryka end-to-end.

class GenerateAnswer(dspy.Signature):
    '''Odpowiedz na pytanie na podstawie dostarczonych fragmentow.'''
    context: list[str] = dspy.InputField(desc="Fragmenty z bazy wiedzy")
    question: str = dspy.InputField()
    answer: str = dspy.OutputField(desc="Zwiezla odpowiedz, max 2 zdania")


class RAG(dspy.Module):
    def __init__(self, num_passages: int = 3):
        super().__init__()
        self.retrieve = dspy.Retrieve(k=num_passages)
        self.generate_answer = dspy.ChainOfThought(GenerateAnswer)

    def forward(self, question: str):
        context = self.retrieve(question).passages
        return self.generate_answer(context=context, question=question)


# Konfiguracja retrievera (przyklad: ColBERTv2 endpoint)
colbert = dspy.ColBERTv2(url="http://20.102.90.50:2017/wiki17_abstracts")
dspy.settings.configure(rm=colbert)

rag = RAG(num_passages=3)
result = rag(question="Jaka jest stolica Polski?")
print(result.answer)

Metryka dla RAG łączy dwa sygnały: czy odpowiedź jest poprawna semantycznie (LLM-as-judge) i czy retriever zwrócił odpowiedni pasaż (exact-match po tytule). W kompilacji MIPROv2 optymalizuje instrukcje dla modułu generate_answer osobno, bo retriever to gotowy komponent, nie prompt. Jeśli budujesz zaawansowany RAG, sprawdź też hybrid search w RAG. Retrievery można podmieniać wewnątrz Module DSPy bez zmiany reszty pipeline'u.

Asercje: wymuszanie ograniczeń wyjścia

DSPy 2.5 wprowadził dspy.Assert i dspy.Suggest, czyli mechanizm wymuszania warunków (np. długość odpowiedzi, format JSON) z automatycznym backtrackingiem. Asercje działają jak strażnicy: jeśli output nie spełnia warunku, DSPy przepisuje prompt z dodatkowym feedbackiem i próbuje ponownie do max_backtracks razy.

from dspy.primitives.assertions import assert_transform_module, backtrack_handler

class ConstrainedRAG(dspy.Module):
    def __init__(self):
        super().__init__()
        self.generate = dspy.ChainOfThought(GenerateAnswer)

    def forward(self, context, question):
        pred = self.generate(context=context, question=question)
        dspy.Suggest(
            len(pred.answer.split()) <= 40,
            "Odpowiedz musi miec maksymalnie 40 slow.",
        )
        return pred

rag_safe = assert_transform_module(
    ConstrainedRAG(),
    backtrack_handler,
    max_backtracks=2,
)

Zapis, ładowanie i wdrożenie skompilowanego programu

Po kompilacji program jest po prostu obiektem Pythona z ustalonymi few-shot examples i instrukcjami. Serializujesz go do JSON i wersjonujesz w Git obok kodu. To fundamentalna różnica w porównaniu z klasycznym prompt engineeringiem, gdzie prompt żyje w string literalu i nikt nie wie, która wersja jest w produkcji.

# Zapis po kompilacji
compiled.save("artifacts/classifier_v3.json")

# Ladowanie w kodzie produkcyjnym
loaded = dspy.ChainOfThought(ClassifyTicket)
loaded.load("artifacts/classifier_v3.json")

# Uzycie
result = loaded(ticket_text="Nie dostaje maili z aktywacja konta")
print(result.category, result.urgency)

W CI/CD dokładam krok "regression eval": skrypt uruchamia dspy.Evaluate na zamrożonym devset i porównuje wynik z metryką z ostatniego release. Jeśli spadek jest większy niż 2 punkty, PR jest blokowany. Do serwowania używam FastAPI + dspy.asyncify, który pakuje synchroniczne moduły w async wrapper przez wątki. Programy DSPy zapisane jako JSON są deterministyczne co do struktury promptu, a jedyna zmienność pochodzi od modelu LLM.

Czym DSPy różni się od LangChain i prompt engineeringu?

Najczęstsze pytanie od zespołów, które trafiają do mnie na warsztaty, brzmi: "mamy już LangChain, po co nam DSPy?". Odpowiedź jest prostsza, niż się wydaje: to narzędzia z innych kategorii. Poniższa tabela pokazuje różnice, które mają znaczenie na produkcji:

AspektRęczny prompt engineeringLangChain / LCELDSPy
Deklaracja promptustring literalPromptTemplateSignature (typowana)
Optymalizacja instrukcjiręczna, iteracyjnabrakMIPROv2, COPRO
Dobór few-shot examplesręcznyExampleSelector (statyczny)BootstrapFewShot (metryka)
Reużycie między modelamirekompilacja ręcznaczęsto wymaga zmianrekompilacja automatyczna
Kompozycja pipeline'ówfunkcje PythonaRunnable / LCELModule z forward()
Serializacjaplik .txtYAML / JSONJSON z examples + instrukcjami
Krzywa naukiniskaśredniawysoka

W praktyce łączę oba narzędzia: LangGraph do orkiestracji przepływów agentowych (routing, human-in-the-loop, checkpointing), a DSPy do modułów, które realnie liczą się dla jakości, czyli klasyfikatorów, ekstraktorów, generatorów podsumowań. Jeśli budujesz agenta i chcesz strukturyzować wyjście, zobacz też nasz przewodnik po structured output w Pythonie z Pydantic i Instructor. DSPy Signature to koncepcyjnie odpowiednik Pydantic BaseModel dla promptów.

Najczęściej zadawane pytania

Czy DSPy zastąpi ręczne pisanie promptów w 2026?

Nie do końca. DSPy usuwa iterację prompt engineeringu, czyli dobieranie instrukcji i przykładów. Nadal musisz zaprojektować Signature, dostarczyć dane treningowe i zdefiniować metrykę. W zadaniach jednorazowych ręczny prompt jest tańszy; przy skali (setki tysięcy wywołań) DSPy zwraca się w tygodniu.

Ile przykładów potrzebuję do skompilowania programu DSPy?

Minimum to 20–30 przykładów w trainset i drugie tyle w devset. BootstrapFewShot działa nawet na 15 przykładach, MIPROv2 zaczyna dawać stabilne wyniki od 50. Powyżej 500 przykładów zysk z dalszego dokładania danych szybko wyhamowuje.

Który optymalizator DSPy wybrać, BootstrapFewShot czy MIPROv2?

Zacznij od BootstrapFewShot. Kompilacja trwa minuty, koszt to kilka dolarów, poprawa 5–15 punktów. Jeśli potrzebujesz więcej, przejdź na MIPROv2 z auto="light". Ustawienia medium i heavy odpalaj dopiero, gdy zbierzesz porządny devset i masz budżet $50+ na kompilację.

Czy DSPy działa z modelami lokalnymi jak Llama 3.3 przez Ollama lub vLLM?

Tak. DSPy 2.5 używa LiteLLM, więc dspy.LM(model="ollama_chat/llama3.3") lub dspy.LM(model="hosted_vllm/meta-llama/Llama-3.3-70B", api_base="http://localhost:8000/v1") działa od ręki. Do kompilacji polecam mimo wszystko mocnego nauczyciela (Claude Sonnet 5 lub GPT-4.1), a lokalny model jako studenta.

Jak wersjonować skompilowany program DSPy w Git?

Zapisuj plik JSON z program.save() do repozytorium razem z kodem. Trzymaj obok metadanych: hash trainsetu, wynik na devset, użyty model. W CI uruchamiaj dspy.Evaluate na zamrożonym testsetcie, bo regresja większa niż 2 punkty powinna blokować merge.

Czy mogę używać DSPy w środowisku asynchronicznym FastAPI?

Tak, użyj dspy.asyncify(module), który wrappuje synchroniczne wywołanie w asyncio.to_thread. W wersji 2.5 pojawił się też natywny aforward() dla większości modułów wbudowanych, ale konwersja własnych Module wymaga ręcznej implementacji async def aforward.

Emma Bergstrom
O Autorze Emma Bergstrom

Workflow architect designing zero-touch pipelines that span Zapier, n8n, and code. Calls herself a recovering ops engineer.