Ранас Мукминов – Оркестрация ИИ-агентов. Claude Opus 4.7 (страница 52)
Без явного контракта граница работает на неписаных предпосылках, и они расходятся со временем. Агрегатор fan-in начинает добавлять поле, которого не ждёт следующая стадия; следующая стадия молча его игнорирует; через месяц кто-то начинает на это поле полагаться — и система ломается в месте, где никто не менял код. Контракт границы превращает неписаные предпосылки в проверяемое условие, нарушение которого обнаруживается на стыке, а не тремя стадиями позже.
Отдельного внимания заслуживает то, что на границе гарантии не складываются, а берутся по минимуму, и часто теряются вовсе. Если внутренняя топология обеспечивает упорядоченную доставку, а внешняя — нет, то на стыке порядок теряется: система в целом упорядоченности не имеет. Если одна топология идемпотентна по повторам, а другая нет, то составная система не идемпотентна — повтор на неидемпотентной стороне границы вызовет дублирование, даже если другая сторона повтор переживала бы безболезненно (см. главу 43).
Это правило — гарантии берутся по минимуму — означает, что нельзя проектировать топологии гибрида независимо, рассчитывая, что хорошие свойства каждой сложатся. Свойство сохраняется в составной системе, только если его обеспечивают обе стороны границы и сам стык. Поэтому требования к гарантиям нужно протягивать через все границы сквозным образом: если системе в целом нужна exactly-once-семантика результата, её должны обеспечить все звенья и все стыки, а не одно «надёжное» звено посередине.
Рассмотрим обобщённый, иллюстративный сценарий, типичный для гибрида «fan-in, затем конвейер». Параллельная стадия собирает данные из нескольких источников и агрегирует их в один объект, который потом обрабатывает конвейер. На границе действует неписаный контракт: объект содержит поле со списком найденных элементов, и конвейер исходит из того, что список непуст, потому что на ранних запусках источники всегда что-то возвращали.
Дальше происходит дрейф. Один из источников начинает иногда возвращать пустой результат — например, при недоступности. Агрегатор честно собирает то, что есть, и кладёт в объект пустой список. Объект формально валиден: поле на месте, тип верный. Конвейер получает его, проходит проверку формата и обрабатывает пустой список как «ничего не найдено», хотя на самом деле это «часть данных потеряна». Результат внешне корректен и неотличим от случая, когда данных действительно нет. Отказ тихий: ни одна сторона границы не нарушила свой локальный контракт, но система выдала неверный результат, потому что граница не специфицировала семантику пустого списка — отсутствие данных против потери данных.
Этот сценарий показывает три вещи. Во-первых, контракт границы — это не только форма (схема, типы), но и семантика (что означают граничные значения вроде пустоты, нуля, отсутствующего поля). Во-вторых, дрейф происходит без изменения кода на стыке: меняется поведение источника глубоко внутри одной топологии, а ломается стык с другой. В-третьих, тихий отказ на границе обнаруживается не на стыке, а далеко вниз по течению — или вообще только в постмортеме, — потому что формальная валидация его пропустила. Защита — специфицировать на границе различимость граничных случаев и проверять её явно: агрегатор должен отличать «источник вернул пусто» от «источник недоступен», и эта различимость должна пересекать границу, а не схлопываться в один пустой список.
Несмотря на разнообразие, на практике повторяется небольшое число гибридных конфигураций. Их полезно знать как готовые скелеты — не для слепого копирования, а как точки отсчёта, у которых известны типичные стыки и типичные отказы.
Самый распространённый гибрид: оркестратор раздаёт независимые задачи (fan-out), а каждый воркер внутри устроен как конвейер из нескольких стадий. Это вложение конвейера в воркер. Применяется, когда задача дробится на независимые куски (например, по модулям или по документам), но обработка каждого куска — это фиксированная последовательность шагов.
Свойства: верхний уровень параллелится хорошо, и его пропускная способность растёт с числом воркеров до предела диспетчеризации (см. главу 33). Латентность определяется самым медленным воркером — а внутри воркера это сумма стадий конвейера. Отказ воркера локализуется одной задачей, остальные не страдают, если оркестратор корректно обрабатывает падение воркера (см. главу 69).
Типичные отказы: неравномерность задач — один кусок оказывается на порядок тяжелее остальных, и его воркер становится узким местом, пока прочие простаивают; накопление частичных результатов — если стадия конвейера внутри воркера тихо роняет качество, оркестратор этого не видит, потому что снаружи воркер вернул валидный по форме результат.
Содержательная координация — иерархическая (оркестратор, воркеры, субворкеры), но обмен данными внутри уровня идёт не прямой передачей, а через blackboard: воркеры одного родителя пишут промежуточные находки на общую доску и читают находки друг друга. Это наложение blackboard на иерархию.
Свойства: косвенная координация развязывает воркеров по времени и позволяет одному воспользоваться результатом другого без явного запроса. Доска становится общей памятью уровня (см. главу 48). Это полезно, когда подзадачи не полностью независимы и выигрывают от обмена промежуточными результатами.
Типичные отказы: общая доска — это разделяемое изменяемое состояние, со всеми его рисками. Гонки за запись (см. главу 41), отравление доски ошибочной записью одного агента, которая распространяется на всех читателей (см. главу 53), и превращение доски в узкое место координации, если все агенты конкурируют за неё. Иерархия, наложенная на доску, не отменяет этих рисков — она их наследует.
Линейный конвейер, в котором между содержательными стадиями вставлены агенты-критики или ревьюеры, проверяющие результат предыдущей стадии перед передачей дальше. Это вложение состязательной пары «генератор — критик» (см. главу 64) в стадию конвейера.
Свойства: качество на каждом стыке стадий контролируется явно, и брак не проходит дальше по цепочке. Это особенно ценно в конвейере, где надёжность всей цепочки определяется слабейшим звеном: критик на каждом стыке поднимает нижнюю границу.
Типичные отказы: критик добавляет латентность на каждой стадии и сам может ошибаться — пропускать брак (ложноотрицательно) или заворачивать корректный результат (ложноположительно). Цикл «генератор — критик — повторная генерация» способен зациклиться, если критик никогда не удовлетворён, и нужен ограничитель числа итераций, иначе стадия не завершается (см. главу 74 о зацикливании).
Перед входом стоит агент-маршрутизатор, который по типу запроса направляет его в одну из нескольких подсистем, каждая со своей внутренней топологией: простые запросы — одному агенту, исследовательские — оркестратору с воркерами, требующие высокой надёжности — ансамблю с голосованием. Это последовательное соединение маршрутизатора с набором разнородных топологий, выбираемых динамически.
Свойства: система применяет дорогую топологию только там, где она оправдана, а дешёвую — где достаточно. Это прямой способ управлять координационным налогом: не платить за рой там, где хватает одного агента.
Типичные отказы: маршрутизатор — единая точка входа и потенциальный SPOF (см. главу 75); его ошибка отправляет запрос в неподходящую топологию — например, сложную задачу к одиночному агенту, который её не вытянет, или тривиальную к дорогому рою. Маршрутизатор недетерминирован, и один и тот же запрос может маршрутизироваться по-разному, что затрудняет воспроизводимость и отладку.
Гибрид | Способ комбинирования | Когда уместен | Главный риск стыка
Оркестратор + конвейерные воркеры | Вложение | Независимые куски, фиксированная обработка каждого | Неравномерность задач; тихая потеря качества внутри воркера
Иерархия + blackboard | Наложение | Подзадачи выигрывают от обмена промежуточным | Гонки и отравление общей доски
Конвейер + критик | Вложение | Нужен контроль качества на каждом переходе | Зацикливание генератор-критик; ошибки критика
Маршрутизатор + разнородные топологии | Последовательное | Запросы сильно различаются по сложности | Ошибка маршрутизации; SPOF и невоспроизводимость
Гибрид проектируется не «снизу вверх» наращиванием паттернов и не «сверху вниз» выбором одной схемы, а итеративным разбиением задачи с явной фиксацией границ. Полезен следующий порядок рассуждения.
Первый шаг — декомпозировать задачу по форме координации, а не по предметным областям. Вопрос на каждом уровне разбиения: эта работа независима (просит fan-out), последовательна (просит конвейер), требует обмена промежуточным (просит общую доску), требует выбора лучшего из попыток (просит ансамбль) или требует исследования с непредсказуемым ветвлением (просит оркестратора с динамическим порождением)? Ответ определяет топологию уровня. Разные уровни дадут разные ответы — это и есть источник гибридности.
Второй шаг — для каждого фрагмента зафиксировать его внешний контракт прежде, чем проектировать внутренность. Контракт фрагмента — это то, что видят соседние топологии: вход, выход, инварианты, поведение при отказе. Пока контракт не зафиксирован, внутреннюю топологию фрагмента проектировать рано: она может смениться, а контракт должен оставаться стабильным, иначе соседи сломаются.