Головна / Статті / Умовні ребра в 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'
}

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