Главная / Статьи

От чат-бота с одним узлом до агента, поддерживаемого MCP в LangGraph

Создавайте приложение LangGraph постепенно, слой за слоем: состояние и редьюсеры, ребра, циклы инструментов, потоки с сохранением состояния, три режима потоковой обработки и инструменты, предоставляемые через MCP.

Издатель: REACTAPP.TOP18 мин чтения4252 слов

Источники: Building AI Applications with LangGraph: From a Basic Chatbot to Tools, Memory, Streaming and MCP

Большинство прототипов больших языковых моделей начинаются с одного вызова: отправляется запрос, выводится ответ. Такая структура рушится сразу, когда приложению требуется поиск в интернете, принятие решения о следующем шаге, сохранение информации из предыдущих взаимодействий или отображение текущего прогресса в интерфейсе. LangGraph решает эту проблему, превращая поток управления в явную графовую структуру, основанную на общем состоянии. В этом обзоре одно приложение превращается из чат-бота с одним узлом в агента типа ReAct, использующего инструменты, с сохранением состояния в формате чекпоинтов, потоковую обработку данных и инструменты, предоставляемые отдельным сервером MCP; также указываются детали, которые часто мешают пользователям на этом пути.

Ментальная модель: состояние передается между узлами по ребрам

Простая интеграция LLM представляет собой прямую последовательность от вводного запроса к модели и получению ответа. LangGraph требует мыслить циклически: объект state попадает в граф, узлы читают его и возвращают изменения, рёбра определяют, какой узел будет обрабатываться далее, а в конце получается обновлённое состояние. Следовательно, три основных компонента — это:

  • State: данные, передающиеся между этапами, такие как история сообщений.
  • Nodes: обычные функции, выполняющие определённую задачу и возвращающие частичное обновление.
  • Edges: правила, соединяющие узлы, включая начало и конец процесса обработки.

Самый простой возможный граф содержит один узел между точкой входа и выхода:

                ┌─────────────┐
                │    State    │
                └──────┬──────┘
                       │
                       ▼
                ┌─────────────┐
                │    Node     │
                │   chatbot   │
                └──────┬──────┘
                       │
                       ▼
                ┌─────────────┐
                │     END     │
                └─────────────┘

Разделение информации о «существующих данных» и информации о «том, что выполняется в каком порядке», является основным принципом проектирования. Можно изменить порядок выполнения операций, не затрагивая способ хранения данных, а также можно проверить текущее состояние в любой момент без необходимости разбирать последовательность вызовов функций.

Почему граф вместо цепи вызовов

Минимальный чат-бот представляет собой прямую связь от пользователя к модели и обратно:

User
 ↓
LLM
 ↓
Response

Теперь добавим реальные требования: поиск в интернете, несколько инструментов, логика разветвления, память, этап утверждения человеком, более одного агента, потоковый вывод и разное поведение в зависимости от результата работы модели. Каждое из этих требований добавляет новую ветвь или цикл, а вложенные операторы if вокруг вызовов API быстро становятся сложными для понимания. Даже самый распространенный случай — «вызвать инструмент, если модель этого просит» — уже содержит цикл:

                         ┌─────────────┐
                         │     LLM     │
                         └──────┬──────┘
                                │
                         Need a tool?
                         /           \
                       Yes            No
                        ↓              ↓
                     Tool          Response
                        │
                        ↓
                       LLM

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

Шаг 1: чат-бот с одним узлом и StateGraph

Первая версия преднамеренно проста — в ней один узел соединён между начальным и конечным маркерами:

START
  ↓
Chatbot Node
  ↓
END

Определение состояния и его редьюсера

Для чат-бота состояние в основном нуждается в истории разговора. Первая попытка — использование TypedDict с списком messages:

from typing import TypedDict

class State(TypedDict):
    messages: list

Концептуально этот список накапливает поочередные сообщения от человека и ИИ:

State
 └── messages
      ├── HumanMessage
      ├── AIMessage
      ├── HumanMessage
      └── AIMessage

Здесь очень важен один нюанс. В LangGraph каждый ключ состояния может иметь редьюсер, который определяет, как значение, возвращаемое узлом, объединяется с уже существующим значением. Обычная аннотация list не содержит редьюсера, поэтому узел, возвращающий {"messages": [reply]}, заменяет всю список вместо того, чтобы добавить к нему элемент. Для истории чатов почти всегда требуется редьюсер add_messages, объявленный как Annotated[list, add_messages] (или наследуемый от заранее созданного MessagesState). С его использованием возвращаемые сообщения добавляются к списку, а сообщения с совпадающим ID обновляются на месте. Приведенный выше пример подходит для наглядного понимания, но в реальном коде следует использовать аннотированную версию.

Написание узла чат-бота

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

def chatbot(state: State):
    return {
        "messages": [
            llm.invoke(state["messages"])
        ]
    }

Читайте его с конца к началу. Функции передаётся полное состояние:

state

Она извлекает информацию о прошедших диалогах:

state["messages"]

Затем эта история отправляется в модель:

llm.invoke(state["messages"])

Ответ модели оборачивается в словарь, ключами которого являются поля, обновлённые в состоянии. Обратите внимание, что узел не изменяет состояние напрямую; он возвращает частичное обновление, позволяя LangGraph объединить его с оригинальным состоянием:

{
    "messages": [
        llm.invoke(state["messages"])
    ]
}

С точки зрения диаграммы потока данных узел находится между двумя версиями одного и того же состояния:

              State
                │
                │ messages
                ▼
          ┌─────────────┐
          │  Chatbot    │
          │    Node     │
          └──────┬──────┘
                 │
                 │ LLM Response
                 ▼
               State

Регистрация узла

Функция становится частью графа после того, как она регистрируется в конструкторе под определённым именем:

graph_builder.add_node(
    "llmchatbot",
    chatbot
)

Первый аргумент — это идентификатор узла, к которому будут обращаться ребра и логи:

"llmchatbot"

Второй аргумент — это функция, которая выполняется каждый раз, когда выполнение достигает этого узла:

chatbot

Шаг 2: подключение ребер и компиляция

Узел сам по себе никогда не выполняется; ребра указывают LangGraph, как добраться до него и куда двигаться дальше. Для этой схемы требуются два ребра. Первое соединяет специальный маркер START с чат-ботом:

graph_builder.add_edge(
    START,
    "llmchatbot"
)

На диаграмме выполнение входит в граф и напрямую переходит к узлу:

START
  ↓
llmchatbot

Второе ребро передает управление от чат-бота к END, что завершает выполнение:

graph_builder.add_edge(
    "llmchatbot",
    END
)

Это соответствует:

llmchatbot
  ↓
 END

Вместе они образуют полную, хоть и минимальную, схему:

       START
         │
         ▼
 ┌────────────────┐
 │   llmchatbot   │
 │      Node      │
 └────────┬───────┘
          │
          ▼
         END

Компиляция и вызов

В процессе компиляции проверяется структура (например, что каждая рёбра указывают на известный узел) и создаётся объект, готовый к выполнению:

graph = graph_builder.compile()

Его запускают, передав начальное состояние. Принимаются обычные словари ролей/контента, которые затем преобразуются в объекты сообщений LangChain:

result = graph.invoke({
    "messages": [
        {
            "role": "user",
            "content": "What is LangGraph?"
        }
    ]
})

По сути, входное состояние попадает в состояние START, узел чат-бота вызывает модель, ответ объединяется со состоянием, и выполнение завершается:

Input State
    ↓
START
    ↓
llmchatbot
    ↓
LLM
    ↓
Updated State
    ↓
END

Значением, возвращаемым функцией invoke, является окончательное состояние, поэтому ответ модели — это последний элемент в result["messages"].

Шаг 3: отрисовка графа

Скомпилированный граф может отрисовываться самостоятельно. В Jupyter Notebook инструменты отображения IPython показывают PNG-изображение, сгенерированное на основе описания графа в формате Mermaid:

from IPython.display import Image, display

display(
    Image(
        graph.get_graph().draw_mermaid_png()
    )
)

Для этого графа изображение имеет небольшие размеры:

   ┌───────┐
   │ START │
   └───┬───┘
       │
       ▼
┌──────────────┐
│  llmchatbot  │
└──────┬───────┘
       │
       ▼
   ┌───────┐
   │  END  │
   └───────┘

С трёмя блоками значение кажется неочевидным, но оно быстро растёт. Как только появляются условные рёбра, узлы инструментов и циклы, отрисованный диаграмма — это самый быстрый способ убедиться, что построенная вами графика действительно та, которую вы хотели построить. Имейте в виду, что draw_mermaid_png() по умолчанию отрисовывает изображение через внешний сервис Mermaid, поэтому ему требуется доступ в сеть; если такой доступ отсутствует, draw_mermaid() возвращает текст Mermaid, который можно отрисовать в другом месте.

Шаг 4: добавление инструмента поиска к модели

Чат-бот может отвечать только на основе того, чему он научился во время обучения. Вопросы о последних достижениях в области агентных ИИ требуют свежей информации, поэтому на следующем этапе добавляется инструмент веб-поиска. В примере используется Tavily Search, и предполагаемый алгоритм работы выглядит следующим образом:

User
 ↓
LLM
 ↓
Search required?
 ↓
Tavily Search
 ↓
Search Results
 ↓
LLM
 ↓
Final Answer

Привязка инструментов не является тем же, что и их запуск

Первая часть работы — это привязка инструментов: указание модели, какие инструменты существуют и какие аргументы они принимают.

llm_with_tools = llm.bind_tools(
    [tavily_search]
)

После привязки модель может отвечать структурированным вызовом инструмента («Вызвать tavily_search с этим запросом») вместо простого текста. Она сама не может выполнить этот вызов. Что-то в вашем приложении должно заметить вызов инструмента, запустить его, добавить результат в виде объекта ToolMessage и отправить всё обратно модели. Предоставление модели доступа к инструменту не делает её агентом; это делает окружающий поток выполнения. Именно такой поток предоставляет граф.

Шаг 5: преобразование в цикл ReAct

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

START
  ↓
LLM
  ↓
END

превращается в граф с точкой принятия решения и путем, ведущим обратно к модели:

                 START
                   │
                   ▼
                 Agent
                   │
                   ▼
              ┌─────────┐
              │   LLM   │
              └────┬────┘
                   │
             Need a tool?
              /         \
            Yes          No
             │            │
             ▼            ▼
          Tool          END
             │
             ▼
           Agent
             │
             ▼
            LLM

В коде LangGraph такое решение реализуется с помощью условной грани. Заранее созданный элемент ToolNode выполняет команды, содержащиеся в последнем сообщении ИИ, отправленном соответствующим инструментом; функция tools_condition направляет поток данных к этому узлу, если имеются вызовы инструментов, или к узлу END, если их нет. Грань, ведущая от узла инструмента обратно к модели, закрывает цикл. Вспомогательная функция create_react_agent, заранее подготовленная для использования, формирует структуру аналогичным образом; для получения точного местоположения этой функции ознакомьтесь с актуальной документацией LangGraph, поскольку вспомогательные функции для агентов со временем перемещались между разными пакетами.

Отслеживание одного прохождения цикла

Предположим, пользователь запрашивает последние новости о LangGraph. Одно прохождение цикла приводит к следующей последовательности сообщений и шагов:

HumanMessage
     ↓
    Agent
     ↓
    LLM
     ↓
"I need current information."
     ↓
Tool Call
     ↓
Tavily Search
     ↓
Search Results
     ↓
ToolMessage
     ↓
    Agent
     ↓
    LLM
     ↓
Final Answer

Важное структурное отличие заключается в наличии цикла. Первый чат-бот работал по прямой линии:

START → LLM → END

Граф ReAct может обращаться к модели столько раз, сколько потребуется:

  START
    ↓
  Agent
    ↓
  LLM
    ↓
  Tool?
 ┌───────┐
 │       │
Yes      No
 │       │
 ↓       ↓
Tool    END
 │
 ↓
Agent

Поскольку циклы в принципе могут выполняться бесконечно, LangGraph устанавливает ограничение на глубину рекурсии для каждой работы (его можно настроить с помощью параметра recursion_limit). Если модель продолжает запрашивать инструменты, появляется ошибка вместо бесконечного счета.

Шаг 6: хранение разговоров с помощью MemorySaver

До сих пор каждый вызов функции invoke начинался с нуля. Рассмотрим два взаимодействия: сначала пользователь представляется:

User:
My name is Himanshi.

Затем, в отдельном запросе, он задает дополнительный вопрос:

User:
What is my name?

Второй ответ может быть дан только в том случае, если граф всё ещё содержит первый ответ. LangGraph решает эту проблему с помощью checkpointer: после каждого шага сохраняется снимок состояния, и следующая обработка той же конверсации возобновляется с этого снимка. MemorySaver — это самая простая реализация:

             ┌─────────────┐
             │    Agent    │
             └──────┬──────┘
                    │
                    ▼
              ┌───────────┐
              │   State   │
              └─────┬─────┘
                    │
                    ▼
             ┌─────────────┐
             │ MemorySaver │
             └─────────────┘

Его привязывают во время компиляции с помощью graph_builder.compile(checkpointer=memory). Вместо того чтобы каждый вызов был изолированной обработкой, теперь каждая итерация относится к одной конверсации, а её состояние сохраняется между итерациями.

Нити разделяют конверсации

Ключ, связывающий итерации между собой, — это ID нити, который передаётся в конфигурации как {"configurable": {"thread_id": "..."}}. У каждой нити есть своя история и своё текущее состояние:

Thread 1
 ├── Message 1
 ├── Message 2
 ├── Message 3
 └── Current State

Thread 2
 ├── Message 1
 ├── Message 2
 └── Current State

Таким образом, два пользователя в двух потоках никогда не увидят сообщения друг друга:

User A → Thread A → State A

User B → Thread B → State B

Существуют два важных момента. Класс MemorySaver хранит всё в оперативной памяти процесса, поэтому вся история исчезает при перезагрузке и не делится между инстансами сервера; в производственных развертываниях используется постоянный чекпоинт, такой как в Postgres или SQLite. В последних версиях класс также экспортируется как InMemorySaver. Чтобы узнать подробнее о том, что на самом деле хранится в чекпоинте, ознакомьтесь с статьей о том, как чекпоинты, записи и блобы взаимодействуют в InMemorySaver.

Шаг 7: потоковое отображение состояния во время работы графа

Всё вышесказанное использовало блокирующие вызовы:

graph.invoke(...)

Он ждет, пока вся структура не будет обработана, прежде чем вернуть результат. Для агентов с несколькими шагами это может означать несколько секунд пустого экрана, что кажется пользователям неправильным. Компилированная структура также предоставляет объект stream, который выдает результаты в процессе выполнения.

stream_mode="values">: полное состояние после каждого шага

Первый режим, о котором стоит знать:

stream_mode="values"

При использовании в цикле он выдает кадр всего состояния после каждого шага:

for chunk in graph.stream(
    input_state,
    stream_mode="values"
):
    print(chunk)

Каждый фрагмент показывает, как выглядит полное состояние в данный момент, так что можно наблюдать его изменения по мере работы узлов и инструментов:

START
  ↓
State Update #1
  ↓
Node Execution
  ↓
State Update #2
  ↓
Tool Execution
  ↓
State Update #3
  ↓
END

Это удобно, когда состояние небольшое и вы просто хотите отображать «последнюю версию» каждый раз.

stream_mode="updates">: только изменения в каждом узле

Второй режим:

stream_mode="updates"

Разница заключается в том, что содержит каждый фрагмент. В одном режиме вы получаете полное состояние:

values

В другом режиме вы получаете только дельту, привязанную к имени узла, который её сгенерировал:

updates

Простой способ запомнить эту разницу:

  • values отвечают на вопрос «Как выглядит состояние прямо сейчас?»
  • updates отвечают на вопрос «Что именно изменил этот конкретный узел?»

updates обычно является лучшим выбором для отладки и логирования, поскольку он указывает, какой узел был ответственен за каждую изменение, и позволяет сохранять фрагменты небольшими при большом объеме состояния. Ни в одном из режимов не передаются отдельные токены модели; для вывода по токенам существует отдельный режим потока messages, и вы можете передать список режимов, чтобы получить несколько потоков одновременно.

Шаг 8: детальный контроль видимости с astream_events

Снимки состояния и изменения узлов показывают, что изменилось, но не всё, что произошло. astream_events позволяет углубиться на ещё один уровень и генерировать событие для каждого момента жизненного цикла каждого компонента во время выполнения:

Graph Execution
      │
      ├── Graph Started
      │
      ├── Node Started
      │
      ├── LLM Started
      │
      ├── LLM Streaming
      │
      ├── LLM Completed
      │
      ├── Tool Started
      │
      ├── Tool Completed
      │
      ├── Node Completed
      │
      └── Graph Completed

Вы обрабатываете эти события асинхронно. Обратите внимание, что вызов print в приведённом ниже фрагменте должен находиться в отступах внутри тела цикла; иначе, как показано, Python выдаст ошибку относительно отступов:

async for event in graph.astream_events(
    input_state,
    version="v2"
):

print(event)

Каждое событие содержит тип (например, on_chat_model_stream или on_tool_start), имя компонента и его данные. Благодаря этому поток событий полезен для:

  • отладки и структурированной логгирования
  • обеспечения общей видимости процесса выполнения агента
  • отслеживания каждого вызова LLM и каждого вызова инструмента
  • создание пользовательского интерфейса потоковой передачи данных, отображающего токены и активность инструментов
  • определение того, где тратится время
  • Инженеры с опытом работы с бэкендом узнают эту схему: речь идет скорее о отслеживании распределенных запросов, чем о чтении значения возврата отдельной функции. Вопрос меняется с «работал ли агент?» на «что именно делал агент, в каком порядке, и сколько времени заняла каждая часть?»

    Как соединяются все элементы

    Если оглянуться назад, каждый шаг добавлял новую функциональность к уже существующей структуре. Всё начиналось с одного вызова модели:

    START
     ↓
    LLM
     ↓
    END
    

    Инструменты добавили возможность двусторонней связи с внешним миром:

    START
     ↓
    LLM
     ↓
    Tool
     ↓
    LLM
     ↓
    END
    

    Функция контроля состояния позволила циклу запоминать информацию:

    START
     ↓
    Agent
     ↓
    LLM ↔ Tools
     ↓
    Memory / Checkpoint
     ↓
    END
    

    Потоковая передача данных и события обеспечили возможность отображения хода выполнения в интерфейсе и для мониторинга:

    Graph
     ↓
    Events
     ↓
    UI / Logs / Observability
    

    Ни один из этих шагов не требовал переписывания предыдущих; каждый из них представляет собой новый узел, ребро, опцию компиляции или вызов метода в той же графике.

    Шаг 9: размещение инструментов за сервером MCP

    До сих пор инструменты находились внутри приложения. Протокол Model Context Protocol (MCP) использует иной подход: отдельный сервер предоставляет инструменты (и ресурсы), и любой MCP клиент может их обнаружить и использовать. Приложению больше не нужно содержать реализацию каждого инструмента:

                     ┌──────────────┐
                     │ MCP Client   │
                     └──────┬───────┘
                            │
                            │
                            ▼
                     ┌──────────────┐
                     │ MCP Server   │
                     └──────┬───────┘
                            │
                 ┌──────────┼──────────┐
                 ↓          ↓          ↓
              add()     multiply()   weather()
    

    Минимальный сервер с тремя инструментами

    Для понимания архитектуры достаточно небольшого учебного сервера. Он предоставляет инструмент для сложения:

    add(a, b)
    

    инструмент для умножения:

    multiply(a, b)
    

    и инструмент для получения информации о погоде по названию города:

    weather(city)
    

    Инструмент для прогноза погоды не обращается к реальному сервису погоды. Он возвращает фиксированную строку, что сосредотачивает внимание на протоколе, а не на ключах API:

    "It's sunny in Mumbai."
    

    Важен сам процесс обмена данными, а не прогноз погоды:

    Client
      ↓
    MCP Server
      ↓
    Tool
      ↓
    Result
      ↓
    Client
    

    Инструментам больше не обязательно находиться в приложении-используемом

    Суть заключается в том, что инструменты могут храниться независимо от приложений, которые их используют. Приложение взаимодействует с клиентом MCP, клиент передает данные по протоколу на сервер, а сервер отвечает за реализацию:

                    Application
                         │
                         ▼
                     MCP Client
                         │
                   MCP Protocol
                         │
                         ▼
                     MCP Server
                    /     |      \
                   /      |       \
                  ↓       ↓        ↓
                Add    Multiply   Weather
    

    Это делает возможным использование повторно используемых серверов инструментов. Одна команда может запустить сервер, предоставляющий целую категорию функций:

    Database Tools
    File Tools
    Git Tools
    Search Tools
    Weather Tools
    Internal APIs
    

    И несколько различных агентов, клиентов чата или интеграций с IDE могут использовать тот же сервер без дублирования кода.

    Шаг 10: объединение LangGraph и MCP

    Эти две технологии решают разные аспекты проблемы. LangGraph управляет рабочим процессом: состоянием, маршрутизацией, циклами, памятью и потоковой обработкой. Стандарт MCP стандартизирует способ предоставления инструментов и ресурсов. В совмещенной архитектуре узел инструментов агента взаимодействует с клиентом MCP вместо того, чтобы вызывать локальные функции:

                              User
                               │
                               ▼
                         LangGraph Agent
                               │
                               ▼
                              LLM
                               │
                        Need a tool?
                               │
                               ▼
                          MCP Client
                               │
                               ▼
                          MCP Server
                        /      |       \
                       ↓       ↓        ↓
                     Math    Search   Weather
    

    Агенту не нужно знать, как реализован инструмент. Ему достаточно знать, какие возможности существуют и как ими пользоваться, а остальное берет на себя сервер MCP. В Python пакет langchain-mcp-adapters — это распространенный способ загрузки инструментов MCP в качестве инструментов LangChain, чтобы их можно было привязать к модели и использовать в ToolNode. Существующая статья по адресу стандартизация интеграции инструментов в LangGraph с помощью MCP более подробно рассматривает эту интеграцию.

    Полная картина процесса

    Если объединить все элементы, агент LangGraph выглядит следующим образом: ввод пользователя превращается в состояние, узлы работают с ним, модель определяет, нужен ли инструмент, результаты работы инструмента возвращаются в модель, и процесс завершается ответом:

                              USER
                               │
                               ▼
                        ┌────────────┐
                        │ LangGraph  │
                        │   Agent    │
                        └──────┬─────┘
                               │
                               ▼
                             STATE
                               │
                 ┌─────────────┼─────────────┐
                 │             │             │
                 ▼             ▼             ▼
               NODE          NODE          NODE
                 │             │             │
                 └─────────────┼─────────────┘
                               │
                               ▼
                             LLM
                               │
                         Need a tool?
                          /          \
                        Yes           No
                         │             │
                         ▼             ▼
                      TOOL          RESPONSE
                         │
                         ▼
                      RESULT
                         │
                         └──────► LLM
                                   │
                                   ▼
                                  END
    

    Вокруг этого основного цикла находятся функции, которые включаются по мере необходимости:

                  ┌──────────────────────────────┐
                  │          Capabilities        │
                  │                              │
                  │  Memory / Checkpointing      │
                  │  Streaming                   │
                  │  Event Tracking              │
                  │  MCP                         │
                  │  Tools                       │
                  └──────────────────────────────┘
    

    Где заканчивается LangChain и начинается LangGraph

    Поскольку LangGraph обычно разрабатывается после LangChain, полезно четко разграничивать их роли. LangChain предоставляет базовые компоненты для приложений на основе больших языковых моделей: обёртки для моделей, сообщения, инструменты, промпты и структурированный вывод. LangGraph организует эти компоненты в чёткие, имеющие состояние рабочие процессы. Это взаимодополняющие элементы, а не конкурирующие варианты; большинство кодов LangGraph использует компоненты LangChain внутри своих узлов.

    Несколько принципов, сформированных в ходе работы над этой небольшой системой, могут быть использованы без изменений в гораздо более крупных системах.

    Состояние — основа

    Вместо передачи данных через аргументы функций каждый узел читает и записывает в одно общее состояние, причем редьюсеры определяют, как происходит слияние обновлений:

    State
     ↓
    Node
     ↓
    Updated State
     ↓
    Next Node
    

    Узлы — это единицы работы

    Каждый узел должен выполнять одну четко определенную задачу, что облегчает разбиение на составные части и тестирование сложных рабочих процессов:

    chatbot node
    search node
    tool node
    validation node
    human approval node
    

    Ребра кодируют рабочий процесс

    Ребро отвечает на один вопрос: куда будет продолжаться выполнение после завершения этого узла? Простые ребра образуют последовательности; условные ребра создают ветвления и циклы.

    Диаграммы эффективнее чтения кода

    Для графа с большим количеством узлов и условных путей отрисованная диаграмма быстрее передает информацию об архитектуре, чем чтение кода-строителя:

    Node → Decision → Tool → Agent → END
    

    Чекпоинты — это не просто история чатов

    Чекпоинтер сохраняет полное состояние графа, а не только сообщения. Именно это позволяет осуществлять многократные диалоги, а также является основой для агентов, работающих в течение длительного времени, перерывов с участием человека и возобновления работы после сбоев.

    Стриминг полезен как для пользователей, так и для разработчиков

    Стриминг улучшает восприятие скорости реакции, а тот же вывод помогает отлаживать и наблюдать за процессом выполнения.

    Выполнение агента включает множество операций

    Благодаря astream_events становится ясно, что один вызов агента представляет собой вложенную последовательность операций с графом, узлами, моделями и инструментами:

    Graph
     ↓
    Node
     ↓
    LLM
     ↓
    Tool
     ↓
    LLM
     ↓
    Node
     ↓
    Graph
    

    Чем сложнее приложение, тем больше необходима такая прозрачность.

    MCP отделяет инструменты от приложений

    Инструменты, предоставляемые через MCP, могут быть использованы в разных приложениях и развиваться независимо от агентов, которые их вызывают.

    Куда двигаться дальше

    Эта основа открывает несколько направлений, заслуживающих исследования: условные ребра и параллельные ветви, прерывания с участием человека, долгосрочная память и постоянные точки контроля, генерация с усилением на основе поиска внутри графа, многоагентные системы, более глубокая интеграция MCP, наблюдаемость и оценка агентов, развертывание в производстве и толерантность к сбоям с повторными попытками.

    Общая идея заключается в том, что прикладное решение на основе ИИ — это не просто модель, генерирующая текст. Это система, разработанная вокруг модели, способная принимать запросы, принимать решения, действовать, наблюдать за результатом, запоминать его и продолжать работу:

    Understand
        ↓
    Decide
        ↓
    Act
        ↓
    Observe
        ↓
    Remember
        ↓
    Continue
    

    Граф — это естественный способ представления такого цикла.

    Заключение

    Весь путь начался с самого простого возможного графа:

    START → Chatbot → END
    

    и постепенно, слой за слоем, превратился в систему с инструментами, циклом агентов, памятью, потоковой обработкой и MCP:

                  ┌─────────────┐
                  │    LLM      │
                  └──────┬──────┘
                         │
                         ▼
                  ┌─────────────┐
                  │    Tools    │
                  └──────┬──────┘
                         │
                         ▼
                  ┌─────────────┐
                  │    Agent    │
                  └──────┬──────┘
                         │
              ┌──────────┼──────────┐
              ↓          ↓          ↓
           Memory    Streaming     MCP
              │          │          │
              └──────────┼──────────┘
                         ↓
                    AI Application
    

    Если рассматривать их вместе, эти компоненты уже не кажутся просто списком, а представляют собой единую архитектуру:

    • State хранит данные и, с помощью контрольных точек, память; для списков сообщений используется редьюсер add_messages.
    • Nodes выполняют работу и возвращают частичные обновления.
    • Edges определяют поток данных, а условные ребра превращают его в цикл.
    • Модель обеспечивает логику и язык; инструменты позволяют ей взаимодействовать с внешним миром.
    • Стриминг и события делают каждую работу прослеживаемой — от обновлений интерфейса до логов.
    • MCP стандартизирует способ предоставления инструментов, что позволяет им существовать вне агента, который их использует.