Узнайте, как построить гардрейл-движок для LLM-агентов: одна дверь, тиры автономии, откат. Подключите к LiteLLM и защитите свой код.
В этом году я дал LLM-агенту ключи от своего дома: расписание батарей, климат, календарь, банковские транзакции. Не как демо, а как систему, которая реально управляет всем каждый день, пока я на работе.
И мне пришлось ответить на вопрос, который большинство фреймворков для агентов обходят стороной: что происходит, когда модель ошибается? Не в смысле бенчмарков, а в 3 часа ночи, с отоплением в феврале, с моими деньгами. Когда агент может действовать, галлюцинация перестаёт быть метрикой качества и становится физическим событием. Стандартный ответ «мы попросили её быть осторожной» — это не ответ. Причина проста: инструкция — это не ограничение.
Для языковой модели ваше правило — просто текст, который делает определённые продолжения более вероятными. Он наклоняет распределение вероятностей, но не может его зафиксировать. Контракт с компилятором исполняется. Контракт с моделью — лишь предлагается. Если ваша безопасность живёт в промпте, у вас нет безопасности. У вас есть настроение безопасности.
Поэтому я построил принуждение вне модели и запускал его месяцами с реальными последствиями. Этот пост — о дизайне, который выжил. Код открыт, а в последнем разделе показано, как тот же движок управляет вызовами внутри LiteLLM.
В моей системе ничто не действует напрямую. Планировщик, YAML-правила, LLM-агент, даже мой палец на кнопке UI — всё может только создать ActionRequest и отправить его единственному исполнителю, который является единственным кодом, имеющим право вызывать коннекторы. Есть одна дверь, структурно. Агент не может обойти контрольную точку, которая владеет единственной дорогой.
Это звучит очевидно, но почти ни один фреймворк так не делает. В большинстве стеков авторизация живёт в промпте («используй этот инструмент только когда...») или в разбросанных проверках на уровне инструментов — то есть там, где модель может их обойти.
Каждый тип действия имеет уровень в таблице политик (данные, не код):
Последний пункт — первое жёсткое правило: запрет по умолчанию. Тип действия, отсутствующий в таблице политик, — не ошибка и не пропуск. Он переходит на Tier 3, и спрашивают человека. Новизне никогда не доверяют; её эскалируют. Когда мой агент изобретает новый способ быть полезным, система отвечает: «интересно — спроси человека».
Вот дизайн-решение, которое я сохранил бы, если бы пришлось выбросить все остальные: действие без зарегистрированного отката не может быть авто-исполнено. Обратимость — не фича. Это предварительное условие автономии.
Каждая Tier-1 политика должна объявлять компенсирующую команду (восстановить предыдущее значение, отменить расписание) при регистрации. Загрузчик политик отказывается запускаться, если команда отсутствует. Исполнитель проверяет это и в рантайме: авто-действие без отката молча понижается до Tier 3, с записью причины в лог. Если нельзя откатить — подпись человека. По определению, а не по настроению.
Выполненные действия показывают кнопку «отменить» в течение 15 минут. Сам откат отправляется через тот же конвейер как новый запрос, помеченный как отмена родительского. Даже сожаление идёт через парадную дверь.
Исполнитель — маленький детерминированный конечный автомат. Последовательность его проверок кодирует все приоритеты системы. Если порядок нарушен, те же проверки дадут сломанную машину.
До поиска политики. Если он включён, все действующие уровни сжимаются до Tier 3. Чтения остаются разрешёнными (нужны глаза во время инцидента). Один крайний случай занял вечер размышлений: уже одобренное действие, приходящее при включённом выключателе, блокируется без создания нового одобрения. Иначе блок породил бы одобрение, чьё одобрение было бы заблокировано, что породило бы одобрение... Кнопка паники важнее всего, включая предыдущее согласие человека.
Климат 17–23°C, мощность батарей в пределах железа, закрытые перечисления, неизвестные параметры отклоняются. Смысл не только в остановке модели. Смысл в том, что экран одобрения становится надёжным: человек, решающий «должно ли это произойти?», не должен ловить «погоди, 45 градусов — это безумие?». Человек решает стоит ли. Машина уже решила, в пределах ли это разумного.
Однострочная причина, которую я осознал смущающе долго: репетиция не должна тратить реальный бюджет. Каждый новый тип действия начинается с сухого прогона — две недели «выполнилось бы» в логе, прежде чем ему разрешат что-то трогать. Вы видите, что ваш агент собирался сделать, до того как он это сделает. Рекомендую это больше, чем любую другую фичу.
SQLite, BEGIN IMMEDIATE, проверка и резервирование под write lock — два параллельных запроса не могут оба уместиться в последний слот дневного лимита. Скучная дисциплина баз данных, отсутствующая в каждом фреймворке агентов, который я читал.
Транзакция A решает, резервирует лимит и записывает намерение в аудит-лог. Затем вызов коннектора — сетевой I/O, который реально переключает рубильник, — выполняется вне блокировок с жёстким таймаутом. Транзакция B добавляет результат. Зависший API умной розетки не может удерживать базу движка, а сбой между фазами оставляет честную запись «намерено, не подтверждено» вместо лжи.
Решения, результаты, отказы, сухие прогоны, блокировки — типизированные коды причин, никогда не обновляются на месте. Когда я хочу узнать, почему отопление вело себя странно во вторник, ответ — это запрос, а не археологические раскопки.
Я строил это для одного дома и одного пользователя, поэтому читаю литературу по безопасности агентов с некоторым весельем. Анализ пробелов говорит, что авторизации агентов не хватает: принуждения в инфраструктуре, которую агент не может обойти (а не в промптах); типов решений помимо allow/deny — defer, step-up; контроля в рантайме вместо конфигурации на этапе дизайна.
Современное состояние стоит назвать точно: оно лучше, чем думают комментаторы, но структурно ограничено. LiteLLM — самый распространённый открытый AI-шлюз — теперь имеет реальное управление разрешениями MCP: доступ на уровне сервера по ключу, команды и организации, списки разрешённых/запрещённых инструментов, даже разрешённые имена параметров, с иерархией разрешений, где самый строгий уровень побеждает. Это настоящая система контроля доступа. Но она категорически статична: allow/deny. Нет defer (вызов, ждущий человека), нет сухого прогона, нет отката, нет семантики kill-switch, а контроль параметров останавливается на именах: шлюз может сказать «этот инструмент принимает amount_eur», но не «amount_eur должен быть не более 500».
Этот отсутствующий список — фичи моего движка. Одна дверь — ответ на обход. Tier 3 — defer как полноценный результат, а step-up аутентификация — поле в политике денег (которая навсегда Tier 3 — некоторые уровни никогда не должны быть заработаны хорошим поведением). Kill-switch, окна сухого прогона и исчерпание лимитов — всё это состояние рантайма. Я не считаю себя умнее создателей агентских платформ. У меня было преимущество последствий. Замечательно, как быстро «модель обычно права» перестаёт быть архитектурой, когда модель может открыть шторы в 3 часа ночи.
Чего у меня нет и чего я не видел ни у кого: доверие, зависящее от сессии — авторизатор, который помнит, что агент делал в этой сессии, и снижает его автономию. Это следующая идея в моём списке.
После извлечения движка из дома я сделал рефакторинг, который мир авторизации назвал бы запоздалым: разделил судью и пристава. Решающая половина — упорядоченные проверки, резервирование лимита, строка намерения в аудит-логе — это одна публичная функция (decide_and_reserve); получение результата — вторая (report_result). Внутрипроцессный исполнитель — просто композиция этих двух вокруг вызова коннектора. Принуждение теперь может жить где угодно, потому что решение не перемещается.
MCP — это точка схождения вызовов инструментов агентов. Прокси сидит между любым MCP-хостом и любым stdio-сервером инструментов, невидимый для обоих. Каждый tools/call становится ActionRequest и проходит весь конвейер. Из демо дословно:
3) Вне границ: отклонено прокси, до инструмента не дошло:
ERR onedoor: 'set_thermostat' denied (reason: bounds — param
'temperature'=30 above max 23.0). The call was not forwarded.
4) Деньги — Tier 3: предложено, не переслано:
ERR onedoor: 'send_payment' requires approval (tier 3;
approval_id=1). A human can release it.
5) Человек одобряет — только тогда вызов пересылается:
ok Sent €49.99 to webshop (simulated).
6) Неизвестный инструмент по умолчанию запрещён:
ERR onedoor: 'delete_everything' requires approval
(reason: default_deny; approval_id=2).Направьте агента на прокси вместо сервера, напишите файл политики — и агент, которым вы не управляете, внезапно управляется вашими правилами.
LiteLLM загружает кастомные гардрейлы и вызывает async_pre_call_hook перед каждым вызовом, включая call_type="call_mcp_tool" для своего MCP-шлюза. Этот хук — готовое место принуждения, поэтому адаптер маленький: строим ActionRequest из вызова, консультируемся с движком, подчиняемся.
class onedoorGuardrail(CustomGuardrail):
async def async_pre_call_hook(self, user_api_key_dict, cache, data, call_type):
if call_type == "call_mcp_tool":
self._decide(f"mcp.{data['name']}", data.get("arguments", {}))
elif call_type in ("completion", "acompletion"):
self._decide("llm.completion", {"model": data.get("model", "")})
return dataЗапуск самопроверки против реального класса LiteLLM, без прокси:
completion gpt-4o-mini -> ok (permitted, audited)
completion gpt-4o-experimental -> BLOCK denied (bounds — model not in whitelist)
call_mcp_tool get_weather -> ok (permitted, audited)
call_mcp_tool send_payment -> BLOCK requires human approval (approval_id=1)
call_mcp_tool send_payment €5000 -> BLOCK denied (amount_eur=5000 above max 500.0)
call_mcp_tool delete_everything -> BLOCK requires approval (default_deny)Шесть строк — и шлюз получил четыре решения, которых ему не хватало: defer с id одобрения, границы на уровне значений, запрет по умолчанию для несписочных инструментов и строка аудита с причиной для каждого вердикта — в дополнение к ACL, которые он уже хорошо делает. Две системы компонуются: иерархия ключей/команд LiteLLM решает, кто может спрашивать; движок решает, что может произойти.
Движок открыт (Apache-2.0) — SQLite, один процесс, намеренно скучный; набор из 58 тестов — блокер релиза, одно демо проходит все механизмы от начала до конца без внешних зависимостей, второе управляет MCP-прокси с точки зрения агента, а адаптер LiteLLM самопроверяется без прокси. Но самое ценное — не код, а архитектурный принцип: инструкция — не ограничение. Вынесите принуждение за пределы модели, сделайте одну дверь, требуйте обратимости для автономии и всегда оставляйте человеку последнее слово.
Что делать прямо сейчас: возьмите свой агент и спросите: «Что произойдёт, если модель ошибётся в 3 часа ночи?» Если ответ — «промпт», значит, у вас нет ответа. Начните с малого: добавьте таблицу политик, где каждое действие имеет уровень и откат, и пропустите через неё все вызовы инструментов. Это займёт вечер, но превратит вашу систему из «настроения безопасности» в настоящую безопасность.
Хочешь закрепить знания на практике?
Решай задачи на Algolit — интерактивная платформа для обучения
Начать бесплатно →