Strona główna / Artykuły

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.

Wydawca: REACTAPP.TOP19 min czytania4469 słów

Ź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ć

  • ChatOpenAI lub ChatAnthropic to klasa modelu językowego, „mózg” agenta. Stwórz ją tutaj raz i używaj jej wszędzie.
  • StateGraph to klasa podstawowa, która buduje graf, podobny do planu, który rysujesz.
  • END to specjalna stała oznaczająca punkt końcowy. Krawędź wskazująca na niego zatrzymuje działanie.
  • MemorySaver to 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ć

  • TypedDict to klasa w Pythonie, która opisuje strukturę stanu, podobnie jak schemat. Twoja klasa State dziedziczy od niej.
  • Annotated to 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ć

    • @tool to 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.
    • ToolNode to gotowy węzeł dostępny w langgraph.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ć

    • state to parametr, który otrzymuje każdy węzeł – to słownik w formie struktury AgentState. W jego wnętrzu można odczytać wartości takie jak state["messages"] lub state["current_task"].
    • HumanMessage, AIMessage oraz SystemMessage to 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.
    • invoke to funkcja służąca do wywołania LLM lub llm_with_tools w celu uzyskania odpowiedzi. Przekazujesz listę wiadomości i otrzymujesz AIMessage.
  • Wartość zwracana to zawsze słownik, którego klucze odpowiadają nazwom pól w 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ą od source. Funkcja routingu otrzymuje aktualny stan i zwraca ciąg znaków, a słownik mapping przekształ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: TypedDict określa jego strukturę. Annotated[T, reducer] dołącza reduktor do pola. add_messages to standardowy reduktor list wiadomości, który dodaje elementy zamiast je zastępować. operator.add kumuluje listy lub liczby.
    • LLM: ChatOpenAI lub ChatAnthropic to klasa, zawsze powiązana z nazwą llm. Funkcja bind_tools(tools) informuje model o dostępnych narzędziach. Funkcja invoke(messages) wywołuje model i zwraca odpowiedź.
    • Wiadomości: HumanMessage zawiera dane wprowadzone przez użytkownika, AIMessage zawiera wynik generowany przez model, a SystemMessage zawiera twoje instrukcje. Zawsze są one przekazywane do funkcji invoke w formie listy.
    • Narzędzia: Tag @tool umożliwia wywołanie funkcji w Pythonie przez model, przy czym jej dokumentacja służy jako opis. Funkcja ToolNode(tools) automatycznie uruchamia wywołania narzędzi. Funkcja tools_condition sprawdza, 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, a END oznacza 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ą, a MemorySaver() 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.