Как защитить multi-agent систему от регрессий своих же агентов: три слоя
3 июня 2026 года я открыл 4bos.ru/blog/ чтобы сделать скриншот для презентации Terra Business Club. На экране была голая страница — 121 статья списком на белом фоне, без шапки, без футера, без стилей. Как будто кто-то взял и вырвал всю обвязку сайта.
Два дня назад SEO-агент получил задачу оптимизировать скрипт генерации индекса блога. Агент оптимизировал честно: убрал всё что посчитал лишним. Шапка, футер, подключение стилей — они не упоминались в задаче явно. Агент их убрал. Каждая отдельная статья открывалась нормально. Индекс блога я смотрел редко. Поэтому 48 часов этот регресс работал в проде незамеченным.
У меня 26 агентов. Любой из них в любой момент может сделать то же самое с другим куском инфраструктуры. И это не ошибка агента — он выполнил задачу. Это архитектурная проблема: система не была защищена от собственных исполнителей.
Откуда берётся проблема: агент не знает что он не знает
Большинство людей, сталкиваясь с ошибкой AI-агента, ищут проблему в промпте: «написал недостаточно чётко», «не уточнил требования», «надо было добавить ограничение». Это ловушка. Промпт-инженерия решает класс задач где агент не понял что делать. Она не решает задачи где агент понял правильно, но не имел контекста о последствиях.
SEO-агент получил задачу с полным набором требований к оптимизации скрипта. Он её выполнил корректно в рамках того, что видел. Проблема в том, что он не видел: что этот скрипт генерирует публичную страницу сайта, что шапка и футер — это не «лишний» HTML, а часть дизайн-системы, что 121 статья живёт за этим индексом.
Агент работает в пузыре своей задачи. Он не знает что не знает. Это фундаментальное ограничение любого автономного исполнителя — не только AI-агента. Разработчик в команде, не знакомый с кодовой базой, может сделать то же самое. Разница в скорости: агент совершает сотни операций в час.
Почему запреты в инструкциях не работают
Когда я починил шаблон, первая мысль была написать в инструкции агента: «не трогай шаблоны блога». Это не работает по трём причинам, которые проявляются по-разному.
Первая причина — контекстное смещение. Агент читает инструкцию в начале сессии. При длинных задачах или многошаговых операциях ранний контекст вытесняется. К моменту когда агент добирается до записи файла, он уже не держит в активном контексте запрет из начала инструкции. Это не баг — это природа работы с большим контекстом. Тест простой: возьмите любой запрет из начала длинных инструкций и проверьте соблюдается ли он ближе к концу многошаговой задачи. Соблюдается хуже.
Вторая причина — изоляция правила. Запрет в инструкциях SEO-агента не покрывает остальные 25 агентов. Любой из них может получить аналогичную задачу: «исправь шаблон», «обнови скрипт», «оптимизируй генератор». И каждый работает со своей копией инструкций.
Третья причина — интерпретация. Агент видит запрет «не трогай шаблоны» и может решить что его задача под это определение не подпадает. «Я не трогаю шаблон, я оптимизирую скрипт который генерирует файл из шаблона» — технически разные вещи с точки зрения буквальной интерпретации.
Запреты — это намерение. Намерение зависит от интерпретации. Для критичных компонентов нужны механизмы, которые делают ошибку физически невозможной независимо от намерений.
Слой первый: sentinel в скрипте
Sentinel — это проверка перед деструктивной операцией. Скрипт не может перезаписать файл если результат нарушает инварианты. Это 15 строк кода, которые работают вечно без обслуживания.
В rebuild_blog_index.py, который перезаписывает /opt/4bos-static/blog/index.html, добавил проверку перед записью. Скрипт читает сгенерированный HTML и ищет четыре обязательных маркера: class="header" — шапка сайта, class="footer" — подвал, /assets/css/style.css — основная таблица стилей, class="tg-float" — плавающая кнопка Telegram. Если хоть один маркер отсутствует — скрипт падает с явной ошибкой и не перезаписывает файл.
Вот реализация:
REQUIRED_MARKERS = [
'class="header"',
'class="footer"',
'/assets/css/style.css',
'class="tg-float"'
]
def validate_template(html: str, source: str = 'unknown') -> None:
missing = [m for m in REQUIRED_MARKERS if m not in html]
if missing:
raise ValueError(
f"[SENTINEL] Template validation failed. "
f"Missing markers: {missing}. "
f"Source: {source}. File NOT written."
)
new_html = generate_index(articles)
validate_template(new_html, source='rebuild_blog_index')
with open(INDEX_PATH, 'w') as f:
f.write(new_html)
Ключевое свойство sentinel: он fail-loud. Скрипт кричит об ошибке с явным сообщением, а не молча пропускает. Агент, который пытается записать сломанный шаблон, получит ошибку в логах, задача не завершится успешно, я увижу failed run. Вместо 48 часов до обнаружения — несколько секунд.
Когда писать sentinel: любая операция которая перезаписывает публичный файл, удаляет записи в базе данных, изменяет конфигурацию работающего сервиса. Не надо sentinel на каждое действие — только на те где регресс незаметен сразу и критичен для пользователя или для другой части системы.
Как найти точки для sentinel в своей системе
Практический метод: перебери все операции которые агенты делают с финальными артефактами. Финальный артефакт — это то что видит пользователь или от чего зависит работа другой части системы.
Для каждой операции задай вопрос: «если здесь пройдёт ошибка, через сколько времени я об этом узнаю?» Если ответ — «через день», «через неделю», «случайно» — это кандидат на sentinel.
В моей системе такие точки: запись HTML-файлов сайта (обнаружение через часы), изменение nginx конфигов (обнаружение когда перезапустится сервер), обновление скриптов которые читают продовую базу данных (обнаружение на следующем cron-прогоне), запись в таблицы с финансовыми данными инвесторов (обнаружение при следующем отчёте — через несколько дней). На каждую добавил проверку инвариантов перед записью.
«Юрий Солар, основатель Solar OS: главный вопрос при проектировании защиты — не "как запретить агенту ошибиться", а "через сколько времени я узнаю об ошибке". Если ответ больше часа — там нужен sentinel.»
Слой второй: правило в конституции с прецедентом
Sentinel защищает конкретный скрипт. Но инфраструктура шире: есть другие скрипты, другие шаблоны, другие операции которым нужен контекст для принятия решений. Для всего этого нужен второй слой.
Конституция — это документ /opt/constitution.md, который является источником правды для всей системы агентов. Он инжектируется в начало AGENTS.md каждого агента и обновляется каждые 15 минут. В него добавил запрет:
Публикация статей в блог 4bos.ru — только через agent_publish.py. Прямая правка blog/index.html, rebuild_blog_index.py, blog_template.py — запрещена. Прецедент 01.06: SEO-агент переписал скрипт с упрощённым шаблоном без header/footer — регресс ушёл в прод, 121 статья 48 часов отображалась без обвязки сайта.
Формулировка важна. «Прецедент 01.06» — это не просто запрет, это история. Агент, видящий что это правило появилось из реального инцидента, понимает ставки. Он не просто следует правилу — он понимает почему оно существует и может применять его к граничным случаям которые явно не описаны.
Это принципиально отличается от запрета «не трогай шаблоны». Агент с контекстом про инцидент, получив задачу «улучши скрипт блога», задаст уточняющий вопрос. Агент без контекста — просто сделает.
Что ещё стоит добавить в конституцию: любая операция которая уже приводила к инциденту. Конституция становится коллективной памятью всех агентов об ошибках системы. Каждый новый инцидент расширяет её на одну строку. Через полгода это живая документация реальных отказов.
Слой третий: автораспределение по всем агентам
Правило в конституции работает только если оно актуально у всех агентов. Это третий слой — и самый важный для масштабируемости.
constitution_distribute.py запускается по cron каждые 15 минут. Скрипт: читает /opt/constitution.md как источник правды, находит все AGENTS.md во всей файловой системе Paperclip, в каждом файле находит маркеры CONSTITUTION-START и CONSTITUTION-END, заменяет содержимое между маркерами актуальной версией, пишет лог с timestamp и списком обновлённых файлов.
После добавления нового правила оно попадает во все AGENTS.md за максимум 15 минут. Обновляю один документ — получаю синхронизацию по всей системе. Это единственный способ держать 26 агентов консистентными без ручного труда.
Структура маркеров в каждом AGENTS.md:
<!-- CONSTITUTION-START — auto-distributed -->
[§0 Идентичность и голос]
[§1 Основной продукт — клуб]
[§13 Запреты — свод всех правил с прецедентами]
<!-- CONSTITUTION-END -->
# Инструкции конкретного агента
<!-- AGENT_CONTENT_START -->
[зона ответственности, KPI, инструменты]
<!-- AGENT_CONTENT_END -->
Агент видит конституцию в начале каждой сессии. Если задача противоречит конституции — у него есть контекст для отказа, уточнения или эскалации к CEO-агенту.
Почему 15 минут оптимальный интервал: достаточно быстро чтобы правило применялось к следующей задаче любого агента, достаточно редко чтобы не создавать лишний I/O. Для критичных обновлений можно запустить скрипт вручную немедленно.
Параллельный инцидент: nginx и осколки удалённых сервисов
В тот же день обнаружил что 4bos.online не отвечает. Инвестор-дашборд, нужный для презентации, лежал. Это другой класс проблемы — не агент сломал что-то активно, а осколок прошлого лежал и ждал своего часа.
Причина: конфигурационный файл nginx от Chatwoot. Сервис удалили полгода назад — перешли на другой инструмент для поддержки. Удалили правильно: остановили процесс, убрали из systemd. Но nginx-конфиг в /etc/nginx/sites-enabled остался. Полгода он висел и ничему не мешал. Потом что-то изменилось в конфигурации сервера, старый конфиг начал создавать конфликт, и nginx при старте завершался с ошибкой.
Диагностика заняла 20 минут. Решение — 2 минуты. Это несоразмерно, и именно несоразмерность — маркер системной проблемы. Если диагностика занимает в 10 раз больше лечения, значит причина не была встроена в процесс.
Добавил в конституцию агентов явный чеклист при отключении любого сервиса: остановить процесс, удалить nginx конфиг из sites-enabled, nginx -t для проверки, reload. Добавил quarterly-задачу: аудит sites-enabled, сопоставление с работающими сервисами. Теперь любой агент, который отключает сервис, получает этот чеклист в контексте своих инструкций. И я сам тоже.
Система на 26 агентов: что работает, что не работает
За год работы с мультиагентной системой я прошёл через несколько конфигураций управления. От ручных инструкций в каждом агенте (не масштабируется при изменениях), к shared документу который агенты читают по запросу (не гарантирует что прочитают), к автодистрибуции конституции (текущий вариант).
Что работает на 26 агентов: единая точка правды (конституция), автораспределение по cron, sentinel на критичных операциях, иерархия с CEO-агентом как точкой маршрутизации, явные прецеденты в правилах.
Что не работает: ручное обновление инструкций каждого агента, запреты без объяснения почему, доверие что агент «запомнит» что говорили месяц назад, попытка покрыть все edge cases в промпте.
При переходе от 26 к 50 и более агентов нужны иерархические компании: несколько CEO-агентов с собственными командами, каждая с отдельной конституцией. Единая конституция на 50 агентов становится слишком объёмной для эффективного восприятия в начале сессии.
Как тестировать sentinel до попадания в прод
Sentinel который никогда не срабатывал — это неизвестная переменная. Он может быть написан правильно и проверять не то. Поэтому после добавления sentinel нужно убедиться что он работает намеренно.
Три теста на каждый sentinel. Первый: передай намеренно сломанный input — убедись что скрипт падает с ошибкой и не записывает файл. Для rebuild_blog_index.py: создай шаблон без class=header, запусти скрипт, убедись что он упал и файл не изменился. Второй: передай правильный input — убедись что скрипт завершается успешно и файл записан корректно. Третий: проверь что сообщение об ошибке достаточно информативно чтобы понять что сломано без чтения кода.
Третий тест важнее первых двух. Я видел sentinel'ы которые падали с сообщением «validation error» без деталей. Пока разбираешься какой именно маркер потерялся — теряешь 15 минут. Хороший sentinel пишет: «Missing markers: class=footer. Source: rebuild_blog_index. File index.html NOT written.» Это самодиагностирующийся сбой.
Ещё важный момент: sentinel должен быть идемпотентным. Если запустить скрипт дважды с правильным input, второй прогон не должен ломать результат первого. Это кажется очевидным, но при написании validate-функций легко добавить side effect который нарушает идемпотентность.
Мониторинг: как узнать что sentinel сработал
Sentinel кричит об ошибке в stdout/stderr. Но если никто не читает stdout — кричит в пустоту. Для автономных агентов нужна доставка сигнала до человека.
В моей системе: все failed runs агентов попадают в дайджест который собирается в 07:30 WITA ежедневно. Если задача агента завершилась с ошибкой, я вижу это утром. Для критичных sentinel'ов добавил прямой алерт в Telegram: скрипт ловит исключение от validate-функции и пишет сообщение в чат. Это сокращает время реакции с нескольких часов до нескольких минут.
Структура алерта простая: что сломалось, где, кто запустил, что делать дальше. Если sentinel сработал при запуске агентом — в алерте должен быть идентификатор задачи. Если при cron — метка cron-задачи. Это избавляет от детективной работы «а кто вообще запускал rebuild сегодня ночью».
Дополнительно: раз в квартал проверяю что sentinel'ы не начали тихо отключаться. Бывает что в процессе рефакторинга validate-функция переезжает в другое место и вызов убирают «временно». Аудит: просматриваю все публичные скрипты на наличие validate-вызова перед деструктивными операциями. Если вызов пропал — добавляю обратно и добавляю тест.
Практический чеклист: как внедрить три слоя
Для команды из 5-10 агентов можно начать за неделю. Для крупной системы из 20-30 агентов тот же план работает, но добавляется шаг с инвентаризацией.
Прежде чем начать: сделай список всех мест где агенты пишут данные. Не только файловая система — база данных, nginx конфиги, systemd unit-файлы, S3 если используешь. Это занимает час, но без этого список sentinel-кандидатов будет неполным. Пропущенный критичный артефакт — это регресс который произойдёт именно там где проверки нет.
День 1-2: инвентаризация. Список всех финальных артефактов (файлы, таблицы БД, конфиги). Для каждого — оценка «через сколько узнаю об ошибке». Приоритизация по этому критерию. Сначала делаешь топ-5 по «самый долгий обнаружение + самый критичный», именно на них идут первые sentinel'ы.
День 3-4: sentinel на топ-3. Начать с трёх самых критичных операций. Написать validate-функцию для каждой. Добавить в скрипты перед записью. Протестировать намеренной ошибкой — убедиться что sentinel кричит с понятным сообщением.
День 5: конституция. Создать /opt/constitution.md или аналог. Добавить существующие правила и известные прецеденты. Формат: запрет + «прецедент DD.MM: что произошло».
День 6-7: дистрибуция. Написать distribute.py с маркерами CONSTITUTION-START/END. Добавить cron на 15 минут. Проверить что все AGENTS.md обновились. Убедиться что новый агент добавляет маркеры по умолчанию при создании.
После этого: каждый новый инцидент → добавление прецедента в конституцию → автораспределение через 15 минут. Система самообучается от своих ошибок, а не только от твоих инструкций.
День 3-4: sentinel на топ-3. Начать с трёх самых критичных операций. Написать validate-функцию для каждой. Добавить в скрипты перед записью. Протестировать намеренной ошибкой — убедиться что sentinel кричит.
День 5: конституция. Создать /opt/constitution.md или аналог. Добавить существующие правила и известные прецеденты. Формат: запрет + «прецедент DD.MM: что произошло».
День 6-7: дистрибуция. Написать distribute.py с маркерами CONSTITUTION-START/END. Добавить cron на 15 минут. Проверить что все AGENTS.md обновились.
После этого: каждый новый инцидент → добавление прецедента в конституцию → автораспределение через 15 минут. Система самообучается от своих ошибок.
Итог
Регресс с блогом я заметил через 48 часов. После внедрения трёх слоёв sentinel поймал бы ошибку за секунды: агент получил бы failed run, я бы увидел алерт сразу.
За две недели после внедрения sentinel'ов зафиксировал три случая когда они сработали: при тестовом запуске скрипта с неправильными параметрами, при попытке агента записать HTML с незакрытым тегом, при моём собственном эксперименте с шаблоном. Во всех трёх случаях файл не был перезаписан, ошибка была немедленно видна в логах.
Три слоя защиты multi-agent инфраструктуры: sentinel в скриптах (fail-loud проверка перед деструктивной операцией, независимая от намерений исполнителя), конституция с прецедентами (агент знает что нельзя, почему это важно и какой реальный инцидент это вызвал), автораспределение по cron (правила актуальны у всех 26 агентов через 15 минут после обновления без ручного труда).
Ключевой сдвиг: от «объяснять каждому агенту каждый раз» к «делать ошибку невозможной технически». Запреты — это намерение, которое зависит от интерпретации. Sentinel — это механизм, который не зависит от неё.
Ещё одно наблюдение из практики: sentinel'ы меняют поведение агентов в лучшую сторону. Когда агент знает что его задача может упасть из-за конкретной проверки, он начинает явно сообщать о том что собирается делать — перед тем как сделать. Это не запрограммировано, это следствие того что агент работает в системе с явными инвариантами. Вместо молчаливого выполнения — «собираюсь перезаписать index.html, проверяю маркеры». Стало удобнее контролировать что происходит.
Если хотите посмотреть как устроена конституция изнутри, как выглядит distribution скрипт, и какие ещё классы sentinel'ов я добавил в свою инфраструктуру — всё это в клубе «Solar — внутрянка». Реальные артефакты: скрипты, AGENTS.md, дашборды. Бери и адаптируй: https://4bos.ru/inside/
Другие статьи по теме: AGENTS.md — как писать инструкции для AI-агента, AI-агенты для бизнеса без найма.
— Solar OS.