Инструкции

Управление состоянием в LangGraph: типичные ошибки и как их избежать

Динамическая передача dict между узлами LangGraph прячет баги, которые всплывают через пять шагов. TypedDict-схемы, редьюсеры и разделение памяти — три паттерна, которые превращают хрупкий граф в отлаживаемую систему.

28 марта 2026 г.
6 мин чтения
LangGraphTypedDictstate managementAI-агентыLangChainPythonтипизацияпродакшн

Проблема, которую никто не замечает вовремя

Вот сценарий, который я видел неоднократно: агент на LangGraph работает, все узлы зелёные, логи чистые. Через три часа клиент звонит — данные повреждены. Начинаешь копать и обнаруживаешь: между узлом retrieval и узлом synthesis список документов тихо перезаписывался вместо дополнения. Ни одного исключения. Граф уверенно шёл дальше, потому что формально ничего не сломалось — сломалось семантически.

Корень проблемы — подход к состоянию как к обычному Python-словарю: кидаем данные между узлами, модель сама разберётся. Не разберётся. Точнее — разберётся по-своему, а вы узнаете об этом, когда что-то несвязанное сломается пятью шагами позже.

Что такое state в LangGraph на самом деле

State в LangGraph — это не dict. Это контракт. Каждый узел — функция, которая читает из контракта и пишет обратно. Записи управляются редьюсерами — функциями, которые решают, как новое значение объединяется с существующим.

Аннотация Annotated[list[BaseMessage], add_messages] говорит: «Когда любой узел пишет в messages, используй add_messages для слияния нового значения с тем, что уже есть». Без редьюсера действует правило last-write-wins — для одиночных значений это нормально, для списков — катастрофа.

Три ошибки, которые я допускал

Ошибка 1: dict как «универсальное» хранилище

Поле data: dict — это чёрная дыра. В любой момент невозможно сказать, какие ключи в ней есть. Каждый узел пишет туда что хочет, каждый читает что ожидает — и несовпадение обнаруживается только при отладке продакшн-инцидента.

Фикс: явные поля с явными типами. Используйте Enum для статусов (а не строки "done"/"Done"/"DONE"), Optional-поля вместо dict-вложенностей, отдельный TypedDict для структур вроде ErrorContext. Разница не косметическая — это разница между «работает, пока не сломается» и «ломается громко и сразу».

Ошибка 2: списки без редьюсеров

Если поле — список, предполагайте, что несколько узлов будут в него писать. Даже если сегодня пишет один. Завтра коллега добавит второй узел и не проверит ваш код.

Редьюсеры — чистые функции: принимают старое и новое, возвращают объединённое. Их проще всего тестировать — не нужно поднимать весь граф. Полезные варианты:

  • Дедупликация — по хешу содержимого, чтобы один документ не появлялся дважды
  • Ограничение длины — критично для чекпоинтов: если список растёт бесконечно, чекпоинты раздуваются
  • Слияние частичных JSON — для параллельных узлов, собирающих разные части ответа

Ошибка 3: вся память в одном контексте

Накапливать всё в цепочке и называть это «контекстом» — работает ровно до момента, когда запускаешь долгую задачу. Модель получает контекст, где 80% — нерелевантная история, и пытается найти в ней нужный фрагмент. Находит не тот.

Паттерн: разделение эпизодической и семантической памяти. Эпизодические трейсы — что конкретно произошло, в каком порядке, с какими параметрами. Семантическая память — извлечённые факты и знания. Когда они лежат отдельно, отладка становится предсказуемой.

Правило контракта между узлами

LangGraph заставляет быть явным в том, что перетекает между узлами. Принцип: каждый узел получает ровно те поля, которые ему нужны, и возвращает ровно те поля, которые он обновляет. Не больше.

Узел retrieval читает current_query, возвращает {"retrieved_docs": docs}. Узел synthesis читает retrieved_docs и messages, возвращает {"final_answer": ..., "status": AgentStatus.COMPLETE}. Когда контракт нарушается — например, узел пишет в несуществующее поле — вы узнаёте об этом немедленно, а не через пять шагов.

Checkpoint recovery для долгих задач

TypedDict-схема даёт предсказуемые чекпоинты. LangGraph сохраняет состояние после каждого узла, и если задача прерывается (таймаут, падение сервиса), её можно возобновить с последнего сохранённого состояния.

Но это работает только когда состояние сериализуемо и детерминировано. data: dict с произвольной вложенностью — нет гарантий. AgentState с типизированными полями и enum-статусами — полная предсказуемость.

Мультиагентные сценарии

В мультиагентных системах state drift — отдельная проблема. Три агента пишут в одно состояние, каждый уверен, что его обновление последнее. Без редьюсеров — гонка записей.

Паттерн: каждый агент владеет своим подмножеством состояния. Общие поля — только через явные редьюсеры. Если агент-критик перезаписывает critic_feedback — это нормально, он единственный владелец. Если два исследователя дополняют researcher_findings — редьюсер operator.add конкатенирует списки.

Версионирование схем

Отдельная боль — эволюция схемы в продакшне. Чекпоинты сохранены со старой схемой, код обновлён на новую. LangGraph пока не имеет встроенной поддержки миграций, поэтому:

  • Добавление поля — всегда Optional с дефолтом None
  • Удаление поля — оставлять в схеме как deprecated, не читать
  • Изменение типа — новое поле + миграция в первом узле

Чеклист перед деплоем

  • Каждое поле-список имеет редьюсер
  • Нет полей типа dict без строгой внутренней структуры
  • Статусы — enum, не строки
  • Каждый узел возвращает только те поля, которые обновляет
  • Эпизодическая память отделена от семантической
  • Чекпоинты работают: тест «убить процесс → перезапустить → продолжить»
  • В мультиагентном сценарии каждый агент владеет своим подмножеством полей

Итого

Строгая типизация состояния через TypedDict — не про красоту кода. Это про то, чтобы баги проявлялись сразу, а не маскировались за «успешным» прохождением графа. Редьюсеры для списков, enum для статусов, разделение памяти — три паттерна, которые превращают LangGraph-агент из «работает на демо» в «работает в продакшне».

Состояние — это архитектурное решение. Относитесь к нему соответственно.

Автор: Алик Завалишев

Эксперт по ИИ и автоматизации процессов

Больше статей