Standardowy układ siedmiu sekcji dla każdego agenta LangGraph
Ustalona struktura plików dla agentów LangGraph, obejmująca elementy od importów i stanu po narzędzia, węzły, krawędzie, proces montażu i wywoływania, wraz z szablonami oraz rozszerzeniem wieloagentowym.
Źródła: The LangGraph Mental Model: A standardized architecture guide for every agent you’ll ever build
Każdy, kto już przez jakiś czas bada LangGraph, zna to dziwne uczucie: pomysł wydaje się sensowny, ale kod nadal nie przychodzi do głowy. Graf zawiera węzły, które wykonywają zadania, krawędzie decydują, dokąd iść dalej, a stan przechowuje informacje. Można to narysować na tablicy w ciągu pięciu minut. Potem otwierasz pusty plik, a nic nie pojawia się. Zapomniałeś o importach, klasa, której potrzebujesz, nie ma oczywistego miejsca umieszczenia, a wydaje się, że istnieje pięć sposobów na napisanie tego samego kodu.
Dlaczego kod wydaje się trudniejszy niż sam koncept
Podstawowym powodem jest to, że LangGraph nie narzuca żadnej własnej struktury. Dostarcza sprawne elementy budulcowe i pozostawia organizację tobie. Każdy tutorial oraz każdy repozytorium przedstawia rzeczy w inny sposób, więc twoja pamięć nigdy nie ustala się na jednej, konkretnej strukturze.
Rozwiązanie proponowane tutaj polega na wyborze jednego standardowego układu i jego stosowaniu dla każdego agenta, niezależnie od tego, czy jest to mały chatbot, czy złożony system z wieloma współpracującymi agentami. Można to porównać do szablonu, który można zapamiętać i bez trudu przywołać, podobnie jak diagramy pokazujące, co agent już zrobił, tylko że w tym przypadku odnoszą się one do kodu źródłowego, a nie do historii działania w czasie rzeczywistym.
Cały plik na pierwszy rzut oka
Każda aplikacja LangGraph może być zapisana jako jeden plik składający się z siedmiu sekcji w ustalonej kolejności, od góry do dołu:
langgraph_agent.py
│
├── MODULE 1: IMPORTS & CONFIGURATION
│ └── All your libraries, API keys, model setup
│
├── MODULE 2: STATE
│ └── The TypedDict that defines your agent's memory
│
├── MODULE 3: TOOLS (optional, but common)
│ └── Functions decorated with @tool that the LLM can call
│
├── MODULE 4: NODES
│ └── Functions that do the actual work at each graph step
│
├── MODULE 5: EDGES & ROUTING
│ └── Functions that decide what happens next
│
├── MODULE 6: GRAPH ASSEMBLY
│ └── Where you build, wire, and compile the graph
│
└── MODULE 7: ENTRYPOINT
└── The __main__ block or invoke() call that runs everything
To jest szkielet. Gdy rozpoczynasz projekt, otwórz plik i wpisz siedem tytułów sekcji jako komentarze, a następnie wypełniaj je po kolei. W dalszej części omówiono każdą sekcję: jej przeznaczenie, nazwy, które musisz znać, oraz szablon do skopiowania.
Sekcja 1: importy i konfiguracja
Przeznaczenie
Traktuj tę sekcję jako zestaw narzędzi. Stolarz układa wszystkie narzędzia przed rozpoczęciem pracy, a tutaj mówisz Pythonowi, jakie zewnętrzne funkcje są ci potrzebne, oraz konfigurujesz model, który będzie używany przez agenta.
Nazwy, które należy znać
ChatOpenAIlubChatAnthropicto klasa modelu językowego, „mózg” agenta. Stwórz ją tutaj raz i używaj jej wszędzie.StateGraphto klasa podstawowa, która buduje graf, podobny do planu, który rysujesz.ENDto specjalna stała oznaczająca punkt końcowy. Krawędź wskazująca na niego zatrzymuje działanie.MemorySaverto wskaźnik kontrolny, który przechowuje pamięć przez kilka kolejnych rozmów.
Śablon
# --- Standard Library ---
import os
from typing import TypedDict, Annotated, Literal
# --- LangChain Core ---
from langchain_openai import ChatOpenAI # or ChatAnthropic, ChatGroq, etc.
from langchain_core.messages import HumanMessage, AIMessage, SystemMessage, BaseMessage
from langchain_core.tools import tool
# --- LangGraph Core ---
from langgraph.graph import StateGraph, END
from langgraph.graph.message import add_messages # The message reducer
from langgraph.prebuilt import ToolNode # Pre-built node for tool execution
from langgraph.checkpoint.memory import MemorySaver
# --- Configuration ---
# Always name your model variable 'llm' - it's the standard in every node
llm = ChatOpenAI(
model="gpt-4o", # or "claude-3-5-sonnet-20241022", etc.
temperature=0, # 0 = deterministic; raise for creativity
api_key=os.environ.get("OPENAI_API_KEY")
)
Dlaczego jest tak ułożone
Importy są podzielone na cztery grupy: bibliotekę standardową, LangChain (warstwa modelu), LangGraph (warstwa grafu) oraz konfigurację. Takie rozwiązanie odpowiada strukturze samych bibliotek – LangChain dostarcza elementy podstawowe AI, natomiast LangGraph zajmuje się orkiestracją. Oddzielanie grup umożliwia szybkie zidentyfikowanie tego, co należy gdzie, gdy coś się zepsuje.
Najlepszym nawykiem jest nazywanie modelu llm. Każda funkcja węzła będzie później odwoływać się do tej zmiennej, a jeśli jeden projekt używa nazwy model, a inny chat, kod traci spójność i staje się trudniejszy do szybkiego przeglądania.
Część 2: stan
Do czego służy
State to pamięć robocza agenta, jedyny źródło prawdy, które przepływa przez cały graf. Każdy węzeł może z niego czytać i do niego pisać, podczas gdy silnik zarządza jego cyklem życia. Spośród wszystkiego w LangGraph prawidłowe użycie State jest prawdopodobnie najważniejsze.
Dobrym przykładem może być tablica informacyjna w biurze. Każda osoba wchodząca do pomieszczenia, czyli każdy węzeł, może przeczytać to, co na niej jest, i coś dodać, a tablica pozostaje nietknięta przez całe spotkanie, co odpowiada całkowitemu uruchomieniu grafu.
Nazwy, które należy znać
TypedDictto klasa w Pythonie, która opisuje strukturę stanu, podobnie jak schemat. Twoja klasa State dziedziczy od niej.Annotatedto otoczka do wskazówek typów. LangGraph używa jej do dołączenia reducera do pola, czyli funkcji, która decyduje, jak nowa wartość łączy się z tą, która już tam jest.
add_messages to wbudowany reduktor dla pola messages. Zamiast zastępować listę za każdym razem, gdy węzeł dodaje wiadomość, on dodaje do końca. To reduktor, którego będziesz używał najczęściej.operator.add to kolejny powszechny reduktor, przeznaczony dla pól, które powinny się akumulować poprzez dodawanie, takich jak lista wyników lub licznik.Śablon
# ============================================================
# MODULE 2: STATE
# ============================================================
class AgentState(TypedDict):
# 'messages' is the heartbeat of almost every LangGraph agent.
# Annotated[list, add_messages] means: "this is a list, and when
# a node writes to it, append - don't replace."
messages: Annotated[list[BaseMessage], add_messages]
# Add custom fields below for your specific agent's needs.
# Fields without a reducer are REPLACED each time a node writes to them.
# Example: a simple string field (gets replaced each write)
current_task: str
# Example: a list you want to accumulate (use operator.add as reducer)
# results: Annotated[list[str], operator.add]
# Example: a counter
# iteration_count: int
Rozważania na temat reducerów
Reducerzy mylą wielu początkujących, więc oto analogia. Wyobraź sobie stan jako formularz papierowy przekazywany po biurze. Pole messages to zeszyt notatek klejących: każdy, kto zajmuje się formularzem, dodaje notatkę, a nikt nie usuwa tych wcześniejszych. To właśnie jest add_messages.
Pole bez reducera to pojedyncze pole tekstowe. Każda osoba nadpisuje to, co napisał poprzedni użytkownik, co jest standardowym zachowaniem w LangGraph – ostatnia edycja zwycięża.
Obowiązuje prosta zasada: za każdym razem przypisz pole messages wartość Annotated[list, add_messages], dla każdej dodatkowej listy, która ma się dalej rozwijać, wybierz Annotated[list, operator.add], a wszystkie pozostałe pola pozostaw niezmienione.
Rozdział 3: narzędzia
Dla czego służą
Narzędzia to ręce agenta. LLM jest mózgiem, który rozumuje i podejmuje decyzje, ale sam nie może działać w świecie. Dzięki narzędziom może przeglądać internet, korzystać z API, wykonywać kod, szukać informacji w bazie danych lub uruchamiać dowolną funkcję w Pythonie, którą zdefiniujesz.
Ta sekcja jest opcjonalna. Agent, który wytwarza tylko tekst i nigdy nie korzysta z zewnętrznych systemów, może ją pominąć, chociaż większość rzeczywistych agentów potrzebuje narzędzi.
Nazwy, które warto znać
@toolto dekorator z LangChain, który przekształca zwykłą funkcję w Pythonie w narzędzie zrozumiałe dla LangGraph. Dokumentacja funkcji staje się opisem, który LLM czyta podczas decydowania o jej użyciu.ToolNodeto gotowy węzeł dostępny wlanggraph.prebuilt, który uruchamia narzędzia wybrane przez LLM, dzięki czemu nie musisz sam pisać logiki wykonywania.
bind_tools to metoda w instancji twojego LLM, która do niej dołącza narzędzia. Dzięki temu model wie, co jest dostępne, i może poprosić o wykonanie zadania, wysyłając specjalną wiadomość tool_calls zamiast zwykłego tekstu.Śablon
# ============================================================
# MODULE 3: TOOLS
# ============================================================
@tool
def search_web(query: str) -> str:
"""Search the web for current information about a topic.
Use this when you need real-time information that is not in
your training data, such as recent news or live prices.
Args:
query: The search query string.
Returns:
A string containing search results.
"""
# Your actual implementation here (e.g., Tavily, SerpAPI, etc.)
# Placeholder for illustration:
return f"Search results for: {query}"
@tool
def calculate(expression: str) -> str:
"""Evaluate a mathematical expression and return the result.
Use this for any arithmetic, algebra, or numerical computation.
Args:
expression: A valid Python math expression as a string, e.g. '2 + 2 * 10'
Returns:
The computed result as a string.
"""
try:
return str(eval(expression))
except Exception as e:
return f"Error: {e}"
# Collect all tools into a list - this is the pattern you always follow
tools = [search_web, calculate]
# Bind tools to the LLM so it knows they exist and can choose to call them
llm_with_tools = llm.bind_tools(tools)
# Create the pre-built ToolNode that will execute tool calls automatically
tool_node = ToolNode(tools)
Docstring ma większe znaczenie, niż się wydaje
Początkujący często tego nie dostrzegają: model polega na twoim docstringu, aby wybrać narzędzie. Czyta tekst znajdujący się w cudzysłowach potrójnych. Jeśli docstring jest niejasny lub brakuje go, model LLM nie będzie wiedział, kiedy twoje narzędzie jest odpowiednie, dlatego pisz jasne, konkretne opisy, które określają co robi narzędzie i kiedy należy je używać.
Rozdział 4: węzły
Do czego służą
Węzły to „pracownicy”. Każdy z nich to funkcja w Pythonie, która otrzymuje aktualny stan, wykonuje jakieś działanie, takie jak wezwanie modelu LLM, przetwarzanie danych lub uruchomienie narzędzia, a następnie zwraca słownik z aktualizacjami stanu. To jest pełny opis zadań.
Węzeł nigdy nie decyduje o tym, co dzieje się później. Wykonuje swoją pracę i aktualizuje stan, a kwestie routingu są omawiane w rozdziale 5.
Nazwy, które należy znać
stateto parametr, który otrzymuje każdy węzeł – to słownik w formie strukturyAgentState. W jego wnętrzu można odczytać wartości takie jakstate["messages"]lubstate["current_task"].HumanMessage,AIMessageorazSystemMessageto trzy typy wiadomości, których będziesz stale używać. Pierwszy reprezentuje to, co powiedział użytkownik, drugi to odpowiedź modelu, a trzeci zawiera twoje instrukcje skierowane do modelu, czyli prompt systemowy.invoketo funkcja służąca do wywołania LLM lubllm_with_toolsw celu uzyskania odpowiedzi. Przekazujesz listę wiadomości i otrzymujeszAIMessage.
AgentState. Jego wartościami są aktualizacje, a nie pełny stan, a LangGraph łączy je za pomocą reduktorów, które zdefiniowałeś.Śablon
# ============================================================
# MODULE 4: NODES
# ============================================================
# ── Node: Agent (the reasoning brain) ───────────────────────
def agent_node(state: AgentState) -> dict:
"""The central reasoning node. Calls the LLM and decides
whether to respond or call a tool."""
# Build the message list to send to the LLM.
# Always include a system message to set behavior.
system_prompt = SystemMessage(content=(
"You are a helpful assistant. Use the available tools "
"when you need real-time information or computation. "
"Respond clearly and concisely."
))
# The LLM receives the system prompt + all previous messages in state
messages_to_send = [system_prompt] + state["messages"]
# Call the LLM. Use llm_with_tools if you have tools; plain llm if not.
response = llm_with_tools.invoke(messages_to_send)
# Return the LLM's response as a state update.
# add_messages will APPEND this AIMessage to state["messages"].
return {"messages": [response]}
# ── Node: Summarizer (example of a non-LLM processing node) ─
def summarize_node(state: AgentState) -> dict:
"""Summarizes the conversation so far to keep context short.
This shows that nodes don't have to call an LLM - they can
do any Python processing."""
all_messages = state["messages"]
# Summarize with the LLM (a different prompt, same LLM)
summary_prompt = [
SystemMessage(content="Summarize the following conversation in 2-3 sentences."),
HumanMessage(content=str(all_messages))
]
summary_response = llm.invoke(summary_prompt)
# Replace messages with a fresh start containing just the summary
return {
"messages": [AIMessage(content=f"[Summary] {summary_response.content}")]
}
Zdjęcie przedstawiające węzły
Wyobraź sobie węzeł jako biegacza przekazującego batutę. Bierze batutę, która reprezentuje stan, przebiega swoją trasę, czyli wykonywa pracę, a następnie przekazuje batutę z zaktualizowanymi danymi. Nie wybiera następnego biegacza – to zadanie należy do osoby kierującej wyścigiem, czyli krawędzi.
Każda specyfikacja węzła wygląda tak samo: def my_node(state: AgentState) -> dict. Wejściem jest cały stan, a wyjściem jest słownik zawierający tylko te pola, które chcesz zmienić.
Rozdział 5: krawędzie i routowanie
Dla czego służą
Krawędzie stanowią logikę decyzyjną grafu. Łączą węzły ze sobą i określają sposób przepływu wykonywania zadań. LangGraph posiada dwa rodzaje krawędzi.
Krawędzie statyczne łączą jeden węzeł z drugim bez żadnych warunków: „po sumatorze przejść do agenta”. Nie ma tu żadnej decyzji, tylko bezpośrednie połączenie.
Krawędzie warunkowe biorą pod uwagę aktualny stan i zwracają ciąg znaków określający następny węzeł. To właśnie tutaj znajduje się inteligencja agenta dotycząca jego własnego przepływu. Czy powinien on użyć narzędzia, zakończyć działanie, czy wrócić, by spróbować ponownie?
Nazwy, które należy znać
add_edge(A, B)tworzy statyczną krawędź, co oznacza, że kontrola zawsze przechodzi od A do B.add_conditional_edges(source, routing_function, mapping)tworzy gałąź wychodzącą odsource. Funkcja routingu otrzymuje aktualny stan i zwraca ciąg znaków, a słownikmappingprzekształca ten ciąg w rzeczywistą nazwę następnego węzła.
tools_condition to gotowa funkcja routingu z modułu langgraph.prebuilt. Sprawdza ostatnią wiadomość w stanie pod kątem wywołań narzędzi. Jeśli takie istnieją, kieruje ruch do "tools", a w przeciwnym razie do END. Jest to standardowy wzorzec dla agentów używających narzędzi i warto go zapamiętać.START to stała oznaczająca początek grafu. Ustawienie punktu wejścia oznacza dodanie krawędzi od START do pierwszego węzła.Śablon
# ============================================================
# MODULE 5: EDGES & ROUTING
# ============================================================
# Import the pre-built tool routing function
from langgraph.prebuilt import tools_condition
# ── Custom Routing Function Example ─────────────────────────
def should_continue(state: AgentState) -> Literal["tools", "summarize", "__end__"]:
"""Custom router for the agent node.
Routing functions always:
1. Receive the current state as input
2. Return a string that maps to the next node (or END)
The return values must match the keys in add_conditional_edges' mapping.
"""
last_message = state["messages"][-1] # Look at what the LLM just said
# Case 1: The LLM decided to call a tool
if hasattr(last_message, "tool_calls") and last_message.tool_calls:
return "tools"
# Case 2: The conversation is getting long - summarize before continuing
if len(state["messages"]) > 20:
return "summarize"
# Case 3: The LLM gave a direct answer - we're done
return "__end__" # LangGraph's internal name for END
Zdjęcie przedstawiające proces routingu
Można porównać funkcję routingu do operatora sygnałów kolejowych. Pociąg przyjeżdża na stację, gdy węzeł kończy swoją pracę. Operator sprawdza aktualny stan, określony w manifestacji, i ustawia odpowiedni przełącznik, aby pociąg skręcił na właściwą torową linię. Operator nigdy nie przesuwa pociągu – to robi LangGraph.
Jedna zasada, którą należy pamiętać: funkcje routingu nigdy nie modyfikują stanu. Mogą go jedynie odczytywać. Wszelkie zmiany powinny odbywać się w węźle, więc jeśli chcesz zaktualizować stan podczas routingu, przenieś tę logikę do węzła.
Rozdział 6: budowa grafu
Dla czego służy
Tutaj wszystko się łączy. Traktuj ten rozdział jako etap budowy. Rozdziały od 1 do 5 stanowiły przygotowanie – określono materiały, wycięto i ułożono elementy, a teraz łączysz je ze sobą, podłączając odpowiednie części i kompilując całość.
Składanie zawsze odbywa się według tych samych pięciu kroków, które warto zapamiętać: inicjalizacja, rejestracja węzłów, ustawienie punktu wejścia, łączenie krawędzi, kompilacja.
Nazwy, które należy znać
StateGraph(AgentState)tworzy narzędzie budujące, używając Twojej klasy State jako schematu, więc każdy węzeł musi zwracać aktualizacje z nią kompatybilne.add_node(name, function)rejestruje węzeł. Nazwa to ciąg znaków, który będzie używany w krawędziach, a funkcja to węzeł z sekcji 4.set_entry_point(name)określa, który węzeł zostanie uruchomiony jako pierwszy po wywołaniu grafu.add_edge(A, B)tworzy bezpośrednie, bezwarunkowe połączenie od A do B.add_conditional_edges(source, router, mapping)dodaje warunkowy rozgałęzienie od źródła, wykorzystując Twoją funkcję routingu oraz mapowanie od zwracanych ciągów znaków do nazw destynacji.compile(checkpointer=...)zamienia graf na obiekt wykonywalny (CompiledGraph). Po tym kroku nie można już dodawać węzłów ani krawędzi. Opcjonalny parametr checkpointer włącza pamięć trwałą.
Śablon
# ============================================================
# MODULE 6: GRAPH ASSEMBLY
# ============================================================
# ── Step 1: Initialize ──────────────────────────────────────
# Always pass your State class to StateGraph
graph_builder = StateGraph(AgentState)
# ── Step 2: Register All Nodes ──────────────────────────────
# Format: add_node("node_name_as_string", node_function)
# The string name is what you use in ALL edge definitions
graph_builder.add_node("agent", agent_node)
graph_builder.add_node("tools", tool_node) # The pre-built ToolNode from Module 3
graph_builder.add_node("summarize", summarize_node)
# ── Step 3: Set Entry Point ─────────────────────────────────
# Which node runs first when we invoke the graph?
graph_builder.set_entry_point("agent")
# ── Step 4: Wire the Edges ──────────────────────────────────
# Conditional edge from agent: check if we need tools, a summary, or we're done
graph_builder.add_conditional_edges(
"agent", # Source node
should_continue, # Routing function from Module 5
{
"tools": "tools", # If router returns "tools" → go to tools node
"summarize": "summarize", # If router returns "summarize" → go to summarize node
"__end__": END, # If router returns "__end__" → stop the graph
}
)
# Static edge: after tools run, always go back to agent (the ReAct loop)
graph_builder.add_edge("tools", "agent")
# Static edge: after summarization, always return to agent
graph_builder.add_edge("summarize", "agent")
# ── Step 5: Compile ─────────────────────────────────────────
# Without checkpointer: no persistent memory (stateless per invocation)
# With checkpointer: memory persists across turns (stateful conversations)
memory = MemorySaver()
graph = graph_builder.compile(checkpointer=memory)
Zdjęcie przedstawiające proces montażu
Najlepszym porównaniem jest płyta drukowana. Każda wywołanie funkcji add_node umieszcza komponent. Funkcje add_edge oraz add_conditional_edges łączą przewody pomiędzy komponentami. Funkcja compile() przeprowadza końcową weryfikację i finalizuje płytkę. Po tym etapie jest ona gotowa do podłączenia zasilania, co realizuje funkcja invoke, oraz do wykonywania swoich zadań.
Śablon zawiera podstawowy pętlę ReAct: agent → tools → agent → tools i tak dalej, aż agent zdecyduje, że skończył, i przejdzie do punktu END. Ten cykl „rozumowanie – działanie” leży u podstaw niemal wszystkich agentów LangGraph wykorzystujących narzędzia, a jego rozpoznanie pomaga zrozumieć większość grafów, z którymi się spotkasz.
Rozdział 7: punkt wejścia i wywołanie
Dla czego to służy
To jest platforma startowa. Wszystko, co było wcześniej, to definicja, a ta sekcja to realizacja: kod, który uruchamia graf, dostarcza dane wejściowe i zbiera wyniki.
Istnieją dwa sposoby jego uruchomienia – invoke do jednorazowego przetworzenia oraz streaming dla stopniowego wyświetlania wyników. Potrzebny jest również config, słownik zawierający parametry działania, w tym przede wszystkim thread_id używany do zarządzania pamięcią.
Nazwy, które należy znać
graph.invoke(input, config)uruchamia cały graf i zwraca końcowy stan. Ponieważ jest to proces synchroniczny, nic nie otrzymujesz, dopóki nie zakończy się każdy krok.graph.stream(input, config)również uruchamia graf, ale zwraca częściowe aktualizacje stanu w miarę ich powstawania, co jest odpowiednie dla aplikacji przeznaczonych dla użytkowników, które muszą pokazywać aktualny postęp.
config to słownik przekazywany przy każdej wywołaniu. Jego klucz ma wartość {"configurable": {"thread_id": "some_id"}}. ID wątku funkcjonuje jak ID sesji, które wskazuje checkpointerowi, jaką pamięć należy przywrócić i zapisać. Dwóch użytkowników z różnymi ID wątków w ogóle nie dzieli pamięci.HumanMessage(content=...) to sposób na opakowanie danych wprowadzonych przez użytkownika przed przekazaniem ich do grafu.Śablon
# ============================================================
# MODULE 7: ENTRYPOINT & INVOCATION
# ============================================================
if __name__ == "__main__":
# ── Config: defines this conversation's memory session ──
# Change thread_id to start a fresh conversation.
# Keep the same thread_id to continue an existing one.
config = {"configurable": {"thread_id": "user-session-001"}}
# ── Single Invocation (synchronous) ─────────────────────
user_input = "What is the current price of Bitcoin?"
result = graph.invoke(
input={"messages": [HumanMessage(content=user_input)]},
config=config
)
# The result is the final state dictionary.
# Access the last message to get the agent's final answer.
final_answer = result["messages"][-1].content
print(f"Agent: {final_answer}")
# ── Streaming Invocation (for real-time output) ──────────
for chunk in graph.stream(
input={"messages": [HumanMessage(content=user_input)]},
config=config,
stream_mode="values" # Yields the full state after each node runs
):
# Each chunk is a state snapshot. The last message shows progress.
latest = chunk["messages"][-1]
if hasattr(latest, "content") and latest.content:
print(f"[Streaming] {latest.content}")
# ── Multi-turn Conversation Loop ─────────────────────────
print("\n--- Starting Interactive Session ---")
while True:
user_text = input("You: ").strip()
if user_text.lower() in ("exit", "quit", "bye"):
break
response = graph.invoke(
input={"messages": [HumanMessage(content=user_text)]},
config=config # Same config = same memory thread
)
print(f"Agent: {response['messages'][-1].content}\n")
Pełny szablon
Poniżej znajduje się kompletna, minimalna struktura do skopiowania do nowego projektu. To jedyny plik, który warto zaznaczyć jako ulubiony – zastąp jego placeholderami własną logikę, a uzyskasz podstawę dowolnego agenta LangGraph.
# ============================================================
# LANGGRAPH CANONICAL AGENT TEMPLATE
# Modules: Imports → State → Tools → Nodes → Edges → Assembly → Entrypoint
# ============================================================
# ── MODULE 1: IMPORTS & CONFIGURATION ───────────────────────
import os
from typing import TypedDict, Annotated, Literal
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage, AIMessage, SystemMessage, BaseMessage
from langchain_core.tools import tool
from langgraph.graph import StateGraph, END
from langgraph.graph.message import add_messages
from langgraph.prebuilt import ToolNode, tools_condition
from langgraph.checkpoint.memory import MemorySaver
llm = ChatOpenAI(model="gpt-4o", temperature=0)
# ── MODULE 2: STATE ─────────────────────────────────────────
class AgentState(TypedDict):
messages: Annotated[list[BaseMessage], add_messages]
# Add your custom fields here
# ── MODULE 3: TOOLS ─────────────────────────────────────────
@tool
def my_tool(input: str) -> str:
"""Describe clearly what this tool does and when the LLM should use it."""
return f"Result for: {input}"
tools = [my_tool]
llm_with_tools = llm.bind_tools(tools)
tool_node = ToolNode(tools)
# ── MODULE 4: NODES ─────────────────────────────────────────
def agent_node(state: AgentState) -> dict:
"""The reasoning node. Calls the LLM, optionally triggers tool calls."""
messages = [SystemMessage(content="You are a helpful assistant.")] + state["messages"]
response = llm_with_tools.invoke(messages)
return {"messages": [response]}
# ── MODULE 5: EDGES & ROUTING ───────────────────────────────
def should_continue(state: AgentState) -> Literal["tools", "__end__"]:
"""Decide: did the LLM call a tool, or did it give a final answer?"""
last_message = state["messages"][-1]
if hasattr(last_message, "tool_calls") and last_message.tool_calls:
return "tools"
return "__end__"
# ── MODULE 6: GRAPH ASSEMBLY ────────────────────────────────
graph_builder = StateGraph(AgentState)
graph_builder.add_node("agent", agent_node)
graph_builder.add_node("tools", tool_node)
graph_builder.set_entry_point("agent")
graph_builder.add_conditional_edges(
"agent",
should_continue,
{"tools": "tools", "__end__": END}
)
graph_builder.add_edge("tools", "agent")
memory = MemorySaver()
graph = graph_builder.compile(checkpointer=memory)
# ── MODULE 7: ENTRYPOINT ────────────────────────────────────
if __name__ == "__main__":
config = {"configurable": {"thread_id": "session-001"}}
while True:
user_text = input("You: ").strip()
if not user_text or user_text.lower() in ("exit", "quit"):
break
response = graph.invoke(
{"messages": [HumanMessage(content=user_text)]},
config=config
)
print(f"Agent: {response['messages'][-1].content}\n")
Kroki dalej: systemy wieloagentowe
Gdy podejście z jednym agentem wydaje się naturalne, systemy wieloagentowe przestają być przerażające. Podstawowa idea polega na tym, że każdy agent to po prostu skompilowany graf, a skompilowany graf może sam być węzłem w większym grafie nadzorczym.
Nazwy, które warto znać w pracy z systemami wieloagentowymi
- Węzeł nadzorczy to agent, którego zadaniem nie jest odpowiadanie użytkownikowi, lecz wybór który specjalista powinien zająć się zadaniem. On kieruje ruchem pomiędzy podagentami.
- Podgraf to skompilowany graf zarejestrowany jako węzeł w innym grafie. Przyjmuje i zwraca stan, tak jak każda zwykła funkcja węzła.
Command to nowszy wzorzec dostępny od wersji LangGraph 0.2, w którym jeden węzeł zwraca zarówno aktualizację stanu, jako też instrukcję routingu w jednej wartości. W niektórych projektach wielu agentów eliminuje to konieczność używania oddzielnych krawędzi warunkowych.Szablon strukturalny
# ── MULTI-AGENT PATTERN ─────────────────────────────────────
# Each specialist is a compiled graph (a subgraph)
# Sub-agent 1: A researcher
researcher_graph = StateGraph(AgentState)
# ... (built with its own nodes, edges, and tools)
researcher = researcher_graph.compile()
# Sub-agent 2: A writer
writer_graph = StateGraph(AgentState)
# ... (built with its own nodes, edges, and tools)
writer = writer_graph.compile()
# ── SUPERVISOR NODE ─────────────────────────────────────────
def supervisor_node(state: AgentState) -> dict:
"""Decides which sub-agent should handle the current task."""
# The supervisor LLM decides: "researcher" or "writer" or "FINISH"
response = supervisor_llm.invoke(state["messages"])
return {"messages": [response], "next_agent": response.content}
def route_to_agent(state: AgentState) -> Literal["researcher", "writer", "__end__"]:
"""Routes to the appropriate sub-agent based on supervisor's decision."""
return state.get("next_agent", "__end__")
# ── SUPERVISOR GRAPH ────────────────────────────────────────
supervisor_builder = StateGraph(AgentState)
supervisor_builder.add_node("supervisor", supervisor_node)
supervisor_builder.add_node("researcher", researcher) # Subgraph as a node!
supervisor_builder.add_node("writer", writer) # Subgraph as a node!
supervisor_builder.set_entry_point("supervisor")
supervisor_builder.add_conditional_edges(
"supervisor",
route_to_agent,
{"researcher": "researcher", "writer": "writer", "__end__": END}
)
supervisor_builder.add_edge("researcher", "supervisor")
supervisor_builder.add_edge("writer", "supervisor")
supervisor_graph = supervisor_builder.compile(checkpointer=MemorySaver())
Wersja z wieloma agentami ma taką samą strukturę jak wersja z jednym agentem i obowiązują w niej te same siedem sekcji. Jedyną różnicą jest to, że niektóre z „węzłów” to kompletne, skompilowane grafy. Model mentalny skaluje się bez żadnych trudności.
Szybki przewodnik
Oto skrócona karta, którą można wydrukować lub przymocować, zawierająca główne idee w jednym miejscu.
- Stan:
TypedDictokreśla jego strukturę.Annotated[T, reducer]dołącza reduktor do pola.add_messagesto standardowy reduktor list wiadomości, który dodaje elementy zamiast je zastępować.operator.addkumuluje listy lub liczby. - LLM:
ChatOpenAIlubChatAnthropicto klasa, zawsze powiązana z nazwąllm. Funkcjabind_tools(tools)informuje model o dostępnych narzędziach. Funkcjainvoke(messages)wywołuje model i zwraca odpowiedź. - Wiadomości:
HumanMessagezawiera dane wprowadzone przez użytkownika,AIMessagezawiera wynik generowany przez model, aSystemMessagezawiera twoje instrukcje. Zawsze są one przekazywane do funkcjiinvokew formie listy. - Narzędzia: Tag
@toolumożliwia wywołanie funkcji w Pythonie przez model, przy czym jej dokumentacja służy jako opis. FunkcjaToolNode(tools)automatycznie uruchamia wywołania narzędzi. Funkcjatools_conditionsprawdza, czy model poprosił o użycie jakiegoś narzędzia. - Wykres:
StateGraph(State)tworzy narzędzie budujące,add_node(name, fn)rejestruje węzeł,set_entry_point(name)wybiera pierwszy węzeł,add_edge(A, B)łączy węzły bezwarunkowo,add_conditional_edges(source, router, mapping)łączy węzły na podstawie wartości zwracanej przez routera,compile(checkpointer=...)finalizuje proces, aENDoznacza punkt końcowy. - Wywołanie:
graph.invoke(input, config)zwraca końcowy stan,graph.stream(input, config)dostarcza aktualizacje stopniowo,config = {"configurable": {"thread_id": "..."}}identyfikuje sesję pamięciową, aMemorySaver()jest wskaźnikiem do sprawdzania w pamięci.
Zrobienie tego nawykiem
Celem nie jest uczenie LangGraph od zera. Chodzi o dostarczenie mózgowi stabilnej ramy, którą może automatycznie przyjąć, tak jak muzyk sięga po znajomą progresję akordową lub kucharz przygotowuje składniki przed rozpoczęciem pracy.
Siedem sekcji – importy, stan, narzędzia, węzły, krawędzie, montaż i punkt wejścia – tworzy tę strukturę. Nazwy w każdej sekcji to nuty, a cały szablon stanowi partyturę.
Zarówno prosty chatbot, jak i łańcuch z dziesiątkami węzłów pasują do tej samej struktury. Złożoność rośnie, ale struktura pozostaje niezmienna, a właśnie ta stabilność czyni ją standardem.
Rozsądny sposób rozpoczęcia pracy to zaczęcie od małego modelu z minimalnym pętlą ReAct: jeden węzeł agenta, jeden węzeł narzędzia oraz jedna warunkowa krawędź wykorzystująca tools_condition. Najpierw sprawdź, czy to działa, potem dodaj kolejny węzeł, a później kolejnego agenta. Struktura pozostaje ta sama niezależnie od rozmiaru modelu.
Jeśli chodzi o backendy do przechowywania danych, wzorce z udziałem człowieka oraz opcje wdrażania, następnym miejscem, gdzie należy poszukać informacji, jest oficjalna dokumentacja LangGraph.