Ранас Мукминов – Оркестрация ИИ-агентов. Claude Opus 4.7 (страница 68)
Зеркальный к молчаливому нарушению постусловия режим отказа — чрезмерно тесное предусловие. Желая обезопасить агента, его контракт делают требовательным: «вход обязан быть полностью нормализован, без пропусков, в каноническом формате». Локально это упрощает агента, но глобально делает рой хрупким: чем уже предусловие, тем больше реальных входов в него не попадает, тем чаще стык требует адаптера и тем выше шанс, что producer на нетипичном случае выдаст вход вне контракта. Тесное предусловие перекладывает сложность с агента на стыки и на соседей, и эта сложность нигде не исчезает — она расходится по рою в виде адаптеров и отказов. Правило вариантности (предусловие ослаблять, постусловие усиливать) — это не только условие безопасной замены, но и направление здоровой эволюции контракта: устойчивый рой со временем расширяет то, что его узлы готовы принять, и сужает то, на что нельзя положиться, а не наоборот.
Когда контракты не стыкуются напрямую, между агентами помещают адаптер: узел, чей контракт — превратить выход producer в допустимый вход consumer. Адаптер может быть детерминированным кодом (переформатирование, переименование полей, дополнение значений по умолчанию) или, в худшем случае, ещё одним агентом (если преобразование требует понимания смысла). Различие принципиально: детерминированный адаптер сам по себе надёжен и проверяем, агентный адаптер вносит ещё один недетерминированный стык со своим контрактом и своими режимами отказа.
Практическое правило: предпочитать стыки, не требующие адаптера (контракты спроектированы совместимыми), затем детерминированные адаптеры, и лишь в крайнем случае — агентные. Рой, где половина узлов — агенты-переводчики между несовместимыми контрактами соседей, платит за это и токенами, и латентностью, и умножением точек отказа. Несовместимость контрактов — это не данность, которую обслуживают адаптерами, а дефект проектирования ролей, который дешевле устранить в контрактах.
Совместимость контрактов выглядит по-разному на трёх каналах, через которые агенты обмениваются информацией (см. главу 23), и это влияет на то, где и как проверять контракт.
Через сообщения контракт — это схема сообщения плюс семантические гарантии его полей; стык явный, producer и consumer известны, проверку удобно разместить на границе сообщения. Через общую память (blackboard, см. главу 11) контракт распадается: писатель и читатели разъединены во времени и не знают друг о друге, постусловие записи должно держаться без знания о том, кто и когда прочитает, а предусловие чтения — без знания о том, кто записал. Это самый трудный для контрактов канал: гарантии должны быть свойствами самих данных в общем пространстве, а не свойствами конкретного обмена. Через артефакты (файлы, ветки, документы) контракт — это инварианты артефакта (компилируется, проходит схему), проверяемые тем, кто его подхватывает; здесь естественны контрактные проверки в момент handoff (см. главу 47).
В системе, живущей долго и меняющейся по частям, главная угроза совместимости — независимая эволюция контрактов на двух концах стыка. Producer обновили — он стал отдавать новое поле или изменил семантику старого; consumer не трогали — он продолжает читать по старому контракту. Структурно может ничего не сломаться (лишнее поле проигнорировано), а семантически стык уже разъехался. Это та же проблема эволюции схем, что и в распределённых системах, и решается тем же принципом: совместимость вперёд и назад на стыке, явное версионирование контракта и недопущение «тихих» изменений семантики при неизменной структуре. Глубоко тема разобрана в главе 22; здесь важно зафиксировать связь: контракт — это то, что версионируется, а совместимость контрактов — то, что версионирование защищает.
Поскольку контракт агента нельзя доказать анализом реализации, единственный способ удержать его от дрейфа — проверять поведение на границе при каждом изменении. Это переносит на рой практику контрактного тестирования из распределённых систем, с двумя поправками на недетерминизм.
Поправка первая: тест контракта агента проверяет не точное равенство выхода, а соблюдение постусловия. Тест не утверждает «на вход X агент вернёт ровно Y» — это ложно для недетерминированного узла. Тест утверждает «на наборе входов агент в доле не ниже целевой возвращает выход, удовлетворяющий постусловию, и ни разу не нарушает инвариант». Метрика контрактного теста — частота соблюдения, а не побитовое совпадение.
Поправка вторая: набор входов должен включать вход вне контракта, чтобы проверить не только «делает обещанное на хорошем входе», но и «честно отказывается на плохом» (частичный контракт, раздел выше). Агент, проходящий тесты только на типичных входах, не проверен на самом ценном пункте контракта — поведении на границе.
Контрактные тесты — это исполнимая часть контракта, и они служат двум целям сразу: ловят дрейф при обновлении агента (новая версия модели может тихо изменить поведение) и документируют контракт точнее, чем проза, потому что их нельзя оставить устаревшими незаметно — они начинают падать. Их место в эксплуатации роя — в воротах изменений: ни один агент не обновляется в продакшене, не пройдя контрактные тесты своих стыков.
Тесты проверяют контракт до запуска роя на репрезентативной выборке входов. Но рой недетерминирован и встречает в продакшене входы, которых не было в выборке, поэтому проверки контракта нужны и во время работы — как непрерывный механизм, а не разовое событие. Это отличает контракт агента от контракта обычного компонента, который, пройдя тесты и тип-чекер, считается надёжным: у агента прохождение тестов снижает риск, но не снимает его, и часть проверок остаётся обязательной на каждом реальном вызове.
Различие между тестами и работающей проверкой удобно держать явным. Тест отвечает на вопрос «достаточно ли часто агент соблюдает контракт, чтобы его подключать»; работающая проверка отвечает на вопрос «соблюдён ли контракт на этом конкретном вызове, прежде чем результат пойдёт дальше». Дешёвые детерминированные пункты контракта (структура, синтаксис домена, проверяемые свойства из нижних уровней лестницы) проверяются на каждом вызове в продакшене — это и есть тот внешний механизм, который превращает обещание в обязательство. Дорогие пункты (семантика) на каждом вызове проверить нельзя, поэтому по ним собирают статистику соблюдения: доля выходов, прошедших выборочную проверку критиком или человеком, становится метрикой здоровья контракта (см. часть XI) и сигналом тревоги при её падении.
Этот непрерывный контроль смыкается с наблюдаемостью роя: нарушение контракта на стыке — это событие, которое нужно записать с привязкой к производителю, потребителю и конкретному входу, чтобы постмортем (см. главу 82) мог восстановить, где именно обещание разошлось с поведением. Рой без записи нарушений контрактов отлаживается вслепую: видно, что результат плох, но не видно, какой стык его испортил.
Контракт — это связующая абстракция, и полезно видеть, как он смыкается с соседними уровнями сквозной модели, не дублируя их.
С топологией (часть II): топология задаёт, какие стыки вообще существуют (кто кому передаёт), контракт — что именно гарантируется на каждом стыке. Fan-out/fan-in (см. главу 8) предъявляет особое требование: контракты всех воркеров должны быть достаточно однородны, чтобы их выходы агрегировались на fan-in (см. главу 37); расходящиеся постусловия воркеров делают сборку результата невозможной.
С коммуникацией (часть IV): схема сообщения (см. главу 25) — это структурная часть контракта; семантика доставки (см. главу 29) определяет, при каких условиях контракт вообще достигает потребителя (потеря или дублирование сообщения нарушают предусловие до того, как агент начал работать).
С координацией (часть VI): идемпотентность (см. главу 43) — это инвариант контракта относительно повторов («повторный вызов с тем же ключом не меняет результат»); без него повтор зависшей задачи (см. главу 72) нарушает контракт, даже если каждый отдельный вызов ему следует.
С надёжностью (часть X): частичный контракт с честным отказом — основа graceful degradation (см. главу 73); инвариант на бюджет — основа изоляции отказов (см. главу 71), не дающая одному агенту истощить ресурс роя.
С безопасностью (часть XII): инварианты прав — это контрактная форма least privilege (см. главу 88); внешнее принуждение инвариантов — единственная защита контракта от инъекции (см. главу 86).
Эта связность и есть причина, по которой контракт стоит в части о ролях, а не в каждой из перечисленных частей по кусочку: контракт — это единый язык, на котором роль предъявляет себя всем остальным уровням системы. Где у роли нет контракта, там каждый из этих уровней вынужден угадывать поведение соседа, и рой держится на совпадении ожиданий, а не на проверяемых обязательствах.
— Контракт агента — это обязательство на трёх уровнях: вход (предусловие — при каких условиях агент работает корректно), выход (постусловие — что гарантировано на выходе, структурно и содержательно) и инварианты (что не нарушается никогда, независимо от вызова). Это взгляд на роль снаружи, дополняющий взгляд изнутри (промпт, инструменты, права).