LangGraph: мультиагентна система на прикладі
У вступі про LangGraph я обіцяв показати multi-agent передачу роботи на живому прикладі — ось він. Зберемо маленьку мультиагентну систему: супервайзер отримує запит і вирішує, кому його віддати — агенту-досліднику чи агенту-райтеру. Усі троє ділять один стан. Код буде по етапах, короткими шматками, з прозою між ними — щоб ви бачили не «магію фреймворка», а звичайний граф, який ви могли б написати самі.
Якщо ви ще не читали базу — що таке вузли, ребра і стан — почніть з вступу до LangGraph українською → Тут я не повторюю основи, а одразу будую систему з кількох агентів.
Що таке патерн «супервайзер»
Найпростіша мультиагентна архітектура — це не «агенти, що балакають один з одним по колу». Це один вузол-маршрутизатор, який дивиться на стан і вирішує, який спеціаліст потрібен наступним. Спеціалісти не знають один про одного — вони знають лише про спільний стан і про те, що після них керування повертається до супервайзера.
Чому саме так, а не «кожен агент сам вирішує, кому передати»? Бо децентралізована передача швидко перетворюється на той самий клубок if-ів, від якого ми тікали. Супервайзер — це одна точка рішення. Один вузол, один промпт, одне місце, де ви дивитесь у трейсі «чому пішло сюди, а не туди». Для системи з 2-5 агентів це майже завжди правильний старт.
Етап 1: стан, який тече крізь усіх
Перше і найважливіше рішення — стан. Це те, що відрізняє мультиагентну систему від купи окремих викликів LLM. Замість того щоб передавати між агентами рядки («ось тобі текст, зроби щось»), ми описуємо спільну структуру, яку кожен агент читає і доповнює.
from typing import TypedDict, Annotated, Literal
from langgraph.graph import add_messages
from langchain_core.messages import BaseMessage
class AgentState(TypedDict):
# історія діалогу; add_messages дописує, а не перетирає
messages: Annotated[list[BaseMessage], add_messages]
# куди супервайзер вирішив віддати наступний крок
next: str
# робочі артефакти спеціалістів
research: str
draft: strЗверніть увагу на Annotated[list, add_messages]. Це не косметика. Без редюсера кожен вузол, що повертає messages, перетер би історію. З add_messages LangGraph розуміє: нові повідомлення треба дописати до наявних. Це і є та сама причина, чому стан кращий за передачу рядків — він має правила злиття, а не просто «останній переміг».
Поля research і draft — це робочі артефакти. Дослідник пише в research, райтер читає його і пише в draft. Ніхто нікому нічого не «передає» вручну — всі просто працюють з однією дошкою.
Етап 2: вузли-спеціалісти
Тепер самі агенти. Кожен — це звичайна функція: приймає стан, робить свою роботу, повертає оновлення стану (не весь стан, лише те, що змінилось). Усередині — той самий виклик LLM, що й на голому SDK.
def researcher(state: AgentState) -> dict:
topic = state["messages"][-1].content
notes = llm.invoke(
f"Збери 4-5 ключових фактів по темі: {topic}. "
"Лише факти, без вступів."
)
return {
"research": notes.content,
"messages": [notes],
}
def writer(state: AgentState) -> dict:
text = llm.invoke(
"Напиши короткий абзац на основі цих нотаток:\n"
+ state["research"]
)
return {
"draft": text.content,
"messages": [text],
}Це і вся «спеціалізація». Дослідник нічого не знає про райтера. Райтер не знає, звідки взявся research — він просто читає поле стану. Кожен вузол повертає dict лише з тими ключами, які оновлює — LangGraph сам зллє це з рештою стану за правилами з AgentState.
Етап 3: супервайзер як маршрутизатор
Серце системи. Супервайзер дивиться на стан і повертає рішення — рядок з іменем наступного вузла. Це рішення може бути детермінованим (як тут) або через виклик LLM з structured output, якщо логіка складніша.
def supervisor(state: AgentState) -> dict:
# просте правило: спершу дослідити, потім написати, потім стоп
if not state.get("research"):
return {"next": "researcher"}
if not state.get("draft"):
return {"next": "writer"}
return {"next": "FINISH"}
def route(state: AgentState) -> Literal["researcher", "writer", "__end__"]:
nxt = state["next"]
if nxt == "FINISH":
return "__end__"
return nxtЯ навмисно розділив supervisor (приймає рішення, пише його в стан) і route (читає рішення і повертає мітку для ребра). Так рішення супервайзера лишається у стані — ви бачите його в трейсі й можете відмотати. Якби логіка була складнішою, тіло supervisor просто кликало б LLM: «ось стан, що робити далі — researcher, writer чи FINISH?». Сам каркас графа не змінюється.
Етап 4: збираємо граф
Тепер з'єднуємо все. Ключова деталь: після кожного спеціаліста ребро веде назад до супервайзера. Це й робить його супервайзером — він отримує керування знову і знову, поки не скаже FINISH.
from langgraph.graph import StateGraph, START, END
graph = StateGraph(AgentState)
graph.add_node("supervisor", supervisor)
graph.add_node("researcher", researcher)
graph.add_node("writer", writer)
graph.add_edge(START, "supervisor")
# супервайзер -> один зі спеціалістів або кінець
graph.add_conditional_edges("supervisor", route, {
"researcher": "researcher",
"writer": "writer",
"__end__": END,
})
# кожен спеціаліст завжди повертається до супервайзера
graph.add_edge("researcher", "supervisor")
graph.add_edge("writer", "supervisor")
app = graph.compile()Подивіться на форму графа: це зірка. Супервайзер у центрі, спеціалісти по краях, керування завжди повертається в центр. Додати третього агента (наприклад, редактора) — це один add_node, один add_edge назад до супервайзера і один рядок у словнику маршрутів. Жодного переписування того, що вже працює.
Етап 5: запуск
Запускаємо так само, як будь-який StateGraph — передаємо початковий стан.
from langchain_core.messages import HumanMessage
result = app.invoke({
"messages": [HumanMessage(content="LangGraph для мультиагентних систем")],
"next": "",
"research": "",
"draft": "",
})
print(result["draft"])Один invoke — і всередині відбувся цілий цикл: супервайзер → дослідник → супервайзер → райтер → супервайзер → кінець. Ви не писали жодного while і жодного лічильника кроків. Логіка «хто наступний» вся в графі, а не розмазана по тілу циклу. Це головна різниця з тим, як це виглядало б на голому SDK.
Чому стан, а не передача рядків
Найпоширеніша помилка новачків — будувати мультиагентну систему як ланцюжок: агент A повертає рядок, ви вручну пхаєте його в агента B, той повертає інший рядок. Працює на демо, ламається на третьому агенті. Ось що дає спільний стан натомість:
- Будь-який агент бачить усю картину. Райтеру треба і нотатки, і початковий запит — у стані є все. На рядках ви б тягали контекст руками і щось би загубили.
- Правила злиття замість «останній переміг». Редюсер на кшталт
add_messagesакумулює історію. Поля артефактів оновлюються незалежно й не затирають одне одного. - Стан серіалізується. Раз він — звичайна структура, його можна зберегти, відновити, подивитись у трейсі. Рядки, що літають між функціями, ви так не відмотаєте.
Де сюди вписуються чекпоінти
Поки що граф живе в пам'яті: відпрацював invoke — стан зник. Для справжньої мультиагентної системи цього мало, особливо якщо між агентами є пауза на людину або робота триває довго. Тут вступає checkpointer — і це майже безкоштовно за кодом.
from langgraph.checkpoint.postgres import PostgresSaver
checkpointer = PostgresSaver.from_conn_string(POSTGRES_URL)
app = graph.compile(checkpointer=checkpointer)
# кожен прогін прив'язаний до thread_id
config = {"configurable": {"thread_id": "user-42"}}
app.invoke({"messages": [HumanMessage(content="...")],
"next": "", "research": "", "draft": ""}, config)Один аргумент при compile — і граф пише стан після кожного вузла. Тепер система переживає рестарт сервера, ви можете поставити паузу між дослідником і райтером на підтвердження людини, а потім продовжити з того ж місця. thread_id ізолює стан кожного користувача чи сесії. Саме чекпоінти перетворюють «гарне демо» на щось, що можна тримати в продакшні.
Коли мультиагент — це перебір
Чесно, як завжди: більшості задач кілька агентів не потрібні. Розбиття на дослідника й райтера має сенс, коли ролі справді різні — різні промпти, різні інструменти, різні моделі під кожну роль. Якщо ж усе робить один промпт, то «мультиагентність» — це три виклики LLM там, де вистачило б одного, плюс зайва латентність і втричі більше токенів.
Лишайтеся на одному агенті, коли:
- Ролі насправді не відрізняються — це один і той самий промпт, розрізаний штучно.
- Немає інструментів чи моделей, специфічних для ролі. Спеціалізація без різниці в інструментах — це ілюзія спеціалізації.
- Латентність критична. Кожен стрибок до супервайзера — це ще один раунд до моделі.
Я кажу це клієнтам прямо: спочатку один агент з кількома інструментами. Розбивайте на кілька, лише коли один промпт почав тягнути на себе надто багато несумісних ролей. Деталі про те, коли система дійсно стає мультиагентною — у статті про multi-agent системи →
Поширені помилки в мультиагентних графах
- Спеціалісти знають один про одного. Щойно агент A вирішує, що далі йде агент B, ви втратили єдину точку рішення. Хай вирішує лише супервайзер.
- Стан без редюсерів. Якщо два вузли пишуть у те саме поле без правила злиття — один затре інший. Для історій повідомлень завжди
add_messages. - Роздутий стан. Не кидайте у стан усе підряд «про всяк випадок». Стан має бути мінімальним і явним, інакше граф читається так само важко, як роздутий промпт.
- Немає умови виходу. Якщо супервайзер ніколи не повертає
FINISH, зірка крутиться вічно. Завжди має бути шлях уEND.
Як це виглядає в реальному проєкті
У продакшні цей самий каркас обростає деталями: супервайзер кличе LLM зі structured output, щоб обирати маршрут за змістом, а не за порожніми полями; під дослідника я ставлю модель з пошуком і tool-calling, під райтера — сильнішу на тексті; чекпоінтер у Postgres тримає стан і дає паузи на людину. Зірка лишається тією самою — росте лише начинка вузлів.
Такі мультиагентні системи під ключ — від проєктування графа до деплою, чекпоінтів і моніторингу — я будую в рамках розробки multi-agent систем →
Підсумок
Мультиагентна система в LangGraph — це не магія, а зірка з супервайзером у центрі та спеціалістами по краях, що ділять один стан. Стан із правилами злиття замінює ручну передачу рядків, чекпоінтер додає persistence одним аргументом, а вся логіка «хто наступний» живе в графі, а не в циклі. Але пам'ятайте головне: беріть кілька агентів тоді, коли ролі справді різні — інакше один агент із набором інструментів зробить те саме дешевше.
Не впевнені, чи ваша задача дозріла до кількох агентів? Напишіть — за 30 хвилин дзвінка чесно скажу, де у вас один агент, а де справді граф.