Od chatbota z jednym węzłem do agenta wspieranego przez MCP w LangGraph
Stwórz aplikację LangGraph warstwa po warstwie: stan i reduktory, krawędzie, pętle narzędziowe, wątki z punktami kontrolnymi, trzy tryby przepływu danych oraz narzędzia udostępniane przez MCP.
Źródła: Building AI Applications with LangGraph: From a Basic Chatbot to Tools, Memory, Streaming and MCP
Większość prototypów LLM zaczyna się od jednego wywołania: wysyła się prompt, a następnie wyświetla odpowiedź. Taka struktura rozpada się, gdy aplikacja potrzebuje wyszukiwania w sieci, decyzji o kolejnym kroku, pamięci wcześniejszych rozmów lub aktualnego stanu w interfejsie użytkownika. LangGraph rozwiązuje ten problem, czyniąc przepływ sterowania wyraźnym grafem opartym na wspólnej stanie. Ten przewodnik pokazuje, jak przekształcić aplikację składającą się z pojedynczego czatbota w agenta typu ReAct wykorzystującego narzędzia, z pamięcią zapisywaną w punktach kontrolnych, przepływem danych oraz narzędziami dostarczanymi przez oddzielny serwer MCP, a także wskazuje na szczegóły, które często sprawiają trudności.
Model mentalny: stan przepływa przez węzły wzdłuż krawędzi
Zwykła integracja z LLM przebiega w linii prostej od promptu przez model do odpowiedzi. LangGraph wymaga natomiast myślenia w cyklu: obiekt state trafia do grafu, nodes go odczytują i zwracają zmiany, edges decydują, który węzeł ma działać dalej, a na końcu powstaje zaktualizowany stan. Trzema podstawowymi elementami są więc:
- State: dane, które przemieszczają się pomiędzy poszczególnymi krokami, takie jak historia wiadomości.
- Nodes: zwykłe funkcje, które wykonywają określoną czynność i zwracają częściową aktualizację.
- Edges: reguły łączące węzły, w tym punkt startowy i zakończenie procesu.
Najprostszy możliwy graf składa się z jednego węzła pomiędzy punktem wejścia a wyjściem:
┌─────────────┐
│ State │
└──────┬──────┘
│
▼
┌─────────────┐
│ Node │
│ chatbot │
└──────┬──────┘
│
▼
┌─────────────┐
│ END │
└─────────────┘
Rozdzielenie „jakie dane istnieją” od „co jest wykonywane w jakiej kolejności” stanowi kluczową decyzję projektową. Można zmienić kolejność wykonywania operacji bez ingerencji w sposób przechowywania danych, a także sprawdzić stan systemu w dowolnym momencie bez konieczności analizy sekwencji wywołań funkcji.
Dlaczego graf zamiast łańcucha wywołań
Minimalny chatbot to prosta ścieżka od użytkownika do modelu i z powrotem:
User
↓
LLM
↓
Response
Teraz dodajmy rzeczywiste wymagania: wyszukiwanie w internecie, kilka narzędzi, logikę rozgałęzień, pamięć, krok zatwierdzenia przez człowieka, więcej niż jeden agent, wyjście w formie strumienia oraz różne zachowania w zależności od tego, co zwraca model. Każde z tych elementów dodaje gałąź lub pętlę, a nawarstwione instrukcje if wokół wywołań API szybko stają się trudne do zrozumienia. Nawet najczęstszy przypadek, „wywołaj narzędzie, jeśli model o to poprosi”, już zawiera cykl:
┌─────────────┐
│ LLM │
└──────┬──────┘
│
Need a tool?
/ \
Yes No
↓ ↓
Tool Response
│
↓
LLM
Gdy przepływ staje się grafem, gałęzie i pętle stają się widocznymi obiektami, które można nazwać, przetestować i narysować, zamiast być ukrytym przepływem sterowania wewnątrz funkcji pomocniczych.
Krok 1: bot do czatowania z jednym węzłem przy użyciu StateGraph
Pierwsza wersja jest celowo prosta – składa się z jednego węzła połączonego między znacznikami początkowym i końcowym:
START
↓
Chatbot Node
↓
END
Definicja stanu i jego reduktora
Dla bota do czatowania stan najczęściej wymaga historii rozmowy. Pierwszą próbą jest użycie TypedDict z listą messages:
from typing import TypedDict
class State(TypedDict):
messages: list
Koncepcyjnie ta lista gromadzi naprzemiennie wpisy od człowieka i AI:
State
└── messages
├── HumanMessage
├── AIMessage
├── HumanMessage
└── AIMessage
Jeden szczegół ma tu ogromne znaczenie. W LangGraph każdy klucz w stanie może mieć reducera, który decyduje, w jaki sposób wartość zwracana przez węzeł jest łączona z istniejącą wartością. Zwykła adnotacja list nie posiada reducera, więc węzeł zwracający {"messages": [reply]} zastępuje całą listę, zamiast do niej dodawać element. W przypadku historii rozmów prawie zawsze chcemy używać reducera add_messages, zdefiniowanego jako Annotated[list, add_messages] (lub odziedziczonego od gotowego MessagesState). Dzięki niemu zwracane wiadomości są dodawane, a te o odpowiadającej identyfikatorze są aktualizowane na miejscu. Powyższy fragment jest przydatny jako obraz mentalny, ale w rzeczywistym kodzie należy używać formy z adnotacją.
Pisanie węzła chatbotu
Węzeł to po prostu funkcja, która otrzymuje bieżący stan i zwraca klucze, które chce zmienić. Węzeł chatbotu przekazuje historię do modelu i zwraca odpowiedź:
def chatbot(state: State):
return {
"messages": [
llm.invoke(state["messages"])
]
}
Czytaj to od wewnątrz na zewnątrz. Funkcji przekazywany jest cały stan:
state
Wybiera dotychczasową rozmowę:
state["messages"]
Przekazuje tę historię do modelu:
llm.invoke(state["messages"])
A następnie otacza odpowiedź modelu słownikiem, którego klucze odpowiadają polom, które zostały zaktualizowane. Należy zauważyć, że węzeł nie modyfikuje stanu bezpośrednio; zwraca częściową aktualizację i pozwala LangGraphowi ją połączyć:
{
"messages": [
llm.invoke(state["messages"])
]
}
Jako diagram przepływu danych węzeł znajduje się pomiędzy dwiema wersjami tego samego stanu:
State
│
│ messages
▼
┌─────────────┐
│ Chatbot │
│ Node │
└──────┬──────┘
│
│ LLM Response
▼
State
Rejestracja węzła
Funkcja staje się częścią grafu po rejestracji w narzędziu budującym pod określonym nazwą:
graph_builder.add_node(
"llmchatbot",
chatbot
)
Pierwszym argumentem jest identyfikator węzła, do którego odnoszą się krawędzie i logi:
"llmchatbot"
Drugim argumentem jest funkcja wywoływalna, która uruchamia się za każdym razem, gdy wykonanie dociera do tego węzła:
chatbot
Krok 2: łączenie krawędzi i kompilacja
Sam węzeł nigdy się nie uruchamia; krawędzie informują LangGraph, jak tam dotrzeć i dokąd dalej iść. Ten graf wymaga dwóch krawędzi. Pierwsza łączy specjalny znacznik START z chatbotem:
graph_builder.add_edge(
START,
"llmchatbot"
)
W formie diagramu wykonanie wchodzi do grafu i udaje się prosto do węzła:
START
↓
llmchatbot
Druga krawędź przekazuje kontrolę z chatbotu do END, co kończy proces wykonywania:
graph_builder.add_edge(
"llmchatbot",
END
)
To odpowiada:
llmchatbot
↓
END
Razem tworzą one kompletny, choć minimalny, graf:
START
│
▼
┌────────────────┐
│ llmchatbot │
│ Node │
└────────┬───────┘
│
▼
END
Kompilacja i wywołanie
Kompilacja weryfikuje strukturę (na przykład to, że każdy krawędź wskazuje na znany węzeł) i tworzy obiekt gotowy do uruchomienia:
graph = graph_builder.compile()
Uruchamia się go, przekazując początkowy stan. Przyjmowane są zwykłe słowniki ról/contentu, które są konwertowane na obiekty wiadomości LangChain:
result = graph.invoke({
"messages": [
{
"role": "user",
"content": "What is LangGraph?"
}
]
})
W rzeczywistości stan wejściowy trafia do START, węzeł chatbota wywołuje model, odpowiedź jest łączona ze stanem, a proces kończy się:
Input State
↓
START
↓
llmchatbot
↓
LLM
↓
Updated State
↓
END
Wartość zwracana przez invoke to ostateczny stan, więc odpowiedź modelu jest ostatnim elementem w result["messages"].
Krok 3: rysowanie grafu
Kompilowany graf może sam się wyświetlić. W notatniku Jupyter pomocniki wyświetlania IPythona pokazują plik PNG utworzony na podstawie opisu grafu w formacie Mermaid:
from IPython.display import Image, display
display(
Image(
graph.get_graph().draw_mermaid_png()
)
)
Dla tego grafu obrazek jest krótki:
┌───────┐
│ START │
└───┬───┘
│
▼
┌──────────────┐
│ llmchatbot │
└──────┬───────┘
│
▼
┌───────┐
│ END │
└───────┘
Wartość nie jest oczywista przy trzech polach, ale szybko rośnie. Gdy pojawią się krawędzie warunkowe, węzły narzędziowe i pętle, wyrenderowany diagram to najszybszy sposób na potwierdzenie, że graf, który stworzyłeś, to właśnie ten, który chciałeś stworzyć. Pamiętaj, że draw_mermaid_png() domyślnie renderuje treść za pośrednictwem zewnętrznego serwisu Mermaid, więc wymaga dostępu do sieci; jeśli taki dostęp nie jest możliwy, draw_mermaid() zwraca tekst Mermaid, który można wyrenderować gdzie indziej.
Krok 4: dostarczenie modelowi narzędzia do wyszukiwania
Chatbot może odpowiadać tylko na podstawie tego, czego model nauczył się podczas treningu. Pytanie dotyczące najnowszych osiągnięć w dziedzinie AI agentowej wymaga aktualnych informacji, dlatego w następnym kroku dodaje się narzędzie do wyszukiwania w internecie. Przykład wykorzystuje Tavily Search, a zamierzony przepływ wygląda następująco:
User
↓
LLM
↓
Search required?
↓
Tavily Search
↓
Search Results
↓
LLM
↓
Final Answer
Przypinanie narzędzi nie jest tym samym co ich uruchamianie
Pierwsza część zadania to powiązanie narzędzi: informowanie modelu, jakie narzędzia istnieją i jakie argumenty przyjmują.
llm_with_tools = llm.bind_tools(
[tavily_search]
)
Po powiązaniu model może odpowiedzieć strukturyzowanym wywołaniem narzędzia („wywołaj tavily_search z tą zapytaniem”) zamiast zwykłym tekstem. Sam nie może wykonać tego wywołania. Coś w twojej aplikacji musi zauważyć to wywołanie, uruchomić narzędzie, dodać wynik jako ToolMessage i odesłać wszystko z powrotem do modelu. Udzielenie modelowi dostępu do narzędzia nie czyni go agentem – to otaczający go przepływ wykonywania ma taką rolę. Właśnie taki przepływ dostarcza graf.
Krok 5: przekształcenie go w pętlę ReAct
ReAct łączy rozumowanie z działaniem: model przemyśla zadanie, decyduje, czy musi działać, wykonuje czynność za pomocą narzędzia, obserwuje wynik i kontynuuje. W terminach grafu, prosta linia z wcześniej:
START
↓
LLM
↓
END
staje się grafem z punktem decyzyjnym i ścieżką prowadzącą z powrotem do modelu:
START
│
▼
Agent
│
▼
┌─────────┐
│ LLM │
└────┬────┘
│
Need a tool?
/ \
Yes No
│ │
▼ ▼
Tool END
│
▼
Agent
│
▼
LLM
W kodzie LangGraph ta decyzja jest reprezentowana jako krawędź warunkowa. Z góry przygotowany element ToolNode wykonywać będzie dowolną czynność określoną w ostatniej wiadomości wysłanej przez narzędzie, a tools_condition kieruje ruch do tego węzła, gdy istnieją takie wywołania narzędzi, lub do END, gdy ich nie ma. Krawędź prowadząca z węzła narzędzia z powrotem do modelu zamknie pętlę. Gotowy pomocnik create_react_agent tworzy dla nas tę samą strukturę; sprawdź aktualną dokumentację LangGraph, aby dowiedzieć się o jego dokładnej lokalizacji, ponieważ pomocniki agentów zmieniały się wraz z czasem pomiędzy różnymi pakietami.
Śledzenie jednego przejścia przez pętlę
Załóżmy, że użytkownik prosi o najnowsze wiadomości dotyczące LangGraph. Jedno przejście przez pętlę generuje następującą sekwencję wiadomości i kroków:
HumanMessage
↓
Agent
↓
LLM
↓
"I need current information."
↓
Tool Call
↓
Tavily Search
↓
Search Results
↓
ToolMessage
↓
Agent
↓
LLM
↓
Final Answer
Ważną różnicą strukturalną jest obecność pętli. Pierwszy chatbot funkcjonował w sposób liniowy:
START → LLM → END
Graf ReAct może ponownie korzystać z modelu tyle razy, ile jest to konieczne:
START
↓
Agent
↓
LLM
↓
Tool?
┌───────┐
│ │
Yes No
│ │
↓ ↓
Tool END
│
↓
Agent
Ponieważ pętle mogą w zasadzie działać wiecznie, LangGraph narzuca ograniczenie rekurencji przy każdym uruchomieniu (możliwe do skonfigurowania za pomocą ustawienia recursion_limit). Jeśli model nadal żąda narzędzi, otrzymuje się błąd zamiast nieskończonego rachunku.
Krok 6: przechowywanie rozmów za pomocą MemorySaver
Do tej pory każde wezwanie do invoke rozpoczynało się od zera. Rozważmy dwa etapy rozmowy. Najpierw użytkownik przedstawia się:
User:
My name is Himanshi.
Następnie, w oddzielnym żądaniu, zadaje dodatkowe pytanie:
User:
What is my name?
Druga tura może zostać odpowiedziana tylko wtedy, gdy graf nadal zawiera pierwszą. LangGraph rozwiązuje to za pomocą checkpointera: po każdym kroku zapisuje on kopię stanu, a kolejne uruchomienie w ramach tej samej rozmowy kontynuuje się od tej kopii. Najprostszą implementacją jest MemorySaver:
┌─────────────┐
│ Agent │
└──────┬──────┘
│
▼
┌───────────┐
│ State │
└─────┬─────┘
│
▼
┌─────────────┐
│ MemorySaver │
└─────────────┘
Przytwierdza się go w czasie kompilacji za pomocą graph_builder.compile(checkpointer=memory). Zamiast każdego wywołania być oddzielną eksploatacją, każde uruchomienie należy teraz do konkretnej rozmowy, a jego stan jest przechowywany pomiędzy uruchomieniami.
Wątki oddzielają rozmowy
Kluczem łączącym uruchomienia jest identyfikator wątku, przekazywany w konfiguracji jako {"configurable": {"thread_id": "..."}}. Każdy wątek ma swój własny historial oraz aktualny stan:
Thread 1
├── Message 1
├── Message 2
├── Message 3
└── Current State
Thread 2
├── Message 1
├── Message 2
└── Current State
Zatem dwóch użytkowników w dwóch wątkach nigdy nie widzi wiadomości drugiego:
User A → Thread A → State A
User B → Thread B → State B
Obowiązują dwie uwagi. MemorySaver przechowuje wszystko w pamięci procesu, więc cała historia znika po ponownym uruchomieniu i nie jest udostępniana pomiędzy instancjami serwera; w produkcji używa się trwałych punktów kontrolnych, takich jak te w Postgresie lub SQLite. W najnowszych wersjach klasa jest również eksportowana jako InMemorySaver. Aby lepiej zrozumieć, co faktycznie przechowuje punkt kontrolny, zapoznaj się z informacjami na temat tego, jak punkty kontrolne, zapisy i bloki danych współpracują w InMemorySaver.
Krok 7: przesyłanie stanu w czasie rzeczywistym podczas działania grafu
We wszystkich powyższych przykładach użyto wywołania blokującego:
graph.invoke(...)
Czeka, aż cały graf zostanie zakończony, zanim zwróci wynik. W przypadku agentów wieloetapowych może to oznaczać kilka sekund pustej ekranu, co wydaje się nieprawidłowe dla użytkowników. Skompilowany graf ujawnia również stream, który dostarcza wyniki podczas trwania wykonywania.
stream_mode="values">: pełny stan po każdym kroku
Pierwszy dostępny tryb to:
stream_mode="values"
Używany w pętli, dostarcza zrzut ekranu całego stanu po każdym kroku:
for chunk in graph.stream(
input_state,
stream_mode="values"
):
print(chunk)
Każdy fragment pokazuje, jak wygląda cały stan w danym momencie, więc można zobaczyć, jak rośnie on w miarę działania węzłów i narzędzi:
START
↓
State Update #1
↓
Node Execution
↓
State Update #2
↓
Tool Execution
↓
State Update #3
↓
END
Jest to wygodne, gdy stan jest mały i chce się po prostu renderować „najnowszą wersję” za każdym razem.
stream_mode="updates">: tylko to, co zmienił każdy węzeł
Drugi tryb to:
stream_mode="updates"
Różnica polega na tym, co zawiera każdy fragment. W jednym trybie otrzymuje się pełny stan:
values
W drugim otrzymuje się tylko różnicę, oznaczoną nazwą węzła, który ją wygenerował:
updates
Szybki sposób na zapamiętanie tej różnicy:
- values odpowiadają na pytanie „jak wygląda obecnie stan?"
- updates odpowiadają na pytanie „co dokładnie zmienił ten konkretny węzeł?"
updates jest zazwyczaj lepszym wyborem do debugowania i logowania, ponieważ pokazuje, który węzeł był odpowiedzialny za każdą zmianę, oraz pomaga utrzymać małe rozmiary fragmentów przy dużym stanie. Żaden z tych trybów nie przesyła pojedynczych tokenów z modelu; do wyświetlania token po tokenie istnieje oddzielny tryb strumienia messages, a można przekazać listę trybów, aby otrzymać kilka naraz.
Krok 8: szczegółowa widoczność za pomocą astream_events
Zrzuty stanu i różnice między węzłami pokazują, co się zmieniło, ale nie wszystko, co wydarzyło się. astream_events zagłębia się o jeden poziom głębiej i emituje zdarzenie dla każdego momentu cyklu życia każdego komponentu w trakcie działania:
Graph Execution
│
├── Graph Started
│
├── Node Started
│
├── LLM Started
│
├── LLM Streaming
│
├── LLM Completed
│
├── Tool Started
│
├── Tool Completed
│
├── Node Completed
│
└── Graph Completed
Konsumuje się je asynchronicznie. Należy zauważyć, że wywołanie print w poniższym fragmencie musi być wciśnięte wewnątrz ciała pętli; jak pokazano, Python odrzuci je z błędem dotyczącym wciśnięć:
async for event in graph.astream_events(
input_state,
version="v2"
):
print(event)
Każde zdarzenie zawiera typ (na przykład on_chat_model_stream lub on_tool_start), nazwę komponentu oraz jego dane. Dzięki temu strumień zdarzeń jest przydatny do:
- debugowania i uporządkowanego logowania
- ogólnej obserwowalności działania agentów
- śledzenia każdego wywołania LLM oraz każdego wywołania narzędzia
Inżynierowie z doświadczeniem w obszarze backendu rozpoznają ten wzorzec: chodzi tu bardziej o śledzenie zapytań rozproszonych niż o czytanie wartości zwracanej przez pojedynczą funkcję. Pytanie zmienia się z „czy agent zadziałał?” na „co dokładnie zrobił agent, w jakiej kolejności i ile czasu zajęła każda część?”
Jak łączą się poszczególne elementy
Patrząc wstecz, każdy krok dodawał nową funkcjonalność do tego samego „szkieletu”. Wszystko zaczęło się od pojedynczego wywołania modelu:
START
↓
LLM
↓
END
Narzędzia dodawały przejazd tam i z powrotem do świata zewnętrznego:
START
↓
LLM
↓
Tool
↓
LLM
↓
END
Używanie punktów kontrolnych pozwalało pętli zapamiętywać informacje:
START
↓
Agent
↓
LLM ↔ Tools
↓
Memory / Checkpoint
↓
END
Strumieniowanie i zdarzenia umożliwiały prezentację wyników w interfejsie oraz ich monitorowanie:
Graph
↓
Events
↓
UI / Logs / Observability
Żaden z tych kroków nie wymagał przepisywania poprzednich; każdy z nich stanowi nowy węzeł, krawędź, opcję kompilacji lub wywołanie metody w tym samym grafie.
Krok 9: przeniesienie narzędzi za serwer MCP
serwer udostępnia narzędzia (oraz zasoby), a każdy MCP klient może je odkryć i wywołać. Aplikacja nie musi już zawierać implementacji wszystkich narzędzi:
┌──────────────┐
│ MCP Client │
└──────┬───────┘
│
│
▼
┌──────────────┐
│ MCP Server │
└──────┬───────┘
│
┌──────────┼──────────┐
↓ ↓ ↓
add() multiply() weather()
Minimalny serwer z trzema narzędziami
add(a, b)
Narzędzie do mnożenia:
multiply(a, b)
I narzędzie do sprawdzania pogody, które przyjmuje nazwę miasta:
weather(city)
Narzędzie pogodowe nie korzysta z rzeczywistego serwisu pogodowego. Zwraca stałą ciąg znaków, co skupia uwagę na protokole zamiast na kluczach API:
"It's sunny in Mumbai."
Chodzi o całą trasę komunikacji, a nie tylko prognozę pogody:
Client
↓
MCP Server
↓
Tool
↓
Result
↓
Client
Narzędzia nie muszą już znajdować się w aplikacji, która je używa
Główna idea polega na tym, że narzędzia mogą być hostowane niezależnie od aplikacji, które je wykorzystują. Aplikacja komunikuje się z klientem MCP, klient przekazuje dane protokołem do serwera, a serwer odpowiada za implementacje:
Application
│
▼
MCP Client
│
MCP Protocol
│
▼
MCP Server
/ | \
/ | \
↓ ↓ ↓
Add Multiply Weather
To umożliwia użycie serwerów narzędzi wielokrotnie. Jedna zespołowa grupa może uruchomić serwer, który udostępnia całą kategorię funkcjonalności:
Database Tools
File Tools
Git Tools
Search Tools
Weather Tools
Internal APIs
A kilka różnych agentów, klientów czatu lub integracji z IDE może korzystać z tego samego serwera bez konieczności duplikowania kodu.
Krok 10: łączenie LangGraph i MCP
Te dwie technologie rozwiązują różne aspekty tego problemu. LangGraph zarządza przepływem pracy: stanem, routowaniem, pętlami, pamięcią oraz przesyłaniem danych w czasie rzeczywistym. Standard MCP standaryzuje sposób prezentacji narzędzi i zasobów. W takim połączonym rozwiązaniu węzeł narzędzi agenta komunikuje się z klientem MCP zamiast wywoływać lokalne funkcje:
User
│
▼
LangGraph Agent
│
▼
LLM
│
Need a tool?
│
▼
MCP Client
│
▼
MCP Server
/ | \
↓ ↓ ↓
Math Search Weather
Agent nie musi znać sposobu implementacji narzędzia. Wystarczy mu wiedzieć, jakie funkcje są dostępne i jak je wywołać, a serwer MCP zajmuje się resztą. W Pythonie pakiet langchain-mcp-adapters to powszechny sposób na ładowanie narzędzi MCP jako narzędzi LangChain, aby można je było powiązać z modelem i używać w ToolNode. Istniejący artykuł na temat standaryzacji integracji narzędzi w LangGraph za pomocą MCP omawia tę integrację bardziej szczegółowo.
Kompletny model mentalny
Gdy połączymy wszystko razem, agent LangGraph wygląda w ten sposób: dane wejściowe użytkownika stają się stanem, węzły na nich operują, model decyduje, czy potrzebne jest narzędzie, wyniki narzędzia są przekazywane z powrotem do modelu, a proces kończy się odpowiedzią:
USER
│
▼
┌────────────┐
│ LangGraph │
│ Agent │
└──────┬─────┘
│
▼
STATE
│
┌─────────────┼─────────────┐
│ │ │
▼ ▼ ▼
NODE NODE NODE
│ │ │
└─────────────┼─────────────┘
│
▼
LLM
│
Need a tool?
/ \
Yes No
│ │
▼ ▼
TOOL RESPONSE
│
▼
RESULT
│
└──────► LLM
│
▼
END
wokół tego głównego pętli znajdują się funkcje, które włączasz w zależności od potrzeb:
┌──────────────────────────────┐
│ Capabilities │
│ │
│ Memory / Checkpointing │
│ Streaming │
│ Event Tracking │
│ MCP │
│ Tools │
└──────────────────────────────┘
Gdzie kończy się LangChain, a zaczyna LangGraph
Ponieważ LangGraph jest zazwyczaj wdrażany po LangChain, pomocne jest rozdzielenie ich ról. LangChain dostarcza elementy budulcowe dla aplikacji opartych na LLM: otoczki modeli, wiadomości, narzędzia, prompty oraz strukturyzowany wynik. LangGraph koordynuje te elementy, tworząc wyraźne, stanowe procesy pracy. Są to uzupełniające się warstwy, a nie konkurencyjne opcje; większość kodu LangGraph wykorzystuje komponenty LangChain w swoich węzłach.
Lekcje przenoszone na większe struktury grafowe
Kilka zasad z tego małego przykładu aplikuje się bez zmian do znacznie większych systemów.
Stan jest filarem
Zamiast przekazywać dane przez argumenty funkcji, każdy węzeł czyta i zapisuje do jednego wspólnego stanu, a reduktory określają, w jaki sposób łączą się aktualizacje:
State
↓
Node
↓
Updated State
↓
Next Node
Węzły to jednostki pracy
Każdy węzeł powinien wykonywać jedno konkretne zadanie, co ułatwia rozbieranie i testowanie złożonych procesów pracy:
chatbot node
search node
tool node
validation node
human approval node
Krawędzie kodują proces prac
Krawędź odpowiada na jedno pytanie: po zakończeniu pracy tego węzła, dokąd idzie dalsza realizacja? Proste krawędzie tworzą sekwencje; warunkowe krawędzie tworzą gałęzie i pętle.
Diagramy są lepsze od czytania kodu
W przypadku grafu z wieloma węzłami i warunkowymi ścieżkami, wygenerowany diagram przekazuje informacje o architekturze szybciej niż czytanie kodu budującego go:
Node → Decision → Tool → Agent → END
Checkpointy to coś więcej niż historia rozmów
Checkpointer przechowuje pełny stan grafu, a nie tylko wiadomości. To umożliwia rozmowy wielokrotnych rund oraz stanowi podstawę dla agentów działających przez długi czas, pauz z udziałem człowieka oraz wznowienia działania po awariach.
Strumieniowanie służy zarówno użytkownikom, jak i programistom
Strumieniowanie poprawia wrażenie szybkości reakcji, a ten sam wynik pomaga w debugowaniu i obserwowaniu przebiegu działań.
Działanie agenta to wiele operacji
Dzięki astream_events staje się jasne, że jedna wywołanie agenta to zagnieżdżona sekwencja operacji na grafie, węzłach, modelach i narzędziach:
Graph
↓
Node
↓
LLM
↓
Tool
↓
LLM
↓
Node
↓
Graph
Im bardziej złożone jest aplikacja, tym bardziej potrzebna jest ta widoczność.
MCP oddziela narzędzia od aplikacji
Narzędzia udostępniane za pośrednictwem MCP mogą być dzielone między różnymi aplikacjami i rozwijane niezależnie od agentów, które je wywołują.
Kуда iść dalej
To podejście otwiera kilka kierunków wartych zbadania: warunkowe krawędzie i równoległe gałęzie, przerwy z udziałem człowieka, pamięć długoterminowa i trwałe punkty kontrolne, generowanie wzbogacone o możliwości wyszukiwania w ramach grafu, systemy wieloagentowe, głębsze integracje MCP, obserwowalność i ocena agentów, wdrożenie w produkcję oraz tolerancja na błędy z możliwością ponownych prób.
Wspólnym mianownikiem jest to, że aplikacja AI to nie tylko model wytwarzający tekst. To system zaprojektowany wokół modelu, który potrafi przyjąć żądanie, podjąć decyzję, działać, zaobserwować wynik, zapamiętać go i kontynuować działanie:
Understand
↓
Decide
↓
Act
↓
Observe
↓
Remember
↓
Continue
Graph jest naturalnym sposobem na przedstawienie takiego cyklu.
Podsumowanie
Cała ta droga zaczęła się od najmniejszego możliwego grafu:
START → Chatbot → END
i stopniowo, warstwa po warstwie, rozwinęła się w system wyposażony w narzędzia, cykl agentów, pamięć, przepływ danych oraz MCP:
┌─────────────┐
│ LLM │
└──────┬──────┘
│
▼
┌─────────────┐
│ Tools │
└──────┬──────┘
│
▼
┌─────────────┐
│ Agent │
└──────┬──────┘
│
┌──────────┼──────────┐
↓ ↓ ↓
Memory Streaming MCP
│ │ │
└──────────┼──────────┘
↓
AI Application
Gdy spojrzeć na nie razem, te elementy przestają wyglądać jak lista i zaczynają przypominać jedną architekturę:
- State przechowuje dane, a za pośrednictwem punktów kontrolnych – pamięć; do list wiadomości stosuje się reduktora
add_messages. - Nodes wykonywają pracę i zwracają częściowe aktualizacje.
- Edges określają przepływ, a krawędzie warunkowe zamieniają go w pętlę.
- The model dostarcza rozumowanie i język; tools umożliwiają mu działanie w świecie zewnętrznym.
- Streaming i events sprawiają, że każdy proces jest widoczny – od aktualizacji interfejsu po ślady działania.
- MCP standaryzuje sposób prezentacji narzędzi, dzięki czemu mogą one istnieć poza agentem, który je używa.
Literatura pokrewna
- Od surowego tekstu do pipelin: parserzy, LCEL, elementów wykonywalnych i pamięci w LangChain — Jak LangChain przekształca surowy tekst modelu w dane strukturyzowane, łączy kroki za pomocą LCEL i interfejsu Runnable oraz zarządza pamięcią konwersacyjną obecnie.
- Budowanie agenta badawczego ReAct w LangGraph: Brain, Hands, Router — Dowiedz się, jak zaimplementować pętlę ReAct (reason-act-observe) jako podgraf LangGraph, z wymuszoną refleksją, budżetami iteracji oraz równoległymi metodami badawczymi typu scatter-gather.