Вот смотри. Ты берёшь GitHub, коммитишь туда шаги агента один за другим и думаешь, что управляешь процессом. А на деле ты просто ведёшь дневник. Красивый, последовательный, но дневник. И когда у тебя не один агент, а пять, и они должны работать вместе, этот дневник превращается в мусорку из merge-коммитов.
Проблема не в том, что Git плохой. Проблема в том, что он придуман для людей, которые пишут код по очереди. Один человек, одна ветка, одно состояние в момент времени. ИИ-агенты так не работают. Совсем.
Где именно ломается линейная модель
Агенты параллельны по своей природе. Пока один копает конкурентов, второй уже может собирать цены. Третий вообще ни от кого не зависит, ему дали задачу и он пошёл. В линейной истории ты это никак не выразишь. У тебя всё равно получится цепочка: сначала это, потом то, потом третье. Даже если реально они могли бежать одновременно.
Дальше. Зависимости у агентов разреженные. Агенту B нужен результат агента A, а агенту D плевать и на A, и на B. В коммитах ты эту связь не видишь. Ты видишь порядок, но не видишь, кто кого реально ждёт. А это разные вещи.
И самое больное. Отказы. Один агент вернул пустую строку. Токены сожгли, ответа нет. Классика с DeepSeek и другими «думающими» моделями. В линейной цепочке это стоп для всего. В нормальной системе это должно быть локальное событие: этот узел упал, соседние ветки живут дальше.
DAG как способ перестать врать себе
В agent-hub-core мы взяли и выкинули идею последовательности. Вместо неё у нас граф. Направленный, без циклов. Узлы это задачи агентов, рёбра это зависимости по данным. Всё.
Что это даёт на практике? Узлы без зависимостей стартуют параллельно. Узлы с зависимостями ждут ровно тех, кто им нужен, и никого больше. Отказ одного узла не роняет граф, соседи продолжают.
Это не наша гениальная находка. Про DAG для мультиагентных систем писали и в IEEE, и в AWS Strands Agents SDK используют похожую идею для аудита. Просто у нас своя реализация: Go рулит оркестрацией, Rust держит протокол, Vue показывает всё это человеку.
Как выглядит flow_json
Вот реальный формат. Не YAML для красоты, а структура, которую можно проверить и отсортировать топологически.
{ "name": "market-analysis", "description": "Анализ рынка перед запуском продукта", "nodes": [ { "id": "research", "task": "Изучить конкурентов и прикинуть объём рынка", "inputs": ["product_description"], "outputs": ["competitor_list", "market_size"] }, { "id": "pricing", "task": "Собрать цены и фичи конкурентов", "inputs": ["competitor_list"], "outputs": ["pricing_data", "feature_matrix"] }, { "id": "strategy", "task": "Найти окна для позиционирования и ценовой стратегии", "inputs": ["pricing_data", "feature_matrix", "market_size"], "outputs": ["positioning_report", "pricing_recommendation"] }, { "id": "merge", "task": "Собрать executive summary из всех находок", "inputs": ["positioning_report", "pricing_recommendation"], "outputs": ["executive_summary"] } ], "max_parallel": 5, "timeout_per_agent": 300, "retry_on_failure": true, "quality_threshold": 0.7}
Смотри что тут происходит. research и pricing стартуют почти одновременно. pricing ждёт только competitor_list, который отдаёт research, и всё. strategy собирает три входа. merge финализирует. Никакой очереди из коммитов, оркестратор сам решает, что можно запускать вместе.
Что происходит до запуска
Прежде чем граф поедет, flow_json проходит мясорубку проверок.
Сначала синтаксис. Потом семантика: нет ли циклов, все ли входы кто-то производит, нет ли висячих узлов, не улетел ли критический путь за разумные пределы. Дальше топологическая сортировка. И только потом выделение параллельных групп.
Именно здесь отваливается то, что в линейной истории всплывает уже в рантайме. Циклы. Входы, которые никто не производит. Ветки, до которых никогда не дойдёт очередь.
Боевой сценарий: один агент рулит другими через WebSocket
Вот как это выглядит живьём. Управляющий агент через Vue 3 интерфейс командует остальными в реальном времени.
Vue 3 Frontend Dashboard | Flow Editor | Live Chat \ | / WebSocket (WSS) | agent-hub-core (Go) WebSocket Hub / Orchestrator Agent1 Agent2 Agent3 | MCP Engine (Rust via CGO) | Agent A Agent B Agent C
Поток простой. Человек в Vue создаёт workflow. Управляющий агент в Go транслирует задачу через сокет. Исполнители получают, начинают работать. Статусы летят обратно в реальном времени.
Типы событий:
type WebSocketEvent = | { type: 'chat', data: ChatEvent } | { type: 'agent', data: AgentStream } | { type: 'activity', data: Activity } | { type: 'tool_call', data: ToolCall }
Жизненный цикл узла:
Init → Run → Spawn → Eval → Merge → Status
Init это разбор графа. Run это старт оркестрации. Spawn создаёт агентов пачками, сколько позволяет max_parallel. Eval проверяет качество. Merge склеивает результаты. Status отчитывается.
Состояния агентов
|
Состояние |
Что значит |
|---|---|
|
PENDING |
Ждёт зависимости |
|
READY |
Все зависимости готовы, стоит в очереди |
|
RUNNING |
Работает |
|
COMPLETED |
Успешно закрыт |
|
FAILED |
Упал после всех попыток |
|
EVALUATING |
Идёт проверка качества |
Вот EVALUATING это как раз то место, где лечится та самая пустая генерация. Агент закончил, но content пустой при потраченных токенах. Это не COMPLETED. Это сигнал, что retry_on_failure должен сработать. И повтор идёт на уровне всего узла, а не отдельного вызова, чтобы перезапустить его в контексте графа, а не в вакууме.
Что получаешь в итоге
Параллелизм вместо очереди. Независимые ветки бегут вместе, а не ждут друг друга.
Отказы локальны. Один узел упал, соседи живут.
Зависимости видны. Ты знаешь, кто кого ждёт, а не выясняешь это по конфликтам.
Наблюдаемость. У каждого узла есть состояние, таймаут, порог качества и история попыток.
Git хорош для людей, которые пишут код по очереди. Для агентов, которые работают параллельно, падают по одному и обмениваются данными по графу, нужен DAG. Это не хайп. Это просто следствие того, как агенты устроены.
Проект: Zettaverse AgentHub. Go бэкенд (agent-hub-core), Rust MCP движок (agent-hub-mcp), Vue 3 консоль (agent-hub-ui).
ссылка на оригинал статьи https://habr.com/ru/articles/1090710/