LangGraph 1.2: ситуации, когда следует внимательно читать ошибку и порядок выполнения
Файлы
ВНИМАНИЕ: Файлы из Базы знаний - это исходный код разработки. Это примеры решения задач, шаблоны, заготовки, "строительные материалы" для учетной системы. Файлы ориентированы на специалистов 1С, которые могут разобраться в коде и оптимизировать программу для запуска в базе данных. Гарантии работоспособности нет. Возврата нет. Технической поддержки нет.
Подписка PRO — скачивайте любые файлы со скидкой до 85% из Базы знаний
Оформите подписку на компанию для решения рабочих задач
Оформить подписку и скачать решение со скидкойВы можете заказать платную доработку или адаптацию этой разработки под вашу конфигурацию на «Бирже заказов».
- 0% комиссии — оплата напрямую исполнителю;
- Исполнители любого масштаба — от отдельных специалистов до команд под проект;
- Прямой обмен контактами между заказчиком и исполнителем;
- Безопасная сделка — при необходимости;
- Рейтинги, кейсы и прозрачная система откликов.
Разработчику агента на LangGraph, который обращается к 1С: что означают InvalidUpdateError и GraphRecursionError, чем редьюсер отличается от join и какой код выполняется повторно при RetryPolicy и после interrupt. Прогон 05.10.2026 на 1.2.12.
InvalidUpdateError- две ветви пишут в один ключ без редьюсера; лечится схемой состояния.- Редьюсер и join решают разные задачи: первый объединяет значения, второй ждёт обе ветви.
GraphRecursionErrorв цикле без условия выхода не лечится увеличением лимита.RetryPolicyиresumeпослеinterruptвыполняют начало узла заново: внешние вызовы там повторятся.
Ошибки LangGraph указывают место исправления: схему состояния, рёбра или условие выхода. А повтор узла и resume повторяют всё, что было до исключения или паузы.
Для кого: разработчикам агентов на LangGraph, которые подключают их к 1С.
Я сверила четыре минимальных примера на LangGraph 1.2.12: конфликт параллельных обновлений, сбор результатов через join, остановку цикла по лимиту и повтор всего узла после исключения. Их общий файл не подключается к модели и не вызывает 1С. Поэтому текст ошибки и повтор операций можно разобрать отдельно от сети, промпта и содержимого базы.
До вызывающего кода в четырёх случаях доходят два исключения. В первом примере появляется InvalidUpdateError, в третьем GraphRecursionError, второй завершается успешно, а четвёртый обрабатывает ConnectionError повтором. Я оставила четыре случая и отдельно разобрала ошибки чтения состояния и возобновления без чекпоинтера из другого записанного прогона.
Если вы строите агента, который обращается к 1С, эти различия влияют на место исправления. Для конфликта обновлений нужно определить правило объединения состояния. Для цикла нужно проверить переход к завершению. Для повторного вопроса нужно посмотреть, какие действия выполнялись до исключения. Увеличение лимита графа исправляет лишь часть возможных причин.
🧪 Как устроен прогон
Прогон закреплён на langgraph==1.2.12. В файле используются StateGraph, служебные вершины START и END, типизированные словари и RetryPolicy. Узлы возвращают словари обновлений. Всё внешнее поведение сведено к задержке одного узла и добавлению строк в список вызовов.
Для установки и запуска в комментарии скрипта записаны команды:
Примеры можно сохранить отдельными файлами или собрать в один файл в порядке разделов. Это отдельные графы, между которыми не передаётся состояние предыдущего запуска.
Вывод общего прогона:
1 без редьюсера: исключение InvalidUpdateError At key 'evidence': Can receive only one value per step. Use an Annotated key to handle multiple values.
2 join: {'evidence': ['fast', 'slow'], 'seen': 2}
3 recursion_limit: GraphRecursionError
4 retry_policy: вызовов вопросов = 4 ['вопрос 1', 'вопрос 2', 'вопрос 1', 'вопрос 2']
Это четыре наблюдения на одной закреплённой версии. Строки «вопрос 1» и «вопрос 2» обозначают действия внутри Python-функции; модель таких вопросов не получала. Значение seen=2 показывает, что собирающий узел прочитал оба результата. Оно не измеряет скорость параллельной работы или время сетевого обращения.
Для первоначальной диагностики я составила карту:
| Наблюдение | Где искать причину | Что проверяет пример |
|---|---|---|
InvalidUpdateError при обновлении evidence |
схема поля и параллельные записи | два узла возвращают один ключ без редьюсера |
Join возвращает seen=2 |
правило объединения и входящие рёбра | собирающий узел получает оба значения |
GraphRecursionError |
условие выхода и конфигурация запуска | цикл достигает заданного лимита |
| Два вопроса повторились | граница узла с retry_policy |
после ошибки функция запускается заново |
В отдельном опыте с подтверждением есть ещё два сообщения: ValueError No checkpointer set и RuntimeError Cannot use Command(resume=...) without checkpointer. Они относятся к операциям после паузы. Сам первый запуск без чекпоинтера в том опыте исключения не вызвал. Если смешать эти события в одну фразу «interrupt падает», получится неверное описание точки отказа.
🧱 Откуда берётся InvalidUpdateError
Первый граф запускает a и b от START. Каждый узел возвращает список под ключом evidence. В схеме состояния это обычный list, без дополнительного правила объединения. Код из проверочного файла:
Запуск завершается InvalidUpdateError. Сообщение называет ключ evidence и указывает, что за один шаг пришло несколько значений. По этой строке я сначала проверяю места, где узлы возвращают этот ключ, и схему его хранения.
Оба списка сами по себе допустимы как значения Python. Проблема появляется при попытке применить обновления к одному полю состояния в параллельном шаге. Если ваш агент одновременно получает журнал регистрации и описание объекта, а обе ветви складывают результат в evidence, потребуется решить, как объединять эти результаты.
Вариант решения из следующего опыта использует Annotated[list, operator.add]. В аннотации задаётся функция, которая объединяет прежнее и поступившее значения. Для списков operator.add даёт конкатенацию. Это правило подходит, когда вы хотите накопить элементы от ветвей в одной коллекции.
Выбор редьюсера определяется назначением поля. Для списка свидетельств конкатенация может быть приемлема. Для единственного итогового статуса такое правило само по себе смысла не задаёт. До правки схемы я бы записала, допускает ли поле несколько результатов и кто должен принять окончательное решение по ним. Тогда исправление ошибки не превращается в накопление несовместимых значений.
Есть и другой проектный вариант: ветви пишут в разные поля, а отдельный узел собирает их в итог. Минимальный прогон этот вариант не проверяет. Я называю его как направление изменения схемы, без обещания готового результата на вашем графе. Проверенное исправление здесь относится к списку с operator.add и join из следующего раздела.
Обработка except Exception в примере нужна для печати наблюдения и продолжения общего проверочного файла. Для рабочего агента такой обработчик не задаёт политику восстановления. Он не исправляет схему и не выбирает результат одной ветви. После регистрации ошибки потребуется изменить построение графа или модель состояния.
🔗 Зачем нужен join, если есть редьюсер
Во втором графе поле evidence получает редьюсер. Узел slow задерживается на 0.3 секунды, fast возвращает результат сразу. Узел sup записывает количество элементов, которые увидел. Входящее ребро к нему задаётся списком имён обеих ветвей:
Сохранённый результат содержит ['fast', 'slow'] и seen=2. Здесь изменены две части графа. Аннотация поля задаёт объединение обновлений, а ребро со списком источников задаёт ожидание обеих ветвей перед sup. Проверка показывает их совместную работу на этом примере.
Разделять эти задачи полезно при чтении собственного кода. Редьюсер не описывает, от каких узлов должен зависеть итоговый анализ. Ребро ожидания не определяет, что делать с несколькими значениями одного ключа. Если в графе агента исправить только одну из этих частей, вопрос о другой останется открытым.
Например, перед подготовкой объяснения ошибки 1С вам могут понадобиться два независимых результата: записи журнала и сведения об объекте. Для такого проектного случая я проверяла бы отдельно наличие каждого результата на входе узла объяснения. В минимальном опыте эту роль играет счётчик seen; он даёт наблюдаемое условие успешного сбора без обращения к модели.
Порядок элементов в сохранённой строке не стоит использовать как обозначение приоритета. Опыт показывает конкретный вывод, но не устанавливает универсальное правило сортировки свидетельств для любого графа. Если итоговая обработка зависит от источника, его нужно обозначать в данных. Само имя fast в списке этого не решает для будущего формата результата.
Задержка time.sleep(0.3) нужна для различия ветвей в тесте. Вывод проверяет состав состояния в sup; замера производительности здесь нет.
Перед переносом такого решения я бы проверила, что ветви возвращают обновления согласованной формы. Если один узел пишет список записей, а второй возвращает объект другой структуры, конкатенация не определяет смысл итоговой коллекции. В нашем опыте оба узла возвращают одноэлементные списки строк, так что наблюдение не охватывает сложный контракт свидетельств.
🔁 Когда граф останавливается сам
Третий пример содержит цикл без условия завершения. Узел увеличивает n, а условный переход каждый раз выбирает этот же узел. В запуске указан recursion_limit=12:
Результат: GraphRecursionError. Число в конфигурации ограничивает продвижение графа; его нельзя читать как таймаут HTTP-запроса или размер контекста модели. В этом коде нет рекурсивного вызова Python-функции loop из самой себя. Повтор создаёт переход графа.
Я сначала проверила бы условие выбора следующей вершины. Здесь оно возвращает "loop" при любом состоянии, поэтому увеличение лимита только отодвинет остановку. Для рабочего агента нужно определить состояние, при котором анализ закончен или дальнейший поиск следует прекратить. Затем уже выбирать лимит, соответствующий допустимому пути.
В отдельном прогоне графа с подтверждением recursion_limit=3 также завершился GraphRecursionError. Полный путь с паузой и одобрением укладывался в recursion_limit=6. Эти числа относятся к тому конкретному графу. Из них нельзя получить универсальную настройку для агента с другими узлами, ветвлениями или условиями.
Четвёртая проверка касается другого повторения: функция узла начинает выполняться заново после подходящего исключения. Политика привязана к узлу w. Она допускает две попытки и явно выбирает ConnectionError:
На первой попытке список получает два элемента, затем функция поднимает исключение. На второй оба append выполняются снова. Список содержит четыре строки, и узел возвращает n=1. Таким способом тест наблюдает повтор начала функции без необходимости подключать платный внешний вызов.
Строка "502" здесь только сообщение искусственно созданного ConnectionError. HTTP-сервер не отвечает кодом 502, сетевой запрос не выполняется. Поэтому результат не подтверждает автоматическое повторение любого HTTP-ответа с таким кодом. Проверено правило для класса исключения, который указан в retry_on.
Для агента, вызывающего 1С, последствие нужно оценивать по границе узла. Если перед ошибкой узел успел отправить два вопроса внешнему сервису, повтор функции может отправить их ещё раз. В опыте доказан повтор действий до исключения; расходы модели и повтор записи документов не измерялись. Их нельзя добавлять к выводу как уже произошедшие события.
Я бы разделила узел с несколькими внешними действиями, если для них нужны разные правила повтора. Ещё до реализации следует определить, какие действия допускают повтор и как распознать уже выполненное. Этот опыт показывает причину проверки, но не содержит готовой защиты от дублей в 1С. Само успешное завершение второй попытки не отменяет действий первой.
💤 Что выполняется заново после interrupt
Дополнительный прогон проверяет граф с подтверждением человека. Его результат дополняет четыре минимальных примера, поскольку здесь появляется сохранение состояния и возобновление. Записанные наблюдения я привожу без расширения до поведения всех возможных графов:
1. recursion_limit=3: GraphRecursionError 2. создание RemediationAction в узле: до паузы 1, после resume 2 3. без чекпоинтера: исключения нет; ключи результата: ['__interrupt__', 'audit_trail', 'enterprise_context', 'identified_error_signature', 'is_approved_by_human', 'messages', 'next', 'remediation', 'root_cause_analysis'] | есть __interrupt__: True get_state без чекпоинтера: ValueError No checkpointer set resume без чекпоинтера: RuntimeError Cannot use Command(resume=...) without checkpointer 4. полный проход (пауза и одобрение) укладывается в recursion_limit=6
По третьему пункту первый вызов вернул результат с __interrupt__. Исключения в этот момент не было. Затем попытка get_state завершилась ValueError, а попытка возобновления через Command(resume=...) завершилась RuntimeError. Это три разных операции, и у каждой свой результат.
Документация Interrupts описывает чекпоинтер и идентификатор потока как части сценария сохранения и возобновления. Для вашего графа их нужно проверять при компиляции и запуске. Наличие маркера паузы в возвращённом словаре само по себе ещё не подтверждает, что следующий запрос сможет продолжить выполнение.
Во втором пункте счётчик создания RemediationAction увеличился с одного до двух после resume. Это согласуется с описанным в документации возвратом к началу узла при продолжении после interrupt: код, находящийся до паузы, выполняется повторно. Такой повтор следует учитывать независимо от настройки RetryPolicy.
В опыте наблюдается создание объекта, без доказательства повторной записи в базу. Однако место для проверки побочных действий уже определено: участок узла перед interrupt. Если ваш узел сначала готовит предложение, затем запрашивает подтверждение, создание предложения может повториться при продолжении. Если там находится внешний вызов, его допустимость нужно проверить отдельно.
Я бы вынесла выполнение подтверждённого действия в участок, который запускается после получения решения, и проверила повторяемость подготовки до паузы. Конкретную переработку графа эти строки не тестируют. Они помогают составить перечень мест, где нужен счётчик вызовов или другая наблюдаемая проверка.
Для чтения ошибок удобно сохранить различия в таблице:
| Симптом | Что установлено прогоном | Следующий шаг диагностики |
|---|---|---|
Пауза есть, get_state падает |
чекпоинтер не задан | проверить конфигурацию хранения состояния |
Command(resume=...) вызывает RuntimeError |
возобновление вызвано без чекпоинтера | проверить сохранение и привязку запуска к потоку |
Счётчик действия вырос после resume |
участок до паузы выполнился снова | проверить побочные действия до interrupt |
GraphRecursionError на коротком лимите |
путь не уложился в конфигурацию | проверить переходы и обоснованность лимита |
Увеличение recursion_limit не добавляет чекпоинтер. Настройка повтора не сохраняет состояние для подтверждения человека. Редьюсер списка не препятствует повторному внешнему действию. Эти механизмы встречаются в одном агенте, но проверять их удобнее отдельными минимальными опытами, каждый со своим наблюдаемым результатом.
Перед подключением к 1С я бы повторила аналогичный набор на функциях без внешних эффектов. Для параллельного участка проверила бы содержимое итогового состояния; для паузы сохранила бы счётчик до и после продолжения. В узле с повтором разместила бы наблюдение перед искусственным исключением. После этого можно отдельно проверять фактические вызовы сервиса и их допустимые повторы.
🧭 Ограничения
Четыре минимальных примера я повторила 05.10.2026 на LangGraph 1.2.12, Python 3.12.10, Windows 11: вывод совпал строка в строку. Другие выпуски библиотеки не проверяла; при повторении сохраните свою версию рядом с результатом.
Ни один из четырёх минимальных примеров не вызывает модель или 1С. ConnectionError("502") создан в Python вручную. Не измерялись расходы, задержки внешних сервисов или влияние повтора на документы базы. По списку строк можно подтвердить только повтор действий внутри функции узла.
Дополнительный вывод относится к конкретному графу с подтверждением. Значение 6 описывает успешный проход этого графа; универсальный лимит из него не получается. Счётчик RemediationAction показывает повтор создания объекта, без подтверждения повторного проведения документа или иной записи в 1С.
Успешный join проверен на двух ветвях с одноэлементными списками. Не проверяла универсальный порядок результатов, объединение сложных свидетельств и дедупликацию. operator.add задаёт конкатенацию списков; пригодность этого правила для вашего состояния требует отдельного решения.
Страницы документации открыты как пояснение механизмов. Они могут описывать более новый выпуск библиотеки, чем закреплённый опыт. Для конкретных сообщений исключений и чисел я использовала сохранённые прогоны.
📚 Источники
- INVALID_CONCURRENT_GRAPH_UPDATE: пояснение конфликтующих обновлений и редьюсеров. Точный вывод
InvalidUpdateErrorвзят из минимального прогона. - GRAPH_RECURSION_LIMIT: пояснение остановки графа по лимиту. Значения 12, 3 и 6 относятся к приведённым опытам.
- Use the graph API: справка по построению графа,
RetryPolicyи настройкам запуска. Листинги с параметрами перенесены из проверочного файла для версии 1.2.12. - Interrupts и Persistence: пояснение паузы и сохранения состояния. Наблюдения без чекпоинтера и счётчик
RemediationActionвзяты из отдельного прогона графа с подтверждением. - Проверочный файл
check_claims.pyи сохранённый вывод на LangGraph 1.2.12: источник четырёх минимальных случаев. Вывод дополнительного опытаprobe_langgraph.txt: источник наблюдений при паузе и возобновлении. - Предшественник на Infostart: LangGraph для 1С-разработчика: агент ждёт вашего «да» перед записью, стенд за 3 минуты (Ninel_S, 30.09.2026). Там предметом была сборка агента с паузой; здесь - сообщения ошибок и порядок повторного выполнения.
📎 Файл к статье
В архиве к статье - 13.4-companion.zip: check_claims.py с четырьмя минимальными графами, эталонный вывод output-langgraph-1.2.12.txt, чек-лист chek-list-grafa.md и README. Скрипт запускается без модели и без 1С; сравните четыре строки вывода со своим окружением. Чек-лист - для проверки чекпоинтера, лимита графа и действий перед паузой до подключения внешних сервисов.
Вступайте в нашу телеграмм-группу Инфостарт