Budowa agenta asystenta wiedzy w Databricks przy użyciu LangGraph
Stwórz agenta LangGraph w Databricks krok po kroku: minimalnego agenta, Vector Search RAG, mechanizm checkpointing w Lakebase do przechowywania krótkoterminowej pamięci oraz magazyn do przechowywania długoterminowej pamięci.
Źródła: Building Custom Code Agents on Databricks — Getting Started
Bazowy, dostosowany agent w Databricks może ewoluować od prostego wywołania modelu LLM do asystenta wiedzy, który przeszukuje dokumenty i pamięta użytkowników, przy czym każdy krok jest niewielki. Gotowy agent został stworzony przy użyciu frameworka langgraph. Posiada komponent do wyszukiwania wektorowego służący do znajdowania tekstu oraz moduł pamięci, dzięki czemu może przywołać to, co powiedział użytkownik końcowy, zarówno w ramach danej rozmowy, jak i w różnych rozmowach. Po uruchomieniu następuje ocena: pomiar wydajności podczas rozwoju oraz ponownie w środowisku produkcyjnym.
Przewodnik składa się z serii notatek, zgrupowanych w folderach dotyczących podstaw, pipeline’u RAG oraz mechanizmów pamięci.
Przygotowanie przestrzeni roboczej
Pełny agent zawiera indeksy wektorowe, narzędzia oraz logi, a one wymagają najpierw uporządkowanego środowiska. W Databricks warstwą zarządzania jest Unity Catalog (UC), który obsługuje wszystkie zasoby w ramach trójpoziomowego nazewnictwa. Uruchomienie notatek w folderze 00_foundations w odpowiedniej kolejności tworzy wszystkie zasoby, od których zależą późniejsze kroki, w tym mieszankę danych strukturalnych i niestrukturalnych, do których agent będzie się łączył.
Pamięć będzie przechowywana w Lakebase – bazie danych kompatybilnej z Postgresem, posiadającej funkcję automatycznego skalowania. Aby później można było bez problemu tworzyć tabele, notatka konfiguracyjna tworzy również projekt Lakebase oraz gałąź do ich przechowywania.
Pierwszy, minimalny agent
Następnie znajdują się notatki w folderze 01_rag_pipeline, począwszy od 0_your_first_agent_on_databricks.
Każdy agent jest zbudowany wokół modelu podstawowego. Databricks Model Serving wymienia dostępne modele, a ich lista może się różnić w zależności od konfiguracji Twojego środowiska pracy. Punkty końcowe są kompatybilne z formatem API OpenAI, ale dla lepszej integracji z langgraph oficjalna biblioteka databricks-langchain jest lepszym wyborem.
Kilka słów o langgraph. Agent produkcyjny rzadko polega na pojedynczym poleceniu – zazwyczaj zawiera logikę rozgałęziającą się, która zależy od pytania i od sposobu odpowiedzi użytkownika, a także pętle służące do konsultacji z zewnętrznymi systemami oraz przepisywania zapytań na podstawie otrzymanych odpowiedzi. Langgraph organizuje to poprzez modelowanie agenta jako grafu: węzły przechowują funkcje i wywołania LLM, a krawędzie określają, w jaki sposób kontekst przemieszcza się między nimi. Początkowy graf jest bardzo prosty.
Zawołaj agenta i pojawi się ślad działania, który wymienia pytanie wejściowe, wynik modelu oraz każdy krok logiczny pomiędzy nimi. Pochodzi to z MLflow, który integruje się natywnie z langgraph. Jedno wywołanie mlflow.langchain.autolog() na początku notatnika włącza te logi i diagnostykę. MLflow zostanie omówione bardziej szczegółowo później.
Ta pierwsza wersja po prostu przekazuje pytanie użytkownika modelowi. Prawdziwy agent dodaje prompt systemowy, aby ustalić ton, kształtować wynik oraz dostarczyć kontekstu tła – na przykład co ma robić asystent i na co powinien odmówić odpowiedzi. Dlatego w drugiej iteracji do grafu dodawany jest prompt systemowy.
Dodawanie funkcji wyszukiwania
Jednym z najczęściej spotykanych typów agentów jest agent do wyszukiwania wiedzy, nazywany również agentem RAG. Pomaga on ludziom przeszukiwać zbiór dokumentów i odpowiadać na pytania dotyczące ich treści. Aby agent miał tę możliwość, potrzebuje indeksu wyszukiwania wektorowego, który w istocie jest bazą danych dla tekstu niestrukturyzowanego.
Zeszyt 01_building_a_doc_store_on_vector_search opisuje proces tworzenia takiego indeksu. Zanim indeks może zostać przypisany do agenta, musi on już istnieć. Ta część koncentruje się na podstawowych krokach i pomija dostrojenie oraz optymalizację.
Tworzenie indeksu wymaga trzech działań:
- Parsowanie oznacza otwarcie dokumentu i przeczytanie jego treści.
- Dzielenie na fragmenty oznacza użycie narzędzia do podziału dokumentu na sekcje.
- Wkładanie wektorowe oznacza przekształcenie każdego fragmentu w wektor semantyczny przechowywany w indeksie.
Każdy z tych podejść ma swoje własne niuansy, a błędy na wczesnym etapie po cichu psują wszystko później. W tutoriale używane są domyślne ustawienia. W środowisku produkcyjnym różne dokumenty mogą wymagać różnych parserów do prawidłowego wydobywania tekstu, a różne przypadki użycia mogą wymagać innych rozmiarów fragmentów tekstu, tabel i rysunków, wraz z innymi modelami embedowania.
Databricks dostarcza narzędzia, które ułatwiają to zadanie. Volumes oferuje chmurowy, w pełni zarządzany magazyn obiektów do przechowywania plików surowych. Funkcja ai_parse_documents wydobywa tekst, rysunki i tabele przy minimalnej konfiguracji i dobrze skaluje się. Funkcje UDF w Spark mogą dzielić wydobyty tekst na fragmenty o różnej drobności, gotowe do embedowania za pomocą dowolnego z dostępnych modeli embedowania.
Gdy już istnieją embeddingi, wyszukiwanie podobieństw wymaga indeksu do wyszukiwania wektorowego. Nowy magazyn danych wydaje się dodatkowym obciążeniem, ale Delta Sync radzi sobie z tym poprzez automatyczne tworzenie embeddingów i indeksowania fragmentów w momencie ich dodawania, modyfikowania lub usuwania. Nie ma konieczności zarządzania żadną nową infrastrukturą ani utrzymywania zadań synchronizacji pomiędzy tabelą fragmentów a indeksem.
W pierwszych dwóch krokach notatnika pliki markdown utworzone podczas konfiguracji są przetwarzane i dzielone na nakładające się fragmenty po 800 znaków, a następnie ładowane do Databricks Vector Search. Biblioteka databricks_langchain zawiera gotowy element VectorSearchRetrieverTool, który łączy indeks z agentem langgraph.
Gdy indeks jest już przygotowany i przetestowany, trzeci krok polega na ponownym zdefiniowaniu agenta. Wygląda on bardzo podobnie do poprzednich wersji, z jedną dodatkową cechą: ToolNode, który przyjmuje listę narzędzi. W tym przypadku lista zawiera wyłącznie VectorSearchRetrieverTool.
W powstałym procesie pytanie trafia do szablonu promptu i przechodzi przez niego. Model widzi prompt wraz z narzędziami i ich opisami, a następnie decyduje, czy skorzystać z indeksu. Może to robić wielokrotnie, odpowiadając użytkownikowi dopiero wtedy, gdy znajdzie to, czego szukał.
To jest działający agent RAG. Graf pozostaje zwarty, a jedynym dodatkiem jest węzeł narzędziowy, co pokazuje, jak mało struktury potrzebuje pętla wyszukiwania. Asystent wiedzy wymaga jednak jeszcze jednego elementu: pamięci.
Dodawanie pamięci agentowi
W obecnej wersji agent nie ma żadnej pamięci. Pytanie następne, odnoszące się do poprzedniej odpowiedzi, po prostu nie działa, ponieważ model nie wie, co zostało powiedziane chwilę wcześniej.
Aby agent mógł brać pod uwagę wcześniejsze kroki w rozmowie, langgraph wymaga checkpointera, który przechowuje historię czatu. Checkpointery mogą mieć postać zwykłych plików tekstowych lub pełnych baz danych. Ze względu na skalowalność, opóźnienia i bezpieczeństwo lepszym wyborem jest baza danych, a w Databricks oznacza to Lakebase.
Pamięć dzieli się na dwa rodzaje. Pamięć krótkoterminowa obejmuje tylko bieżącą, aktywną sesję. Pamięć długoterminowa przechowuje informacje między sesjami. Prawie każda aplikacja potrzebuje pierwszej, a konieczność używania drugiej zależy od przypadku zastosowania.
Pamięć krótkoterminowa
Katalog 02_memory otwiera się razem z 01_short_term_memory, który dodaje wskaźnik kontrolny. Kroki od 1 do 3 tworzą StateGraph identyczny z poprzednim, a krok 4 pokazuje, jak niezręczna staje się rozmowa bez pamięci.
Stworzenie wskaźnika kontrolnego powiązanego z Lakebase przywraca ciągłość, a databricks_langchain oferuje wbudowaną klasę CheckpointSaver do jej utworzenia i przymocowania do grafu.
Agent to usługa udostępniana wielu użytkownikom, dlatego zapisane rozmowy wymagają właściciela. thread_id pełni tę funkcję. W aplikacji produkcyjnej front end powinien generować unikalny thread_id dla każdej aktywnej sesji, aby każdy użytkownik widział tylko swój własny historial rozmów.
Do kroku 6 wskaźnik kontrolny jest już w miejscu, a rozmowa przebiega naturalnie.
Pamięć długoterminowa
Pamięć krótkoterminowa znika po zakończeniu sesji. Aby zapamiętać użytkownika i jego preferencje, agent potrzebuje pamięci długoterminowej, a decyzja o jej dodaniu zależy od produktu. Asystent osobisty działający przez długi czas powinien pamiętać wcześniejsze rozmowy, takie informacje jak imię użytkownika oraz jego preferencje, np. krótkie, zwięzłe odpowiedzi w porównaniu z szczegółowymi wyjaśnieniami. Usługa pomocy w portalu klienta może lepiej nie przechowywać tych szczegółów, ponieważ oznaczałoby to długotrwałe obciążenie związane z zabezpieczaniem, szyfrowaniem i zarządzaniem danymi osobowymi.
Pamięć krótkoterminowa wymagała jedynie elementu CheckpointSaver. Pamięć długoterminowa wymaga odpowiedniego projektowania. Zazwyczaj nie chce się nagrywać każdej rozmowy na późniejsze użycie. Warto zachować informacje takie jak imię lub miejsce urodzenia oraz preferencje użytkownika.
Zeszyt 02_long_term_memory omawia te mechanizmy. Kroki od 1 do 4 odbudowują agenta RAG z pamięcią krótkoterminową, a krok 5 pokazuje, że preferencje znikają, gdy zmienia się sesja oraz thread_id.
Krok 6 wprowadza obiekt DatabricksStore, który przechowuje dane w Lakebase. Ta część jest prosta. Prawdziwym wyzwaniem projektowym jest decyzja o tym, co należy przechowywać. Krok 7 stosuje podejście podstawowe: do magazynu zapisywane jest kilka faktów ustalonych wcześniej w kodzie.
Aby je wykorzystać, krok 8 odtwarza agenta. Wcześniej węzeł agenta stanowiła zwykła funkcja, która wywoływała metodę invoke w modelu bazowym. Teraz jest to funkcja bardziej złożona – bierze identyfikator użytkownika (ustalony sztywno w demonstracji), pobiera dane tego użytkownika z DatabricksStore i łączy je w kontekst wysyłany do modelu. Ten wzorzec wyraźnie wymaga starannego zaprojektowania, aby funkcjonował skutecznie.
Bardziej elastyczne podejście pozwala samemu modelowi decydować, kiedy zapisywać, usuwać i pobierać informacje. Krok 12 implementuje wersję podstawową: trzy nowe narzędzia współpracują z modułem pamięci, a komenda systemowa ulega zmianie, aby poinformować model o nich i zachęcić do ich używania.
W rezultacie powstaje kompletny asystent, a każdą z tych funkcji można przetestować osobno, ponieważ została ona wprowadzona za pomocą odrębnego notatnika oraz odrębnej modyfikacji struktury grafu. Asystent przeszukuje dokumenty za pomocą indeksu wektorowego, śledzi bieżącą rozmowę dzięki pamięci krótkoterminowej oraz przechowuje szczegóły o użytkownikach pomiędzy sesjami za pomocą pamięci długoterminowej.
Kierunek dalszych rozwojów
Ścieżka rozwoju przechodziła od botu, który przekazywał pytania bezpośrednio do LLM, do wersji wyposażonej w szablon promptu, pamięć krótkoterminową i długoterminową oraz funkcję Vector Search do uzyskiwania kontekstu. Kolejnym krokiem jest wprowadzenie tego rozwiązania do produkcji: ocena jakości agenta, dodanie kolejnych funkcjonalności oraz udostępnienie go jako usługi dla użytkowników końcowych.