Умовні ребра в LangGraph: маршрутизація графа через стан і рішення
Як працюють умовні ребра в LangGraph: пишемо роутер, що зберігає рішення у стані, зіставляємо результати з вузлами й відправляємо кожен шлях туди, куди треба.
Перш ніж додавати рішення, корисно коротко пригадати першу частину цієї серії про 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'
}
Умовне ребро читає це значення й вирішує, чи надіслати запит до вузла для коду, чи до загального. На цьому етапі робочий процес уже не проста послідовність. Це граф, що ухвалює рішення. Наступний крок — цикли, коли граф повертається назад, повторює крок і пробує знову.