DSPy, czyli koniec ręcznego dłubania w promptach
Typowa praca nad promptem wygląda tak: piszesz instrukcję, sprawdzasz na kilku przykładach, dopisujesz zdanie, znowu sprawdzasz. Po dwóch dniach masz tekst na półtorej strony, którego nikt nie chce ruszać, bo nie wiadomo, które zdanie za co odpowiada.
DSPy proponuje inne podejście. Opisujesz, co ma wejść i co ma wyjść, wybierasz strategię, definiujesz miarę jakości i podajesz przykłady. Optymalizator sam dobiera instrukcję i przykłady w prompcie, kierując się tą miarą. Biblioteka powstała w grupie badawczej Uniwersytetu Stanforda i jest otwarta na licencji MIT.
Sygnatura zamiast promptu
Sygnatura opisuje kontrakt: jakie pola wchodzą, jakie wychodzą i co znaczą. Nie zawiera instrukcji, bo ta ma powstać później.
pip install dspyimport dspy
class Klasyfikacja(dspy.Signature):
"""Klasyfikuje zgloszenie klienta do kategorii i priorytetu."""
tresc: str = dspy.InputField(desc="tresc zgloszenia od klienta")
kategoria: str = dspy.OutputField(desc="jedna z: logowanie, platnosci, dostawa, inne")
priorytet: int = dspy.OutputField(desc="od 1 do 5, gdzie 5 to najpilniejsze")
klasyfikator = dspy.Predict(Klasyfikacja)
wynik = klasyfikator(tresc="Nie moge sie zalogowac od wczoraj, pilne")
print(wynik.kategoria, wynik.priorytet)Pod spodem biblioteka buduje prompt z opisów pól, wysyła go do modelu i parsuje odpowiedź do zadeklarowanych pól. Sam nie piszesz ani jednego zdania instrukcji.
To rozwiązanie ma dwie zalety widoczne od razu. Pierwsza to czytelność: sygnatura mówi, co robi ten fragment, w pięciu linijkach zamiast w akapicie prozą. Druga to możliwość zmiany strategii bez ruszania reszty kodu.
Moduły, czyli strategie
Moduł określa, jak model ma dojść do odpowiedzi. Ta sama sygnatura z innym modułem daje inne zachowanie.
prosty = dspy.Predict(Klasyfikacja)
z_rozumowaniem = dspy.ChainOfThought(Klasyfikacja)
z_narzedziami = dspy.ReAct(Klasyfikacja, tools=[sprawdz_historie])Pierwszy pyta wprost. Drugi każe modelowi najpierw rozpisać rozumowanie, co poprawia trafność przy zadaniach wymagających kilku kroków, kosztem tokenów. Trzeci pozwala modelowi wywoływać narzędzia i pracować iteracyjnie.
Zmiana strategii to podmiana jednej linii, a nie przepisanie promptu. To właśnie odróżnia to podejście od pracy z tekstem: przy ręcznym prompcie dopisanie rozumowania oznacza przeredagowanie instrukcji i ryzyko, że przy okazji zepsujesz coś innego.
Moduły składa się w programy, gdzie wyjście jednego wchodzi do drugiego. Program jest zwykłą klasą, więc obowiązuje w nim cały Python: warunki, pętle, obsługa błędów.
Metryka, czyli rzecz najważniejsza
Cała biblioteka opiera się na jednym założeniu: potrafisz zmierzyć, czy odpowiedź jest dobra. Bez metryki optymalizator nie ma czego optymalizować.
def poprawnosc(przyklad, predykcja, trace=None) -> float:
trafiona_kategoria = przyklad.kategoria == predykcja.kategoria
bliski_priorytet = abs(przyklad.priorytet - predykcja.priorytet) <= 1
return (trafiona_kategoria + bliski_priorytet) / 2Metryka może być prostym porównaniem, sumą kilku warunków albo oceną innym modelem, gdy sprawdzasz rzeczy nieporównywalne wprost, jak trzymanie się faktów.
Tu leży realna trudność tego podejścia i warto ją nazwać wprost. Jeśli zadanie ma jednoznaczną poprawną odpowiedź, metryka jest oczywista i biblioteka daje dużo. Jeśli zadanie polega na napisaniu ładnego tekstu, metryki nie ma, a bez niej optymalizacja jest kręceniem gałkami na ślepo.
Zestaw przykładów też musi powstać. Pięćdziesiąt przypadków z oczekiwanym wynikiem to minimum sensowne, a przy trudniejszych zadaniach dwieście. To jest praca, tylko przeniesiona z redagowania promptu na przygotowanie danych.
Optymalizatory
Mając moduł, metrykę i przykłady, uruchamiasz optymalizator. Wynikiem jest ten sam program z dobranymi instrukcjami i przykładami w prompcie.
from dspy.teleprompt import MIPROv2
optymalizator = MIPROv2(metric=poprawnosc, auto="medium")
zoptymalizowany = optymalizator.compile(
klasyfikator,
trainset=przyklady_uczace,
valset=przyklady_sprawdzajace,
)
zoptymalizowany.save("klasyfikator.json")Do wyboru jest kilka podejść. Najprostsze dobiera przykłady do promptu spośród podanych przypadków. Bardziej rozbudowane przeszukuje przestrzeń instrukcji i przykładów naraz, oceniając kombinacje na zbiorze sprawdzającym.
Nowszym podejściem jest optymalizator refleksyjny, który analizuje pełne zapisy wykonania, wskazuje, który krok zawiódł, i proponuje poprawioną instrukcję dla tego konkretnego kroku. Przy programach wieloetapowych to daje więcej niż optymalizacja całości naraz, bo problem zwykle siedzi w jednym miejscu.
Wynik zapisujesz do pliku i wczytujesz w produkcji, więc optymalizacja jest krokiem budowania, a nie czymś, co dzieje się przy każdym żądaniu. Ten plik warto trzymać w repozytorium razem z kodem, bo wtedy wiadomo, która wersja programu odpowiada której wersji aplikacji.
Programy wieloetapowe
Pojedyncza sygnatura wystarcza do zadania, które da się opisać jednym wejściem i jednym wyjściem. Prawdziwe zastosowania rzadko takie są, więc moduły składa się w program.
class OdpowiedzNaPytanie(dspy.Module):
def __init__(self):
self.przeformuluj = dspy.ChainOfThought("pytanie -> zapytanie_wyszukiwania")
self.odpowiedz = dspy.ChainOfThought("kontekst, pytanie -> odpowiedz")
def forward(self, pytanie: str):
zapytanie = self.przeformuluj(pytanie=pytanie).zapytanie_wyszukiwania
fragmenty = self.baza.szukaj(zapytanie, limit=5)
return self.odpowiedz(kontekst=fragmenty, pytanie=pytanie)Zapis skrócony w cudzysłowie to sygnatura wpisana jednym wierszem, przydatna przy prostych krokach. Wyszukiwanie w bazie wektorowej pokroju Chromy jest tu zwykłym wywołaniem, bo program to normalny kod.
Największa zaleta ujawnia się przy optymalizacji. Optymalizator dostraja oba kroki naraz, kierując się jakością końcowej odpowiedzi, a nie jakością pośredniego zapytania. To ważne, bo najlepsze zapytanie wyszukiwania nie zawsze wygląda tak, jak zaprojektowałby je człowiek.
Przy takim układzie warto od początku myśleć o metryce dla całości. Ocena pośredniego kroku kusi, bo jest łatwiejsza, tylko że optymalizacja pod nią poprawia coś, co nie jest celem.
Ocena i porównywanie wersji
Poza optymalizacją biblioteka udostępnia narzędzie do pomiaru, którego warto używać nawet bez optymalizatora.
from dspy.evaluate import Evaluate
ocena = Evaluate(devset=przyklady_sprawdzajace, metric=poprawnosc, num_threads=8)
wynik = ocena(zoptymalizowany)
print(f"Trafnosc: {wynik}")Ta jedna liczba pozwala porównać wersje między sobą: przed optymalizacją i po niej, na modelu tańszym i mocniejszym, z rozumowaniem i bez. Bez niej decyzja o wyborze wariantu opiera się na wrażeniach z kilku ręcznych sprawdzeń.
Warto uruchamiać ocenę także w potoku budowania, tak samo jak zwykłe testy. Zmiana modelu u dostawcy albo aktualizacja biblioteki potrafi obniżyć trafność, a bez pomiaru zauważysz to dopiero po zgłoszeniach użytkowników.
Praktyczna uwaga o kosztach: ocena na dwustu przykładach to dwieście wywołań, więc przy każdym zapisie kodu to za dużo. Rozsądny podział to mały zestaw przy każdej zmianie i pełny przed wydaniem.
Ile to kosztuje
Optymalizacja to wiele wywołań modelu: każda kombinacja instrukcji i przykładów jest sprawdzana na zbiorze walidacyjnym. Przy dwustu przykładach i kilkudziesięciu kombinacjach idą tysiące wywołań.
Trzy sposoby ograniczają ten koszt. Pierwszy to optymalizacja na modelu tańszym i przeniesienie wyniku na mocniejszy, co często działa, bo dobrana instrukcja jest przenośna. Drugi to mniejszy zbiór sprawdzający, bo pięćdziesiąt przykładów zwykle wystarcza do porównania kombinacji. Trzeci to ustawienie trybu oszczędnego, który ogranicza liczbę prób.
Warto policzyć ten koszt przed uruchomieniem, mnożąc liczbę wywołań przez stawkę. Bywa, że jednorazowa optymalizacja kosztuje tyle, co miesiąc działania systemu, i wtedy trzeba zdecydować, czy poprawa to uzasadnia.
Szacunek warto potem zestawić z rachunkiem faktycznym, bo optymalizator wykonuje zwykle więcej wywołań, niż wynika z prostego mnożenia. Najmniejszym kosztem robi to warstwa pośrednicząca zapisująca każde wywołanie razem z ceną, bez zmian w kodzie poza adresem bazowym. Helicone działało dokładnie tak, ale po przejęciu przez Mintlify w marcu 2026 roku przeszło w tryb utrzymania, więc do nowego projektu trzeba sprawdzić, co z tej kategorii jest dziś rozwijane.
Kiedy to się opłaca, a kiedy nie
Opłaca się przy zadaniach mierzalnych i powtarzalnych: klasyfikacja, wydobywanie danych ze struktury, odpowiadanie na pytania z podanego kontekstu, przekształcenia o sprawdzalnym wyniku. Tam optymalizator zwykle znajduje układ lepszy od tego, który napisałby człowiek.
Opłaca się też przy zmianie modelu. Prompt dopracowany pod jeden model rzadko działa równie dobrze na innym, a ponowna optymalizacja to uruchomienie jednego polecenia zamiast dwóch dni redagowania.
Nie opłaca się przy jednorazowych zadaniach, przy tekstach ocenianych subiektywnie i przy prostych zastosowaniach, gdzie zwykły prompt daje wynik wystarczający. Koszt wejścia obejmuje przygotowanie zestawu i napisanie metryki, więc przy jednym prompcie w aplikacji ten nakład się nie zwróci.
Trzecim przypadkiem, w którym warto się wstrzymać, jest brak zgody na to, że prompt nie będzie czytelny dla człowieka. Wynik optymalizacji bywa dziwny: instrukcja z nieoczywistymi sformułowaniami i przykładami dobranymi w sposób, którego nikt by nie wymyślił. Działa lepiej i trudniej się o niej rozmawia, co bywa problemem w zespołach, gdzie treść promptu podlega uzgodnieniu z osobami spoza działu technicznego.
Budowanie zestawu przykładów
To najbardziej pracochłonna część i zarazem ta, która decyduje o wyniku. Kilka zasad skraca ją znacząco.
Pierwsza: bierz przykłady z prawdziwego ruchu, nie wymyślaj ich. Przypadki wymyślone przy biurku są zbyt czyste i pomijają dokładnie to, co w praktyce sprawia kłopot: literówki, zdania urwane w połowie, dwa pytania w jednej wiadomości.
Druga: włącz przypadki, w których obecne rozwiązanie zawodzi. To one niosą najwięcej informacji, bo zestaw złożony wyłącznie z przypadków łatwych nie pokaże żadnej różnicy między wariantami.
Trzecia: zachowaj proporcje zbliżone do rzeczywistych. Jeśli w praktyce dziewięćdziesiąt procent zgłoszeń dotyczy jednej kategorii, zestaw z równym podziałem na pięć kategorii doprowadzi do optymalizacji pod rozkład, który nie występuje.
Czwarta: rozdziel zbiór uczący od sprawdzającego przed pierwszym uruchomieniem. Podział na przykład siedemdziesiąt do trzydziestu wystarcza, a mieszanie tych zbiorów daje wynik, który wygląda dobrze i nie mówi nic.
przyklady = [
dspy.Example(tresc=t, kategoria=k, priorytet=p).with_inputs("tresc")
for t, k, p in dane_z_produkcji
]
uczacy, sprawdzajacy = przyklady[:140], przyklady[140:]Metoda wskazująca pola wejściowe jest tu obowiązkowa, bo bez niej biblioteka nie wie, co jest wejściem, a co oczekiwanym wyjściem.
DSPy kontra alternatywy
| Podejście | Mocna strona | Słabość | Kiedy wybrać |
|---|---|---|---|
| DSPy | Optymalizacja pod metrykę, łatwa zmiana modelu | Wymaga zestawu i metryki | Zadania mierzalne, powtarzalne, o dużym wolumenie |
| Ręczne prompty | Zero kosztu wejścia, pełna kontrola nad tekstem | Trudne w utrzymaniu, źle znoszą zmianę modelu | Proste zadania i prototypy |
| PydanticAI | Typowane wyjście, testy bez modelu | Brak optymalizacji promptu | Wynik trafiający wprost do kodu |
| LangChain | Największy zbiór integracji | Prompt nadal piszesz sam | Projekt łączący wiele źródeł i narzędzi |
Te podejścia nie wykluczają się. Można zbudować agenta w bibliotece agentowej, a pojedynczy krok wymagający wysokiej trafności, na przykład klasyfikację, zoptymalizować tutaj i wczytać gotowy wynik.
Warto też zauważyć, że korzyść jest największa tam, gdzie zadanie powtarza się tysiące razy dziennie. Poprawa trafności o kilka punktów procentowych przy takim wolumenie znaczy więcej niż przy stu wywołaniach na tydzień.
Jest jeszcze jeden zysk, o którym rzadko się mówi, a bywa decydujący przy dłuższym utrzymaniu. Zoptymalizowany program przechowuje instrukcję w pliku, a nie w kodzie, więc powrót do wersji sprzed miesiąca jest podmianą pliku. Przy prompcie wpisanym w kod ta sama operacja wymaga przekopania się przez historię zmian i odgadnięcia, która wersja obowiązywała, gdy system działał dobrze.
Podobnie wygląda sprawa przy zmianie dostawcy modelu. Zamiast redagować instrukcję pod nowy model, uruchamiasz optymalizację jeszcze raz i porównujesz wynik liczbowo. To zamienia decyzję o migracji z dyskusji o wrażeniach w porównanie dwóch liczb.
Typowe błędy
Pierwszy to metryka mierząca coś innego, niż Ci zależy. Optymalizator dopasuje program dokładnie do niej, więc miara nagradzająca długie odpowiedzi da długie odpowiedzi, niezależnie od tego, czy są lepsze.
Drugi to optymalizacja i sprawdzanie na tym samym zbiorze. Wynik wygląda świetnie i nie mówi nic o zachowaniu na nowych danych, bo program nauczył się właśnie tych przykładów.
Trzeci to zbyt mały zestaw. Dwadzieścia przykładów pozwala optymalizatorowi dopasować się do przypadkowości, a nie do prawidłowości.
Czwarty to uruchomienie optymalizacji bez policzenia kosztu. Tysiące wywołań na mocnym modelu potrafi kosztować więcej, niż ktokolwiek zakładał.
Piąty to traktowanie zoptymalizowanego programu jako gotowego na zawsze. Zmiana modelu, zmiana rozkładu danych albo nowa kategoria zgłoszeń wymagają ponownego przebiegu.
Szósty to sięganie po to podejście przy zadaniu, którego nie da się zmierzyć. Bez metryki cała maszyneria nie ma czego optymalizować, a ręczny prompt jest wtedy prostszy i uczciwszy.
FAQ
Czym DSPy różni się od pisania promptów?
Zamiast pisać instrukcję, opisujesz wejście, wyjście i miarę jakości, a instrukcję oraz przykłady dobiera optymalizator na podstawie Twoich danych. Koszt wejścia jest wyższy, bo trzeba przygotować zestaw i metrykę, a zysk pojawia się przy zadaniach mierzalnych o dużym wolumenie.
Czy DSPy jest darmowy?
Tak, biblioteka jest otwarta na licencji MIT. Płacisz wyłącznie za wywołania modelu, przy czym sama optymalizacja generuje ich wiele, więc to ona bywa głównym kosztem, a nie późniejsze działanie systemu.
Czy działa z dowolnym modelem?
Tak, warstwa modelu jest niezależna od dostawcy i obejmuje między innymi Claude, modele OpenAI oraz modele lokalne uruchomione przez Ollamę. Zmiana modelu wymaga zwykle ponownej optymalizacji, bo dobrana instrukcja jest dopasowana do konkretnego modelu.
Ile przykładów potrzeba?
Pięćdziesiąt to rozsądne minimum przy zadaniu klasyfikacyjnym, dwieście przy trudniejszym. Ważniejsza od liczby jest jakość: przykłady powinny pochodzić z prawdziwego ruchu i obejmować przypadki, w których system dotąd zawodził.
DSPy czy zwykła biblioteka agentowa?
To nie jest wybór rozłączny. Biblioteka agentowa organizuje przepływ i integracje, a to podejście optymalizuje pojedynczy krok wymagający wysokiej trafności. Częsty układ to agent zbudowany gdzie indziej z jednym zoptymalizowanym tutaj modułem w środku.
Dokumentacja stoi na stronie projektu, a kod źródłowy w repozytorium na GitHubie.