LangGraph для 1С-разработчика: агент ждёт вашего «да» перед записью, стенд за 3 минуты
Файлы
ВНИМАНИЕ: Файлы из Базы знаний - это исходный код разработки. Это примеры решения задач, шаблоны, заготовки, "строительные материалы" для учетной системы. Файлы ориентированы на специалистов 1С, которые могут разобраться в коде и оптимизировать программу для запуска в базе данных. Гарантии работоспособности нет. Возврата нет. Технической поддержки нет.
Подписка PRO — скачивайте любые файлы со скидкой до 85% из Базы знаний
Оформите подписку на компанию для решения рабочих задач
Оформить подписку и скачать решение со скидкойВы можете заказать платную доработку или адаптацию этой разработки под вашу конфигурацию на «Бирже заказов».
- 0% комиссии — оплата напрямую исполнителю;
- Исполнители любого масштаба — от отдельных специалистов до команд под проект;
- Прямой обмен контактами между заказчиком и исполнителем;
- Безопасная сделка — при необходимости;
- Рейтинги, кейсы и прозрачная система откликов.
От монолита к автономным агентам (Часть 3): супервайзер LangGraph в контуре 1С с паузой на подтверждение человека
Серия «От монолита к автономным агентам», часть 3 из 4. Часть 1: архитектура и граф состояний ИИ-агента в 1С:Предприятие. Часть 2: почему линейный граф состояний не масштабируется.
В этой части собран рабочий стенд из трёх звеньев: общий модуль 1С, шлюз на FastAPI и граф LangGraph, который перед записью в базу останавливается и ждёт решения человека. Код стенда приложен к статье. Модуль 1С проверен встроенной проверкой Конфигуратора платформы 8.3.27, а весь путь от вызова из 1С до записи в карточку номенклатуры пройден на этой платформе через внешнее соединение.
Что здесь заглушка. Агенты Auditor, Inspector и Remediator возвращают фиксированные ответы, а роль модели играет детерминированный мок. Стенд показывает каркас: маршрутизацию, состояние, паузу interrupt(), возобновление и обмен с 1С. Разбор реальных логов и метаданных в него не входит. Чего ещё нет, перечислено в разделе «Что стенд не умеет».
- Что получится и на чём проверено
- Ретроспектива: что мы решили в частях 1 и 2
- Архитектура стенда
- Состояние графа и контракты Pydantic
- Шлюз FastAPI: асинхронный протокол 1С и графа
- Супервайзер и исполнители на LangGraph
- Пауза на подтверждение:
interrupt()иresume - Адаптер в 1С
- Сквозной прогон: 1С, шлюз, граф, запись в карточку
- Тесты
- Что стенд не умеет
- Как повторить
- Итоги и что дальше
🔍 Что получится и на чём проверено
К концу статьи у вас будет стенд, в котором регламентное задание 1С передаёт инцидент внешнему графу, не блокируя сеанс. Граф собирает исправление и останавливается на паузе. Пока человек не ответит, адаптер 1С отказывается что-либо записывать. После одобрения пакет применяется в транзакции, а реквизит СтавкаНДС у карточки номенклатуры заполняется.
Стенд собран и проверен на одной машине:
| Компонент | Версия |
|---|---|
| ОС | Windows 11 Pro 10.0.22631 |
| Платформа 1С | 8.3.27.2130 (x86), файловая база, внешнее соединение |
| Python | 3.12.10 |
| LangGraph | 1.2.12 (langchain-core 1.6.6) |
| FastAPI / Uvicorn / Pydantic | 0.141.1 / 0.54.0 / 2.13.5 |
| pytest | 9.1.1 |
Версии закреплены в requirements.txt. От пустой папки до паузы графа и одобрения в консольном клиенте прошло 100 и 117 секунд в двух прогонах (evidence/fresh_run.py): окружение, pip install без кэша (77 и 92 с), тесты, запуск шлюза. Три минуты в названии статьи - это эти замеры, округлённые вверх. Платформа 1С в них не участвует.
Проверено и не проверено:
| Что | Статус |
|---|---|
| Три теста pytest: полный цикл с паузой, отказ оператора, шлюз с дедупликацией | прошли |
Консольный эмулятор 1С (app.mock_1c_service) против запущенного шлюза |
отработал |
Модуль НОП_АгентИнтеграция: проверка модулей платформой 8.3.27 |
ошибок нет |
| Вызов из 1С через внешнее соединение, пауза, решение, запись в справочник | пройден |
docker compose, образ из Dockerfile |
не запускал |
PostgresSaver вместо MemorySaver |
не запускал |
| Настоящая модель вместо мока | не запускал |
| Тесты YAxUnit для модуля 1С | не писал, фреймворк на стенде не стоял |
| Уведомление оператора в Telegram | не реализовано |
🔍 Ретроспектива: что мы решили в частях 1 и 2
В первой части агент был линейной цепочкой: принять запрос из формы 1С, классифицировать, выполнить одним узлом и вернуть команду в 1С.
|
Запрос из 1С
|
|
analyze_intent
|
|
plan_executor
|
|
deliver_to_1c
|
|
Применение в 1С
|
Во второй части мы разобрали, почему с ростом числа сценариев узел plan_executor превращается в узкое место. Ему нужны описания всех инструментов сразу, ошибка внутри узла обрывает всю цепочку, а цикл уточнения («сходи в 1С, проверь метаданные, если реквизита нет, возьми лог ошибки, потом предложи исправление») в линейной схеме не выразить.
Выход: разделить монолитный узел на независимых исполнителей и поставить над ними распорядителя, супервайзера. В этой части мы делаем это на LangGraph.
🔍 Архитектура стенда
Мы взяли LangGraph по трём причинам, которые проверили на стенде. Граф допускает циклы: исполнитель возвращает управление распорядителю. Переход задаётся кодом (Command(goto=...)), свободного диалога между моделями нет. И в одной библиотеке есть остановка графа interrupt() с сохранением состояния. С другими библиотеками для агентов мы этот стенд не сравнивали.
Вычисления модели вынесены за пределы серверов 1С: граф живёт отдельным процессом и общается с базой только по HTTP. Часть 1 называла это Zero-Footprint Core. Стенд состоит из трёх звеньев.
Рис. 1. Три звена стенда. Жёлтая плашка внизу перечисляет, что в стенде заглушка и чего нет.
Граф в стенде плоский: один узел-распорядитель и три исполнителя, подграфов нет. Иерархию из части 2 (подграфы для каждого исполнителя) стенд не строит.
| Узел | Что должен делать | Что делает в стенде | Запись в 1С |
|---|---|---|---|
| Supervisor | выбирает следующего исполнителя, решает, когда закончить | мок по правилам: нет причины - Auditor, нет метаданных - Inspector, нет одобренного исправления - Remediator, иначе завершение; после отказа оператора завершение | нет |
| Inspector | читает метаданные: реквизиты, типы, допустимые значения | возвращает строку про перечисление СтавкиНДС |
нет, только чтение |
| Auditor | разбирает журнал регистрации и технологический журнал | возвращает фиксированную причину: не заполнена СтавкаНДС |
нет, только чтение |
| Remediator | формирует исправление | возвращает фиксированный пакет и вызывает interrupt() |
только через подтверждение человека |
Рабочие инструменты этих узлов (чтение журнала регистрации, запрос метаданных, разбор техжурнала) в стенде не написаны. Это следующий шаг, и в части 4 он пойдёт через MCP.
🔍 Состояние графа и контракты Pydantic
Состояние графа - типизированный словарь TypedDict. Вложенные структуры описаны моделями Pydantic v2: контекст инцидента, шаг аудиторского следа, пакет исправления, ответ распорядителя.
Листинг 1. Состояние и контракты (app/state.py)
Две детали, которые пригодятся при доработке. Поле operator_decision появилось в стенде после проверки: без него граф после отказа оператора возвращался к Remediator и спрашивал заново. Поле messages накапливает сообщения по правилу add_messages.
🔍 Шлюз FastAPI: асинхронный протокол 1С и графа
Синхронный вызов модели из пользовательского сеанса или из транзакции проведения занимает серверный вызов на всё время ожидания ответа. Поэтому 1С только отправляет инцидент и получает в ответ 202 Accepted. Дальше граф работает в фоне шлюза, а 1С опрашивает статус. На стенде вызов из 1С вернул управление за 35 и 74 миллисекунды в двух сохранённых прогонах (замер ниже).
Шлюз принимает инцидент, регистрирует его и запускает граф в фоне:
Ключ идемпотентности нужен, потому что клиент, не получивший ответ, повторит запрос и не узнает, дошёл ли первый. Шлюз хранит ключи в словаре и на повтор возвращает уже созданную задачу:
В стенде этот словарь лежит в памяти процесса. Для рабочей базы нужна таблица с индексом по ключу, где вторая вставка с тем же ключом отклоняется самой базой. Такой таблицы в стенде нет.
Шлюз выставляет четыре метода. Интерфейс OpenAPI, который FastAPI строит сам, показывает их так:
Рис. 2. Снимок http://127.0.0.1:8000/docs запущенного стенда.
Обработчик run_graph_task объявлен async и вызывает ainvoke. В исходной редакции стенда там стоял синхронный invoke: внутри async-функции он занимает цикл событий FastAPI на всё время работы графа, и при работе с моделью шлюз не отвечал бы на опросы. Узлы графа у нас синхронные; ainvoke их выполняет, и тесты с прогоном это подтверждают.
🔍 Супервайзер и исполнители на LangGraph
Распорядитель выбирает следующий шаг. Когда включена модель (USE_MOCK_LLM=false, заданы ключ и имя модели), решает она со структурным ответом SupervisorOutput. Иначе работает мок с теми же правилами маршрутизации. С реальной моделью стенд не запускался, поэтому все прогоны в статье идут через мок.
Листинг 2. Распорядитель (app/graph.py)
Возвращая Command, узел сам говорит, куда идти дальше и что записать в состояние. Ребро в графе объявлено только одно, от START к распорядителю, остальные переходы задают команды.
Исполнители в стенде выглядят так. Auditor целиком:
Это заглушка: сигнатура и причина записаны в коде, а error_raw попадает только в текст обоснования. Чтение журнала регистрации на месте этой заглушки - задача, которую в стенде никто не решал.
🔍 Пауза на подтверждение: interrupt() и resume
Правило стенда: агенты свободно читают и никогда не пишут. Запись в 1С проходит через решение человека.
Remediator формирует пакет исправления и вызывает interrupt(). Граф останавливается, состояние сохраняется в чекпоинтере, а шлюз выставляет задаче статус INTERRUPTED_AWAITING_APPROVAL вместе с данными паузы (interrupt_data). Решение приходит запросом POST /tasks/{id}/resume и превращается в Command(resume=...).
Листинг 3. Исполнитель с паузой и сборка графа (app/graph.py)
Рис. 3. Путь одного инцидента. Числа на стрелках - порядок шагов, статусы взяты из ответов шлюза.
Три свойства interrupt() мы проверили отдельным скриптом (evidence/probe_langgraph.py), потому что от них зависит устройство узла:
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
- Узел выполняется с начала. После
resumeтело Remediator запускается заново: объектRemediationActionсоздан дважды, до паузы и после. Всё, что стоит в узле передinterrupt(), должно быть безопасно повторять. Запись в базу или отправка сообщения там недопустимы. - Без чекпоинтера пауза не работает.
invokeвозвращает результат с ключом__interrupt__, ноget_stateпадает сValueError, аresumeсRuntimeError. - Цикл ограничивает
recursion_limit. При лимите 3 граф падает сGraphRecursionError. Полный проход с паузой и одобрением укладывается в 6, в стенде стоит 15.
Кто нажимает «Одобрить». У шлюза стенда нет авторизации. Любой, кто достучится до /resume, сыграет роль человека. Пауза защищает от автономных действий агента, а от злоумышленника в сети защиты нет: для рабочей базы шлюз закрывают аутентификацией и TLS, а решение привязывают к пользователю.
🔍 Адаптер в 1С
Общий модуль НОП_АгентИнтеграция (серверный, с флагами «Вызов сервера» и «Внешнее соединение») делает четыре вещи: запускает задачу, опрашивает статус, передаёт решение оператора и применяет одобренный пакет.
Листинг 4. Общий модуль НОП_АгентИнтеграция
Барьер в ПрименитьПакетИсправлений работает на стороне 1С и не зависит от шлюза: пакет без is_approved не применится, даже если шлюз отдаст его без одобрения.
Модуль я прогнал через проверку модулей Конфигуратора платформы 8.3.27.2130 (/CheckModules для тонкого клиента, сервера и внешнего соединения). Исходная редакция модуля не прошла её:
{Общий модуль.NOP_AgentIntegration.Модуль(146,12)}: Ожидается выражение
Возврат<<?>> [ТипОбъекта][ИмяОбъекта].ПолучитьСсылку(УИД);
Конструкции [ТипОбъекта][ИмяОбъекта] в языке нет. Функция ПолучитьСсылкуПоGUID теперь обращается к менеджеру справочников: Справочники[Части[1]]. Стенд поддерживает только справочники и говорит об этом исключением.
Исправлено ещё три места:
| Было | Стало |
|---|---|
соединение через ЗащищенноеСоединениеOpenSSL к шлюзу, который слушает обычный HTTP |
соединение без TLS для стенда; для рабочей базы TLS ставят перед шлюзом |
условие барьера Не requires_human_approval Или Не is_approved бросало исключение при любом пакете, где подтверждение не требуется |
requires_human_approval И Не is_approved |
| ключ идемпотентности всегда новый: повтор из своего кода запускал вторую задачу | необязательный параметр КлючИдемпотентности; для повторов ключ строят из инцидента |
Адаптер вызывается напрямую. В базе на БСП вызов оформляют длительной операцией (ДлительныеОперации), чтобы не держать сеанс. Этот вариант в стенде не проверялся.
🔍 Сквозной прогон: 1С, шлюз, граф, запись в карточку
Проверочная база создана скриптом evidence/1c-check/build_ib.py: справочник Номенклатура с реквизитом СтавкаНДС, перечисление СтавкиНДС, модуль адаптера. Скрипт e2e.ps1 подключается к ней через внешнее соединение и проходит весь путь при запущенном шлюзе. В стенде Remediator всегда указывает один и тот же GUID карточки, поэтому скрипт заводит карточку Арт-10492 с этим GUID и пустой ставкой.
карточка Арт-10492 создана, ставка НДС: не заполнена t=35 мс: задача запущена (c24f2a15-545a-4182-8532-6f1501a9b357) t=47 мс: опрос 1 -> INTERRUPTED_AWAITING_APPROVAL применение без подтверждения оператора: Попытка несанкционированного применения изменений без подтверждения HITL! t=73 мс: решение оператора отправлено t=79 мс: опрос 1 -> RESUMING t=193 мс: опрос 2 -> COMPLETED применение пакета: [] t=219 мс: пакет применён в транзакции ставка НДС в карточке после применения: НДС20 два вызова с одним ключом вернули один идентификатор: True (dup-key-1)
Построчно:
- карточка создана без ставки;
- задача запущена, граф дошёл до паузы (
INTERRUPTED_AWAITING_APPROVAL) с первого опроса; - попытка применить пакет без решения оператора отклонена барьером;
- после
ОтправитьРешениеОператорашлюз ещё держит статусRESUMING, пока граф доходит до конца, следующий опрос видитCOMPLETED; пакет применён в транзакции, у карточки стоитНДС20; - два вызова с одним ключом вернули один идентификатор (то, что шлюз отвечает на такой повтор как на дубль, проверяет тест шлюза).
Рис. 4. Время от начала прогона до каждого события.
В двух сохранённых прогонах (e2e-run1.txt, e2e-run2.txt) весь путь занял 307 и 219 миллисекунд, в четырёх более ранних - от 114 до 197 (их вывод не сохранён). Это накладные расходы стенда: заглушки отвечают сразу, а время человека и модели в замер не входит. Сколько будет длиться разбор с моделью и сколько человек будет решать, стенд не показывает.
Сценарий отказа проверен тестом. Оператор отвечает approved: false, граф завершается, remediation остаётся пустым, повторного вопроса нет.
🔍 Тесты
Тестов три, обходятся без 1С и сети:
test_supervisor_full_cycle_with_hitl- полный цикл графа: пауза,Command(resume=...), завершение, проверка аудиторского следа;test_remediation_rejected_by_operator- отказ: граф завершается, пакета нет;test_api_end_to_end_gateway_flow- шлюз целиком:202, повтор с тем же ключом возвращаетDUPLICATE_ACCEPTED, пауза,resume, статусCOMPLETED.
Тест отказа из стенда (после исправления он проверяет, что граф не возвращается к вопросу):
tests/test_api_gateway.py::test_api_end_to_end_gateway_flow PASSED [ 33%] tests/test_supervisor.py::test_supervisor_full_cycle_with_hitl PASSED [ 66%] tests/test_supervisor.py::test_remediation_rejected_by_operator PASSED [100%] ============================== 3 passed in 1.42s ==============================
Первая версия этого теста проверяла только is_approved_by_human is False и проходила, хотя граф после отказа снова вставал на паузу. Проверка not state_final.next эту ошибку ловит.
Тестов для модуля 1С в статье нет. Их удобно писать на YAxUnit, но фреймворк на стенде не стоял, и выдавать непроверенный набор за тесты я не стал. Проверка адаптера на платформе - скрипт e2e.ps1 выше.
🔍 Что стенд не умеет
- Рабочие инструменты агентов. Чтение журнала регистрации и техжурнала, запрос метаданных, разбор блокировок: везде заглушки.
- Модель вместо мока. Ветка с
ChatOpenAIиwith_structured_outputнаписана, но не запускалась. - Постоянный чекпоинтер.
MemorySaverтеряет состояние вместе с процессом, зависшие паузы пропадут при перезапуске. Для рабочей базы нужен постоянный чекпоинтер, напримерPostgresSaverиз пакетаlanggraph-checkpoint-postgres. В стенде его нет,docker-compose.ymlPostgreSQL не поднимает. - Идемпотентность в базе. Ключи лежат в словаре в памяти.
- Защита шлюза. Нет аутентификации и TLS,
/resumeоткрыт всем. - Уведомление оператора. Стенд не отправляет карточку в Telegram или на форму 1С, оператор сам читает
interrupt_dataиз статуса. - Ограничение нагрузки на шлюз. Circuit breaker не реализован.
- Опрос из БСП. Адаптер вызывается напрямую, длительные операции не подключены.
🔍 Как повторить
Стенд (Python 3.12, около трёх минут вместе с установкой):
В соседнем терминале консольный эмулятор 1С пройдёт тот же путь с паузой и одобрением:
Проверка на платформе 1С: python evidence/1c-check/build_ib.py stand/1c/NOP_AgentIntegration.bsl создаёт файловую базу и проверяет модули, затем e2e.ps1 запускается 32-битным PowerShell (платформа x86) при работающем шлюзе.
Если что-то не заводится, напишите в комментариях версию Python и платформы и на каком шаге остановились.
🔍 Итоги и что дальше
Стенд показывает три вещи: 1С не ждёт ответа графа, запись в базу отделена от рассуждений агента паузой, а барьер в модуле 1С работает независимо от шлюза. На пути к рабочей базе остаются рабочие инструменты агентов, постоянный чекпоинтер и защита шлюза.
В части 4 мы заменим заглушки инструментами: агент сам обратится к метаданным и коду 1С через Model Context Protocol. Какая часть задуманного заработает, покажем замером, как здесь.
Вопросы к вам: где в вашей базе граница между «агент читает» и «агент пишет»? Какие действия вы бы поставили за подтверждение в первую очередь?
Вступайте в нашу телеграмм-группу Инфостарт