Приветствую! Дошли руки для того, чтобы оформить свои знания по теме 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, с которыми вы сможете ознакомиться здесь:

Это на случай, если информации по теме 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)

То есть всё действительно просто. Этой простой записью мы говорим:

  1. Сначала вызови узел START

  2. А после него уже узел calculate_age

  3. Далее вызывай 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"] &gt;= 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. Данный метод:

  1. Первым аргументом принимает название узла, с которого начинается условная логика

  2. Вторым аргументом принимает функцию условия (не строку с названием, а саму функцию!)

  3. Третьим аргументом принимает словарь маршрутизации

В качестве ключей словаря выступают варианты строк, полученные от условной функции, а в качестве значений — названия узлов.

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

Завершение графа

Далее нам остаётся обработать следующий шаг после условного ребра:

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

Создание цикла: ключевая логика

Таким образом, мы зациклили граф. У нас есть два возможных исхода:

  1. Выход из логики графа — когда наш пользователь становится совершеннолетним

  2. Продолжение цикла — мы запускаем логику с момента определения возраста, если это не так

Схематично это выглядит так:

Настройка лимита рекурсии

Компилируем и вызываем граф:

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 для создания диаграмм. Процесс происходит в несколько этапов:

  1. Анализ структуры — LangGraph анализирует все узлы и рёбра в вашем графе

  2. Генерация Mermaid-кода — создаётся текстовое описание графа в формате Mermaid

  3. Рендеринг в PNG — Mermaid-код преобразуется в изображение

Сгенерированная диаграмма отображает:

  • Узлы — прямоугольники с названиями ваших функций

  • Рёбра — стрелки, показывающие направление потока выполнения

  • Условные рёбра — разветвления с подписями условий

  • Системные узлы — START и END выделены особым образом

  • Циклы — стрелки, возвращающиеся к предыдущим узлам

Визуализация особенно полезна для:

  • Поиска ошибок в логике — сразу видно, если граф идёт не туда, куда задумано

  • Оптимизации производительности — можно выявить избыточные пути

  • Документирования — диаграммы отлично подходят для технической документации

  • Обучения команды — новые разработчики быстрее понимают логику

Полезные советы

  • Именование узлов — используйте понятные названия, они отображаются на диаграмме

  • Группировка логики — родственные узлы стоит именовать с общим префиксом

  • Версионирование диаграмм — сохраняйте диаграммы с версиями для отслеживания изменений

  • Автоматизация — интегрируйте генерацию в CI/CD для автоматического обновления документации

Визуализация графов в LangGraph — это не просто красивая картинка, а мощный инструмент для понимания, отладки и документирования сложной логики. Используйте его активно, и ваши графы станут гораздо более понятными и поддерживаемыми!

Заключение: от простых схем к ИИ-агентам

Вот мы и подошли к концу нашего путешествия в мир основ LangGraph. Давайте оглянемся назад и посмотрим, какой путь мы прошли вместе.

Что мы освоили

Мы начали с самых азов — разобрались, что такое состояния, узлы, рёбра и как они взаимодействуют между собой. Казалось бы, простые концепции, но именно они составляют фундамент для создания сложнейших ИИ-систем.

  • Состояния — мы научились описывать "память" нашего графа через TypedDict, понимать, как данные путешествуют между узлами и накапливаются по мере выполнения.

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

  • Простые рёбра — научились строить линейные последовательности выполнения, связывать узлы в осмысленные цепочки.

  • Условные рёбра — открыли для себя мощь ветвящейся логики, когда граф может принимать решения и идти разными путями в зависимости от данных.

  • Циклы — поняли, как создавать итеративные процессы, когда граф может повторять действия до достижения нужного результата.

  • Визуализация — научились "видеть" наши графы, превращать абстрактный код в понятные диаграммы.

Почему это было важно

Возможно, кто-то подумает: "Зачем такие сложности? Можно же просто писать if-else и циклы в обычном коде." И это справедливый вопрос.

Но представьте, что вы пишете систему, которая должна:

  • Принять заказ от клиента

  • Проверить товар на складе

  • Если товара нет — предложить аналог

  • Согласовать с клиентом

  • Обработать платёж

  • При ошибке платежа — попробовать другой способ

  • Отправить товар

  • Отследить доставку

  • При проблемах — связаться с клиентом

В традиционном коде это превратится в запутанный лабиринт условий и состояний. А в LangGraph — в красивую, понятную схему, где каждый шаг визуален и контролируем.

Мост к ИИ-агентам

Сейчас, когда вы понимаете основы, давайте заглянем в будущее. Почему именно графы так идеально подходят для ИИ-агентов?

  • Принятие решений — ИИ постоянно анализирует ситуацию и выбирает следующее действие. Условные рёбра — это естественный способ моделировать такие решения.

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

  • Модульность — разные "навыки" ИИ можно оформить как отдельные узлы: один узел для работы с текстом, другой для поиска в интернете, третий для анализа данных.

  • Прозрачность — когда ИИ-агент принимает решения, важно понимать его логику. Граф показывает весь путь мышления агента.

Что нас ждёт в следующих частях

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

  • Узлы становятся умными — вместо простых функций расчёта возраста мы будем использовать узлы, которые анализируют тексты, генерируют ответы, принимают сложные решения.

  • Состояния обогащаются — помимо простых данных мы будем хранить историю диалога, контекст задач, промежуточные результаты анализа.

  • Условные рёбра становятся интеллектуальными — решения будут принимать не по простым if-else, а на основе анализа нейросетью всего контекста ситуации.

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

Мы создадим ИИ-агента, который сможет:

  • Анализировать входящие запросы

  • Выбирать подходящие инструменты

  • Вызывать внешние API и сервисы

  • Обрабатывать ошибки и повторять попытки

  • Запоминать контекст и учиться на опыте

В следующих частях мы также рассмотрим вопросы деплоя и масштабирования ИИ-агентов. Покажу, как разворачивать LangGraph-приложения в облаке (на примере Amvera Cloud), настраивать мониторинг и обеспечивать стабильную работу агентов в продакшене.

Практическая ценность

К концу этой серии у вас будет не просто понимание теории, а реальные навыки создания производственных ИИ-систем. Вы сможете:

  • Создавать чат-ботов с многоступенчатой логикой

  • Строить системы автоматизации бизнес-процессов

  • Разрабатывать ИИ-помощников для конкретных задач

  • Интегрировать различные ИИ-сервисы в единые решения

Благодарность

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

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

До встречи

Полный код всех примеров из этой статьи, как всегда, доступен в моём телеграм-канале "Лёгкий путь в Python". Там же я буду анонсировать выход следующих частей серии.

Увидимся во второй части, где мы наконец-то подружим наши графы с нейросетями и создадим первого настоящего ИИ-агента!

До скорых встреч, и пусть ваши графы всегда ведут к нужному результату!

Комментарии (0)