Guardrails до автономии: чек-лист для AI-агентов, которые лезут в продакшн
Промпт - это не guardrail. Разбираю слой enforcement, который я пишу кодом до того, как агент получит доступ к данным клиента: allowlist инструментов, ключи идемпотентности, бюджеты, dry-run, гейты подтверждения и воспроизводимые трейсы.
Pavel Duglas
AI Automation & MVP Architect
В прошлом квартале я сдавал клиенту агента для холодного аутрича. В системном промпте было два правила: никогда не писать одному лиду дважды и не отправлять ничего вне интервала 9:00–18:00 по местному времени. На третий день он отправил 41 дубль в два часа ночи. Промпт был нормальный. Модель была нормальная. Проблема была в архитектуре: я написал правила там, где надо было написать код.
Это самая частая ошибка в агентных проектах, которую я вижу сейчас. Команда две недели крутит промпты и ноль дней строит слой принуждения. А потом обнаруживает, что вероятностная система, которую вежливо попросили вести себя хорошо, рано или поздно поведёт себя плохо. Ниже - чек-лист, который я прогоняю перед тем, как агент получит токен с правом записи в живую систему.
Базовый принцип: модель предлагает, код решает
Вывод агента - это заявка, а не действие. Всё, что производит модель, должно проходить через детерминированный код, который может её отклонить. Если ограничение реально важно - деньги, дедупликация, rate limit, удаление данных - оно живёт в функции с if, а не в абзаце текста на английском.
По-другому: считайте, что модель - это талантливый, но слегка выпивший подрядчик с добрыми намерениями и без памяти. Вы бы не выдали такому человеку креды к продакшн-базе с сообщением в Slack «пожалуйста, поаккуратнее». Вы бы давали ему по одному инструменту за раз и проверяли работу.
Слой 1: allowlist инструментов на уровне задачи, а не агента
Большинство фреймворков предлагает регистрировать инструменты на агенте. Это уже слишком грубо. Саппорт-агент, у которого есть refund_order, - это саппорт-агент, который однажды сделает возврат посреди разговора про сроки доставки.
Я ограничиваю набор инструментов по типу задачи, и он резолвится до старта прогона:
TOOLSETS = {
"answer_question": ["search_docs", "get_order_status"],
"process_refund": ["get_order", "create_refund_request"], # заявка, не возврат
"update_address": ["get_order", "propose_address_change"],
}
def tools_for(intent: str) -> list[Tool]:
return [TOOLS[name] for name in TOOLSETS[intent]]
Обратите внимание на нейминг: create_refund_request, а не refund. Пишите инструменты, которые кладут намерение в очередь, а исполняет его отдельный скучный воркер. Одна эта привычка убирает процентов 70 радиуса поражения: теперь каждая запись идёт через место, которое вы контролируете, логируете и можете придушить по rate limit.
Слой 2: подписи инструментов, безопасные по построению
Инструмент с параметром «свободный текст» - это дырка в системе. Сравните:
# плохо: модель пишет SQL, вы молитесь
def query_db(sql: str) -> list[dict]: ...
# хорошо: модель выбирает из закрытого множества
def get_orders(customer_id: int,
status: Literal["pending", "paid", "shipped"],
limit: int = 20) -> list[dict]: ...
Энумы, целочисленные ID, ограниченные диапазоны. Валидация через Pydantic (или zod), и на ошибке возвращайте структурированный ответ, чтобы модель могла сама себя починить: {"error": "limit must be <= 50", "retryable": true}. Расплывчатое "invalid input" приводит к тому, что модель начинает выдумывать новые названия параметров и жечь ваш бюджет шагов.
Слой 3: ключи идемпотентности и журнал записей
У моего ночного инцидента с дублями была скучная причина: таймаут сети, ретрай и отсутствие идемпотентности. Агент «знал», что уже писал этому лиду, но память, к которой он обращался, - это его собственный контекст, который к тому моменту подрезали.
Каждый инструмент записи получает детерминированный ключ, выведенный из бизнес-смысла, а не из UUID:
key = sha256(f"outreach:{lead_id}:{campaign_id}".encode()).hexdigest()
if ledger.exists(key):
return {"status": "already_done", "at": ledger.get(key).created_at}
ledger.reserve(key) # реальную работу делает unique index в Postgres
try:
result = send_message(...)
ledger.commit(key, result)
except Exception:
ledger.release(key)
raise
Guardrail здесь - это уникальный индекс. Промпт - это пожелание. Если журнал живёт в той же транзакции, что и эффект, дубли становятся структурно невозможными, а не статистически маловероятными.
Слой 4: четыре бюджета, все - вне модели
Агенты падают громко, а зацикливаются дорого. Я ограничиваю четыре вещи на прогон:
| Бюджет | Типичное значение | Что предотвращает |
|---|---|---|
| Шаги (вызовы инструментов) | 12–20 | Бесконечные циклы «план → переплан» |
| Wall clock | 90 с интерактив, 10 мин батч | Зависшие HTTP-запросы, повисшие браузеры |
| Токены | 60k–150k на прогон | Раздувание контекста сырым HTML |
| Деньги | $0.15 на тикет саппорта | Смерть от тысячи ретраев |
Когда бюджет исчерпан, второго шанса нет: агент отдаёт частичный результат и reason code. budget_exceeded:steps в логах - это продуктовый сигнал, обычно он значит, что какой-то инструмент возвращает мусор, который модель не может распарсить.
Про деньги: я считаю расход на прогон, а не за месяц. Месячные дашборды прячут тот один воркфлоу, который стоит в 40 раз дороже остальных. У меня была классификация документов, где один битый PDF съел больше, чем предыдущие 900 документов вместе: экстрактор упорно возвращал одну и ту же тридцатитысячную стену лигатурного шума, а модель упорно пыталась её осмыслить.
Слой 5: dry-run как полноценная фича, а не костыль
Каждый инструмент записи у меня принимает флаг dry_run, протянутый из контекста прогона. В режиме dry-run инструмент валидирует всё, логирует ровно тот payload, который отправил бы, и возвращает правдоподобный фейковый ответ.
Это не приятная мелочь для тестов, это способ выходить в прод. Новый воркфлоу несколько дней крутится в dry-run на реальном продакшн-трафике. Я читаю предложенные действия как diff. Примерно каждый третий агент получает изменение в дизайне на этой фазе - обычно потому, что он хочет сделать что-то разумное, под что я не сделал инструмент, или что-то безумное, что я не додумался запретить.
Слой 6: гейты подтверждения на необратимом
Разложите действия по трём корзинам:
- Обратимые и дешёвые - черновики текста, теги, чтение. Полная автономия.
- Обратимые, но видимые снаружи - отправить сообщение, поменять статус. Автономия с rate limit и kill switch.
- Необратимые или денежные - возвраты, удаления, выплаты, массовая рассылка. Подтверждение человеком, всегда, пока не накопится несколько месяцев чистой статистики.
UI подтверждения не обязан быть красивым. Мой дефолт - сообщение в Telegram с инлайн-кнопками Approve/Reject, в callback_data лежит ID действия из очереди, плюс 30-минутный TTL, чтобы ничего не висело вечно. Десять минут рабочего дня человека дешевле одной неправильно проведённой выплаты.
Слой 7: воспроизводимые трейсы и проверки без LLM
Логируйте каждый прогон структурированным трейсом: вход, версия модели, каждый вызов инструмента с аргументами и результатом, тайминги, стоимость, финальный исход, reason code. Складывайте JSONL в объектное хранилище - дёшево и гребётся grep’ом. Локально я нарезаю это SQLite’ом.
А дальше - и вот этот шаг пропускают почти все - напишите детерминированные ассерты по трейсам. Чтобы поймать большинство регрессий, LLM-судья не нужен:
- Был ли вызов инструмента вне allowlist? (Должно быть невозможно. Проверяйте всё равно.)
- Были ли больше двух ретраев одного инструмента с идентичными аргументами?
- Больше 5% прогонов заканчиваются
budget_exceeded? - Агент выдал финальный ответ, вообще не вызвав retrieval? (Классический признак тихой галлюцинации.)
- P95 по числу шагов растёт после смены модели или промпта?
Это гоняется в CI по фикстуре из 50–100 записанных входов и в продакшне как часовые агрегаты. Дёшево, быстро, без флаки. LLM-as-judge оставьте для качества ответов, где реально нужна семантика, и держите его вне критического пути.
Сверка: считайте отчёт агента художественным вымыслом
Фраза агента «я обновил заказ» - это утверждение, а не факт. Для любого воркфлоу с внешней системой я поднимаю job сверки, который сравнивает то, что заявляет журнал агента, с тем, что говорит source of truth. Классика - статус платежа: у вас paid, в магазине pending, и одиннадцать дней никто этого не видит.
Job скучный и на 60 строк. Тянет обе стороны за последние N часов, диффит по бизнес-ключу, кидает расхождения в канал. Каждая нетривиальная автоматизация, которую я сдавал, поймала что-то реальное в первый месяц - и обычно это был молча отвалившийся вебхук, а вовсе не сбой AI.
Лестница выката
Последовательность, которую я использую и по которой не перепрыгиваю ступени:
- Shadow - агент работает на реальных входах, все записи в dry-run. Каждый день читаете диффы.
- Assisted - агент предлагает, человек нажимает «подтвердить». Мерите approval rate; ниже 85% - возвращайтесь в shadow.
- Autonomous, narrow - полная автономия только для самой дешёвой обратимой корзины, жёсткие лимиты, kill switch в конфиге, который переключается без деплоя.
- Autonomous, wide - расширяете корзину по одному типу действия, каждый со своими двумя неделями метрик.
Все семь слоёв на новом проекте стоят мне примерно полтора дня, и модуль ledger/budget/trace я переношу от клиента к клиенту. В этом весь смысл: guardrails - это инфраструктура, а не проектная изобретательность. Промпт вы перепишете пятьдесят раз. Слой принуждения - это то, что позволяет спать, пока агент работает.
Если вы на этой неделе собираетесь выдать агенту продакшн-креды, сделайте сначала одну вещь: добавьте флаг dry_run и прочитайте день предложенных действий. Не пожалеете.
Вопросы и ответы
Нельзя ли просто прописать все ограничения в системном промпте и обойтись без кода?
Нет. Промпт влияет на вероятность поведения, но ничего не гарантирует. Достаточно одного таймаута, ретрая, подрезанного контекста или неудачной формулировки от пользователя - и правило перестаёт работать. Ориентир простой: если нарушение ограничения стоит денег, репутации или необратимо меняет данные, оно должно быть выражено кодом - уникальным индексом в базе, проверкой в обёртке инструмента, лимитом в очереди. Промпт хорошо работает для тона, формата и приоритетов, то есть там, где ошибка дешёвая.
Сколько времени реально уходит на такой слой enforcement на новом проекте?
Первый раз - примерно 2–3 дня, потому что вы пишете журнал записей, обёртку с бюджетами и формат трейса с нуля. Дальше это переносимая библиотека, и на новом проекте уходит день-полтора: подключить ledger к базе клиента, описать TOOLSETS по типам задач, протянуть dry_run через инструменты, поднять approval-бота в Telegram. Это дешевле, чем один разбор инцидента с клиентом, у которого агент разослал дубли или сделал лишние возвраты.
У меня MVP и мало времени. С чего начать, если делать всё сразу не получится?
Три вещи в таком порядке. Первое - dry-run на всех инструментах записи и день чтения предложенных действий: это ловит самые грубые ошибки дизайна до того, как они станут инцидентом. Второе - идемпотентность на ключевых записях через уникальный индекс: убивает целый класс дублей навсегда. Третье - бюджет шагов и денег на прогон, чтобы одна зацикленная задача не съела месячный лимит. Approval-гейты, полноценные трейсы и job сверки добавляйте после того, как агент начнёт приносить пользу и вы поймёте, где он на самом деле ошибается.
Похожие статьи
Сделаю под ключ
Соберу ИИ-агента под реальную задачу
С инструментами, памятью и логами, чтобы он работал в проде, а не только в демо.
от 1 500 $ · 1-2 недели