Minęło bardzo dużo czasu od ostatniej publikacji na temat mojego projektu Agent of Mass Production (AMP). Ostatnio pisałem o tym, jak budowałem asystenta do analizy danych Steama, było to w sierpniu 2024 roku. Ten tekst opublikowałem w lutym 2025 roku. Czy to znaczy, że przez te ostatnie miesiące AMP leżał odłogiem? Nie, wprost przeciwnie! Postanowiłem zmienić podejście związane z tworzeniem tego typu rozwiązań. Zacząłem się zastanawiać nad tym, aby asystentów powoli zastępować agentami. Podstawową różnicą miał być poziom autonomii.
Uznałem, że zmiany rozpocznę od czegoś prostego. Wybrałem moduł dotyczący analizy recenzji gier na Steamie. Nie chciałem go jednak po prostu odwzorować czy przenieść do innej struktury. Zależało mi na tym, aby go zmienić w taki sposób, żebym mógł wyciągnąć jeszcze więcej informacji z opinii napisanych przez graczy. Chciałem też, aby działanie tego rozwiązania było jak najbardziej autonomiczne. Asystenci są fajni, jednak konieczne jest zdefiniowanie procesu. Mimo że miejsca na podejmowanie decyzji jest sporo, chciałem, aby model językowy miał jeszcze więcej przestrzeni na tworzenie własnej drogi do analizowania opinii. Oczywiście w ustalonych ramach, o czym jeszcze napiszę.
Rozpisałem podstawy, wręcz fundamenty. Mam recenzje na Steamie — są to opinie napisane przez graczy na temat konkretnej gry. Gry, która ma tagi oraz jest przypisana przez twórców do określonych gatunków. To był mój kontekst, bardzo istotny element całego procesu. Dlaczego? To są właśnie te ramy, wewnątrz których ma się poruszać LLM. Nie zależy mi na tym, aby podsumował recenzje oraz zweryfikował sentyment dla jakiejś ogólnej gry, tylko dla konkretnego tytułu. Jest to istotne w momencie budowania podsumowania opartego na doświadczeniach gracza. RTS ma zupełnie inny zestaw problemów niż visual novel. Zauważyłem, że brak tego kontekstu sprawiał, że model językowy zaczynał udzielać odpowiedzi, które niekoniecznie pasowały do analizowanej gry.
Właśnie te problemy skłoniły mnie do przebudowy AMP. Jednak zanim pojawiły się pierwsze, świeże podsumowania, był długi okres wymiany mózgu. AMP przeszło gruntowną przemianę, która rozpoczęła się od tego, że byłem zmęczony pracą w LangChainie. Nie lubię go, a mimo to wciąż do niego wracałem i na dodatek łączyłem go z innym frameworkiem – LlamaIndex! A obok miałem jeszcze swoje rozwiązania, często w formie opakowanych zapytań do konkretnego modelu językowego, wraz ze wstrzykiwanym kontekstem w postaci odpowiedzi z bazy wektorowej! Praca z AMP była dla mnie coraz trudniejsza i coraz mniej przejrzysta. Uznałem, że coś trzeba z tym zrobić, że trzeba wjechać na szmacie w ten bałagan i raz na zawsze to wszystko poukładać!
U siebie jak u siebie…
Zacząłem od próby napisania bardzo prostego agenta bez użycia jakiegokolwiek frameworka. Absolutnie nie zależało mi na tym, aby coś takiego przygotowywać! Chciałem zobaczyć, z jakimi problemami trzeba się mierzyć przy tworzeniu takiej struktury. Miałem już trochę doświadczenia dzięki temu, że opracowałem kilku odrobinę autonomicznych asystentów. Jednak jakie problemy czyhają na człowieka, gdy zaczyna pisać agenta? W notatniku zapisałem sobie jedną uwagę, którą teraz chciałbym przytoczyć. Musiała być ważna, bo jest podkreślona! Uznałem, że skoro wszyscy piszą agentów oraz frameworki na nich oparte, to raczej mam do czynienia z prymitywną strukturą niż ze złożonym systemem. Z takim nastawieniem zacząłem eksperymenty, które zakończyłem po kilku tygodniach. Czemu? Czyżby agent miał być czymś znacznie bardziej skomplikowanym? Po kolei.
Swoje eksperymenty zacząłem od modułu związanego z recenzjami. Cały czas będę wracał do tego, że jest stosunkowo nieskomplikowany. Tutaj naprawdę nie ma zbyt wielu elementów, które wymagają kontroli lub dodatkowej weryfikacji. Wystarczy tylko przejrzeć recenzje, zinterpretować je, a potem podzielić na doświadczenia pozytywne i negatywne. Moim zdaniem jest to idealny proces do uczenia się pisania agentów! Już w tym opisie widać, że taka struktura potrzebuje dostać recenzje, oceniać je i przygotować podsumowanie na podstawie zebranych opinii.
Pierwszą, kluczową dla mnie kwestią, było zarządzanie pamięcią. Podzieliłem ją na krótkotrwałą oraz długotrwałą. Ta pierwsza przechowywała informacje o ostatnio zrealizowanych zadaniach oraz ich wyniku. Była też zbiorem informacji, który mógł, ale nie musiał zostać przekazany do pamięci długotrwałej! Co w niej było? Na przykład informacje na temat gry, nad którą pracował agent. Były ważnym kontekstem, do którego musiał mieć zawsze dostęp. Poza tym opracowane recenzje też musiały znaleźć swoje miejsce! Nie mogłem cały czas odpytywać API Steama, zależało mi na tym, aby agent pracował na pobranym i opracowanym zbiorze danych.
Mniej więcej miałem pomysł na pamięć. Teraz przyszedł czas na to, aby LLM opracował sobie plan działania i zaczął go realizować, wspomagając się obserwacjami na temat przetwarzanych danych. Przyznaję, że w tym miejscu jedna z iteracji agenta zaczęła przypominać talerz wymieszanego spaghetti bolognese. Za bardzo kombinowałem. Wymyślałem jakieś dodatkowe ewaluacje, które były wstrzykiwane gdzieś bokiem, dodawałem jakieś opcjonalne pytania do użytkownika. Niby efekty były coraz lepsze, ale zarządzanie danymi pojawiającymi się w procesie stawało się coraz mniej przejrzyste.
Postawiłem na prostą strukturę. LLM dostaje rolę, zadanie oraz kontekst w postaci informacji o grze. Ma zaplanować zestaw kroków, który umożliwi mu zrealizowanie danego zlecenia. Każdy zrealizowany krok jest zapisywany w pamięci krótkotrwałej, żeby wiedział, na jakim jest etapie, co przetworzył i jaki był efekt. Tutaj dawałem agentowi opcję podania w wątpliwość kierunku, w jakim zmierzał. Mógł przeanalizować zrealizowane kroki oraz ich efekty i podjąć decyzję, czy zbliża się do realizacji postawionego celu. Informacja ta trafiała do pamięci długotrwałej po to, żeby wiedział, gdzie był, zanim zaczął weryfikować zebrane odpowiedzi oraz czynności. Na początku ustawiłem to na sztywno, po połowie wykonanych kroków, ale w ramach testów uznałem, że agent sam zadecyduje, kiedy wykonać taką ewaluację. Efekt? Wyniki się poprawiły!
Nie opisałem jeszcze narzędzi, z których korzysta agent. Są ważne, ponieważ jakoś musi zdobywać potrzebne do wykonania zadania informacje. Opracowałem mały zestaw metod, które zwracały konkretne informacje. Przy pomocy LLMa opracowałem ich opis oraz przygotowałem zestawienie, do którego cały czas miał dostęp agent i zawsze poszukiwał konkretnego narzędzia w celu realizacji nadrzędnego celu oraz wykonania zaplanowanej akcji. Istotne dla mnie było to, aby nie sugerować użycia konkretnej metody, ale pozwalać agentowi na jej wybór. W tym momencie rozbudowałem krok związany z tworzeniem obserwacji na temat wykonanych akcji i zebranych informacji. Uznałem, że agent powinien mieć możliwość podania w wątpliwość wyboru narzędzia i zdecydować się na inne. A jeśli nie miał do czegoś dostępu? To też chciałem wiedzieć! Ta cała struktura, którą stopniowo opracowywałem była sterowana przez prompty, które były przechowywane w pamięci krótkotrwałej i przekazywane pomiędzy krokami. Istotne stało się logowanie pracy agenta wraz z jego obserwacjami oraz zużytymi tokenami.
W końcu trzeba wiedzieć, na co idą pieniądze. Jestem wrogiem bezmyślnego przepalania tokenów — co prawda w fazie testów potrafię to usprawiedliwić, ale później, gdy już dane rozwiązanie okrzepnie, konieczne jest wprowadzenie sensownej optymalizacji. Poza tym logowanie tego, co robią modele, pomogło mi też w zrozumieniu jak kierować ich uwagą. Samo dostarczanie wiedzy, kontekstu to jedno, istotne jest także to, aby model potrafił się skupić na tym, co jest istotne dla konkretnego zadania. Bez tego ostatniego elementu trudno mówić o poprawnej realizacji postawionego celu. Zresztą logi pomogły mi w aktualizowaniu promptów. Nie ukrywam, że przerzucałem je przez Claude’a i pokazywałem, jakie odpowiedzi otrzymywałem oraz pytałem, jak otrzymać takie, na których bardziej mi zależy. Ciekawe doświadczenie, bardzo pomocne przy budowaniu agenta, którego prompt może ulegać zmianie w trakcie wykonywania zadań.
Liczenie zużytych tokenów oraz logowanie akcji to były ostatnie elementy moich puzzli, z których poskładałem własnego, niewielkiego, agenta. Myślę, że to podsumowanie dobrze opisuje eksperyment, jaki przeprowadziłem. Efekt był dobry, później w poukładaniu tego wszystkiego pomogła mi publikacja „Building effective agents” opublikowana przez Anthropic. Szczególnie polecam ten cookbook, który jest pełen dobrych wzorców. Czy uważam, że każdy powinien sam spróbować zrobić podstawowego agenta? Niekoniecznie. Myślę, że bardzo mogą tutaj pomóc LLMy, zarówno w przypadku projektowania procesu, jak i podpowiadania konkretnych rozwiązań. Dla mnie było to ważne doświadczenie, ponieważ jestem osobą, która lubi zaglądać pod maskę i patrzeć na to, jak poszczególne trybiki ze sobą współpracują. Gdy już to wiem, to lepiej rozumiem swoje potrzeby. Dzięki czemu jestem w stanie określić, jaki framework z nimi rezonuje i jestem gotowy na zderzenie się z czymś nowym.
Czas sprawdzić PydanticAI!
Po długim okresie testów, budowania własnego agenta oraz odbijania się od różnych frameworków, uznałem, że czas się zatrzymać. Jak na złość, pojawiło się wtedy nowe świecidełko, czyli PydanticAI. Z Pydantic korzystam od dłuższego czasu, w dużej mierze za sprawą wykorzystywania structured output w komunikacji z LLMami. Jest to świetny sposób na kontrolowanie tego, co do mnie wraca, gdy model językowy zakończy zadaną pracę. Popatrzyłem na dokumentację i stwierdziłem, że w sumie to właśnie PydanticAI wygląda na opakowanie, które pomoże mi w pracy z agentami. Co mnie do tego przekonało? Myślę, że znalazło się tam kilka elementów, a na pewno na korzyść PydanticAI przemawia to, że jest zdecydowanie mniej skomplikowane niż LangGraph. Podoba mi się filozofia przyświecająca twórcom PydanticAI, czyli dostosowywanie technologii do potrzeb, a nie potrzeb do technologii. Czy do każdego problemu potrzebuję procesu złożonego na podstawie LangGraph? Nie. Wykorzystam tutaj konkretny przykład, o którym już kilka razy wspomniałem — proces analizy recenzji i wyciągania doświadczeń graczy.
Dobrym przykładem jest, cały czas powracające w tym tekście, analizowanie recenzji i wyciąganie doświadczeń graczy. Jakie narzędzia są tutaj potrzebne? Jak je zdefiniować używając PydanticAI? To jest akurat bardzo proste! Poniżej jest przykład dwóch narzędzi, z których korzysta SteamAgent. Pierwsze zwraca informacje na temat gatunków związanych z grą, drugie komunikuje się z bazą wektorową w celu znalezienia fragmentów recenzji odpowiadających na zadane pytanie.

Dwa małe narzędzia, bez żadnego dodatkowego ich opisywania, tylko docstring definiujący zadanie, jakie są w stanie rozwiązać. Na dodatek oba działają asynchronicznie, czyli agent jest w stanie sam je sobie wywołać, interpretując prompt, a potem zapisać wyniki. Oczywiście nic nie stoi na przeszkodzie, aby opracować bardziej skomplikowane narzędzia, jednak ja uważam, że siła leży w prostocie i dzieleniu problemu na mniejsze części. Przekonałem się o tym na własnej skórze. Co się stało? W trakcie pracy z recenzjami zauważyłem, że model zaczyna wymyślać gatunek gry, którą analizuję. Zgadywał na podstawie przesłanych tagów oraz opisu, do którego miał dostęp. Poprawność miał na poziomie rzutu monetą, co negatywnie wpływało na zapytania przesyłane do bazy wektorowej. Jeśli analizowałem opinie graczy RTSa, a model zaczynał rozważać, jaki wpływ na odbiorców mają dialogi z głównymi postaciami, to charakterystyka doświadczenia odbiorców była daleka od poprawności. Czego brakowało? Przed tym akapitem jest fragment kodu, a w nim narzędzie do zwracania gatunków z kontekstu, z którego korzysta agent. To wystarczyło!

Już kilka razy wspomniałem o kontekście, z którego korzysta agent. Myślę, że to dobry moment, aby o nim porozmawiać. W PydanticAI jest to struktura, która przechowuje kluczowe informacje dla wykonywanego procesu. Zerknij na obrazek wyżej. Jak widzisz, jest tam sporo danych, do których odnosi się agent. Jak na przykład opis gry, jej gatunki oraz tagi. Poza tym jest też informacja, z jakiej bazy danych będzie korzystać. To nie wszystko! W tej strukturze siedzą także pozyskane odpowiedzi, pytania, jakie agent wysyła do bazy wektorowej oraz liczba wyników, jakie ma ta baza zwrócić. Po co przechowuję odpowiedzi? Z nich agent tworzy podsumowanie dotyczące doświadczenia gracza, ale dzięki zdefiniowanym narzędziom jest w stanie je opracować, pozyskując informacje o grze, której recenzje analizuje, bo ma je w kontekście!
W jednej iteracji uznałem, że pozwolę agentowi na zamianę pytań, które sobie sam stworzył. Po prostu sprawdza, jakie ma już odpowiedzi i weryfikuje, czy nie poszukać czegoś innego w bazie wektorowej. Okazało się to strzałem w dziesiątkę, ponieważ sprawiło, że przygotowywane podsumowania stały się znacznie bardziej wyczerpujące. W podobny sposób zadziałało umożliwienie agentowi zmiany liczby wyników zwracanych przez bazę wektorową. Mógł to robić w zależności od pytania i, wbrew pozorom, nie zawsze wybierał maksymalną wartość zdefiniowaną w prompcie. Właśnie dlatego uważam, że takie procesy trzeba budować małymi krokami, testować nie tylko same prompty, ale także narzędzia, z jakich może korzystać agent. W przypadku PydanticAI istotne dla mnie było także to, że mogłem narzucić rodzaj wyniku, jaki ma zwrócić agent. Dlatego w kontekście określam, co jest czym — tam lista, tutaj string, a w innym miejscu int. Co równie istotne, każdy proces może mieć więcej niż jeden kontekst z różnymi przypisanymi narzędziami. Polecam zbudować coś takiego, po wprowadzeniu promptu zaczynają się dziać bardzo, bardzo interesujące rzeczy. Agent naprawdę potrafi zaskoczyć wyborem narzędzia lub zwróceniem uwagi, że czegoś mu brakuje i nie może wykonać danego zadania.
Cały czas piszę o tym agencie, a nigdzie nie pokazałem, jak wygląda. Jak się go uruchamia? Wcześniej opisałem, gdzie przechowuje dane i jak się do nich dostaje. Oto najprostszy agent w PydanticAI!

Od razu zaznaczam, że trzymanie promptów w głównym skrypcie to prosta droga do wpadnięcia do głębokiej studni chaosu. Siedziałem w niej na tyle długo, że nauczyłem się trzymać instrukcje dla LLMów w oddzielnym pliku. Łatwiej się je edytuje lub wysyła do innych modeli w celu weryfikacji lub przebudowania. Dlatego mam zmienną prompts
, która odwołuje się do pliku z instrukcjami. Ten agent korzysta z modelu GPT-4, ale to tylko przykład! PydanticAI pozwala na łatwe zmienianie modeli – po prostu są one traktowane jako warstwa, którą użytkownik może w dowolny sposób przerzucać. Tak powinno być, to słuszna droga, bo warto wymieniać modele i sprawdzać ich odpowiedzi! Oczywiście w dalszym ciągu istotne są tutaj prompty, bo w końcu to one instruują agenta, co ma robić.
Spójrz ponownie na fragment kodu, który umieściłem akapit wyżej. Zwróć uwagę na to, że w agencie istnieje zmienna system_prompt
, a przy wywołaniu metody run_sync()
, jako jeden z parametrów, również podaję prompt. W pierwszym przypadku prompt systemowy warto rozumieć jako nadrzędny cel, który ma zrealizować agent lub rolę, w jaką wchodzi. To jest to, co konstytuuje fundament uwagi całego procesu. W drugim przypadku, przy metodzie run_sync()
, jest to zadanie, jakie ma wykonać agent. Jest ono zestawiane z promptem systemowym, działa w obrębie wcześniej postawionych fundamentów. Dzięki temu agent jest w stanie wybrać i wykorzystać narzędzia, jakie ma dostępne, a także pobrać lub zapisać dane w kontekście — tutaj ważną rolę odgrywa zmienna deps
. Co możesz robić dynamicznie? Zmieniać prompt systemowy i wykonywać te same zadania, ale już z innym fundamentem. Modyfikować kontekst, z którego korzysta agent, może to być pomocne z perspektywy obsługi zapytań użytkownika lub złożonego procesu używającego więcej niż jednego agenta. Wtedy dany kontekst możesz przekazywać między nimi.
Jeśli chcesz zobaczyć cały kod tego agenta oraz wykorzystane w nim prompty, kliknij tutaj. Trafisz do mojego repozytorium, w którym zbieram różnego rodzaju eksperymenty związane z generatywną sztuczną inteligencją. Chcę tylko zaznaczyć, że w tym wpisie skupiłem się na podstawowych elementach PydanticAI. Zachęcam do przejrzenia dokumentacji oraz przykładów. Jest tam dużo ważnych informacji, które pokazują, jak budować bardziej skomplikowane procesy wykorzystujące złożone konteksty. Ja tylko powtórzę, że lepiej zaczynać od prostych rzeczy i powoli je rozwijać, niż rzucać się na coś dużego i czuć się źle z brakiem postępów.
AMP będzie się dalej rozwijać
Mam już kolejne dwa pomysły na moduły do AMP. Wstępnie je rozpisałem i teraz nieuchronnie zbliża się moment, w którym będę musiał odpalić VS Code i zacząć je składać. Po ukończeniu i testach przyjdzie chwila, aby napisać kilkaset słów na ten temat. Lubię traktować AMP jako podróż, w której zderzam się z różnymi problemami oraz aspektami pracy z modelami językowymi. Sam cel nie jest dla mnie aż tak istotny, ale nie będę ukrywał — lubię, gdy mój pomysł zaczyna faktycznie działać. Struktura wychodzi z fazy coś tam sobie skreśliłem w notatniku i zaczyna nabierać namacalnych kształtów.
Nic to! Czas brać się za kolejne moduły!
You must be logged in to post a comment.