Ранас Мукминов – Оркестрация ИИ-агентов. Claude Opus 4.7 (страница 31)
Полезно отметить асимметрию доверия между ролями. Декомпозитор и сборщик — роли, где недетерминизм модели допустим, потому что их ошибки видны на следующем шаге: плохое разбиение проявится конфликтами или дырами, плохая сборка — несогласованным итогом, и оба ловятся проверкой. Надзиратель — роль, где недетерминизм недопустим в принципе: решение «ждать ли воркера ещё» нельзя доверять модели, потому что у этого решения нет следующего шага, на котором его можно перепроверить, — неверное решение здесь либо вешает весь рой, либо тратит бюджет впустую. Это общий принцип, к которому книга возвращается: чем ближе роль к гарантиям времени и стоимости, тем меньше в ней места для модели и тем больше для детерминированного кода.
Цель разбиения — получить подзадачи, которые можно вести одновременно и независимо. Независимость здесь — не удобство, а условие корректности паттерна: если подзадачи зависят друг от друга, параллельное исполнение даст гонки и рассогласование, потому что воркеры не видят промежуточных результатов соседей. Поэтому первый вопрос декомпозиции — не «как разбить помельче», а «что здесь действительно независимо».
Хорошее разбиение даёт подзадачи, у которых непересекающиеся входы, непересекающиеся области изменения и самодостаточные критерии готовности. Непересекающиеся области изменения особенно важны там, где воркеры что-то пишут — файлы, записи, ресурсы: если два воркера пишут в одно место, сборка столкнётся с конфликтом, которого можно было избежать разбиением (см. главу 40 о конфликтах изменений). Хорошая декомпозиция предотвращает конфликты, а не разрешает их потом.
Гранулярность подзадач — параметр с двумя обрывами. Слишком крупные подзадачи дают мало параллелизма и упираются в те же пределы одного агента, ради обхода которых рой и строился: подзадача не помещается в контекст воркера, исполняется долго, не выигрывает от распараллеливания. Слишком мелкие подзадачи дают обратную проблему: координационный налог на их раздачу, передачу контекста и сборку превышает выигрыш от параллелизма. На пределе мелкости оркестратор тратит на handoff и fan-in больше, чем стоило бы сделать всё одним агентом.
Между обрывами лежит зона, где подзадача достаточно крупна, чтобы её handoff и сборка окупались, и достаточно мала, чтобы помещаться в контекст воркера и давать параллелизм. Точное положение зоны зависит от задачи и определяется эмпирически, но ориентир таков: подзадача должна быть единицей, результат которой имеет самостоятельный смысл и проверяется отдельно. Если результат подзадачи бессмыслен без результатов соседей, разбиение проведено по неверной границе.
Разбиение порождает три характерных отказа, и все три тихие — то есть не дают явной ошибки, а портят результат.
Перекрытие: две подзадачи покрывают пересекающуюся область. Воркеры делают двойную работу, их результаты конфликтуют на сборке, и в лучшем случае конфликт виден, а в худшем — сборщик молча выбирает один из двух, теряя часть работы другого.
Дыра: часть исходной задачи не попала ни в одну подзадачу. Ни один воркер её не делает, на сборке никто её отсутствия не замечает, и она выпадает из результата. Дыры опаснее перекрытий, потому что перекрытие хотя бы создаёт лишнюю работу, а дыра создаёт тихую неполноту.
Ложная независимость: подзадачи считаются независимыми, но на деле связаны — например, общим ресурсом или неявным допущением. Воркеры работают параллельно, как если бы соседей не было, и приходят к несовместимым результатам. Это самый коварный отказ разбиения, потому что он проявляется только на сборке и выглядит как конфликт исполнения, хотя корень — в декомпозиции.
Защита от всех трёх — явная, проверяемая декомпозиция: набор подзадач должен в сумме покрывать исходную задачу (нет дыр), не пересекаться (нет перекрытий) и не иметь скрытых связей (нет ложной независимости). Когда декомпозицию делает модель, эту проверку не стоит доверять ей же; её выполняет либо код по схеме, либо отдельный агент-критик (см. главу 19 о роли критика).
Полезный приём — требовать от декомпозитора не только список подзадач, но и явную карту покрытия: для каждой части исходной задачи указано, какая подзадача за неё отвечает. Карта покрытия превращает дыры и перекрытия из невидимых в проверяемые: дыра — это часть задачи, на которую не указывает ни одна подзадача; перекрытие — часть, на которую указывают две. Проверка карты — детерминированная операция, и её можно поручить коду, даже когда само разбиение делала модель. Это иллюстрирует общий приём оркестрации: там, где недетерминированный шаг неизбежен, его результат сопровождают проверяемым артефактом, а проверку артефакта возлагают на код (см. главу 18 о проверяемости контракта).
Во время исполнения воркеры работают одновременно и, в идеале, ничего не разделяют. Каждый имеет свой контекст, свою рабочую область, свои инструменты. Это share-nothing-исполнение (см. главу 54), и оно — причина, по которой fan-out/fan-in надёжно параллелится: раз воркеры ничего общего не изменяют, между ними нет гонок, нет блокировок, нет необходимости в согласовании во время работы. Вся координация уже сделана на разбиении и будет доделана на сборке; в середине воркеры одиноки.
Изоляция должна быть не только логической, но и физической, где это возможно: отдельная ветка, отдельный воркспейс, отдельная песочница на воркера (см. главу 55). Физическая изоляция гарантирует, что воркер не сможет повлиять на соседа даже по ошибке или из-за компрометации — частичное падение или захват одного воркера не распространяется на других (см. главу 71 о bulkhead, главу 84 о латеральном движении). Без изоляции воркеров «параллелизм» становится конкуренцией за общий ресурс, и преимущества паттерна теряются.
Сколько воркеров запускать одновременно — решение с жёстким верхним ограничением, и ограничение это не архитектурное, а ресурсное. Провайдеры моделей вводят лимиты на параллельные запросы и на токены в единицу времени; превышение даёт отказы или замедление. Бюджет стоимости конечен; каждый параллельный воркер тратит токены одновременно с другими, и пиковая стоимость роя — это сумма по всем активным воркерам (см. главу 59). Поэтому степень параллелизма должна иметь потолок, заданный кодом и не превышаемый ни при каких решениях модели-оркестратора.
Практическая конструкция — пул воркеров фиксированного размера, разбирающий очередь подзадач (см. главу 33, главу 35). Если подзадач больше, чем мест в пуле, лишние ждут освобождения. Это превращает неограниченный fan-out в управляемый: пиковая нагрузка ограничена размером пула, а не числом подзадач. Цена — частичная сериализация: при пуле размера K и числе подзадач N больше K работа идёт волнами, и общее время растёт. Это прямое следствие закона Амдала для роёв (см. главу 56) и граница, за которой добавление подзадач не ускоряет, а только удлиняет очередь.
Параллельное исполнение порождает отказы, которых нет у одного агента, и большинство из них связано с тем, что часть воркеров ведёт себя не так, как ожидалось, пока остальные работают нормально.
Зависший воркер. Один из воркеров не завершается — застрял в цикле, ждёт недоступного ресурса, ушёл в непродуктивную траекторию (см. главу 74). Без надзора оркестратор ждёт его результата неопределённо долго, и весь fan-in блокируется на одном медленном воркере. Защита — тайм-аут на воркера и решение надзирателя: отбросить, повторить или принять частичный результат (см. главу 70, главу 72).
Воркер с мусором. Воркер завершился, но вернул бессмысленный или несовместимый с контрактом результат. Если сборщик не проверяет результаты на входе, мусор попадает в итог. Защита — валидация результата воркера против его контракта до сборки (см. главу 18, главу 25).
Стоимостный всплеск. Несколько воркеров одновременно уходят в длинные траектории, и пиковая стоимость превышает ожидаемую кратно. Защита — бюджет токенов на воркера и общий бюджет роя, при превышении которого исполнение останавливается контролируемо (см. главу 59, главу 100).
Коррелированный отказ. Все воркеры падают по одной причине — недоступен общий внешний сервис, исчерпана общая квота, в стартовый контекст всех воркеров попала одна и та же ошибка. Параллелизм здесь не помогает: вместо одного отказа получается N одновременных. Это случай, где разнообразие воркеров не спасает (см. главу 63 о коррелированных ошибках), а спасает только защита самого общего ресурса (см. главу 90).
Важно, что в звездообразной топологии отказ воркера локализован: он не распространяется на соседей, потому что горизонтальных рёбер нет. Это сильное свойство паттерна — оно даёт естественный bulkhead. Распространиться отказ может только через две точки: общий ресурс (если изоляция нарушена) и оркестратор (если он не справляется со сбором частичных результатов). Обе точки — предмет надёжности оркестратора как SPOF (см. главу 75).
Сборка часто проектируется последней и в наивной форме — «собрать результаты воркеров вместе». Эта наивность дорого обходится, потому что fan-in — место, где сходятся все отклонения, накопленные в предыдущих фазах. Перекрытия из разбиения проявляются как конфликтующие результаты. Ложная независимость проявляется как несовместимость. Зависшие и отброшенные воркеры проявляются как отсутствующие части. Мусорные результаты проявляются как несоответствие контракту. Сборщик обязан иметь стратегию для каждого из этих случаев, иначе он просто пропускает их в итог.