Последовательно там, где можно параллельно

Допустим, агенту нужно собрать контекст из трёх источников: веб-поиск (1.2с), векторная база (0.8с) и внешний API (1.5с). Если узлы стоят цепочкой, общее время — сумма: 1.2 + 0.8 + 1.5 = 3.5с. Но источники друг от друга не зависят — ответ одного не нужен для запроса другого. Значит, их можно запустить разом, и время станет равно самому медленному: max(1.2, 0.8, 1.5) = 1.5с.

Это та же логика, что у asyncio.gather из модуля про Python, но на уровне графа: LangGraph сам выполняет независимые узлы параллельно, без ручного управления корутинами. Нужно лишь правильно описать структуру — и движок распараллелит.

ℹ️ Параллелизм помогает на I/O, а не на CPU

Как и asyncio, параллельные узлы выигрывают на ожидании (запросы к LLM, API, БД) — пока один ждёт ответа, другие работают. Для тяжёлых вычислений выигрыша не будет. Но агенты почти всегда I/O-bound, так что паттерн применим очень часто.

Модель супершагов

Чтобы понять параллелизм в LangGraph, нужно одно ключевое понятие — супершаг (superstep). Граф исполняется не «узел за узлом», а волнами: на каждом супершаге выполняются все узлы, готовые к запуску. Узел готов, когда отработали все, от кого он зависит (в кого ведут входящие рёбра).

Отсюда правило параллелизма: узлы, у которых нет зависимости друг от друга, попадают в один супершаг и выполняются одновременно. Если из узла split выходят три ребра к A, B, C — все трое запустятся параллельно на следующем супершаге.

100%
колёсико — масштаб  ·  зажать и тянуть — перемещение
START split один супершаг · параллельно search 1.2с vector 0.8с api 1.5с aggregate reducer собирает END fan-out fan-in 3 узла в одном супершаге → время = max(1.2, 0.8, 1.5) = 1.5с вместо 3.5с

Статический fan-out / fan-in

Параллельные ветки задаются просто — несколькими рёбрами из одного узла (fan-out) и несколькими рёбрами в один узел-сборщик (fan-in). Узел-сборщик попадёт в следующий супершаг и запустится, только когда завершатся все входящие ветки — это естественная синхронизация.

Поскольку ветки пишут в общее состояние «одновременно», поле, куда они складывают результаты, обязано иметь reducer (урок про reducers) — иначе конфликт записи и InvalidUpdateError.

Три параллельные ветки → сборщик
python
from typing import Annotated, TypedDict
import operator
from langgraph.graph import StateGraph, START, END

class State(TypedDict):
    query: str
    sources: Annotated[list[str], operator.add]   # ← ОБЯЗАТЕЛЬНО reducer
    answer: str

def split(state):   return {}                      # точка ветвления
def search(state):  return {"sources": ["веб: ..."]}
def vector(state):  return {"sources": ["база: ..."]}
def api(state):     return {"sources": ["api: ..."]}
def aggregate(state):
    return {"answer": f"Собрано из {len(state['sources'])} источников"}

builder = StateGraph(State)
for name, fn in [("split", split), ("search", search),
                 ("vector", vector), ("api", api), ("aggregate", aggregate)]:
    builder.add_node(name, fn)

builder.add_edge(START, "split")
# fan-out: из split в три ветки — они пойдут в ОДНОМ супершаге
builder.add_edge("split", "search")
builder.add_edge("split", "vector")
builder.add_edge("split", "api")
# fan-in: все три ведут в aggregate — он ждёт всех
builder.add_edge("search", "aggregate")
builder.add_edge("vector", "aggregate")
builder.add_edge("api", "aggregate")
builder.add_edge("aggregate", END)

graph = builder.compile()
print(graph.invoke({"query": "LangGraph", "sources": [], "answer": ""}))
# search/vector/api отработали параллельно; aggregate собрал их результаты
⚠️ Сборщик ждёт все ветки

Узел aggregate выполнится один раз и только после завершения всех входящих веток — это поведение fan-in по умолчанию. Не рассчитывай, что он сработает по мере готовности каждой ветки; он дожидается самой медленной (как gather).

Паттерны: sectioning и voting

Параллелизм применяют двумя характерными способами.

sectioning — разные подзадачи
Задачу делят на независимые куски, каждый — своя ветка. Пример: проверить текст параллельно на грамматику, факты и тон, потом свести.
voting — одна задача N раз
Один вопрос гоняют несколько раз параллельно (разные промпты/температуры), а сборщик агрегирует — голосованием или выбором лучшего. Поднимает надёжность.
Voting: три независимые оценки → агрегатор
python
from collections import Counter

class State(TypedDict):
    text: str
    votes: Annotated[list[str], operator.add]   # safe / unsafe от каждого судьи
    verdict: str

def judge_a(state): return {"votes": [classify(state["text"], temp=0.0)]}
def judge_b(state): return {"votes": [classify(state["text"], temp=0.5)]}
def judge_c(state): return {"votes": [classify(state["text"], temp=1.0)]}

def tally(state):                                  # сборщик: большинство голосов
    winner = Counter(state["votes"]).most_common(1)[0][0]
    return {"verdict": winner}

# START → (judge_a | judge_b | judge_c параллельно) → tally → END

Статический параллелизм против Send

Этот урок — про статический параллелизм: число веток известно при сборке графа, ты прописываешь рёбра руками. Если же количество веток определяется в рантайме (обработать каждый элемент списка неизвестной длины) — нужен динамический fan-out через Send из урока про map-reduce.

Критерий Статический (рёбра) Динамический (Send)
Число веток известно при сборке вычисляется в рантайме
Как задаётся add_edge ×N [Send(...) for ...]
Типичный случай фиксированные подзадачи (sectioning, voting) обработать каждый элемент списка
Сбор результатов reducer на поле fan-in reducer на поле fan-in
Общее у обоих — reducer на сборке

И статический fan-out, и Send требуют reducer на поле, куда стекаются ветки. Это сквозное правило параллелизма в LangGraph: где несколько узлов пишут в одно поле одновременно — там Annotated[..., operator.add] (или свой merge). Отличается только как ветки порождаются.

Типичные ошибки

Ошибка 1: поле сборки без reducer

Параллельные ветки, пишущие в поле без operator.add, конфликтуют → InvalidUpdateError или потеря результатов. Поле fan-in всегда с reducer'ом.

Ошибка 2: мнимая параллельность при зависимости

Если ветка B на самом деле использует результат A, поставить их «параллельно» нельзя — это логическая зависимость. Параллелятся только действительно независимые узлы.

Ошибка 3: расчёт на порядок завершения

Ветки завершаются в непредсказуемом порядке. Не полагайся на то, что search запишется раньше api; если порядок важен — сортируй в сборщике по явному признаку.

Ошибка 4: сотни тяжёлых веток без лимита

Массовый параллелизм с LLM-вызовами упрётся в rate-limit или память. Ограничивай одновременность через {"max_concurrency": N} в config (как в map-reduce).

Шпаргалка

Parallelization — всё в одном месте
python
# Модель: СУПЕРШАГ — независимые узлы исполняются одновременно

# Поле сбора — ОБЯЗАТЕЛЬНО reducer
class State(TypedDict):
    results: Annotated[list, operator.add]

# Статический fan-out / fan-in рёбрами:
builder.add_edge("split", "a")     # ┐
builder.add_edge("split", "b")     # ├ a,b,c — один супершаг (параллельно)
builder.add_edge("split", "c")     # ┘
builder.add_edge("a", "agg")       # ┐
builder.add_edge("b", "agg")       # ├ agg ждёт ВСЕХ (fan-in)
builder.add_edge("c", "agg")       # ┘

# Паттерны:
#  sectioning — разные подзадачи параллельно → свести
#  voting     — одна задача N раз → агрегировать (голосование/лучший)

# Контроль параллелизма (тяжёлые ветки):
graph.invoke(inp, {"max_concurrency": 5})

# Правила:
#  • параллелятся ТОЛЬКО независимые узлы (нет рёбер между ними)
#  • поле fan-in — с reducer
#  • сборщик ждёт все ветки; порядок завершения не гарантирован
#  • динамическое число веток → Send (урок map-reduce)

Практическое задание

Распараллель работу графа:

Задание: параллельная проверка текста

  1. Состояние: text: str, issues: Annotated[list, operator.add], report: str.
  2. Сделай три независимых узла-проверки: grammar, facts, tone — каждый дописывает найденные замечания в issues.
  3. Собери fan-out от START (или узла-входа) к трём проверкам и fan-in в узел report, который сводит замечания.
  4. Запусти и через stream убедись, что три проверки идут в одном супершаге, а report срабатывает один раз после всех.
  5. Убери operator.add у issues и посмотри на InvalidUpdateError — наглядно, зачем reducer на fan-in.
  6. Со звёздочкой: переделай в voting — гоняй одну проверку тремя «судьями» с разной температурой и в report выноси вердикт большинством голосов.

Что дальше

Это последний урок раздела «Паттерны агентов» — и последний по теории LangGraph. У тебя в руках полный набор: основы графа, продвинутые возможности, human-in-the-loop и все ключевые паттерны. Дальше — собрать из этого проект модуля.