„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.

Cała struktura: katalog narzędzi uporządkowany w kodzie, jedna płaska lista podpięta do modelu i pętla agent → narzędzia → agent.

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.

DziedzinaNarzędziPrzykładyKiedy na liście
Firma4get_company_info, lookup_company_by_nipzawsze
Finanse1get_financial_summaryzawsze
Kontrahenci4get_contractors, create_contractorzawsze
Faktury10get_invoices, send_invoice, download_invoicezawsze
Użytkownicy wFirma3get_users, get_user_companieszawsze
Płatności5get_payments, add_paymentzawsze
Wydatki3get_expenses, create_expense_from_receiptzawsze
Pojazdy5get_vehicles, add_vehiclezawsze
Terminy i grupy terminów10get_terms, get_term_groupszawsze
Deklaracje i KPiR3get_jpk_vat_declaration, get_pit_declaration, get_tax_registerszawsze
Kalendarz podatkowy i Biała Lista2get_tax_deadlines, verify_bank_account_white_listzawsze, bez wFirma
Dokumenty4get_documents, download_documentzawsze
Księga: lata obrotowe i schematy4get_fiscal_years, get_accounting_schemaszawsze
Kadry i płace15get_employees, calculate_payroll, save_payroll_recordgdy podłączony jest serwis HR
KSeF9send_invoice_to_ksef, create_and_send_to_ksef, get_incoming_ksef_invoicesgdy podłączony jest serwis KSeF
Razem8258 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 modeluRozmiarSkąd liczba
Prompt systemowy: rola, język, reguły narzędzi, formatowanie, dane podatkowe 2026ok. 9 500 znakówzmierzony wynik budowania promptu dla pl, en i ru
Pamięć długoterminowado 20 wpisów i 6000 znakówstałe w serwisie pamięci
Opisy 82 narzędziok. 12 900 znaków; mediana 130, od 33 do 543zmierzone w plikach narzędzi
Opisy parametrów304 opisy, ok. 10 900 znakówzmierzone w plikach narzędzi
Historia rozmowytylko wiadomości użytkownika i odpowiedzi agentawyniki narzędzi nie są zapisywane
Wyniki narzędzi w bieżącej turzelisty do 100 faktur, wydatków lub kontrahentów, 50 dokumentów, 20 rekordów kadrowychlimity w kodzie narzędzi
Kroki na jedno pytaniemaks. 25recursionLimit 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.