Головна / Статті

Стандартна сімсекційна структура для кожного агента LangGraph.

Фіксована структура файлів для агентів LangGraph, яка включає імпорти та стани, інструменти, вузли, ребра, процеси збирання та виклику, шаблони та розширення для багатьох агентів.

Видавець: REACTAPP.TOP19 хв читання4469 слів

Джерела: The LangGraph Mental Model: A standardized architecture guide for every agent you’ll ever build

Кожен, хто деякий час вивчав LangGraph, знає це дивне відчуття: ідея здається логічною, але код так і не з’являється. Граф містить вузли, вузли виконують функції, ребра визначають напрямок подальших дій, а стан зберігає інформацію. Це можна намалювати на дошці за п’ять хвилин. Але коли ви відкриваєте порожній файл, нічого не з’являється. Ви забуваєте про необхідні імпорти, клас, який потрібен, не має очевидного місця розташування, і, здається, існує п’ять способів написати те саме.

Чому код здається складнішим за саму концепцію

Основна причина полягає у тому, що LangGraph не нав’язує жодної власної структури. Він надає вам здатні будівельні блоки та залишає організацію на ваш розсуд. Кожен посібник та кожен репозиторій організовують матеріал по-різному, тому ваша пам’ять ніколи не зафіксується на одній-єдиній схемі.

Пропоноване тут рішення полягає у виборі одного стандартного формату та використанні його для кожного агента, чи то мініатюрний чат-бот, чи ланцюг з багатьма взаємодіючими агентами. Уявіть собі це як шаблон, який можна запам’ятати та легко відтворити, схожий на діаграми, які показують, що агент вже зробив, але спрямований на вихідний код замість історії виконання.

Весь файл в одному погляді

Кожен додаток LangGraph може бути написаний у вигляді одного файлу з сімома розділами у фіксованому порядку, зверху вниз:

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

Це скелет проекту. Коли ви починаєте роботу над проектом, відкрийте файл та введіть назви семи розділів у вигляді коментарів, а потім заповнюйте їх по одному. У решті цього тексту розглядається кожен розділ: для чого він потрібен, які назви слід знати та яку шаблонну структуру можна скопіювати.

Розділ 1: імпорти та налаштування

Для чого він потрібен

Вважайте цей розділ інструментальним набором. Тесляр розкладає всі інструменти перед тим, як щось будувати, а тут ви вказуєте Python, які зовнішні можливості вам потрібні, та налаштовуєте модель, яку буде використовувати агент.

Назви, які потрібно знати

  • ChatOpenAI або ChatAnthropic — це клас мовної моделі, „мозок“ агента. Створіть його тут один раз та використовуйте його скрізь.
  • StateGraph — це основний клас, який створює граф, подібний до креслення, яке ви робите.
  • END — це спеціальна константа для кінцевої точки. Край, який вказує на неї, зупиняє виконання.
  • MemorySaver — це механізм контролю, який зберігає пам’ять протягом кількох етапів розмови.
  • Шаблон

    # --- 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")
    )
    

    Чому це розташовано саме так

    Імпорти поділені на чотири групи: стандартна бібліотека, LangChain (шар моделей), LangGraph (шар графів) та налаштування. Це відповідає структурі самих бібліотек, де LangChain забезпечує примітиви ШІ, а LangGraph керує оркестрацією. Розділення груп дозволяє швидко бачити, що знаходиться де, коли щось ламається.

    Найкориснішою практикою тут є називати модель llm. Кожна функція вузла згодом буде посилатися на цю змінну, і якщо один проект використовує позначення model, а інший — chat, код втрачає єдність та стає складнішим для ознайомлення.

    Розділ 2: стан

    Для чого він потрібен

    State — це робоча пам’ять агента, єдине джерело істини, яке поширюється по всій графіці. Кожен вузол може з нею читати та записувати, тоді як двигун керує її життєвим циклом. Серед усього в LangGraph правильна реалізація State, ймовірно, є найважливішою.

    Годі прикладом може слугувати дошка оголошень у офісі. Кожен, хто заходить до кімнати, тобто кожен вузол, може прочитати написане та щось додати, а дошка залишається на місці протягом усієї зустрічі, що відповідає виконанню всієї графіки.

    Назви, які варто знати

    • TypedDict — це клас у Python, який описує структуру стану, схожу на схему. Ваш клас State успадковує від нього.
    • Annotated — це обгортка для підказок типів. LangGraph використовує її для приєднання редуктора до поля, тобто функції, яка визначає, як нове значення об’єднується з тим, що вже є.
  • add_messages — це вбудований редюсер для поля messages. Замість того, щоб замінювати список щоразу, коли вузол додає повідомлення, він додає його в кінець. Це редюсер, який ви будете використовувати найчастіше.
  • operator.add — ще один поширений редюсер для полів, які мають накопичувати значення шляхом додавання, наприклад, список результатів або лічильник.
  • Шаблон

    # ============================================================
    # 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
    

    Роздуми про редюсери

    Редюсери заплутують багатьох початківців, тому ось аналогія. Уявіть стан як паперову форму, яку передають між працівниками офісу. Поле messages — це блок стікерів: кожен, хто працює з формою, додає стікер, а ніхто не видаляє попередні. Це і є add_messages.

    Поле без редюсера — це окреме текстове поле. Кожен перезаписує те, що написав попередній користувач, що є стандартною практикою LangGraph: останній запис має перевагу.

    Існує просте правило: щоразу вказуйте для поля messages Annotated[list, add_messages], для кожного додаткового списку, який має продовжувати рости, обирайте Annotated[list, operator.add], а всі інші поля залишайте без змін.

    Розділ 3: інструменти

    Для чого вони потрібні

    Інструменти — це руки агента. LLM є мозком, який міркує та приймає рішення, проте сам по собі не може діяти у світі. За допомогою інструментів він може переглядати веб, звертатися до API, виконувати код, шукати інформацію в базі даних чи запускати будь-яку функцію Python, яку ви визначили.

    Цей розділ є необов’язковим. Агент, який створює лише текст та ніколи не взаємодіє з зовнішніми системами, може його пропустити, хоча більшість реальних агентів потребують інструментів.

    Назви, які варто знати

    • @tool — це декоратор LangChain, який перетворює звичайну функцію Python на інструмент, зрозумілий LangGraph. Документація функції стає описом, який читає LLM під час прийняття рішення про її використання.
    • ToolNode — це готовий вузол, який знаходиться у langgraph.prebuilt, і він виконує ті інструменти, які обрав LLM, тож вам не доводиться самостійно писати логіку виконання.
  • bind_tools — це метод у вашій інстанції LLM, який приєднує до неї інструменти. Після цього модель знає, що існує, і може звернутися за виконанням, надсилаючи спеціальне повідомлення tool_calls замість звичайного тексту.
  • Шаблон

    # ============================================================
    # 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)
    

    Документація має більше значення, ніж здається

    Початківці часто не усвідомлюють цього: модель спирається на вашу документацію для вибору інструменту. Вона читає текст у потрійних лапках. Якщо документація є нечіткою або відсутньою, ШІ не зрозуміє, чи є ваш інструмент правильним, тому пишіть чіткі, конкретні описи, у яких буде зазначено, що робить інструмент та коли його використовувати.

    Розділ 4: вузли

    Для чого вони потрібні

    Вузли — це „робітники“. Кожен з них є функцією на Python, яка отримує поточний стан, виконує певні дії, такі як виклик ШІ, обробка даних або використання інструменту, та повертає словник з оновленнями стану. Це і є повний опис їхніх обов’язків.

    Вузол ніколи не вирішує, що відбуватиметься далі. Він виконує свою роботу та оновлює стан, а подальша передача даних регулюється у розділі 5.

    Назви, які варто знати

    • state — це параметр, який отримує кожен вузол, це словник у форматі вашого AgentState. У ньому можна отримати значення такі як state["messages"] або state["current_task"].
    • HumanMessage, AIMessage та SystemMessage — це три типи повідомлень, які ви будете постійно використовувати. Перше — це те, що сказав користувач, друге — відповідь моделі, а третє містить ваші інструкції для моделі, тобто системний запит.
    • invoke — це спосіб виклику LLM або llm_with_tools для отримання відповіді. Ви передаєте список повідомлень та отримуєте AIMessage.
  • Значення, що повертається, завжди є словником, ключі якого збігаються з назвами полів у AgentState. Його значення — це оновлення, а не повний стан, і LangGraph об’єднує їх за допомогою редукторів, які ви визначили.
  • Шаблон

    # ============================================================
    # 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}")]
        }
    

    Зображення для вузлів

    Уявіть вузол як бігуна-передавача. Він бере естафетну паличку, якою є стан, пробігає свою дистанцію, тобто виконує роботу, і передає паличку з оновленим вмістом. Він не обирає наступного бігуна — це завдання того, хто керує гонкою, тобто ребер.

    Кожен сигнатур вузла виглядає однаково: def my_node(state: AgentState) -> dict. Вхідними даними є весь стан, а вихідними — словник, що містить лише ті поля, які потрібно змінити.

    Розділ 5: ребра та маршрутизація

    Для чого це потрібно

    Ребра є логікою прийняття рішень графа. Вони з’єднують вузли між собою та визначають порядок виконання операцій. У LangGraph є два типи ребер.

    Статичні ребра з’єднують один вузол з іншим без жодних умов: «після сумаризатора — до агента». Жодних рішень, просто пряме з’єднання.

    Умовні краї аналізують стан та повертають рядок із назвою наступного вузла. Саме тут знаходиться інтелект агента щодо його власного потоку. Чи слід йому викликати інструмент, завершити роботу чи повернутися для ще одної спроби?

    Назви, які варто знати

    • add_edge(A, B) створює статичний край, що означає, що керування завжди йде від A до B.
    • add_conditional_edges(source, routing_function, mapping) створює гілку, яка виходить від source. Функція маршрутизації отримує поточний стан та повертає рядок, а словник mapping перетворює цей рядок на справжню назву наступного вузла.
  • tools_condition — це готова функція маршрутизації з langgraph.prebuilt. Вона перевіряє останнє повідомлення у стані на наявність викликів інструментів. Якщо такі є, вона направляє потік до "tools", а якщо ні — до END. Це стандартний патерн для агентів, які використовують інструменти, і його варто запам’ятати.
  • START — це константа, що позначає початок графа. Встановлення точки входу означає додавання ребра від START до першого вузла.
  • Шаблон

    # ============================================================
    # 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
    

    Зображення для маршрутизації

    Уявіть функцію маршрутизації як оператора залізничних сигналів. Поїзд прибуває на станцію, коли завершується робота вузла. Оператор перевіряє інформацію про поточний стан та перемикає відповідний перегин, щоб поїзд рухався правильним шляхом. Оператор ніколи не переміщує поїзд; це робить LangGraph.

    Одне правило, яке варто пам’ятати: функції маршрутизації ніколи не змінюють стану. Вони лише його читають. Будь-які зміни мають відбуватися у вузлі, тож якщо ви бажаєте оновити стан під час маршрутизації, перенесіть цю логіку у вузол.

    Розділ 6: складання графа

    Для чого він потрібен

    Саме тут усе об’єднується. Вважайте цей розділ етапом будівництва. Розділи з 1 по 5 були підготовкою — там були визначені матеріали, їх нарізали та розмістили, а тепер ви складаєте структуру, з’єднуючи елементи та компілюючи їх.

    Складання завжди відбувається за п’ятьма однаковими кроками, які варто запам’ятати: ініціалізація, реєстрація вузлів, встановлення точки входу, підключення країв, компіляція.

    Назви, які потрібно знати

    • StateGraph(AgentState) створює інструмент для складання, використовуючи ваш клас State як схему, тому кожен вузол має повертати оновлення, сумісні з нею.
    • add_node(name, function) реєструє вузол. Ім’я — це рядок, який ви будете використовувати всередині країв, а функція — це вузол з розділу 4.
    • set_entry_point(name) визначає, який вузел виконується першим під час запуску графа.
    • add_edge(A, B) створює пряме, безумовне з’єднання від A до B.
    • add_conditional_edges(source, router, mapping) додає умовну гілку від джерела, використовуючи вашу функцію маршрутизації та таблицю зіставлення повернутих рядків із іменами пунктів призначення.
  • compile(checkpointer=...) запечатує граф у виконуваний об’єкт (об’єкт типу CompiledGraph). Після цього додавати вузли та ребра неможливо. Опційний параметр checkpointer увімкнює постійну пам’ять.
  • Шаблон

    # ============================================================
    # 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)
    

    Зображення для інструкцій зібрання

    Найкращою аналогією є друкована плата. Кожен виклик add_node додає компонент. Функції add_edge та add_conditional_edges з’єднують дроти між компонентами. Функція compile() виконує фінальну перевірку та готує плату до використання. Після цього вона готова до подачі живлення, що здійснюється за допомогою функції invoke, та до виконання своїх функцій.

    Шаблон містить основний цикл ReAct: agent → tools → agent → tools та так далі, поки агент не вирішить, що все завершено, і не перейде до точки END. Цей цикл «аналізувати — діяти» є основою майже кожного агента LangGraph, який використовує інструменти, і розуміння його допомагає пояснити більшість графів, з якими ви зустрінетеся.

    Розділ 7: точка входу та виклик

    Для чого це потрібно

    Це платформа запуску. Усе, що було раніше, — це визначення, а ця частина — виконання: код, який запускає граф, надає вхідні дані та збирає результати.

    Існує два способи його запуску: invoke для однократного оброблення та стрімінг для поступового отримання результатів. Також потрібен config — словник із параметрами під час виконання, зокрема thread_id, який використовується для керування пам’яттю.

    Назви, які варто знати

    • graph.invoke(input, config) виконує весь граф та повертає кінцевий стан. Оскільки це синхронний процес, ви нічого не отримаєте, доки не завершаться усі кроки.
    • graph.stream(input, config) також виконує граф, але повертає часткові оновлення стану по мірі їх виникнення, що ідеально підходить для додатків, які потребують відображення поточного прогресу користувачеві.
  • config — це словник, який передається під час кожного виклику. Його ключовий елемент має вигляд {"configurable": {"thread_id": "some_id"}}. ID потоку функціонує як ID сесії, який вказує механізму перевірки, яку пам’ять потрібно відновити та зберегти. Два користувачі з різними ID потоків ніколи не ділять пам’ять.
  • HumanMessage(content=...) — це спосіб обгортки вхідних даних користувача перед їх передачею до графа.
  • Шаблон

    # ============================================================
    # 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")
    

    Повний шаблон

    Нижче наведений повний, мінімальний скелет, який можна скопіювати у новий проект. Це єдиний файл, який варто додати до закладок: замініть місця для імплементацій на власну логіку, і у вас буде основа будь-якого агента 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")
    

    Продовження: багатоагентні системи

    Коли підхід з одним агентом здається природним, багатоагентні системи вже не здаються страшними. Основна ідея полягає у тому, що кожен агент — це просто скомпільована графіка, а скомпільована графіка сама може бути вузлом у більшій керуючій графіці.

    Назви, які варто знати у роботі з багатоагентними системами

    • Вузол-керувальник — це агент, завданням якого є не відповідати користувачеві, а вибрати який спеціаліст має виконати завдання. Він здійснює маршрутизацію між підагентами.
    • Підграфіка — це скомпільована графіка, зареєстрована як вузол у іншій графіці. Вона приймає та повертає стани, як і будь-яка звичайна функція вузла.
  • Command — це новіший паттерн, доступний з LangGraph 0.2, де один вузол повертає як оновлення стану, так і інструкцію маршрутизації в одному значенні. У деяких конфігураціях з кількома агентами це усуває потребу у окремих умовних ребрах.
  • Структурна шаблон

    # ── 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())
    

    Версія з кількома агентами має таку саму структуру, як і версія з одним агентом, і застосовуються ті самі сім розділів. Єдина відмінність полягає у тому, що деякі з „вузлів“ є цілими скомпільованими графами. Ментальна модель масштабується без проблем.

    Швидкий посібник

    Ось стисла картка, яку можна роздрукувати або прикріпити, у якій перелічені основні ідеї в одному місці.

    • Стан: TypedDict визначає його форму. Annotated[T, reducer] додає до поля редуктора. add_messages — це стандартний редуктор списку повідомлень, який додає елементи замість того, щоб їх замінювати. operator.add накопичує списки або числа.
    • LLM: ChatOpenAI або ChatAnthropic — це клас, який завжди пов’язаний із ім’ям llm. Функція bind_tools(tools) інформує модель про наявні інструменти. Функція invoke(messages) викликає модель та повертає відповідь.
    • Повідомлення: HumanMessage містить вхідні дані користувача, AIMessage — результати роботи моделі, а SystemMessage — ваші інструкції. Вони завжди передаються до функції invoke у вигляді списку.
    • Інструменти: Анотація @tool робить функцію Python доступною для виклику моделлю, причому її описом є документація до функції. Функція ToolNode(tools) автоматично виконує виклики інструментів. Функція tools_condition перевіряє, чи просила модель використати певний інструмент.
    • Граф: StateGraph(State) створює конструктор, add_node(name, fn) реєструє вузол, set_entry_point(name) обирає перший вузол, add_edge(A, B) створює безумовний зв’язок, add_conditional_edges(source, router, mapping) створює зв’язок, обраний за значенням, що повертаєтьться маршрутизатором, compile(checkpointer=...) завершує процес, а END позначає кінцеву точку.
    • Виклик: graph.invoke(input, config) повертає кінцевий стан, graph.stream(input, config) поступово надсилає оновлення, config = {"configurable": {"thread_id": "..."}} ідентифікує сеанс пам’яті, а MemorySaver() є контрольним показником у пам’яті.

    Перетворення на автоматичну дію

    Мета не в тому, щоб навчати LangGraph з нуля. Йдеться про те, щоб надати вашому мозку стабільну основу, яку можна буде автоматично використовувати, так само, як музикант береться за знайому прогресію акордів або кухар готує інгредієнти перед початком приготування.

    Сім розділів — імпорти, стан, інструменти, вузли, ребра, збірка та точка входу — утворюють цю структуру. Назви всередині кожного розділу є нотами, а повна схема — це нотний запис.

    Як базовий чат-бот, так і конвеєр із десятками вузлів підходять під однакову структуру. Складність зростає, але структура залишається незмінною, і саме ця стабільність робить її стандартом.

    Розумний спосіб початку — це почати з мінімуму з найпростішого циклу ReAct: один вузол агента, один вузол інструменту та одне умовне ребро з використанням tools_condition. Спочатку зробіть це функціональним, потім додайте ще один вузол, а згодом — ще одного агента. Структура залишається незмінною незалежно від розміру.

    Щодо бекендів для зберігання даних, схем використання людини в процесі та варіантів розгортання, офіційна документація LangGraph — це наступне місце, де варто шукати інформацію.