Jak agenty LangGraph przetrwają restarty: punkty kontrolne, sterowniki punktów kontrolnych, wątki
Dowiedz się, jak działa trwałość w LangGraph: dlaczego bez niej agenci tracą wszystko, oraz jak stan, punkty kontrolne, narzędzia do ich tworzenia i identyfikatory wątków umożliwiają wznowienie działania po awarii.
Agent, który przechowuje wszystko w pamięci procesu, zapomina o tym natychmiast po zatrzymaniu procesu: zarówno uruchomienie, awaria, nierozwiązany timeout, jak i sam koniec żądania wystarczą, by usunąć całą rozmowę oraz wszystkie pośrednie wyniki. LangGraph rozwiązuje ten problem dzięki mechanizmowi trwałości, który rejestruje stan grafu po każdym kroku, dzięki czemu można przerwać jego działanie, przywrócić i kontynuować go zamiast powtarzać od początku. Ten przewodnik buduje model mentalny od podstaw: co idzie nie tak bez mechanizmu trwałości, w jaki sposób stan LangGraph jest powiązany z punktami kontrolnymi, czym jest punkt kontrolny, w jaki sposób identyfikatory wątków oddzielają różne rozmowy oraz jakie jest miejsce zajmowane przez punkt kontrolny w pamięci. Pod koniec będziesz w stanie dodać mechanizm trwałości do grafu, zrozumieć dokładnie, co i kiedy jest zapisywane, oraz wiedzieć, dlaczego opcja działająca w pamięci jest jedynie narzędziem rozwojowym.
Persistentność jest również podstawą kilku funkcji, które zwykle omawia się oddzielnie: kroki wymagające potwierdzenia przez człowieka, wywoływanie narzędzi w trakcie wielu rund oraz agenci działające przez długi czas – wszystkie one polegają na możliwości zatrzymania procesu i jego wznowienia później. Zrozumienie tego koncepcji upraszcza poznawanie tych funkcji.
Dlaczego agenci potrzebują stanu trwającego dłużej niż sam proces
Pomyśl o pisaniu długiego dokumentu przez dwie godziny bez zapisywania postępów, a potem o przerwie w dostępie do energii. Wszystko, co napisałeś, znika i musisz zacząć od nowa od pierwszej strony. Agent bez możliwości przechowywania stanu znajduje się w dokładnie takiej sytuacji za każdym razem, gdy jest restartowany. Nie ma żadnych informacji o tym, co wydarzyło się kilka sekund wcześniej, a tym bardziej kilka dni temu.
W jednym zdaniu wytrwałość oznacza przechowywanie stanu aplikacji w jakimś trwałym miejscu, aby można go było przywrócić i kontynuować po zatrzymaniu programu. Działa jak automatyczna кнопka zapisu, która uruchamia się po niemal każdym kroku, bez konieczności jej naciskania przez użytkownika.
To ma większe znaczenie dla agentów niż w przypadku klasycznego kodu żądanie/odpowiedź, ponieważ nowoczesne agenty rzadko składają się z jednego pytania i jednej odpowiedzi. Zazwyczaj:
- kontynuują rozmowy przez godziny lub dni;
- zatrzymują się i czekają, aż osoba zatwierdzi jakąś akcję;
- łączą kilka wywołań narzędzi, aby ukończyć jedno zadanie;
- przez długi czas podejmują decyzje po wielu krokach;
- muszą funkcjonować nawet po restartach i awariach, nie tracąc postępów.
RAM jest szybki, ale ulotny – zostaje wyczyszczony po zakończeniu procesu. Wszystko, czego agent potrzebuje poza czasem trwania jednego procesu, musi zostać zapisane w trwałym magazynie i odczytane później. W LangGraph to właśnie taki magazyn oraz mechanizmy zarządzające nim są tym, co oznacza pojęcie „trwałości”.
Co się psuje, gdy agent nie ma trwałości
Najłatwiej zrozumieć ten problem poprzez konkretne przypadki awarii. Każdy z poniższych jest częsty w warunkach produkcyjnych.
Utrata zasilania w trakcie długiego zadania
Agent podsumowuje 200-stronicowy raport i dotarł do strony 100, gdy maszyna traci zasilanie. Bez zapisanego stanu nie ma żadnego dowodu na to, że połowa pracy została wykonana, więc kolejna próba rozpoczyna się od strony 1.
Zwykłe restartowanie serwera
Agent działa na serwerze chmurowym, a zwykłe rozruchowanie ponownie go uruchamia. Każdy użytkownik, który był w trakcie rozmowy, traci swoją historię. Chatbot nawet nie pamięta już imienia użytkownika podanego dwa minuty wcześniej.
Awaria w trakcie wieloetapowego zadania
Jako część większego zadań agent wywołuje zewnętrzną API, która wygaśnia, a proces w Pythonie kończy pracę z powodu nieobsłużonej wyjątku. Nieudany etap zostaje utracony, podobnie jak wszystko, co agent ukończył przed nim.
Przepływy pracy trwające godziny lub dni
Część agentów jest celowo wolna. Wyobraźmy sobie takiego, który monitoruje ceny akcji i działa tylko wtedy, gdy zostanie przekroczony określony próg. Jeśli cały jego stan znajduje się w pamięci, nie można go zawiesić, ponownie rozprostować ani przenieść na inny komputer bez rozpoczęcia pracy od nowa.
Ludzka akceptacja trwająca godziny
Agent sporządza dokument prawny, który musi zostać zatwierdzony przez prawnika przed wysłaniem. Prawnik nie może sprawdzać kolejki przez sześć godzin. Utrzymywanie procesu w stanie zamrożenia i zajmowanie zasobów przez tak długi czas jest marnotrawstwem, a jeśli serwer zostanie ponownie uruchomiony w trakcie oczekiwania, zadanie znika.
Rozumowanie wieloetapowe, które zawodzi późno
Złożone agenty często dzielą pracę na cykl planowania, wyszukiwania, weryfikacji i podsumowywania. Jeśli siódmy z dziesięciu kroków zawiedzie, ponowne wykonanie kroków od pierwszego do szóstego marnuje czas, połączenia API oraz pieniądze.
Dlaczego „po prostu uruchom to ponownie” nie jest strategią
Ponowne rozpoczęcie od zera wydaje się do przyjęcia, dopóki nie policzymy kosztów:
- Pieniądze. Każde połączenie z LLM zużywa tokeny, więc ponowne wykonanie sześciu udanych kroków z powodu niepowodzenia siódmego oznacza konieczność zapłaty za nie dwa razy.
- Czas. Użytkownicy muszą czekać, podczas gdy praca, na którą już czekali, jest powtarzana.
Ostatni punkt jest najważniejszy. Celem trwałości nie jest tylko oszczędność wysiłku; chodzi o to, aby aplikacja mogła się zawiesić, awariować, uruchomić ponownie lub czekać, a następnie kontynuować od miejsca, w którym faktycznie się zatrzymała, dzięki czemu ukończone prace pozostają ukończone.
Dokładna definicja trwałości
Biorąc pod uwagę ten problem, przydatna jest bardziej precyzyjna definicja: trwałość to zdolność systemu do zapisywania swoich wewnętrznych danych, swojego stanu, w trwałym magazynie, tak aby dane przetrwały bieżące uruchomienie i mogły zostać ponownie załadowane, by kontynuować wykonywanie z dokładnie tego samego miejsca.
To określenie obejmuje trzy odrębne funkcje:
- Zapisywanie stanu: rejestrowanie tego, co agent obecnie wie oraz co zrobił.
- Odzyskiwanie poprzedniej eksploatacji: odczytywanie tych zapisów ponownie, nawet po restartowaniu.
- Kontynuowanie przepływu pracy: kontynuowanie od odzyskanego punktu zamiast od początku.
Pamięć tymczasowa a trwałe przechowywanie
Częstym źródłem zamieszania jest różnica pomiędzy przechowywaniem danych a ich utrwalaniem. Zwykły słownik w Pythonie zawierający treść rozmowy to pamięć tymczasowa: gdy proces się kończy, słownik znika. Utrwalanie oznacza stworzenie kopii tych danych i zapisanie jej w miejscu, które przetrwa zakończenie procesu, zazwyczaj w bazie danych. To jest cała idea; reszta tego przewodnika wyjaśnia, w jaki sposób LangGraph to realizuje.
Co daje ci trwałość w środowisku produkcyjnym
Poza unikaniem strat w pracy, trwałość zmienia rodzaj systemów, które możesz stworzyć.
- Agenty działające przez długi czas. Agenty, które przeglądają wiele stron, przetwarzają duże zbiory danych lub czekają na zdarzenia zewnętrzne, mogą być wstrzymywane i wznowiane w dowolnym momencie na każdym komputerze, który ma dostęp do przechowywania danych.
- Tolerancja na awarie. System tolerancyjny wobec awarii nadal funkcjonuje prawidłowo podczas awarii, problemów sieciowych lub przekroczenia czasu oczekiwania. Dzięki trwałości awaria dotyka tylko bieżącego kroku, a nie całego zadań.
- Procesy akceptacji. Gdy ktoś musi coś przejrzeć, agent może zatrzymać się na tak długo, jak to konieczne, bez utraty żadnych informacji. Budowa takich rozwiązań staje się prosta, gdy stan jest trwały.
Szybki test, by sprawdzić, czy jest to konieczne: gdyby proces został teraz ponownie uruchomiony, czy użytkownik byłby niezadowolony? Jeśli odpowiedź brzmi „tak”, to graf wymaga persistencji.
Stan: to, co faktycznie jest zapisywane
Persistencja w LangGraph ma sens dopiero po zrozumieniu pojęcia stanu, ponieważ persistencja grafu oznacza właśnie zapisywanie jego stanu w odpowiednio wybranych momentach.
Aplikacja LangGraph to graf: zbiór kroków, zwanych węzłami, połączonych krawędziami, przez które przepływają dane. Gdy wykonywanie programu przechodzi z jednego węzła do drugiego, potrzebna jest wspólna struktura do odczytu i zapisu informacji. Tą wspólną strukturą jest stan. Dobrym przykładem może być tablica w sali konferencyjnej: każdy węzeł podchodzi, czyta to, co tam jest, dodaje swój wkład i przekazuje zadanie następnemu węzłowi.
Stan zazwyczaj deklaruje się jako słownik typowany. Pierwszym krokiem jest import:
from typing import TypedDict
Schemat wymienia następnie klucze, z którymi pracuje graf, oraz ich typy. Tutaj stan rejestruje listę wiadomości, imię użytkownika oraz licznik kroków:
class State(TypedDict):
messages: list
user_name: str
step_count: int
Ponieważ State jest obiektem typu TypedDict, jest to po prostu słownik z ustaloną liczbą kluczy i określonymi typami. Każdy węzeł otrzymuje aktualny stan i zwraca częściową aktualizację, a LangGraph łączy tę aktualizację ze wspólnym stanem.
Dlaczego wszystko obraca się wokół stanu
Stan jest centrum aplikacji LangGraph:
- węzły odczytują go, aby zdecydować, co robić;
- węzły zapisują w nim swoje wyniki;
- ścieżki mogą kierować do różnych węzłów na podstawie wartości zawartych w nim;
- persistence zapisuje go i przywraca.
Gdy to zostanie zrealizowane, kwestia trwałości sprowadza się do jednego zdania: po każdym kroku należy zrobić zdjęcie aktualnego stanu i przechować je w bezpiecznym miejscu.
Zdjęcia stanu po każdym kroku
Niech ten model pozostanie z nami przez resztę przewodnika. Za każdym razem, gdy jakiś węzeł zostanie zakończony, LangGraph przechwytuje obecny stan i zapisuje go w pamięci. Jeśli proces zostanie przerwany zaraz po zakończeniu drugiego węzła, zdjęcie stanu z tego momentu nadal istnieje, więc wykonywanie może być kontynuowane od niego, a nie od pierwszego węzła. To zdjęcie ma nazwę: punkt kontrolny.
Punkty kontrolne: zdjęcia stanu w określonym momencie
Punkt kontrolny to zdjęcie stanu grafu w konkretnym momencie czasu. Termin pochodzi z tego samego źródła co w grach wideo: jest to bezpieczne miejsce, do którego można wrócić, zamiast ponownie rozgrywać cały poziom.
Czym umożliwiają punkty kontrolne
Punkt kontrolny odpowiada w sposób niezawodny na jedno pytanie: jak wyglądał stan tuż po zakończeniu określonego kroku? Bez punktów kontrolnych znasz tylko bieżący stan, i to tylko wtedy, gdy program jest uruchomiony. Dzięki nim możesz sprawdzić dowolny wcześniejszy moment działania programu, a system może przywrócić się do najnowszego punktu kontrolnego po awarii.
Punkty kontrolne są tworzone automatycznie
Cechą, która zaskakuje wielu początkujących, jest to, że nigdy nie musisz ręcznie zapisywać punktów kontrolnych. Gdy tylko punkt kontrolny zostanie dołączony do grafu, LangGraph tworzy nowy punkt kontrolny po każdym superkroku. Superkrok to jeden cykl wykonywania grafu; w prostym grafie liniowym odpowiada on zakończeniu pracy jednego węzła, natomiast w grafie z równoległymi gałęziami wszystkie węzły zaplanowane na ten sam cykl należą do jednego superkroku. Ty piszesz zwykły kod grafu, a zapisywanie punktów kontrolnych odbywa się równocześnie.
Co zawiera punkt kontrolny
Punkt kontrolny zazwyczaj rejestruje:
id: unikalny identyfikator, uporządkowany tak, aby późniejsze punkty kontrolne znajdowały się po tych wcześniejszych;ts: data utworzenia punktu kontrolnego;channel_values: same dane stanu, takie jak wiadomości i inne zmienne w tym momencie;channel_versions: wewnętrzne liczniki wersji, których używa LangGraph do śledzenia, które części stanu uległy zmianie;- metadane: informacje pomocnicze, takie jak węzeł, który stworzył punkt kontrolny, oraz numer kroku.
Nie ma potrzeby zapamiętywania tej struktury. Wystarczy prosta definicja: punkt kontrolny to zrzut stanu wraz z informacjami księgowymi, zapisany w określonym momencie. Jeśli chcesz zobaczyć, jak te elementy są przechowywane wewnętrznie, włączając w to niezapisane dane i bloki danych, znajdziesz bardziej szczegółowe wyjaśnienie w Inside LangGraph's InMemorySaver.
Licznik, krok po kroku
Rozważmy graf składający się z jednego węzła, który zwiększa liczbę, uruchamiany trzy razy z rzędu. Po pierwszym uruchomieniu zapisany stan to {count: 1}, po drugim {count: 2}, a po trzecim {count: 3}; każdy z tych stanów stanowi odrębny punkt kontrolny. Jeśli program ulegnie awarii tuż po zapisaniu drugiego punktu kontrolnego, ponowne uruchomienie może rozpocząć się od {count: 2}, bez konieczności powtarzania pierwszych dwóch zwiększeń.
Checkpointerzy: komponent, który zapisuje i ładuje
Jeśli punktem kontrolnym jest zrzut stanu, to checkpointer to komponent, który tworzy takie zrzuty stanu, przechowuje je i przywraca. Jest to warstwa trwałości w LangGraph.
Dobrą analogią jest aparat fotograficzny z wbudowanym szafką na akta. Gdy tylko jakiś węzeł zostanie zakończony, aparat robi zdjęcie aktualnego stanu i go przechowuje. Szafką tą może być pamięć procesu, plik lokalny lub baza danych – w zależności od wybranego checkpointera.
Trzy zadania checkpointera
Checkpointer jest odpowiedzialny za:
- Zapisywanie stanu: zapisywanie nowego zrzutu ekranu do pamięci po każdym kroku.
- Ładowanie stanu: odczytywanie najnowszego zrzutu ekranu, gdy graf jest ponownie uruchamiany dla tego samego wątku (wątki zostaną omówione w następnej sekcji).
- Przywracanie wykonywania: przekazywanie tego zrzutu ekranu systemowi czasu wykonywania, aby graf mógł kontynuować od miejsca przerwy, zamiast zaczynać od nowa.
Dodawanie checkpointera w czasie kompilacji
Persistentność jest włączana podczas kompilacji grafu. Imporтуje się klasę checkpointer oraz narzędzie do budowania grafu; źródło oznacza ten fragment jako JavaScript, ale w rzeczywistości jest to Python:
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.graph import StateGraph
Następnie tworzy się obiekt checkpointer i przekazuje go do metody compile(). Należy zauważyć, że w wyświetlonym fragmencie komentarz oraz instrukcja checkpointer = InMemorySaver() zostały połączone na jednej linii, co sprawiłoby, że instrukcja ta stałaby się częścią komentarza; w rzeczywistym kodzie powinny znajdować się na osobnych liniach:
# ... assume `builder` is a StateGraph you've already defined ...checkpointer = InMemorySaver()
graph = builder.compile(checkpointer=checkpointer)
Ta jedna instrukcja checkpointer=checkpointer włącza funkcję persistentności dla całego grafu. Bez niej LangGraph nic nie przechowuje, a każda wywołanie invoke() rozpoczyna się od pustego stanu.
Wynikający cykl to załadunek, uruchomienie i zapis, który powtarza się po każdym kroku. Dlatego funkcja trwałości wydaje się automatyczna: nigdy sam nie wywołujesz funkcji zapisu ani załadunku, ponieważ skompilowany graf robi to w ramach normalnej eksploatacji.
Istnieje jedna uwaga, której łatwo nie zauważyć. Checkpointer przechowuje tylko te grafy, do których został przekazany. Jeśli ten sam plik skompiluje drugi graf bez checkpointera, ten drugi graf w ogóle nie ma możliwości przechowywania stanu.
Wątki: oddzielanie rozmów
Wartość thread_id pojawia się we wszystkich fragmentach kodu LangGraph i zasługuje na dokładne wyjaśnienie. Wątek reprezentuje jedną ciągłą rozmowę lub zadanie. Każdy wątek ma unikalny identyfikator, a wszystkie punkty kontrolne utworzone dla danej rozmowy są grupowane pod nim.
Dlaczego każda rozmowa potrzebuje własnego wątku
Załóżmy, że bot do obsługi klientów obsługuje jednocześnie tysiące użytkowników. Jeden z nich pyta o zwrot pieniędzy, podczas gdy inny o opóźnioną dostawę. Są to oddzielne rozmowy prowadzone równolegle, a pomieszanie ich – na przykład poinformowanie pierwszego klienta o paczce drugiego – stanowiłoby poważny błąd.
ID tematu zapobiegają temu, działając jak etykiety na folderach. Każdy punkt kontrolny jest przechowywany pod dokładnie jednym ID tematu, dzięki czemu dane z różnych rozmów nigdy się nie mieszają.
Przekazywanie ID tematu w kodzie
Temat jest wybierany za pomocą słownika konfiguracji. thread_id znajduje się pod kluczem configurable (ten i następny fragment to kod w Pythonie, a nie zwykły tekst):
config = {"configurable": {"thread_id": "customer-a-session-101"}}
Ta konfiguracja jest przekazywana wraz z danymi wejściowymi przy każdym wywołaniu:
result = graph.invoke({"messages": [{"role": "user", "content": "Where's my refund?"}]}, config)
Każde wywołanie graph.invoke() lub graph.stream() przyjmuje parametr config zawierający thread_id. LangGraph wykorzystuje go do:
- znalezienia istniejących punktów kontrolnych dla danego wątku, jeśli istnieje historia do załadowania;
- zapisywania nowych punktów kontrolnych pod tym samym ID, gdy działanie trwa dalej.
Użycie innego thread_id powoduje utworzenie nowej, pustej rozmowy, jakby stan został sformatowany, mimo że skompilowany graf i punkty kontrolne to dokładnie te same obiekty.
Jak wątki odpowiadają rzeczywistym produktom
- Interfejs czatu: każdy otwarty czat jest w praktyce oddzielnym wątkiem, a zmiana czatu odpowiada zmianie ID wątku. To, co powiedziałeś w jednej rozmowie, nie przenika do drugiej.
Praktyczną konsekwencją jest to, że identyfikatory wątków powinny być generowane i przechowywane celowo, na przykład na podstawie numeru ticketa lub identyfikatora sesji w własnej bazie danych. Jeśli ID zostanie utracone, punkty kontrolne nadal istnieją, ale nic ich nie wskazuje.
Jeden złożony graf, wiele wątków
Nie jest potrzebny graf na użytkownika. Jeden złożony graf może obsługiwać dowolną liczbę wątków jednocześnie; potrzebny jest jeden unikalny identyfikator wątku na użytkownika lub rozmowę. Graf określa zachowanie, a wątek określa, na jakim stanie ten zachowanie ma działać.
Śledzenie jednego przebiegu od początku do awarii i dalej
Łączenie stanu, punktów kontrolnych, wskaźnika punktu kontrolnego oraz wątków daje pełny obraz tego, co dzieje się podczas wykonywania. Poniższy schemat (składnia Mermaid, przedstawiony jako tekst) opisuje jeden przebieg, włączając awarię oraz ścieżkę powrotu po niej:
flowchart TD
A[1. Graph starts with invoke] --> B[2. Checkpointer checks thread_id for existing State]
B --> C{State exists for this thread?}
C -->|Yes| D[3a. Load last saved State]
C -->|No| E[3b. Start with fresh empty State]
D --> F[4. Node executes]
E --> F
F --> G[5. State updates in memory]
G --> H[6. Checkpoint saved to storage]
H --> I{More nodes to run?}
I -->|Yes| F
I -->|No| J[7. Return final result to caller]
H -.->|💥 Crash happens here| K[Process restarts]
K --> B
Opisany prozą sekwencja wygląda następująco:
- Rozpoczyna się graf. Wywołuje się metodę
graph.invoke(input, config)z określonymthread_id.
Nie ma konieczności implementacji oddzielnego trybu przywracania. Wystarczy ponowne uruchomienie grafu na tym samym wątku, aby LangGraph mógł określić, od czego kontynuować.
Jest jeden szczegół, w kwestii którego warto być precyzyjnym. Aby kontynuować przebieg, który został przerwany lub nie powiódł się, wywołuje się graf z wartością None jako danymi wejściowymi oraz tą samą konfiguracją, co informuje LangGraph o konieczności kontynuacji od zapisanego punktu kontrolnego, a nie rozpoczęcia nowego przebiegu. Podanie nowych danych wejściowych do istniejącego wątku uruchamia nowy przebieg, który buduje się na podstawie zapisanego stanu: klucze z reduktorem, takie jak lista wiadomości, są akumulowane, natomiast zwykłe klucze są nadpisywane nową wartością. Można sprawdzić, co zostało zapisane, używając graph.get_state(config) dla najnowszego zrzutu stanu oraz graph.get_state_history(config) dla pełnej sekwencji.
To ten sam mechanizm, który pozwala agencie celowo się zatrzymać, na przykład w oczekiwaniu na decyzję człowieka, a następnie wznowić działanie po kilku godzinach lub dniach na innej maszynie, pod warunkiem, że ta maszyna może uzyskać dostęp do tego samego trwałego przechowania. Aby zobaczyć to w praktyce, zapoznaj się z „Zatrzymywanie i wznowianie działania agentów LangGraph z użyciem funkcji interrupt i Command”.
Najprostszy backend: InMemorySaver
LangGraph nie zobowiązuje do użycia jednego systemu przechowywania. Obsługuje kilka backendów, czyli miejsc, gdzie fizycznie przechowywane są punkty kontrolne, a różnią się one głównie trwałością oraz liczbą procesów, które mogą nimi korzystać. Najprostszym z nich jest InMemorySaver.
Czym jest i jak przechowuje punkty kontrolne
InMemorySaver to narzędzie do przechowywania punktów kontrolnych, które zapisuje każdy taki punkt w pamięci RAM bieżącego procesu Python, w zwykłym słowniku pamięciowym indeksowanym według identyfikatora wątku. Nie ma żadnych plików ani bazy danych – istnieje tylko obiekt Pythona.
Pierwszy przykład wykorzystuje skonstruowany typ stanu z jedną kluczem count oraz węzłem, który zwiększa jego wartość o 1. Węzeł jest połączony od START do END, graf jest kompilowany za pomocą InMemorySaver, a funkcja jest wywoływana na thread-1 z początkową wartością 1, co daje wynik 2. Źródło określa to jako JavaScript, ale w rzeczywistości jest to kod Pythona; należy również zauważyć, że zakłada wcześniejsze zaimportowanie obiektów TypedDict, StateGraph, START, END oraz InMemorySaver:
class StateInt(TypedDict):
count: int
def add_one(state: StateInt) -> dict:
return {"count": state["count"] + 1}
builder = StateGraph(StateInt)
builder.add_node("add_one", add_one)
builder.add_edge(START, "add_one")
builder.add_edge("add_one", END)
memory = InMemorySaver()
graph = builder.compile(checkpointer=memory)
config = {"configurable": {"thread_id": "thread-1"}}
result = graph.invoke({"count": 1}, config)
print(result) # {'count': 2}
Następny fragment to jedynie opis kontrastujący minimalną wersję grafu z bardziej realistyczną wersją powyżej:
Two ways to write the same graph — minimal vs. real-world.
Minimalna wersja wymaga użycia asyncio oprócz mechanizmu kontrolnego i narzędzia do budowania grafu:
import asyncio
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.graph import StateGraph
Następnie jako cały stan używa zwykłego typu int, rejestruje lambda jako węzeł, oznacza ten węzeł jako punkt wejścia i wyjścia, a następnie uruchamia graf asynchronicznie za pomocą ainvoke. Jak widać, kilka instrukcji zostało połączonych na jednych liniach (na przykład wywołanie set_finish_point i przypisanie wartości do InMemorySaver()), więc należy je rozdzielić przed uruchomieniem; fragment ten jest również napisany w Pythonie, mimo swojego oznaczenia:
builder = StateGraph(int)
builder.add_node("add_one", lambda x: x + 1)
builder.set_entry_point("add_one")
builder.set_finish_point("add_one")memory = InMemorySaver()
graph = builder.compile(checkpointer=memory)config = {"configurable": {"thread_id": "thread-1"}}
result = asyncio.run(graph.ainvoke(1, config))
print(result) # Output: 2
Przejdźmy do najważniejszych linii:
InMemorySaver()tworzy pusty magazyn punktów kontrolnych w pamięci.
builder.compile(checkpointer=memory) przypina ten magazyn do grafu, co włącza funkcję trwałości danych.config = {"configurable": {"thread_id": "thread-1"}} łączy tę funkcję z konkretną rozmową o określonej nazwie.graph.ainvoke(1, config) to punkt wejścia asynchroniczny; w tym przypadku uruchamiany jest z liczbą całkowitą 1 jako stanem na wątku "thread-1".Ponowne wywołanie z tym samym thread_id korzysta z już zapisanych punktów kontrolnych dla danego wątku, a nie z pustej historii. Należy pamiętać o różnicy w porównaniu z poprzednim rozdziałem: w tym małym grafie proces już się zakończył, a stan to pojedyncza wartość, więc nowy wprowadzony dane po prostu ją zastępują, natomiast None jako dane wejściowe doprowadziłoby do kontynuacji niezakończonego procesu.
Zalety
- Brak konfiguracji: nie potrzeba bazy danych ani usługi zewnętrznej.
- Bardzo szybki, ponieważ nie ma opóźnień dyskowych ani sieciowych.
- Dobrze nadaje się do testów jednostkowych.
- Praktyczny do nauki oraz do eksperymentów w notatkach.
Ograniczenia
- Wszystko znika, gdy proces się zatrzymuje. Jest to zwykła pamięć RAM, co jest dokładnie tym problemem opisanym na początku tego przewodnika.
- Nie można jej udostępniać między procesami ani serwerami, ponieważ każdy proces ma własną pamięć.
- Nie jest bezpieczna do użycia w środowisku produkcyjnym, ponieważ zwykłe restartowanie usuwa wszystkie rozmowy.
Kiedy go używać
Odpowiednie zastosowania to:
- lokalna rozwój i debugowanie;
- testy zautomatyzowane, zarówno testy jednostkowe, jak i pipeline’y CI;
- szybkie prototypy oraz notatki, gdzie przetrwanie restartu nie ma znaczenia.
Wszystko, od czego zależą rzeczywiście użytkownicy, wymaga wsparcia w postaci punktu kontrolnego opartego na bazie danych. Dokumentacja LangGraph jasno wskazuje, że InMemorySaver jest przeznaczony do debugowania i testowania, a zaleca wykorzystanie trwałej implementacji, takiej jak PostgresSaver, w środowisku produkcyjnym. Ustawienie takiej konfiguracji, wraz z innymi backendami jak SQLite i Redis, a także dostosowanymi punktami kontrolnymi oraz procesami zatwierdzania opartymi na interrupt() i Command, stanowi naturalny następny krok; przykład działania LangGraph na Postgresie i Redisie w środowisku produkcyjnym znajduje się pod adresem Samodzielne hostowanie serwera agenta LangGraph.
Główne wnioski
- Persistence zapisuje stan grafu w trwałym magazynie, dzięki czemu aplikacja może kontynuować pracę po awariach, restartach i długich oczekiwaniach, zamiast rozpoczynać wszystko od nowa.
compile().thread_id izoluje jedną rozmowę lub zadanie, dzięki czemu jeden zkompilowany graf może bezpiecznie obsługiwać wielu użytkowników.None; nowy wprowadzony tekst w istniejącym wątku uruchamia nową sesję na podstawie zapisanego stanu.InMemorySaver jest idealny do testów i prototypów, ale ponieważ znajduje się w pamięci procesu, systemy produkcyjne wymagają checkpointera opartego na bazie danych.Literatura pokrewna
- Agenci z akceptacją w LangGraph: interrupt(), Checkpoints i magazyn — Budowanie agenta LangGraph krok po kroku: wyraźny graf ReAct, akceptacja przez człowieka za pomocą interrupt(), pamięć między wątkami z wykorzystaniem magazynu, zakończone asystentem skrzynki odbiorczej, który najpierw zadaje pytania.
- Pauzowanie i wznowienie agentów LangGraph za pomocą interrupt() i Command — Jak funkcje interrupt() i Command w LangGraph wykorzystują checkpointy i wątki do pauzowania agenta w celu uzyskania akceptacji, edycji lub danych od człowieka, a następnie bezpiecznego jego wznowienia później.