Ранас Мукминов – Оркестрация ИИ-агентов. Claude Opus 4.7 (страница 89)
Конверт стоит проектировать как фиксированный набор полей, каждое из которых обслуживает конкретный уровень системы. Ниже — иллюстративный состав, показывающий назначение, а не предписывающий синтаксис.
Поле конверта | Назначение | Какой уровень обслуживает
message_id | Уникальный идентификатор сообщения | Дедупликация, трассировка (см. главу 77)
correlation_id | Идентификатор сессии или задачи верхнего уровня | Корреляция событий, реконструкция сессии (см. главы 78, 81)
causation_id | Ссылка на сообщение-причину | Восстановление причинных цепочек (см. главу 78)
sender / recipient | Адресация | Маршрутизация (см. главу 27)
message_type | Тип сообщения (задача, результат, отказ, событие) | Выбор схемы нагрузки, диспетчеризация
schema_version | Версия схемы полезной нагрузки | Совместимость и эволюция (см. главу 22)
idempotency_key | Ключ для безопасного повтора | Идемпотентность координации (см. главу 43)
timestamp | Время создания | Упорядочивание, тайм-ауты (см. главы 29, 70)
ttl / deadline | Срок годности сообщения | Защита от обработки устаревшего
Здесь существенны два поля, которые часто упускают. Поле schema_version в конверте — это то, что позволяет получателю выбрать правильный валидатор нагрузки, не угадывая версию по содержимому; без него эволюция схемы (см. главу 22) превращается в гадание. Поле causation_id (ссылка на сообщение, вызвавшее это) отличается от correlation_id (общий идентификатор всей задачи): первое строит дерево причинности между конкретными сообщениями, второе группирует все сообщения одной сессии. Их путаница лишает постмортем (см. главу 82) возможности отличить «эти сообщения относятся к одной задаче» от «это сообщение породило вот то».
Поле message_type заслуживает отдельного внимания, потому что оно — точка, в которой конверт управляет разбором нагрузки. Тип сообщения — это дискриминатор: он сообщает получателю, по какой схеме интерпретировать полезную нагрузку. Сообщение типа «задача» имеет одну схему нагрузки, «результат» — другую, «отказ» — третью. Получатель сначала читает тип из конверта (детерминированно), затем применяет соответствующую схему к нагрузке.
Это переносит на сообщения классический приём размеченного объединения: нагрузка — это объединение нескольких возможных форм, а тип в конверте размечает, какая из них здесь. Альтернатива — пытаться угадать тип по структуре самой нагрузки — хрупка ровно потому, что нагрузку производит недетерминированная модель: две разные по смыслу нагрузки могут случайно совпасть по форме, и разбор по угадыванию выберет неверную схему. Явный дискриминатор в детерминированном конверте снимает эту неоднозначность.
Особый и обязательный тип сообщения — отказ. В рое из недетерминированных узлов сообщение «не смог выполнить» должно быть таким же первоклассным типом, как «результат», со своей схемой (код причины, контекст, возможность эскалации). Это прямое следствие частичного контракта с честным отказом (см. главу 18): если у роли нет способа сообщить о невозможности в структурированной форме, она сообщит о ней в форме правдоподобного, но неверного результата, и рой не отличит успех от провала. Тип-отказ в протоколе сообщений — то, что превращает молчаливую галлюцинацию в наблюдаемое событие.
Три идентификатора в конверте — message_id, correlation_id, causation_id — несут на уровне сообщения то, что в части XI станет основой наблюдаемости. Стоит зафиксировать их роль здесь, потому что если этих полей нет в формате сообщения с самого начала, добавить наблюдаемость потом невозможно: нельзя восстановить причинность по сообщениям, в которых не записано, что чему предшествовало.
message_id даёт идентичность: два экземпляра одного сообщения (например, дубликат при повторной доставке, см. главу 29) опознаются как один. correlation_id даёт принадлежность: все сообщения, порождённые одной задачей верхнего уровня, носят один идентификатор, и по нему собирается вся сессия роя. causation_id даёт причинность: сообщение-результат ссылается на сообщение-задачу, его породившее, и из этих ссылок строится дерево, показывающее, какое действие повлекло какое. Эти три поля — минимальный причинно-следственный скелет, без которого рой наблюдаем только как поток несвязанных событий.
Схема — это формальное описание допустимой структуры сообщения: какие поля есть, какого они типа, какие обязательны, какие значения допустимы. Схема — это машиночитаемая, проверяемая форма того, что в главе 18 было названо структурной частью постусловия. Контракт агента говорит «на выходе будет результат в такой форме»; схема — это «такая форма», записанная так, что её соблюдение можно проверить детерминированно валидатором.
Принципиально, что схема покрывает только структуру. Схема может потребовать, чтобы поле total было числом; она не может потребовать, чтобы total равнялось сумме слагаемых, — это уже содержательное постусловие, проверяемое не схемой, а отдельной проверкой свойства (см. лестницу проверяемости в главе 18). Это разграничение — главное, что нужно удерживать, говоря о схемах в рое: схема ловит структурно невалидное (поле отсутствует, тип не тот, значение вне перечня), но пропускает структурно валидный мусор (поля на месте, типы верны, значения выдуманы). Принять прохождение схемы за корректность сообщения — частый и опасный дефект, потому что схема даёт ложное чувство проверенности.
Схема выполняет в рое две роли сразу, и их полезно различать. Как контракт — она фиксирует обязательство производителя о форме выхода и ожидание потребителя о форме входа; стык корректен, когда схема выхода производителя совместима со схемой входа потребителя (см. главу 18 о совместимости контрактов). Как спецификация для валидатора — она исполняемый артефакт, по которому код на границе детерминированно решает, принять сообщение или отвергнуть. Одна и та же схема служит и языком соглашения между ролями, и инструментом принуждения этого соглашения.
Схема — не бинарный выбор «есть или нет». Она располагается на спектре строгости, и место на этом спектре — инженерное решение, привязанное к цене ошибки данного типа сообщения и к тому, кто производит нагрузку.
Уровень | Что фиксирует | Что пропускает | Когда уместен
Отсутствие схемы | Ничего; нагрузка — свободный текст | Всё | Только там, где получатель — снова модель, толерантная к форме, а ошибка дешева
Слабая схема | Несколько обязательных полей; остальное свободно | Лишние поля, типы части полей | Вспомогательные сообщения, ранние стадии, где форма ещё не устоялась
Строгая схема | Все поля, типы, обязательность, перечни значений | Структурно валидный, но семантически неверный контент | Сообщения на критическом пути, машинно обрабатываемые без участия модели
Строгая плюс проверки свойств | То же плюс проверяемые инварианты данных (суммы, ссылки, диапазоны) | Только семантику, не сводимую к проверяемому свойству | Необратимые действия, деньги, права, персональные данные
Типичная ошибка — единый уровень строгости для всех сообщений роя. Строгая схема на каждом служебном сообщении — это координационный налог (см. главу 5) без отдачи: дешёвый обмен между двумя толерантными к форме агентами обвешивается валидацией, которая дороже самой передачи. Слабая схема на сообщении, которое машинно запускает необратимое действие, — это латентный сбой, который пройдёт демонстрацию и выстрелит на нестандартном выводе модели. Строгость схемы нужно поднимать туда, где растёт цена структурной ошибки, и опускать там, где получатель сам устойчив к форме.
Есть и второй фактор выбора строгости, специфичный для роя: кто потребитель нагрузки. Если потребитель — снова модель (сообщение от одного агента читается другим как часть его контекста), чрезмерно строгая схема даёт мало: модель толерантна к небольшим отклонениям формы и сама извлечёт смысл. Если потребитель — детерминированный код (сообщение машинно диспетчеризуется, запускает инструмент, пишется в хранилище), схема обязана быть строгой, потому что код не «поймёт» отклонение — он откажет или, хуже, обработает мусор. Граница «модель читает» против «код читает» — главный водораздел требований к строгости схемы внутри одного роя.
Схему можно держать двумя способами, и выбор влияет на эволюцию и проверяемость. Внешняя схема живёт отдельно от сообщения (в реестре схем, в коде валидатора, в спецификации стыка), а сообщение лишь ссылается на её версию через поле конверта schema_version. Самоописывающее сообщение несёт достаточно структуры, чтобы быть разобранным без внешней схемы (например, размеченные поля с явными именами).
Для роя предпочтительна внешняя схема с версией в конверте, по двум причинам. Во-первых, она делает явной и управляемой эволюцию (см. главу 22): изменение схемы — это новая версия в реестре, а не молчаливое изменение формы сообщений. Во-вторых, она отделяет соглашение о форме от конкретного экземпляра: схема — общий артефакт, которым владеют обе стороны стыка, а не свойство, выводимое из случайного сообщения. Самоописание удобно для отладки и для слабоструктурированных каналов, но как единственный механизм оно лишает рой управляемой эволюции: форма дрейфует вместе с поведением модели, и никакого фиксированного соглашения, относительно которого этот дрейф можно заметить, не существует.