Главная / Статьи / Условные рёбра в LangGraph: маршрутизация графа через состояние и решения

Условные рёбра в LangGraph: маршрутизация графа через состояние и решения

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

1120 слов

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

Состояние (State) — объект, который движется по графу и хранит всю информацию. Любой узел может читать из него и записывать в него.

state = {
    'a': 2,
    'b': 3,
    'sum': None
}

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

def add(state):
    total = state['a'] + state['b']
    return {'sum': total}

Рёбра (Edges) определяют, какой узел запустится следующим, например:

add → multiply → final

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

Идея условного ребра

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

Допустим, граф приветствует человека в зависимости от времени суток. Утром вывод такой:

Good Morning

Ночью такой:

Good Night

В результате меняется форма графа. Это уже не прямая линия

A → B → C

а развилка:

→ Morning → Good Morning
            /
User → Router
            \
             → Night → Good Night

Ветку выбирает роутер (Router). В двух словах, это и есть условное ребро.

Сначала пробуем на чистом Python

Прежде чем подключать LangGraph, вот та же идея в виде обычной функции. Ей нужно текущее время:

from datetime import datetime

А вот сама функция:

def router():    hour = datetime.now().hour

    if 5 <= hour < 12:
        return "Morning"
    else:
        return "Night"

Обратите внимание, как мало эта функция делает. Никакой большой операции она не выполняет. Она принимает решение. Если текущий час от 5 утра до полудня, она возвращает

Morning

а в любом другом случае возвращает

Night

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

state = {
    'time_per': None
}

Теперь роутер может записать свой выбор в состояние. Он принимает состояние как аргумент:

def router(state):

И внутри вычисляет час и записывает результат:

    hour = datetime.now().hour

    if 5 <= hour < 12:
        return {'time_per': 'Morning'}
    else:
        return {'time_per': 'Night'}

После этого в состоянии хранится решение:

{
    'time_per': 'Morning'
}

В этом и суть. Состояние хранит информацию, а условное ребро читает её, чтобы направлять граф. Две вещи происходят друг за другом:

Router
  ↓
updates state
  ↓
time_per = "Morning"
  ↓
conditional edge checks it
  ↓
Morning node

Здесь состояние и рёбра начинают работать вместе.

То же самое в LangGraph

Сначала состоянию нужно типизированное определение. Импортируйте TypedDict:

from typing import TypedDict

Затем объявите схему:

class State(TypedDict):
    time_per: str

Дальше идут узлы. Роутер решает, утро сейчас или ночь, и ему нужны часы:

from datetime import datetime

Сам узел-роутер:

def router(state):    hour = datetime.now().hour    if 5 <= hour < 12:
        return {'time_per': 'Morning'}
    else:
        return {'time_per': 'Night'}

Ещё два узла обрабатывают каждый исход. Один отвечает за утро:

def morning(state):
    return {'time_per': 'Good Morning'}

Другой — за ночь:

def night(state):
    return {'time_per': 'Good Night'}

Теперь важная часть. LangGraph нужна функция, которая говорит, по какому пути идти. Она получает состояние:

def decide_time(state):

и просто возвращает сохранённое решение:

    return state['time_per']

То есть она возвращает либо

Morning

либо

Night

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

graph.add_conditional_edges(
    "router",
    decide_time,
    {
        "Morning": "morning",
        "Night": "night"
    }
)

Разберём, что означает каждая часть. Начало вызова

graph.add_conditional_edges(
    "router",

говорит LangGraph, что после завершения узла router граф не должен переходить к одному фиксированному узлу. Дальше управление берёт решение. Следующий аргумент

decide_time,

— это функция, которая принимает решение. Последний аргумент

{
    "Morning": "morning",
    "Night": "night"
}

— соответствие между каждым возможным решением и узлом, который должен идти следом. Если decide_time() возвращает

Morning

граф идёт по

router → morning

а если возвращает

Night

граф идёт по

router → night

Теперь полный граф выглядит так:

→ morning → END
                 /
START → router
                 \
                  → night → END

В этом и разница между двумя видами рёбер. Обычное ребро всегда говорит

A → B

без исключений, а условное ребро говорит

A → ?

и позволяет решению подставить ответ вместо вопросительного знака.

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

Здесь LangGraph становится по-настоящему интересным. Поток работает как цикл зависимостей:

updates
           State
             ↑
             |
          Router
             |
             ↓
     Conditional Edge
        ↙         ↘
   Morning        Night

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

Реалистичный пример — роутер вопросов:

User Question
      ↓
   Classifier
      ↓
   State
      ↓
 ┌────┴─────┐
 ↓          ↓
Coding    General
 ↓          ↓
Code      Normal
Answer    Answer

Классификатор может записать в состояние что-то такое:

{
    'type': 'coding'
}

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