Какие ключевые абстракции LangGraph нужно понимать, чтобы не получать формально рабочие, но неверные результаты?
Ключевые абстракции LangGraph: State с reducer-функциями (иначе данные перетираются), чистые Node-функции, детерминированный conditional routing и Checkpointer с уникальным thread_id для корректного resume.
Ключевые абстракции LangGraph
LangGraph строится на четырёх уровнях: State, Node, Edge и Checkpointer. Непонимание любого из них ведёт к формально работающему, но неверному поведению.
State — единственный источник истины
State — это TypedDict (или Pydantic-модель), который передаётся между всеми узлами. Ошибка новичков: мутировать внешние переменные вместо возврата нового State. LangGraph применяет reducer-функции для слияния обновлений; по умолчанию последний write выигрывает, но для списков нужен явный Annotated[list, operator.add].
import operator
from typing import Annotated, TypedDict
class State(TypedDict):
messages: Annotated[list, operator.add] # reducer: append, не replace
current_tool: str # replace: последний write побеждает
retry_count: int
Без явного reducer для messages каждый узел перезапишет список, и история диалога потеряется — граф работает, но результат неверный.
Node — чистая функция над State
Узел получает State, возвращает словарь с изменёнными полями. Узел не должен хранить внутреннее состояние между вызовами — только читать из State и писать в State. Типичная ошибка: кешировать результат LLM в closure вместо State, из-за чего при resume после checkpoint данные теряются.
Edge и conditional routing
Рёбра определяют поток управления. add_edge — безусловный переход. add_conditional_edges принимает routing-функцию, которая возвращает строку с именем следующего узла. Routing-функция должна быть чистой и детерминированной — она вызывается при каждом replay из checkpoint.
from typing import Literal
def should_continue(state: State) -> Literal["tools", "__end__"]:
last_msg = state["messages"][-1]
if hasattr(last_msg, "tool_calls") and last_msg.tool_calls:
return "tools"
return "__end__"
builder.add_conditional_edges("agent", should_continue)
Checkpointer — персистентность и resume
Checkpointer сохраняет StateSnapshot после каждого узла. Это позволяет прерваться на interrupt_before=["human_review"] и возобновить позже. Ключевой концепт — thread_id: каждый независимый диалог/сессия должен иметь уникальный thread_id, иначе состояния перемешаются.
from langgraph.checkpoint.sqlite import SqliteSaver
with SqliteSaver.from_conn_string("checkpoints.db") as memory:
graph = builder.compile(checkpointer=memory)
config = {"configurable": {"thread_id": "user-42-session-7"}}
# Первый вызов
result = graph.invoke({"messages": [], "retry_count": 0}, config)
# Возобновление с того же места
snapshot = graph.get_state(config)
graph.update_state(config, {"retry_count": 0}) # коррекция состояния
resumed = graph.invoke(None, config) # None = продолжить с checkpoint
Send API для динамического fan-out
Send позволяет запустить один узел несколько раз параллельно с разными данными — для map-reduce паттернов. Без понимания Send разработчики пишут цикл внутри узла, теряя параллелизм и видимость в трассировке.
from langgraph.types import Send
def spawn_workers(state: State):
return [Send("worker", {"item": item}) for item in state["items"]]
builder.add_conditional_edges("dispatcher", spawn_workers)
Подводные камни
- Мутация State in-place вместо возврата нового словаря: LangGraph не увидит изменение, snapshot окажется устаревшим.
- Отсутствие reducer для списков: каждый узел перетирает предыдущие сообщения вместо добавления.
- Один thread_id на всех пользователей: состояния перемешиваются, получаете чужие данные.
- Не-детерминированная routing-функция (с side effects или random): при replay из checkpoint граф пойдёт другим путём.
- Хранение ссылок на внешние объекты (DB-соединение) в State: checkpoint не сериализует их, resume падает с ошибкой.
- Игнорирование
RunnableConfig: без передачи config в вложенные вызовы теряется thread_id и трассировка LangSmith. - Использование
SqliteSaverв production с параллельными запросами: SQLite не поддерживает concurrent writes — нуженPostgresSaver. - Отсутствие явного терминального условия в цикличных графах: граф зависает в бесконечном цикле без исключения.
What hurts your answer
- Знать термины LangGraph, но не понимать связи между абстракциями
- Объяснять поведение через отдельные примеры вместо причинной модели
- Не связывать mental model с диагностикой ошибок
What they're listening for
- Понимает ключевые абстракции LangGraph
- Может предсказывать поведение системы через mental model
- Связывает модель с debugging и production decisions