От чат-бота с одним узлом до агента, поддерживаемого MCP в LangGraph
Создавайте приложение LangGraph постепенно, слой за слоем: состояние и редьюсеры, ребра, циклы инструментов, потоки с сохранением состояния, три режима потоковой обработки и инструменты, предоставляемые через MCP.
Источники: 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 стандартизирует способ предоставления инструментов, что позволяет им существовать вне агента, который их использует.