„Jak zbudować pojedynczego agenta LangGraph to narzędzie dla programistów do budowania agentów AI, które realizują wieloetapowe procesy z rozgałęzieniami, zamiast odpowiadać jednym krokiem. Pełna definicja →, który ma ponad 80 narzędzi?” Dokumentacja LangGraph pokazuje mechanikę — StateGraph, ToolNode, bindTools — ale nie mówi, co zrobić, gdy katalog narzędzi przestaje mieścić się w głowie. Nasza odpowiedź z produkcji jest krótka: graf zostaje najprostszy z możliwych, dwa węzły w pętli, a cała struktura przenosi się do katalogu narzędzi. Tak działa agent eKsięgowy AI: 58 narzędzi bazowych i do 82 z modułami kadr i KSeF to obowiązkowy w Polsce Krajowy System e-Faktur do wystawiania i odbierania faktur bezpośrednio z organami skarbowymi. Pełna definicja →, bez routera i bez podagentów. Poniżej opisujemy, jak grupujemy narzędzia, jak je nazywamy i opisujemy, co trafia do Prompt to polecenie, które wpisujesz modelowi AI — od jego precyzji zależy, czy odpowiedź będzie użyteczna, czy ogólnikowa. Pełna definicja → systemowego, ile to wszystko zajmuje w kontekście i po czym poznamy, że czas tę strukturę zmienić. Przegląd całego produktu jest w artykule Jak działa Accounting AI Agent; ten tekst jest instrukcją, którą da się przenieść na inny projekt.
Krótka odpowiedź: jeden graf, dwa węzły, 82 narzędzia
Graf to StateGraph na stanie MessagesAnnotation. Węzeł agent wywołuje model z podpiętymi narzędziami (model.bindTools), a węzeł tools to gotowy ToolNode z tą samą listą. Warunkowa krawędź sprawdza jedno: czy ostatnia wiadomość modelu zawiera tool_calls. Jeśli tak — przechodzimy do narzędzi i wracamy do agenta; jeśli nie — koniec. Cały przebieg ma recursionLimit równy 25, więc agent nie zapętli się w niekończących się wywołaniach.
Lista narzędzi powstaje przy każdym żądaniu w jednej funkcji, createAllTools, bo narzędzia są domknięciami: każde dostaje identyfikator użytkownika, jego język i jego połączenie z wFirma. To ważne dla bezpieczeństwa — żadne narzędzie nie przyjmuje userId w argumentach, więc model nie może „poprosić” o dane innego klienta. I ważne dla struktury: skoro katalog i tak składamy w kodzie, w kodzie można go uporządkować.
Dlaczego nie router ani supervisor
W styczniu 2026 roku rozpisaliśmy wariant, który podpowiada intuicja: router i czterech specjalistów — od kontrahentów, finansów, faktur i podatków — każdy z własnym podzbiorem narzędzi i własnym promptem. Router miał zwracać JSON z nazwą specjalisty i poziomem pewności. Produkcyjny czat od początku działał jednak na jednym agencie z pełną listą narzędzi; kod routera nigdy nie został do niego podłączony i w kwietniu usunęliśmy go jako martwy.
Powód widać w samym prompcie routera. Instrukcja brzmiała: jeśli zapytanie dotyczy dwóch lub trzech obszarów, wybierz dominujący. Przykład z tego promptu: „Pokaż faktury i oblicz VAT do zapłaty” → specjalista podatkowy. W księgowości takie pytania są normą, a specjalista podatkowy nie miał narzędzi do faktur. Router zamienia pytanie przekrojowe w pytanie o jedną dziedzinę i do tego dokłada osobne wywołanie modelu przed każdą odpowiedzią. Płaska lista pozwala modelowi zrobić to, co zrobiłby księgowy: pobrać faktury z okresu przez get_invoices, a potem policzyć.
Uczciwie: nie robiliśmy testu A/B obu wariantów. To decyzja projektowa oparta na mechanizmie, nie na benchmarku. Router ma sens, gdy dziedziny naprawdę się nie mieszają albo gdy specjaliści potrzebują innych modeli czy innych uprawnień. W naszym przypadku żaden z tych warunków nie zachodził.
Grupuj w kodzie, nie w grafie
Skoro graf nie dzieli narzędzi, dzieli je kod. Każda dziedzina ma swój plik z fabrykami: invoice.tools.ts eksportuje dziesięć funkcji createXxxTool, contractor.tools.ts — cztery i tak dalej. Fabryka przyjmuje zależności (serwis wFirma, Cache to ponowne użycie tej samej, powtarzalnej części zapytania — dostawcy modeli liczą za nią mniej, więc dobrze ułożony prompt bywa kilka razy tańszy. Pełna definicja →, serwis subskrypcji), identyfikator użytkownika i język, a zwraca gotowe narzędzie LangChain ze schematem Zod. createAllTools składa wszystko w jedną tablicę, z komentarzem nad każdą grupą.
Gdy plik rośnie, dzielimy go dalej, nie zmieniając tego, co widzi model. Kadry to 15 narzędzi w czterech plikach — pracownicy (5), umowy (3), płace (4), nieobecności i podsumowanie (3) — schowanych za jednym plikiem zbiorczym hr.tools.ts. Dla modelu to nadal 15 pozycji na płaskiej liście; dla programisty to cztery pliki po 170–330 linii zamiast jednego wielkiego.
Grupy opcjonalne dokładamy warunkowo: 15 narzędzi kadrowych trafia na listę tylko wtedy, gdy podłączony jest serwis HR, a 9 narzędzi KSeF — gdy podłączony jest serwis KSeF. To najtańszy sposób na mniejszy katalog: narzędzie, którego nie da się wykonać, nie powinno zajmować miejsca w kontekście. Dwa narzędzia — kalendarz terminów podatkowych i weryfikacja rachunku na Białej Liście — nie zależą od wFirma i są dostępne zawsze.
| Dziedzina | Narzędzi | Przykłady | Kiedy na liście |
|---|---|---|---|
| Firma | 4 | get_company_info, lookup_company_by_nip | zawsze |
| Finanse | 1 | get_financial_summary | zawsze |
| Kontrahenci | 4 | get_contractors, create_contractor | zawsze |
| Faktury | 10 | get_invoices, send_invoice, download_invoice | zawsze |
| Użytkownicy wFirma | 3 | get_users, get_user_companies | zawsze |
| Płatności | 5 | get_payments, add_payment | zawsze |
| Wydatki | 3 | get_expenses, create_expense_from_receipt | zawsze |
| Pojazdy | 5 | get_vehicles, add_vehicle | zawsze |
| Terminy i grupy terminów | 10 | get_terms, get_term_groups | zawsze |
| Deklaracje i KPiR | 3 | get_jpk_vat_declaration, get_pit_declaration, get_tax_registers | zawsze |
| Kalendarz podatkowy i Biała Lista | 2 | get_tax_deadlines, verify_bank_account_white_list | zawsze, bez wFirma |
| Dokumenty | 4 | get_documents, download_document | zawsze |
| Księga: lata obrotowe i schematy | 4 | get_fiscal_years, get_accounting_schemas | zawsze |
| Kadry i płace | 15 | get_employees, calculate_payroll, save_payroll_record | gdy podłączony jest serwis HR |
| KSeF | 9 | send_invoice_to_ksef, create_and_send_to_ksef, get_incoming_ksef_invoices | gdy podłączony jest serwis KSeF |
| Razem | 82 | 58 bazowych + 15 + 9 | — |
Nazwy: czasownik_rzeczownik i pary lista–szczegóły
Wszystkie 82 nazwy są unikalne i mają kształt czasownik_rzeczownik w snake_case: get_invoices, get_invoice_details, create_invoice, update_invoice, delete_invoice. Lista i szczegóły zawsze idą w parze — get_payments i get_payment_details, get_vehicles i get_vehicle_details — więc model, który poznał jedną dziedzinę, przewiduje nazwy w następnej.
Nasz katalog nie jest tu idealny i warto to pokazać. Kontrahentów i faktury tworzy create_, a płatności, pojazdy i terminy — add_; to ślad po tym, jak powstawały kolejne moduły. Są też nazwy blisko siebie: send_invoice wysyła fakturę e-mailem, a send_invoice_to_ksef — do państwowego systemu e-faktur. Przy płaskiej liście takie bliskie pary trzeba rozbroić opisem, bo nazwa sama ich nie rozdzieli. Jeśli zaczynasz od zera, ustal słownik czasowników przed pierwszym narzędziem, a nie po pięćdziesiątym.
Opisy narzędzi robią routing
W agencie bez routera to opisy decydują, które narzędzie wybierze model. Mediana długości opisu w naszym katalogu to 130 znaków, najkrótszy ma 33 (add_invoice_note), najdłuższy 543 (verify_bank_account_white_list). Ten rozrzut jest celowy: długość wydajemy tam, gdzie narzędzia się zderzają, a nie po równo na wszystkie.
Najlepiej widać to na parze faktur i wydatków. Do kwietnia 2026 roku opis get_invoices brzmiał: „Get list of invoices from wFirma. Can filter by year, month, and payment status”. Model mylił faktury sprzedaży z kosztami i potrafił pominąć filtr dat, choć użytkownik podał miesiąc. Poprawka zmieniła tylko opisy i schemat. get_invoices mówi teraz wprost, że zwraca faktury WYCHODZĄCE, wystawione klientom, a po rachunki od dostawców trzeba sięgnąć do get_expenses; opis get_expenses mówi to samo w drugą stronę. Oba dodają regułę: jeśli w pytaniu pada okres, przekazanie dateFrom i dateTo jest obowiązkowe, a wynik bez filtra to błąd.
Opis może też wskazać narzędzie, które trzeba wywołać obok. get_terms zwraca terminy, które użytkownik sam wpisał w wFirma; jego opis dodaje, że gdy ktoś pyta ogólnie o terminy (także po rosyjsku, „сроки”), należy wywołać również get_tax_deadlines z ustawowymi terminami VAT, CIT, ZUS i PIT. Bez tego jednego zdania agent odpowiadałby na pytanie o terminy samymi notatkami użytkownika.
Prompt systemowy: tylko reguły ponad jednym narzędziem
Opis narzędzia mówi o tym narzędziu. Zasady, które łączą kilka narzędzi albo zależą od przebiegu rozmowy, trzymamy w sekcji „Tool Usage Guidelines” promptu systemowego. Są tam trzy rodzaje reguł.
Pierwsza to granica między narzędziami a wiedzą: narzędzia — gdy użytkownik pyta o swoje dane (faktury, kontrahenci, wypłaty), wiedza modelu — gdy pyta o przepisy w ogóle. Druga to przepływy wieloetapowe. calculate_payroll liczy tylko podgląd, a save_payroll_record liczy i zapisuje; prompt mówi, że słowa „save”, „zapisz”, „сохранить”, „записать” zawsze oznaczają save_payroll_record, a do zastąpienia rekordu służy delete_payroll_record i ponowny zapis — nigdy narzędzia płatności. Trzecia to obowiązki prawne: przed zapisaniem płatności od 15 000 zł dla polskiego kontrahenta agent wywołuje verify_bank_account_white_list, bo tego wymaga art. 117ba Ordynacji podatkowej.
Zwróć uwagę, że reguły wyzwalające wymieniają słowa w kilku językach. Agent odpowiada po polsku, angielsku i rosyjsku, a język wykrywa z treści wiadomości, więc reguła napisana tylko po angielsku nie zadziała na „zapisz wypłatę”. Poza tymi regułami prompt nie wylicza narzędzi. Robi to bindTools, a lista narzędzi w prompcie byłaby drugą kopią, która prędzej czy później rozjedzie się z kodem.
Parametry: jeden kształt dla narzędzi z rodzeństwa
Ta sama kwietniowa poprawka ujednoliciła schematy. Wcześniej get_invoices przyjmował rok i miesiąc, a get_expenses coś innego; teraz oba przyjmują dateFrom i dateTo w formacie YYYY-MM-DD, a opis parametru zawiera przykład: dla „April 2026” podaj 2026-04-01 i 2026-04-30. Model, który nauczył się filtrować jedną listę, filtruje każdą.
Parametry opcjonalne zapisujemy w Zod jako .nullable().optional() — w katalogu jest ponad 200 takich pól. To dostosowanie do trybu strict w OpenAI: według dokumentacji function calling wszystkie pola muszą być oznaczone jako wymagane, a opcjonalność wyraża się dopuszczeniem typu null. Gdzie zbiór wartości jest zamknięty, używamy enum — na przykład status faktury: all, paid, unpaid, overdue, draft, issued, sent — żeby model nie zgadywał pisowni. Opisów parametrów jest w katalogu 304, i to one niosą przykłady formatów.
Budżet promptu: ile zajmują 82 narzędzia
To pytanie, na które dokumentacja nie odpowiada, a które przy dużym katalogu jest najważniejsze. Zmierzyliśmy statycznie teksty, które idą do modelu przy każdym wywołaniu.
| Co trafia do każdego wywołania modelu | Rozmiar | Skąd liczba |
|---|---|---|
| Prompt systemowy: rola, język, reguły narzędzi, formatowanie, dane podatkowe 2026 | ok. 9 500 znaków | zmierzony wynik budowania promptu dla pl, en i ru |
| Pamięć długoterminowa | do 20 wpisów i 6000 znaków | stałe w serwisie pamięci |
| Opisy 82 narzędzi | ok. 12 900 znaków; mediana 130, od 33 do 543 | zmierzone w plikach narzędzi |
| Opisy parametrów | 304 opisy, ok. 10 900 znaków | zmierzone w plikach narzędzi |
| Historia rozmowy | tylko wiadomości użytkownika i odpowiedzi agenta | wyniki narzędzi nie są zapisywane |
| Wyniki narzędzi w bieżącej turze | listy do 100 faktur, wydatków lub kontrahentów, 50 dokumentów, 20 rekordów kadrowych | limity w kodzie narzędzi |
| Kroki na jedno pytanie | maks. 25 | recursionLimit grafu |
Wniosek jest jeden: przy 82 narzędziach największą stałą częścią kontekstu jest katalog, a nie prompt systemowy. Same opisy narzędzi i parametrów to blisko 24 tysiące znaków, a cały prompt systemowy razem z danymi podatkowymi — około 9,5 tysiąca, czyli dwa i pół raza mniej. Przy zgrubnej regule czterech znaków na Token to fragment tekstu, zwykle kawałek słowa, w którym modele AI mierzą i długość zapytania, i koszt — rachunek wystawiany jest za tokeny, nie za pytania. Pełna definicja →, której nasz kod używa do budżetu pamięci, sama proza katalogu to rząd 6 tysięcy tokenów; serializowane schematy JSON dokładają do tego nazwy pól, typy i wartości enum, czego osobno nie mierzyliśmy. OpenAI pisze wprost, że definicje funkcji liczą się do limitu kontekstu i są rozliczane jako tokeny wejściowe.
Ten koszt płaci się przy każdym wywołaniu węzła agent, a nie raz na pytanie: odpowiedź, która wymaga dwóch narzędzi po kolei, to trzy wywołania modelu z pełnym katalogiem. Dlatego tniemy tam, gdzie to tanie. Grupy opcjonalne wypadają, gdy nie da się ich użyć. Pamięć długoterminowa ma twardy limit 20 wpisów i 6000 znaków. A historia rozmowy zapisywana w bazie zawiera tylko wiadomości użytkownika i odpowiedzi agenta — wyniki narzędzi żyją jedną turę i nie narastają z każdą wymianą. Jak przełożyć tokeny na złotówki, pokazujemy w artykule ile kosztuje agent AI miesięcznie.
Wyniki narzędzi: gotowy markdown, limity i błędy jako tekst
Drugą stroną budżetu jest to, co narzędzia zwracają. Nasze narzędzia nie oddają surowego JSON z API to interfejs, przez który dwa systemy wymieniają dane automatycznie — bez eksportów do Excela i przeklejania ręką. Pełna definicja → wFirma. Każde ma formatter, który zwraca gotowy markdown — tabelę, nagłówek, kilka pogrubionych pól — od razu w języku użytkownika i z polskim formatem liczb. Model dostaje mniej tekstu, nie musi go przepisywać, a prompt każe mu zachować to formatowanie bez zmian.
Listy mają limity: faktury, wydatki i kontrahenci — 100 pozycji, dokumenty — 50, rekordy kadrowe — domyślnie 20. Bez limitu jedno „pokaż wszystkie faktury” u klienta z kilkuletnią historią mogłoby zająć więcej kontekstu niż cały katalog narzędzi.
Błędy nie przerywają grafu. Narzędzie łapie wyjątek i zwraca krótki komunikat w języku użytkownika — że nie udało się pobrać faktur albo że wyczerpano limit zapytań do wFirma — a prompt każe modelowi wyjaśnić, co poszło nie tak, i zaproponować następny krok zamiast zmyślać dane. Treść wyjątków z zewnętrznych systemów przechodzi przez funkcję sanitizeForPrompt: przycięcie do 500 znaków i usunięcie nagłówków oraz pogrubień markdown, żeby dane z zewnątrz nie wyglądały dla modelu jak instrukcje.
Kiedy zmienilibyśmy tę strukturę
Przewodnik OpenAI radzi, by na początku tury mieć mniej niż 20 funkcji — jako miękką sugestię, nie limit. My mamy ich ponad czterokrotnie więcej i ten układ działa w produkcji, ale nie mamy benchmarku, który mówiłby, o ile lepiej działałby mniejszy katalog. Wiemy natomiast, po czym poznamy, że płaska lista przestała wystarczać: dwa narzędzia mylą się dalej mimo odsyłaczy w obu opisach, katalog rośnie o dziedziny, które z niczym się nie łączą, albo koszt katalogu w każdym kroku zaczyna dominować w rachunku.
Są wtedy trzy drogi. Pierwsza to wybór narzędzi przez wyszukiwanie: biblioteka langgraph-bigtool indeksuje opisy narzędzi i daje agentowi narzędzie do wyszukiwania pozostałych, a OpenAI oferuje tool search, który odkłada rzadko używane narzędzia — ale tylko w modelach od gpt-5.4. Druga to podgrafy dla dziedzin, które naprawdę się nie mieszają. Trzecia, często najtańsza, to scalenie narzędzi CRUD jednej dziedziny w mniej narzędzi z parametrem operacji — u nas kandydatami byłyby terminy i grupy terminów, dziesięć narzędzi o bardzo podobnym kształcie.
Jedno ograniczenie jest u nas szczególne i może dotyczyć także ciebie. Klienci podłączają własne klucze: domyślnie to gpt-4o w OpenAI albo gemini-2.5-flash w Google, a użytkownik może wybrać inny model. Struktura agenta musi więc działać tak samo u obu dostawców, dlatego mechanizmy dostępne tylko u jednego z nich, takie jak tool search w OpenAI, nie mogą być jej fundamentem.
Lista kontrolna
Graf. StateGraph z dwoma węzłami, ToolNode, warunkowa krawędź na tool_calls i recursionLimit. Nic więcej, dopóki nie ma konkretnego powodu.
Katalog. Jeden plik z fabrykami na dziedzinę, jedna funkcja składająca listę, grupy opcjonalne dodawane tylko wtedy, gdy da się je wykonać. Kontekst użytkownika w domknięciu, nie w argumentach.
Nazwy i opisy. Jeden słownik czasowników, pary lista–szczegóły, odsyłacze w obie strony między narzędziami, które się mylą, i więcej słów tam, gdzie dochodzi do pomyłek.
Prompt. Tylko reguły obejmujące kilka narzędzi, ze słowami-wyzwalaczami we wszystkich językach użytkowników. Bez listy narzędzi.
Budżet. Zmierz katalog, ogranicz listy w wynikach, oddawaj gotowy markdown, nie przenoś wyników narzędzi do historii i pamiętaj, że katalog płaci się w każdym kroku pętli.
Jak ten agent wygląda w całości — z KSeF, OCR (Optical Character Recognition) zamienia skan albo zdjęcie dokumentu w tekst, który komputer potrafi odczytać — pierwszy krok, żeby faktura z e-maila trafiała do systemu sama. Pełna definicja → paragonów i pamięcią długoterminową — opisaliśmy w artykule Jak działa Accounting AI Agent, a sam produkt jest na stronie eKsięgowy AI.
Najczęstsze pytania
- Ile narzędzi może mieć jeden agent LangGraph?
- LangGraph sam nie wprowadza limitu: ToolNode i bindTools przyjmują dowolną listę. Ograniczeniem jest model — trafność wyboru i koszt kontekstu, bo definicje wszystkich narzędzi idą przy każdym wywołaniu. OpenAI sugeruje mniej niż 20 funkcji na początku tury, ale jako miękką wskazówkę. Nasz agent eKsięgowy AI działa w produkcji z 58 narzędziami bazowymi i do 82 z modułami kadr i KSeF.
- Czy agent z ponad 80 narzędziami potrzebuje routera albo supervisora?
- Nie zawsze. Router opłaca się, gdy dziedziny się nie mieszają albo specjaliści potrzebują innych modeli lub uprawnień. W księgowości pytania są przekrojowe, jak „pokaż faktury i policz VAT”, a router wybiera jedną dziedzinę i dokłada wywołanie modelu. Dlatego zostaliśmy przy jednym agencie z płaską listą, a strukturę przenieśliśmy do katalogu narzędzi: plików na dziedzinę, nazw i opisów.
- Jak sprawić, żeby model nie mylił podobnych narzędzi?
- Opisem, w obie strony. Każde z mylonych narzędzi wymienia drugie z nazwy i mówi, kiedy wybrać tamto — u nas get_invoices (faktury wychodzące) i get_expenses (wydatki przychodzące). Do tego ten sam kształt parametrów w narzędziach z rodzeństwa, na przykład dateFrom i dateTo, oraz reguły w prompcie systemowym dla przepływów wieloetapowych, takich jak podgląd wypłaty kontra jej zapis.
- Ile kontekstu zajmują definicje 82 narzędzi?
- U nas same opisy narzędzi i parametrów to blisko 24 tysiące znaków — około dwa i pół raza więcej niż cały prompt systemowy, który ma około 9500 znaków. Przy zgrubnej regule czterech znaków na token to rząd 6 tysięcy tokenów, bez narzutu schematów JSON. Płaci się to przy każdym kroku pętli agenta, nie raz na pytanie.
- Kiedy przejść na dynamiczny wybór narzędzi?
- Gdy podobne narzędzia mylą się mimo poprawionych opisów, gdy dochodzą dziedziny niezwiązane z resztą albo gdy koszt katalogu dominuje w rachunku. Opcje to wyszukiwanie narzędzi (langgraph-bigtool, tool search w OpenAI od gpt-5.4), podgrafy dla rozłącznych dziedzin albo scalenie narzędzi CRUD. Jeśli użytkownicy wybierają różnych dostawców modeli, unikaj rozwiązań dostępnych tylko u jednego.