Ранас Мукминов – Оркестрация ИИ-агентов. Claude Opus 4.7 (страница 66)
Выход (постусловие). Это то, что агент гарантирует по завершении, если предусловие было выполнено. Здесь два уровня, которые постоянно путают. Первый — структурный: форма результата (схема, набор полей, тип артефакта). Второй — содержательный: семантические гарантии («предложенный патч компилируется», «итоговая сумма равна сумме слагаемых», «в ответе нет персональных данных»). Структурное постусловие проверяется дёшево и почти всегда. Содержательное постусловие — самое дорогое и самое важное место контракта, и именно его агент склонен объявлять выполненным без оснований.
Инварианты (что не меняется никогда). Это свойства, которые истинны до, во время и после работы агента, независимо от входа и выхода. Инвариант не зависит от конкретного вызова — он описывает границу, которую агент не пересекает ни при каких данных. «Не пишет за пределами своей песочницы», «не обращается к эгрессу вне разрешённого списка», «не превышает бюджет в N токенов», «не модифицирует чужие артефакты». Инварианты — это пересечение контракта с правами (см. главу 16) и с безопасностью (см. часть XII): права делают инвариант исполнимым принудительно, контракт делает его явным обязательством.
Различие между постусловием и инвариантом стоит закрепить, потому что оно меняет способ проверки. Постусловие проверяется один раз — на выходе. Инвариант проверяется непрерывно или хотя бы на каждом действии агента: его нельзя «проверить в конце», потому что нарушение инварианта в середине работы уже произошло и могло причинить вред, даже если к финалу состояние выглядит чистым.
Предусловие агента включает не только то, что ему передали явно — сообщение, аргументы, ссылку на артефакт, — но и то, что он подтягивает сам: контекст из общей памяти, состояние внешних систем, результаты собственных наблюдений. Для одиночного агента это было свойством его контура (предмет другой книги); для роя это становится частью контракта, и часто самой хрупкой. Агент-исполнитель, чей контракт молчит о том, какой контекст он читает, на деле зависит от состояния общей памяти, которое в контракт не вписано. Когда это состояние меняется (другой агент перезаписал доску, истёк кэш, изменилась версия внешней системы), предусловие нарушается, хотя явный вход остался прежним.
Из этого следует, что контракт должен фиксировать неявные зависимости не менее явно, чем аргументы. Если роль читает из общей памяти ключ K и опирается на его свойства — это предусловие, и оно должно быть записано и проверено наравне с переданными аргументами. Рой, где агенты молча зависят от непрописанного общего состояния, нельзя ни отлаживать (источник нарушения не виден в контракте), ни безопасно менять по частям (изменение в одном узле незаметно ломает предусловие в другом). Эта связь между контрактом и общим состоянием подробно разбирается в части VII; здесь важно, что неявный вход — полноправная часть предусловия, а не деталь реализации.
Системный промпт и контракт легко спутать, потому что оба описывают поведение агента словами. Разница в адресате и в проверяемости. Промпт обращён внутрь — к модели, он формирует поведение, но ничего не гарантирует: модель может его проигнорировать, переинтерпретировать или быть сбита инъекцией (см. главу 86). Контракт обращён наружу — к остальной системе, и его смысл в том, что система может на него опереться, не доверяя добросовестности агента.
Из этого следует практическое правило: пункт контракта, существующий только как фраза в промпте («всегда возвращай валидный JSON», «никогда не трогай файлы вне каталога задачи»), — это не контракт, а намерение. Контрактом он становится в момент, когда снаружи появляется механизм, проверяющий или принуждающий его: валидатор схемы на выходе, песочница, ограничивающая запись, шлюз, режущий эгресс. Промпт говорит агенту, как себя вести; контракт говорит системе, чему она вправе верить. Хорошая роль содержит и то и другое, но это разные артефакты с разной судьбой при отказе.
Не каждой роли нужен исчерпывающий контракт. Полнота контракта — это инженерный выбор, привязанный к цене ошибки данной роли. Различаются два полюса.
Минимальный контракт фиксирует только структуру входа и выхода и один-два критичных инварианта (обычно границы прав). Этого достаточно для ролей, чья ошибка дешева и локальна: вспомогательный суммаризатор, генератор черновика, агент-разведчик, чей результат всё равно проверит кто-то ниже по конвейеру. Здесь содержательные постусловия делать строгими бессмысленно — проверка обойдётся дороже самой работы.
Полный контракт добавляет содержательные постусловия, явные предусловия и полный набор инвариантов с механизмом принуждения. Он нужен ролям на критическом пути: тому, кто вносит необратимое изменение, тому, чей выход никто не перепроверяет, тому, кто работает с деньгами, правами доступа или персональными данными. Стоимость полного контракта — это стоимость его проверки, и она оправдана ровно там, где цена пропущенной ошибки выше цены проверки.
Между полюсами лежит континуум, и типичная ошибка проектирования роя — выбрать одинаковую полноту контракта для всех ролей: либо обвесить проверками дешёвых исполнителей (координационный налог растёт без отдачи, см. главу 5), либо оставить минимальный контракт у роли на критическом пути (рой проходит демонстрацию и разваливается на проде).
Контракт стоит записывать явно — отдельно от промпта и отдельно от кода обёртки, — потому что его адресат не агент, а инженеры, собирающие и эксплуатирующие рой, и механизмы проверки, привязанные к его пунктам. Форма записи вторична (схема плюс проза, машиночитаемая спецификация, набор аннотаций), важна структура: каждый пункт должен быть отнесён к входу, выходу или инварианту и снабжён указанием, чем он проверяется. Ниже — иллюстративный контракт роли «исполнитель миграции базы данных», роли на критическом пути с необратимым эффектом; он показывает форму, а не предписывает конкретный синтаксис.
# Иллюстративный контракт: agent.db-migrator
вход (предусловие):
- схема входа: { target_version: int, migration_sql: string, dry_run: bool }
проверка: валидатор схемы на входе потребителя
- состояние: текущая версия схемы БД строго меньше target_version
проверка: запрос к таблице версий до начала работы
- предположение: migration_sql синтаксически валиден для целевой СУБД
проверка: разбор парсером СУБД (детерминированно)
выход (постусловие):
структурное:
- схема выхода: { applied: bool, from_version: int, to_version: int, log: string }
проверка: валидатор схемы на выходе производителя
содержательное:
- если applied=true, версия схемы БД равна target_version
проверка: повторный запрос версии после работы (детерминированно)
- набор контрольных запросов к мигрированным данным проходит
проверка: исполнение проверочного набора (детерминированно)
отказ (частичный контракт):
- при невыполнимом предусловии вернуть { applied: false, reason: <код> }
вместо попытки применить миграцию
инварианты (всегда):
- не выполняет операций вне переданного migration_sql
принуждение: роль БД с правами только на объявленные таблицы
- при dry_run=true не коммитит ни одной транзакции
принуждение: соединение в режиме только-чтение на стороне обёртки
- не превышает бюджет в N токенов и M секунд
принуждение: лимиты исполнителя (см. главу 71)
Из этой записи видно главное свойство хорошего контракта: почти каждый пункт сопровождается механизмом проверки или принуждения, и большинство этих механизмов детерминированны. Пункт без механизма — это намерение, а не обязательство (раздел о контракте против промпта выше); если бы рядом с постусловием «версия равна target_version» не стояло «повторный запрос версии», этот пункт был бы пожеланием, на которое рой опирался бы вслепую. Запись также делает явным то, что в промпте растворено: где проходит граница ответственности роли и где начинается ответственность соседей.
В обычном программном компоненте контракт проверяется тем, что реализация детерминирована и инспектируема: можно прочитать код, прогнать тип-чекер, написать тест, который при тех же входах даёт тот же результат. У агента не выполняется ни одно из этих условий. Реализация — веса модели плюс промпт плюс контекст плюс случайность сэмплирования; один и тот же вход даёт разные выходы; «прочитать реализацию» невозможно. Это значит, что контракт агента в принципе не может быть доказан анализом самого агента. Его можно проверять только по наблюдаемому поведению — по входам и выходам на границе.
Отсюда главный сдвиг по сравнению с проверкой обычного кода: контракт агента — это не свойство агента, а свойство стыка. Он живёт не внутри исполнителя, а на границе между ним и системой, и проверяется внешними механизмами, которые агенту не подконтрольны. Если проверка контракта реализована тем же агентом, который должен ему следовать (например, «модель сама себя просит перепроверить JSON»), это не проверка контракта, а ещё один недетерминированный шаг, на который распространяются те же сомнения.
Пункты контракта различаются по тому, насколько дёшево и надёжно их проверить. Полезно расположить их на лестнице — от тривиально проверяемых до принципиально непроверяемых, — потому что место пункта на этой лестнице определяет, можно ли вообще на него опираться.