Приветствую! Дошли руки для того, чтобы оформить свои знания по теме LangGraph и LangChain в оконченный мини-курс. Сейчас вы читаете первую часть из моей 4-х серийной работы. Как вы поняли из названия, говорить мы сегодня будем про LangGraph — инструмент, который произвёл настоящий фурор в мире энтузиастов по созданию полноценных ИИ-агентов на Python и JavaScript.
Сегодня мы начнём с самых основ, а именно:
Разберёмся, что такое LangGraph, и поймём, чем он так хорош
Разберёмся с основными «китами» этого инструмента: графы, узлы (ноды), рёбра и состояния
Научимся описывать свои графы на простых примерах
Поймём, что такое условные и безусловные рёбра, множественные выходы и прочее
Сразу говорю — сегодня мы не будем работать с нейросетями, просто чтобы не уходить от сути. Но при этом к концу статьи вы и сами догадаетесь, почему ИИ просто идеально ложится в концепцию графов.
Что такое LangGraph и зачем он нужен?
Прежде чем погружаться в детали, давайте разберёмся с основным вопросом: что такое LangGraph и почему он стал таким популярным среди разработчиков?
LangGraph — это библиотека для создания сложных приложений с состоянием, построенных на основе графов. Она позволяет координировать множественные цепочки вычислений (или акторов) циклическим образом — что критически важно для большинства агентных и мультиагентных рабочих процессов.
От линейных цепочек к графам: эволюция мышления
Представьте, что вы пишете программу для обработки заказа в интернет-магазине. Классический подход выглядел бы так:
Получить заказ → Проверить товар → Списать деньги → Отправить товар
Это работает, пока всё идёт по плану. Но что, если:
Товара нет в наличии?
Платёж не прошёл?
Клиент хочет изменить заказ?
Линейная цепочка превращается в кошмар из if-else конструкций. Граф решает эту проблему элегантно:
┌─── Товар есть ──→ Списать деньги ──→ Отправить
Заказ ┤
└─── Товара нет ──→ Уведомить клиента ──→ Предложить аналог
↓
Ждать решения ──→ Повторить проверку
Четыре основы LangGraph
LangGraph строится на четырёх фундаментальных концепциях:
1. Граф (Graph) — это дорожная карта возможных путей выполнения логики. Как GPS-навигатор знает все дороги города, так граф знает все возможные маршруты вашей программы.
2. Узлы (Nodes) — это точки на карте, где происходит реальная работа. Каждый узел выполняет конкретную функцию: обращается к базе данных, вызывает API, принимает решение или просто обновляет информацию.
3. Рёбра (Edges) — это дороги между точками. Они определяют, как и когда программа переходит от одного действия к другому. Рёбра бывают простые ("всегда иди туда") и условные ("иди туда, если выполнено условие").
4. Состояние (State) — это багаж, который путешествует вместе с процессом. В нём хранится вся необходимая информация: входные данные, промежуточные результаты, история действий.
Почему именно графы?
Графы дают нам суперспособности в программировании:
Прозрачность — любой может посмотреть на граф и понять логику работы программы. Никаких скрытых механизмов или "магических" переходов.
Гибкость — легко добавить новый сценарий, изменить логику или удалить ненужные шаги без переписывания всего кода.
Отладка — видно точно, где процесс "застрял" или пошёл не туда. Каждый шаг документирован и отслеживаем.
Масштабируемость — от простого калькулятора до сложного многоагентного ИИ-помощника — принципы остаются теми же.
Связь с реальным миром
По сути, LangGraph формализует то, как мы естественно думаем о сложных процессах. Когда вы планируете отпуск, ваш мозг строит граф:
Выбрать направление → Забронировать отель → Купить билеты
↓ ↓ ↓
Если дорого → Найти альтернативу → Если места нет → Изменить даты
LangGraph просто даёт нам инструменты для того, чтобы перенести эту естественную логику в код — структурированно, надёжно и расширяемо.
Подготовка
Если вы читаете мои статьи, то знаете, что я Python-разработчик. Поэтому весь представленный код в этом мини-курсе будет написан исключительно на Python. Сами инструменты LangChain и LangGraph так же хорошо работают на JavaScript. Поэтому, если вы пишете на этом языке — информация, написанная ниже, будет такой же актуальной, как и для Python-разработчиков.
То есть для того, чтобы понять написанное далее, у вас должна быть база либо в Python, либо в JavaScript.
Настройка окружения
Начнём мы с создания виртуального окружения на Python и с установки зависимостей:
Создаём виртуальное окружение и активируем его:
python -m venv venv
source venv/bin/activate # или venv\Scripts\activate на Windows
Теперь установим LangGraph:
pip install langgraph==0.6.2
Я указываю конкретную версию, чтобы для вас не потерялась актуальность этой статьи, когда версия обновится. На момент написания это самая свежая стабильная версия.
Где запускать код:
Для экспериментов с LangGraph отлично подойдёт локальная разработка, но если вы планируете создавать продакшн-решения, стоит подумать о надёжной облачной платформе. Например, Amvera Cloud предоставляет удобную среду для разработки и деплоя Python-приложений с автоматическим CI/CD и встроенной поддержкой различных ИИ-сервисов. А главное, включает бесплатное встроенное проксирование до OpenAI, Gemini, Anthropic и Grok и предоставляет инференс LLaMA с оплатой рублями.
Контекст и дополнительные материалы
Прежде чем приступим к написанию кода, хочу отметить, что я уже описывал 2 статьи по теме LangGraph и LangChain, с которыми вы сможете ознакомиться здесь:
«Как научить нейросеть работать руками: создание полноценного ИИ-агента с MCP и LangGraph за час»
«Как создать MCP-сервер и научить ИИ работать с любым кодом и инструментами через LangGraph»
Это на случай, если информации по теме LangGraph от меня окажется недостаточно или будет долго выходить новый материал по теме.
Также хочу напомнить, что полный исходный код к своим статьям на Хабре, как и прочий эксклюзивный контент (мини-гайды, анонсы, посты и т.д.), я публикую в своём телеграм-канале «Лёгкий путь в Python». Там же вы найдёте полный код к сегодняшней статье.
Первые шаги в LangGraph
Один из основных инструментов в LangGraph — это состояния. Технически, состояние — это просто некий Python-класс или, проще говоря, схема, в которой вы описываете все те переменные и объекты, которые вы хотите получать на протяжении выполнения вашего кода.
Если вы когда-то писали телеграм-ботов на Python (например, на Aiogram 3), вам должно быть хорошо знакомо понятие состояний. Оно применяется в FSM — машине состояний. Например, вы задаёте в этих состояниях следующие переменные:
• имя
• фамилия
• возраст
• дата рождения
Далее — задаёте вопросы пользователю, параллельно сохраняя их ответы в состояния, и затем, когда пользователь выходит из опроса — вы уже что-то делаете с полученными данными.
В LangGraph состояния работают схожим образом, и сейчас мы с вами опишем простое состояние.
Описание состояния
from typing import TypedDict
from datetime import date
class UserState(TypedDict):
name: str
surname: str
age: int
birth_date: date
Первое, что вы можете заметить — это наследование от TypedDict
. Это одна из особенностей описания состояний в LangGraph. Наследуется либо от TypedDict
, либо от BaseModel
Pydantic (усложнять сегодня не будем и остановимся на классическом TypedDict
).
На примере выше я показал, как описывается схема состояния. Мы просто говорим, что у нас есть некое состояние, в которое мы хотим собрать имя, фамилию, возраст и дату рождения. Для того чтобы Graph это понимал — мы указали, какой тип данных будет ожидать каждая переменная.
То есть это состояние позволит нам отслеживать всю информацию по мере работы приложения и передавать данные между различными узлами графа.
Создание графа
Далее, для того чтобы это был не просто класс, а полноценное состояние для графа, нам необходимо выполнить импорт:
from langgraph.graph import StateGraph
Теперь нам необходимо инициализировать нашу «дорожную карту» — граф и передать в него состояние графа:
graph = StateGraph(UserState)
Технической пользы от этого пока мало, так как у нас нет никакой логики, как вы могли заметить. Для того чтобы у нас появилась логика — нам необходимо написать её в виде простых функций.
Создание функции для узла
Давайте представим, что на входе к нам приходит человек, который уже представился: назвал нам своё имя, свою фамилию и сказал дату рождения, а наша задача будет заключаться в том, чтобы посчитать его точное количество лет.
В обычном представлении функция имела бы такой вид:
def calculate_age(birth_date: date) -> int:
"""
Вычисляет точный возраст человека от сегодняшней даты.
"""
today = date.today()
# Вычисляем разность в годах
age = today.year - birth_date.year
# Проверяем, прошёл ли уже день рождения в этом году
if (today.month, today.day) < (birth_date.month, birth_date.day):
age -= 1
return age
На вход принимаем дату рождения, на выходе — целое число с точным количеством лет. Но данный формат нам не подойдёт.
Выполним небольшую модернизацию:
def calculate_age(state: UserState) -> dict:
today = date.today()
age = today.year - state["birth_date"].year
if (today.month, today.day) < (state["birth_date"].month, state["birth_date"].day):
age -= 1
return {"age": age}
Теперь мы получили идеально подходящий вариант для графов. Немного забегая вперёд, объясню почему.
Важная особенность функций узлов
Далее нам предстоит работать с узлами (нодами). Для работоспособности узлов мы всегда должны передавать функцию, которая принимает на вход состояние и которая это состояние возвращает (полностью или частично).
Вы видите, что в моём примере я вернул не состояние целиком, а словарь с обновлением состояния. Это, условно, сокращённая версия, которая позволяет с меньшим количеством затрат по вычислениям передавать состояния. Но вы могли бы напрямую вернуть изменённое состояние:
def calculate_age(state: UserState) -> UserState:
today = date.today()
age = today.year - state["birth_date"].year
if (today.month, today.day) < (state["birth_date"].month, state["birth_date"].day):
age -= 1
state["age"] = age
return state
Результат был бы тот же, но с большими затратами на пересчёт всего состояния. Поэтому рекомендуется возвращать только изменения в виде словаря.
Добавление узла в граф
Несмотря на то что мы сделали функцию, которая подходит узлам LangGraph — это всё ещё просто функция. Теперь нам необходимо включить её в узел (ноду).
Узел добавляется на существующий граф. Его мы уже создали:
graph = StateGraph(UserState)
Для добавления узла используется метод add_node
. На вход метод принимает название узла (всегда строка) и функцию-обработчик, которая обрабатывает состояния (в некоторых случаях это может быть безымянная лямбда-функция).
graph.add_node("calculate_age", calculate_age)
Название узла может быть любым, и оно не обязательно должно соответствовать названию функции. Главное, чтобы название было уникальным в рамках одного графа.
Отлично, узел создан!
Построение маршрута с рёбрами
Далее нам необходимо выстроить маршрут. Вы будете удивлены, но у нас не 1 узел, а целых 3, и вот почему. Все цепочки всегда должны начинаться с системного стартового узла START
и завершаться они должны другим системным узлом END
. Эти 2 узла можно импортировать:
from langgraph.graph import StateGraph, START, END
Для того чтобы выстроить цепочку графов, нам необходимо использовать рёбра. Ребро — это просто связка, которая связывает между собой узлы (ноды).
Вот так мы могли бы описать минимальное линейное ребро:
graph.add_edge(START, END)
То есть при помощи метода add_edge
мы сказали, что после того как выполнится системный узел START
, должен выполниться другой системный узел END
. Толку от этого немного, поэтому мы включим в граф наш собственный узел. В этом случае описание связки будет выглядеть так:
graph.add_edge(START, "calculate_age")
graph.add_edge("calculate_age", END)
То есть всё действительно просто. Этой простой записью мы говорим:
Сначала вызови узел
START
А после него уже узел
calculate_age
Далее вызывай
END
Компиляция и запуск графа
Далее для того чтобы мы могли работать с нашим графом — его необходимо скомпилировать. Делается это просто:
app = graph.compile()
Теперь переменная app
— это наш полноценный граф, который остаётся только вызвать.
result = app.invoke({"name": "Алексей",
"surname": "Яковенко",
"birth_date": date.fromisoformat("1993-02-19")})
print(result)
И вот такой результат мы получили:
{'name': 'Алексей', 'surname': 'Яковенко', 'age': 32, 'birth_date': datetime.date(1993, 2, 19)}
Либо можно просто извлечь возраст:
print(result['age'])
тогда отобразится 32
.

Выводы и ключевые моменты
Мы только что создали наш первый полноценный граф в LangGraph! Давайте подведём итоги того, что мы изучили:
Состояние — это схема данных, которая путешествует по графу и позволяет узлам обмениваться информацией. Оно описывается через TypedDict
и содержит все необходимые переменные.
Узлы — это функции, которые принимают состояние на вход и возвращают его изменения. Каждый узел выполняет конкретную задачу и может модифицировать состояние.
Рёбра — это связи между узлами, которые определяют порядок выполнения. Простые рёбра создают линейную последовательность.
Граф — это контейнер, который объединяет узлы и рёбра в единую логическую структуру. После компиляции он становится исполняемым приложением.
Этот простой пример демонстрирует фундаментальные принципы работы с LangGraph. В следующих разделах мы усложним логику, добавим условные переходы и покажем, как создавать более сложные сценарии выполнения. Но основа уже заложена — вы понимаете, как данные течут через граф и как узлы взаимодействуют друг с другом.
Условные рёбра
Теперь немного усложним задачу и представим, что нам нужна не прямолинейная логика, а условная. Давайте представим ситуацию, что нам представился человек. Сказал, как его зовут, назвал дату рождения, и теперь нам необходимо определить, можно ли ему водить машину.
На территории РФ вождение легкового автомобиля разрешено с 18 лет. Следовательно, если мы получим возраст меньше 18 лет, то машину водить нельзя.
Реализуем данную задачу через LangGraph, а именно его технологию условных рёбер.
Обновление состояния
Первым делом давайте обновим наше состояние, добавив поле для сообщения:
from typing import TypedDict
from datetime import date
class UserState(TypedDict):
name: str
surname: str
age: int
birth_date: date
message: str # Новое поле для хранения сообщения пользователю
Условная функция
Первое, что нам нужно подключить — это условную функцию.
Условная функция — проверяет некое условие и затем возвращает строку. На основании возвращаемой строки далее мы будем понимать, какой узел вызвать следующим.
Напишем простое условие:
def check_drive(state: UserState) -> str:
if state["age"] >= 18:
return "можно"
else:
return "нельзя"
На вход принимаем состояние — на выходе возвращаем строку. Это ключевая особенность условных функций в LangGraph: они всегда возвращают строку, которая определяет следующий шаг в графе.
Создание узлов-обработчиков
Условное ребро должно вызвать тот или иной узел в зависимости от условия. Давайте напишем 2 обработчика, на базе которых мы будем возвращать то или иное сообщение пользователю:
def generate_success_message(state: UserState) -> dict:
return {
"message": f"Поздравляем, {state['name']} {state['surname']}! "
f"Вам уже {state['age']} лет и вы можете водить!"
}
def generate_failure_message(state: UserState) -> dict:
return {
"message": f"К сожалению, {state['name']} {state['surname']}, "
f"вам ещё только {state['age']} лет и вы не можете водить."
}
Обратите внимание, что каждая функция возвращает словарь с одним ключом message
. LangGraph автоматически объединит это с существующим состоянием, обновив только указанное поле.
Построение графа с условными рёбрами
Инициализируем граф:
graph = StateGraph(UserState)
Добавляем все узлы (ноды):
graph.add_node("calculate_age", calculate_age)
graph.add_node("generate_success_message", generate_success_message)
graph.add_node("generate_failure_message", generate_failure_message)
Важное замечание: Обратите внимание, мы не создаём ноду из check_drive
, так как этот обработчик мы будем использовать в условном ребре. Условные функции не являются узлами — они являются логикой маршрутизации между узлами.
Начало, как в первом примере:
graph.add_edge(START, "calculate_age")
Тут мы связываем стартовый системный узел с нашим узлом расчёта возраста.
Создание условного ребра
Теперь переходим к условному ребру:
graph.add_conditional_edges(
"calculate_age",
check_drive,
{
"можно": "generate_success_message",
"нельзя": "generate_failure_message"
}
)
Мы тут задействовали новый метод — add_conditional_edges
. Данный метод:
Первым аргументом принимает название узла, с которого начинается условная логика
Вторым аргументом принимает функцию условия (не строку с названием, а саму функцию!)
Третьим аргументом принимает словарь маршрутизации
В качестве ключей словаря выступают варианты строк, полученные от условной функции, а в качестве значений — названия узлов.
Таким простым и элегантным способом мы, основываясь на условной функции, можем управлять запуском десятков, а то и сотен различных ветвей логики! При этом подход выглядит максимально читаемым и понятным.
Завершение графа
Далее нам остаётся обработать следующий шаг после условного ребра:
graph.add_edge("generate_success_message", END)
graph.add_edge("generate_failure_message", END)
Как вы понимаете — далее цепочка могла бы быть намного длиннее. Мы могли бы снова использовать условные рёбра или прямые рёбра. То есть выстраивать можно было бы действительно сложную и разветвлённую логику.
Например, после успешного сообщения мы могли бы добавить узел для проверки наличия водительских прав, а после неуспешного — узел с информацией о том, когда человек сможет получить права.
Компиляция и запуск
Компилируем и вызываем:
app = graph.compile()
result = app.invoke({
"name": "Алексей",
"surname": "Яковенко",
"birth_date": date.fromisoformat("1993-02-19")
})
print(result)
Результат:
{
'name': 'Алексей',
'surname': 'Яковенко',
'age': 32,
'birth_date': datetime.date(1993, 2, 19),
'message': 'Поздравляем, Алексей Яковенко! Вам уже 32 лет и вы можете водить!'
}
Визуализация логики
Давайте представим наш граф схематично:

Ключевые принципы условных рёбер
Условная функция всегда возвращает строку, которая определяет следующий узел для выполнения.
Словарь маршрутизации должен покрывать все возможные возвращаемые значения условной функции, иначе граф может упасть с ошибкой.
Гибкость — вы можете создавать сколь угодно сложные условия, возвращая разные строки для разных сценариев.
Читаемость — логика ветвления явно видна в коде и легко модифицируется.
Условные рёбра — это мощный инструмент, который превращает простые линейные последовательности в сложные, адаптивные системы принятия решений. В следующих разделах мы рассмотрим ещё более продвинутые паттерны и покажем, как создавать графы с циклами и параллельным выполнением.
Циклы в LangGraph
Бывает, что нам недостаточно простых цепочек, а может быть, и условий недостаточно. Я говорю о ситуации, когда мы хотим выполнять определённый набор действий до достижения нужного условия.
Давайте представим, что наш граф, в случае если пользователь несовершеннолетний, не будет возвращать ему сообщение со словами «Вы не можете водить», а будет ждать, пока пользователь повзрослеет, чтобы дать ему такое разрешение.
Конечно, в реальной жизни это было бы абсурдно, но для демонстрации циклов в графах — отличный пример!
Обновление состояния
Давайте создадим такой специальный узел, который будет при каждом к нему обращении увеличивать сегодняшнюю дату на 1 день. Для этого мы добавим в наше состояние переменную:
from typing import TypedDict
from datetime import date, timedelta
class UserState(TypedDict):
name: str
surname: str
age: int
birth_date: date
today: date # Новое поле для хранения "текущей" даты
message: str
Модификация функции расчёта возраста
Теперь внесём правки в определение возраста:
def calculate_age(state: UserState) -> dict:
"""
Вычисляет точный возраст человека от переданной даты.
"""
today = state["today"] # Берём дату из состояния, а не системную
# Вычисляем разность в годах
age = today.year - state["birth_date"].year
# Проверяем, прошёл ли уже день рождения в этом году
if (today.month, today.day) < (state["birth_date"].month, state["birth_date"].day):
age -= 1
return {"age": age}
Обратите внимание — теперь мы дату сегодня изначально получаем из состояния. То есть при запуске графа нам нужно будет передать это значение, и мы сможем его контролировать.
Функция увеличения даты
Теперь напишем обработчик, который будет увеличивать сегодняшнюю дату на 1 день:
def autoincrement_date(state: UserState) -> dict:
"""
Увеличивает текущую дату на один день.
"""
current_date = state["today"]
new_date = current_date + timedelta(days=1)
print(f"{current_date} -> {new_date}") # Для наглядности процесса
return {"today": new_date}
Не забываем импортировать timedelta
из datetime
.
Я специально добавил принт, чтобы видеть, какое количество итераций мы прошли и как изменяется дата в процессе выполнения цикла.
Построение циклического графа
Теперь нам нужно выстроить следующую логику: если пользователю недостаточно лет, то мы должны вызывать узел, который увеличивает дату на 1 день, и после снова запускать общую проверку. Действовать мы будем до тех пор, пока не вернём сообщение о том, что можно ездить.
Добавляем новый узел:
graph.add_node("autoincrement_date", autoincrement_date)
Выстраиваем цепочку:
graph.add_edge(START, "calculate_age")
Теперь опишем условный граф:
graph.add_conditional_edges(
"calculate_age",
check_drive,
{
"можно": "generate_success_message",
"нельзя": "autoincrement_date"
}
)
Обратите внимание — теперь, если условие вернуло «нельзя», мы запускаем наш новый узел autoincrement_date
.
Если возраст подходит, то мы завершаем выполнение:
graph.add_edge("generate_success_message", END)
Но если он не подходит — мы снова запускаем первый узел, создавая цикл:
graph.add_edge("autoincrement_date", "calculate_age")
Создание цикла: ключевая логика
Таким образом, мы зациклили граф. У нас есть два возможных исхода:
Выход из логики графа — когда наш пользователь становится совершеннолетним
Продолжение цикла — мы запускаем логику с момента определения возраста, если это не так
Схематично это выглядит так:

Настройка лимита рекурсии
Компилируем и вызываем граф:
app = graph.compile()
result = app.invoke({
"name": "Алексей",
"surname": "Яковенко",
"birth_date": date.fromisoformat("2008-02-19"), # Несовершеннолетний
"today": date.today()
}, {"recursion_limit": 1000}) # Увеличиваем лимит итераций
Обратите внимание на второй словарь — настройки. В нём я переопределил внутренний параметр лимита рекурсии. Без данного параметра LangGraph позволил бы нам только 25 итераций по умолчанию.
Это защитный механизм от бесконечных циклов. В продакшн-коде всегда устанавливайте разумные лимиты!
В продакшн-окружении, например при деплое на Amvera Cloud, такие параметры лучше выносить в конфигурационные файлы. Это позволяет гибко управлять поведением агентов без изменения кода.
Результат выполнения
Вызываем и видим следующую картину:
...
2026-02-13 -> 2026-02-14
2026-02-14 -> 2026-02-15
2026-02-15 -> 2026-02-16
2026-02-16 -> 2026-02-17
2026-02-17 -> 2026-02-18
2026-02-18 -> 2026-02-19
{'name': 'Алексей',
'surname': 'Яковенко',
'age': 18,
'birth_date': datetime.date(2008, 2, 19),
'today': datetime.date(2026, 2, 19),
'message': 'Поздравляем, Алексей Яковенко! Вам уже 18 и вы можете водить!'
}
Анализ результата
Граф выполнил 201 итерацию, каждый день увеличивая дату на один день, пока пользователь не достиг совершеннолетия. Как только ему исполнилось 18 лет (19 февраля 2026 года), условие сработало, и граф завершился успешным сообщением.
Практические применения циклов
В реальных проектах циклы в LangGraph используются для:
Повторных попыток — когда нужно повторить операцию до успешного выполнения (например, запросы к API)
Итеративные улучшения — когда ИИ-агент постепенно улучшает свой ответ или решение
Обработка очередей — когда нужно обрабатывать элементы до тех пор, пока очередь не опустеет
Диалоговые сценарии — когда агент ведёт диалог с пользователем до достижения цели
Важные моменты при работе с циклами
Условие выхода — всегда должно быть чёткое условие, которое гарантированно прервёт цикл
Лимит итераций — защита от бесконечных циклов через recursion_limit
Состояние изменяется — каждая итерация должна как-то изменять состояние, иначе цикл будет бесконечным
Отладка — используйте логирование для отслеживания изменений в состоянии
Циклы превращают LangGraph в мощный инструмент для создания сложных, адаптивных систем, способных выполнять итеративные процессы до достижения нужного результата. Это особенно важно при работе с ИИ-агентами, которые могут улучшать свои ответы или повторять действия до получения удовлетворительного результата.
Если хочется начать с условного ребра?
К сожалению, прямой возможности начинать с условного ребра в LangGraph нет. То есть вы не сможете использовать системный START
в качестве отправной точки для условной логики. Сейчас я поделюсь с вами способом, который позволит элегантно обойти это ограничение.
Проблема и её решение
В LangGraph архитектурно заложено, что граф должен начинаться с узла, а не с условного ребра. Это логично с точки зрения дизайна — сначала нужно что-то сделать (узел), а потом принять решение о дальнейших действиях (условное ребро).
Но иногда у нас есть данные, которые уже готовы для принятия решения, и промежуточная обработка не нужна. В таких случаях мы создаём "фиктивный" узел, который просто передаёт состояние дальше без изменений.
Практический пример
Давайте напишем простой граф, который будет принимать возраст пользователя и возвращать соответствующее сообщение:
from typing import TypedDict
from langgraph.graph import StateGraph, START, END
class UserState(TypedDict):
age: int
message: str
def check_age(state: UserState) -> str:
"""Условная функция для проверки возраста"""
return "совершеннолетний" if state["age"] >= 18 else "не_совершеннолетний"
def generate_success_message(state: UserState) -> dict:
"""Генерирует сообщение для совершеннолетних"""
return {"message": f"Вам уже {state['age']} лет и вы можете водить!"}
def generate_failure_message(state: UserState) -> dict:
"""Генерирует сообщение для несовершеннолетних"""
return {"message": f"Вам ещё только {state['age']} лет и вы не можете водить."}
Создание фиктивного узла
Теперь создадим граф с фиктивным узлом:
graph = StateGraph(UserState)
# Фиктивный узел - просто передаёт состояние дальше
graph.add_node("fake_node", lambda state: state)
# Основные узлы обработки
graph.add_node("generate_success_message", generate_success_message)
graph.add_node("generate_failure_message", generate_failure_message)
Построение логики графа
# Связываем START с фиктивным узлом
graph.add_edge(START, "fake_node")
# От фиктивного узла идёт условное ребро
graph.add_conditional_edges(
"fake_node",
check_age,
{
"совершеннолетний": "generate_success_message",
"не_совершеннолетний": "generate_failure_message"
}
)
# Завершаем оба пути
graph.add_edge("generate_success_message", END)
graph.add_edge("generate_failure_message", END)
Объяснение фиктивного узла
Обратите внимание на стартовый узел:
graph.add_node("fake_node", lambda state: state)
Таким образом, мы написали безымянную лямбда-функцию, которая на вход приняла состояние и вернула его без изменений. Таким образом, мы собрали «заглушку», которая была бы эквивалентна:
def fake_node(state: UserState) -> UserState:
"""Фиктивный узел, который просто передаёт состояние дальше"""
return state
Запуск и результат
app = graph.compile()
# Тест для несовершеннолетнего
result_minor = app.invoke({"age": 17})
print("Результат для 17 лет:", result_minor)
# Тест для совершеннолетнего
result_adult = app.invoke({"age": 25})
print("Результат для 25 лет:", result_adult)
Результат:
Результат для 17 лет: {
'age': 17,
'message': 'Вам ещё только 17 лет и вы не можете водить.'
}
Результат для 25 лет: {
'age': 25,
'message': 'Вам уже 25 лет и вы можете водить!'
}
Схема работы графа
Визуально наш граф выглядит так:

Альтернативные варианты фиктивного узла
Помимо простой лямбда-функции, фиктивный узел может выполнять минимальную полезную работу:
Логирование
def log_and_pass(state: UserState) -> UserState:
"""Логирует вход в граф и передаёт состояние дальше"""
print(f"Начинаем обработку пользователя с возрастом: {state['age']}")
return state
graph.add_node("log_node", log_and_pass)
Валидация
def validate_and_pass(state: UserState) -> UserState:
"""Проверяет корректность данных и передаёт состояние дальше"""
if state["age"] < 0 or state["age"] > 150:
raise ValueError(f"Некорректный возраст: {state['age']}")
return state
graph.add_node("validation_node", validate_and_pass)
Инициализация
def initialize_and_pass(state: UserState) -> dict:
"""Инициализирует дополнительные поля и передаёт состояние дальше"""
return {
"timestamp": datetime.now().isoformat(),
"processed": True
}
graph.add_node("init_node", initialize_and_pass)
Когда использовать фиктивные узлы
Прямое условное ветвление — когда входные данные уже готовы для принятия решений
Валидация на входе — проверка корректности данных перед основной логикой
Логирование точек входа — отслеживание начала обработки
Инициализация метаданных — добавление служебной информации в состояние
Важные моменты
Производительность — фиктивные узлы добавляют минимальные накладные расходы
Читаемость — они делают логику графа более явной и понятной
Гибкость — позволяют легко добавлять функциональность в будущем
Совместимость — соответствуют архитектурным требованиям LangGraph
Фиктивные узлы — это элегантный способ обойти архитектурные ограничения LangGraph, сохраняя при этом чистоту и читаемость кода. Они позволяют начинать граф с условной логики, когда это необходимо, не нарушая при этом принципов фреймворка.
Визуализировать граф? Легко!
Выше в статье вы видели схематичные диаграммы с узлами и стрелками. Если вам кажется, что я эти рисунки отрисовывал самостоятельно — вы заблуждаетесь!
В LangGraph есть встроенная логика, позволяющая автоматически генерировать визуализацию графа в виде изображения. Это невероятно полезная функция для понимания логики работы сложных графов и их отладки.
Функция для генерации PNG
Напишем простую функцию, которая на вход будет принимать скомпилированный объект графа и название выходного файла:
def gen_png_graph(app_obj, name_photo: str = "graph.png") -> None:
"""
Генерирует PNG-изображение графа и сохраняет его в файл.
Args:
app_obj: Скомпилированный объект графа
name_photo: Имя файла для сохранения (по умолчанию "graph.png")
"""
with open(name_photo, "wb") as f:
f.write(app_obj.get_graph().draw_mermaid_png())
Использование функции
Вызываем сразу после компиляции графа:
# Компилируем граф
app = graph.compile()
# Генерируем визуализацию
gen_png_graph(app, name_photo="graph_example_4.png")
print("Граф сохранён как graph_example_4.png")
Как это работает под капотом
LangGraph использует библиотеку Mermaid для создания диаграмм. Процесс происходит в несколько этапов:
Анализ структуры — LangGraph анализирует все узлы и рёбра в вашем графе
Генерация Mermaid-кода — создаётся текстовое описание графа в формате Mermaid
Рендеринг в PNG — Mermaid-код преобразуется в изображение
Сгенерированная диаграмма отображает:
Узлы — прямоугольники с названиями ваших функций
Рёбра — стрелки, показывающие направление потока выполнения
Условные рёбра — разветвления с подписями условий
Системные узлы — START и END выделены особым образом
Циклы — стрелки, возвращающиеся к предыдущим узлам
Визуализация особенно полезна для:
Поиска ошибок в логике — сразу видно, если граф идёт не туда, куда задумано
Оптимизации производительности — можно выявить избыточные пути
Документирования — диаграммы отлично подходят для технической документации
Обучения команды — новые разработчики быстрее понимают логику
Полезные советы
Именование узлов — используйте понятные названия, они отображаются на диаграмме
Группировка логики — родственные узлы стоит именовать с общим префиксом
Версионирование диаграмм — сохраняйте диаграммы с версиями для отслеживания изменений
Автоматизация — интегрируйте генерацию в CI/CD для автоматического обновления документации
Визуализация графов в LangGraph — это не просто красивая картинка, а мощный инструмент для понимания, отладки и документирования сложной логики. Используйте его активно, и ваши графы станут гораздо более понятными и поддерживаемыми!
Заключение: от простых схем к ИИ-агентам
Вот мы и подошли к концу нашего путешествия в мир основ LangGraph. Давайте оглянемся назад и посмотрим, какой путь мы прошли вместе.
Что мы освоили
Мы начали с самых азов — разобрались, что такое состояния, узлы, рёбра и как они взаимодействуют между собой. Казалось бы, простые концепции, но именно они составляют фундамент для создания сложнейших ИИ-систем.
Состояния — мы научились описывать "память" нашего графа через
TypedDict
, понимать, как данные путешествуют между узлами и накапливаются по мере выполнения.Узлы — освоили создание функций, которые принимают состояние и возвращают его изменения. Поняли, что каждый узел — это отдельная ответственность, отдельная задача в общем процессе.
Простые рёбра — научились строить линейные последовательности выполнения, связывать узлы в осмысленные цепочки.
Условные рёбра — открыли для себя мощь ветвящейся логики, когда граф может принимать решения и идти разными путями в зависимости от данных.
Циклы — поняли, как создавать итеративные процессы, когда граф может повторять действия до достижения нужного результата.
Визуализация — научились "видеть" наши графы, превращать абстрактный код в понятные диаграммы.
Почему это было важно
Возможно, кто-то подумает: "Зачем такие сложности? Можно же просто писать if-else и циклы в обычном коде." И это справедливый вопрос.
Но представьте, что вы пишете систему, которая должна:
Принять заказ от клиента
Проверить товар на складе
Если товара нет — предложить аналог
Согласовать с клиентом
Обработать платёж
При ошибке платежа — попробовать другой способ
Отправить товар
Отследить доставку
При проблемах — связаться с клиентом
В традиционном коде это превратится в запутанный лабиринт условий и состояний. А в LangGraph — в красивую, понятную схему, где каждый шаг визуален и контролируем.
Мост к ИИ-агентам
Сейчас, когда вы понимаете основы, давайте заглянем в будущее. Почему именно графы так идеально подходят для ИИ-агентов?
Принятие решений — ИИ постоянно анализирует ситуацию и выбирает следующее действие. Условные рёбра — это естественный способ моделировать такие решения.
Итеративные улучшения — ИИ-агенты часто работают циклично: анализируют результат, улучшают подход, пробуют снова. Циклы в графах — идеальный инструмент для этого.
Модульность — разные "навыки" ИИ можно оформить как отдельные узлы: один узел для работы с текстом, другой для поиска в интернете, третий для анализа данных.
Прозрачность — когда ИИ-агент принимает решения, важно понимать его логику. Граф показывает весь путь мышления агента.
Что нас ждёт в следующих частях
В следующей статье серии мы сделаем революционный шаг — подключим к нашим графам настоящие нейросети! Вы увидите, как:
Узлы становятся умными — вместо простых функций расчёта возраста мы будем использовать узлы, которые анализируют тексты, генерируют ответы, принимают сложные решения.
Состояния обогащаются — помимо простых данных мы будем хранить историю диалога, контекст задач, промежуточные результаты анализа.
Условные рёбра становятся интеллектуальными — решения будут принимать не по простым if-else, а на основе анализа нейросетью всего контекста ситуации.
Циклы обретают цель — ИИ будет итеративно улучшать свои ответы, повторять попытки решения задач, вести диалоги до достижения нужного результата.
Мы создадим ИИ-агента, который сможет:
Анализировать входящие запросы
Выбирать подходящие инструменты
Вызывать внешние API и сервисы
Обрабатывать ошибки и повторять попытки
Запоминать контекст и учиться на опыте
В следующих частях мы также рассмотрим вопросы деплоя и масштабирования ИИ-агентов. Покажу, как разворачивать LangGraph-приложения в облаке (на примере Amvera Cloud), настраивать мониторинг и обеспечивать стабильную работу агентов в продакшене.
Практическая ценность
К концу этой серии у вас будет не просто понимание теории, а реальные навыки создания производственных ИИ-систем. Вы сможете:
Создавать чат-ботов с многоступенчатой логикой
Строить системы автоматизации бизнес-процессов
Разрабатывать ИИ-помощников для конкретных задач
Интегрировать различные ИИ-сервисы в единые решения
Благодарность
Спасибо, что прошли со мной этот путь от простых схем к пониманию фундаментальных принципов. Знаю, что временами могло показаться, что мы "изобретаем велосипед" — пишем сложные графы для простых задач.
Но поверьте — это время потрачено не зря. Когда в следующей статье мы подключим к этим графам настоящий ИИ, вы поймёте всю мощь и элегантность этого подхода. То, что сегодня кажется избыточным, завтра станет спасательным кругом при создании сложных интеллектуальных систем.
До встречи
Полный код всех примеров из этой статьи, как всегда, доступен в моём телеграм-канале "Лёгкий путь в Python". Там же я буду анонсировать выход следующих частей серии.
Увидимся во второй части, где мы наконец-то подружим наши графы с нейросетями и создадим первого настоящего ИИ-агента!
До скорых встреч, и пусть ваши графы всегда ведут к нужному результату!