Когда мы начинали строить продукт для распознавания и обработки первичных бухгалтерских документов — OCR, классификация, LLM‑извлечение полей, проверка человеком — казалось, что рынок оркестраторов уже всё решил: есть n8n для визуальных workflow, есть LangGraph для агентных пайплайнов, есть Temporal для durable‑исполнения. Мы попробовали примерить каждый — и в итоге написали свой: AgentsGraph, встраиваемую Java‑библиотеку декларативной оркестрации агентов. Ниже — почему.
Требование № 1: enterprise в private cloud, без внешних сервисов
Наши заказчики — компании, у которых документы не могут покидать контур: банковская первичка, персональные данные, коммерческая тайна. «Отправьте ваш счёт‑фактуру в наш облачный API» — это не разговор. Поэтому базовое требование к оркестратору сформулировалось жёстко:
всё, что нужно для работы графа, должно жить внутри приложения заказчика.
В AgentsGraph это выполнено буквально:
Хранилища — ваш PostgreSQL. Конфигурация графов (
agentsgraph_graph_config), реестр процессоров (agentsgraph_processor), журнал исполнений и пошаговые трейсы (agentsgraph_trace,agentsgraph_step_trace) — обычные таблицы в базе приложения, разворачиваются тем же SQL‑скриптом, что и остальная схема. Ни очередей в чужом облаке, ни телеметрии наружу, ни «позвоните нам за license key».Модели — ваши. Процессоры ходят в те LLM и OCR, которые стоят в стойке заказчика. Наш боевой пайплайн работает на локальном OCR‑сервисе и локальном инференсе qwen — наружу не уходит ни байта. Клиент к LLM говорит и на OpenAI‑, и на Anthropic‑диалекте (автоопределение), так что «своя модель» не означает «свой зоопарк адаптеров».
Библиотека, а не платформа. AgentsGraph — набор jar‑модулей (
context,config,engine,trace,control,core,interaction), которые подключаются в ваше Spring‑приложение как обычная зависимость. Никакого отдельного сервера‑оркестратора, который нужно лицензировать, обновлять и защищать. Админ‑панель (просмотр графов, исполнений, пошаговых трейсов, перезапуск шага) — опциональный модуль, поднимающийся внутри вашего же Boot‑приложения.
Для enterprise это не «фича», а критерий отбора: security‑аудит проходит ваш продукт целиком, и оркестратор в нём — просто ещё одна библиотека в pom‑е, а не внешняя система со своим периметром.
Почему не n8n
n8n — отличная вещь для того, для чего она сделана: связать десяток SaaS‑ов без программиста. Сотни готовых коннекторов, триггеры, визуальный low‑code редактор. Если задача — «когда приходит письмо, положи вложение в Drive и напиши в Slack», n8n закрывает её за вечер, и мы бы не стали писать ради этого ни строчки.
Но у нас другая задача — и два несовместимых с n8n обстоятельства. Первое: n8n — это отдельно стоящая платформа, а не библиотека. Его нельзя растворить внутри своего продукта: это самостоятельный сервис со своим UI, своей моделью пользователей и своим жизненным циклом, рядом с которым ваше приложение — лишь один из «коннекторов». Второе — лицензия: n8n распространяется под Sustainable Use License, которая прямо ограничивает встраивание в коммерческие продукты — за embedding нужно идти за отдельной коммерческой лицензией. AgentsGraph же изначально спроектирован как встраиваемый и лицензирован под Apache 2.0 — встраивание в коммерческий продукт свободно: граф — деталь реализации вашей системы. Процессор — это ваш Java‑класс с вашим DI, вашими транзакциями и вашими юнит‑тестами; шаг пайплайна и сервисный слой приложения — один и тот же код, а не HTTP‑мостик между двумя мирами. Для вендора, продающего свой софт on‑premise, это разница между «поставляем продукт» и «поставляем продукт плюс чужую платформу с отдельным лицензионным договором».
Почему не LangGraph
LangGraph ближе всех по духу: те же агентные графы, состояние, ветвления, human‑in‑the‑loop. Если ваш стек — Python и вы готовы жить в его экосистеме, это сильный выбор. Наши расхождения с ним — про инженерную дисциплину на длинной дистанции.
Типизация и конфигурация. В LangGraph граф — это код на Python: состояние — словарь или TypedDict, рёбра — функции, ошибки конфигурации всплывают в рантайме у пользователя. В AgentsGraph граф — декларативный JSON с жёсткой схемой, который валидируется при деплое: GraphDefinition → ноды со стратегиями роутинга → рёбра со списками шагов. Поток данных между шагами описан явно: output_to_next говорит, какие ключи поедут дальше по пайплайну, output_to_save — какие уйдут на персистенцию. Контекст (ExecutionContext) иммутабелен, а обязательные входы шаг забирает через require(key) — если ключа нет, вы получаете не NullPointerException тремя шагами позже, а немедленную ошибку с перечнем доступных ключей. Целый класс багов «кто‑то переименовал поле в словаре» здесь просто не компилируется или ловится на деплое графа, а не в проде.
А те баги, что всё же случаются, — журналируются и чинятся перезапуском. Каждый шаг исполняется под трейсером: в debug‑режиме (или точечно, для шагов с флагом "snapshot": true, — прямо в проде) в базу пишется полный входной контекст шага, его выход, тайминги, а при падении — стектрейс. Упавший flow — это не строчка в логе, а разборный объект: describeFlow печатает пошаговый отчёт, resumeFrom(flowId, seq) перезапускает граф ровно с упавшего шага на тех же данных — дорогой OCR не выполняется повторно, — а resumeFrom(flowId, seq, overrides) позволяет перед ретраем поправить данные. Дамп трейса скармливается тестовому харнессу, и прод‑инцидент воспроизводится в CI с замоканными ответами внешних сервисов — без сети и без токенов. Ретраи есть и на нижнем этаже: транспортные сбои LLM/OCR (таймаут, обрыв) повторяются с настраиваемым числом попыток, а обрезанный по лимиту токенов ответ — это явная ошибка, а не полу‑JSON, уехавший дальше по пайплайну.
Пример: обработка документов с human‑in‑the‑loop
Покажем, как это выглядит на нашем боевом пайплайне первички. Граф — ромб: документ распознаётся, и дальше маршрут зависит от того, можно ли доверять распознаванию без человека.
{ "id": "ocr-accounting", "nodes": [ { "id": "accuracy_router", "routing_strategy": "rules", "routing_table": { "accuracyOk==false": "edge_review", "default": "edge_llm_pipeline" } }, { "id": "review_router", "routing_strategy": "rules", "routing_table": { "reviewPending==true": "edge_review_pending", "reviewPending==false": "edge_llm_pipeline" } } ], "edges": [ { "id": "edge_ocr_pipeline", "steps": [ { "id": "step_ocr", "processor_id": "docscan-ocr", "output_to_next": ["json"] }, { "id": "step_ocr_visualize", "processor_id": "docscan-ocr-visualize", "output_to_next": ["json", "accuracyOk", "accuracyScore", "lowProbItems"] } ], "next_node_id": "accuracy_router" }, { "id": "edge_review", "steps": [ { "id": "step_human_review", "processor_id": "human-review", "snapshot": true }, { "id": "step_apply_corrections", "processor_id": "apply-corrections", "output_to_next": ["json"] } ], "next_node_id": "review_router" }, { "id": "edge_review_pending", "steps": [ { "id": "step_review_pending", "processor_id": "noop" } ], "tags_to_add": ["review_pending"] } ] }
Работает это так. OCR‑сервис возвращает по каждому распознанному элементу вероятность prob (0..1); шаг визуализации агрегирует их в оценку точности: если хоть один элемент слабее порога (по умолчанию 0.55) или средняя ниже 0.85 — accuracyOk=false. Пороги — параметры процессора, меняются в конфигурации без пересборки. Нода accuracy_router обычным правилом уводит такой flow в review‑ветку.
Шаг human-review — чистый процессор без всякой магии «пауз»: не найдя в контексте ответа человека, он формирует задачу (вопрос, оценка точности, список слабых элементов) и flow штатно завершается с тегом review_pending. Ключевое здесь — флаг "snapshot": true: полный входной контекст этого шага записан в трейс прямо в проде, поэтому шаг рестартуем. Отдельный модуль interaction превращает такие завершённые flow в задачи для человека и доставляет их адаптерами в любой канал — у нас это чат: пользователь видит карточку «точность 42%, проверьте выделенные поля», правит распознанные данные штатным редактором и нажимает «Продолжить».
Ответ человека — это вызов всё того же resumeFrom: шаг human-review перезапускается, на этот раз видит исправления и пропускает их дальше; apply-corrections кладёт их в контекст под тем же ключом json, что выдаёт OCR, — и review_router возвращает flow в общий LLM‑пайплайн. Пост‑обработка не знает и не должна знать, побывал ли документ у человека: дорогой OCR не выполняется повторно, LLM получает проверенные данные. Движку для всего этого не понадобилось ни одного нового состояния — только те же трейсы и перезапуск; повторный ответ на уже закрытую задачу отклоняется, дедлайны обрабатываются sweep‑ом.
Визуализация
Смотреть на граф глазами тоже есть чем: в комплекте — веб‑панель agentsgraph‑ui (Angular + d3 поверх модуля admin-server). Она рисует граф как есть из его конфигурации: ноды‑роутеры и рёбра‑пайплайны, подписи условий на стрелках, fallback‑связи пунктиром, HITL‑рёбра подсвечены розовым, снапшот‑шаги помечены. Вершины можно перетаскивать по холсту, клик по ноде или ребру открывает панель с деталями — таблицей правил роутинга или списком шагов с параметрами. Там же — журнал исполнений с пошаговыми трейсами упавших flow и кнопкой «перезапустить с этого шага».
Что в итоге
Мы не строили «убийцу n8n» и не соревнуемся с LangGraph в ширине экосистемы. AgentsGraph — это узкий и глубокий инструмент: оркестрация LLM‑пайплайнов внутри enterprise‑Java‑приложения, работающего в private cloud, с декларативной конфигурацией, строгими контрактами данных, пошаговой наблюдаемостью и перезапуском с любого шага — включая перезапуск руками человека. Для продуктов, которые продаются on‑premise и обязаны объяснять аудитору каждый байт, покидающий контур, такой инструмент оказался не роскошью, а условием существования.
Библиотека открыта и распространяется под лицензией Apache 2.0 — встраивайте в свои коммерческие системы без ограничений и отдельных договоров. Код разбит на независимые модули, ядро совместимо с Java 11, админ‑панель — Spring Boot 3 / Angular:
ядро и модули: github.com/Provision‑Labs/AgentsGraph
визуализация и админ‑панель: github.com/Provision‑Labs/agentsgraph‑ui
Если вам знакома боль из этой статьи — попробуйте.

