В Дне 13 — машина состояний агента. Состояния, переходы, явная структура. Но недостаточно описать, как агент должен ходить. Нужно описать, чего никогда не должно случаться. Это и есть инварианты — самый недооценённый инженерный артефакт.
Инвариант — это утверждение, которое должно быть истинным всегда, на любом шаге работы агента. «В состоянии "ожидание оплаты" платёж клиента не списан». «Если переписка получила тег "жалоба" — она не уйдёт в архив автоматически». Инварианты — это «защитные перила» вокруг состояний и переходов. Без них умная модель может вести агента «логически» — и нарушить правила, которые ты считал очевидными. Сегодня — как формулировать инварианты, где их проверять и что делать, если нарушены.
В Дне 13 мы построили машину состояний: список состояний, список переходов, условия каждого перехода. Это даёт агенту структуру. Кажется — достаточно?
Не совсем. Машина состояний описывает что разрешено: «из «собираю параметры» можно перейти в «подтверждение заказа», если все обязательные поля заполнены». А инвариант описывает что недопустимо никогда: «в состоянии «подтверждение заказа» сумма заказа не может быть отрицательной», «заказ не уходит в обработку, если клиент не подтвердил итоговую цену».
Это разные вещи. Машина состояний — это маршруты. Инварианты — это правила, действующие на всех маршрутах одновременно.
LLM-агент может найти неожиданный путь к цели — формально допустимый по схеме переходов, но нарушающий неявные правила. Инварианты — это способ сделать эти правила явными.
Состояния: «готов помочь» → «уточняю операцию» → «проверяю баланс» → «выполняю перевод» → «готово». Переходы выглядят логичными.
А теперь смотри: модель решила, что для выполнения перевода нужно сначала «облегчить пользователю задачу» — и в состоянии «уточняю операцию» сама догадалась, как зовут получателя, и предлагает «вы наверное хотели Ивану отправить?». Технически нарушения схемы нет. Но инвариант «получатель назван пользователем явно, агент не угадывает» — нарушен. И это в банковской системе — катастрофа.
Машина состояний об этом не «знает». Это неписаное правило, которое нужно сделать писаным.
Инвариант — это предикат, который должен быть истинным всегда. Формально — булево выражение от состояния системы, проверяемое до и после каждого действия агента.
Хороший инвариант обладает тремя свойствами:
Инварианты проверяются в трёх местах:
На практике инварианты делятся на три уровня — по строгости и последствиям нарушения:
Если нарушены — действие не выполняется ни при каких обстоятельствах. Никаких «модель умная, она разберётся». Жёсткое предохранение.
Не подтвердил — операция не идёт, никакие «но я же уже спрашивал в прошлой реплике» не принимаются. Это первое, что появляется в банковском или платёжном агенте.
Может составить черновик, показать его — но не нажмёт «отправить». Простой инвариант, который спасает email-агентов от катастрофы.
Модель не «догадывается» о значениях. Если пользователь не назвал — спрашивает, а не выдумывает. Защищает от ситуации «модель решила, что заказ нужен на 1000 единиц, потому что «обычно столько берут»».
Нарушение — это повод остановиться и спросить пользователя. Не катастрофа, но и не «прокатит автоматом».
Обычная покупка проходит без переподтверждения. Большая — останавливаем и спрашиваем. Защищает от ошибок в нолях и от случайных дорогих действий.
Защищает от «агент крутится по кругу» (см. День 6). Лучше показать «я не могу решить, помоги», чем сжечь $50 на бесконечных попытках.
Нарушение — это сигнал тревоги для тебя как разработчика. Действие может выполниться, но в логах появляется флаг. Это твоя «нервная система»: ты узнаёшь о странном поведении до того, как это станет проблемой пользователя.
Не катастрофа — может быть, задача правда требует много вызовов. Но 99% случаев это значит — модель буксует. Лог сигнала летит в мониторинг, ты замечаешь паттерн и тюнишь промпт.
Если пользователь назвал имя, дату и сумму — финальный ответ их явно упоминает (не «я выполнил то, что ты просил»). Помогает ловить случаи, когда модель «сделала», но непонятно что именно.
В здоровой системе пропорция примерно такая:
Связаны с деньгами, личными данными, необратимыми действиями. Их мало, но они спасают от катастроф.
Защита от ошибок и зацикливания. Эскалация в нужный момент.
Твои «датчики». Не блокируют, но дают видеть поведение. С них всё начинается — и часть из них потом «промотируется» в средние или строгие.
Не «придумывать в кабинете». Лучший источник инвариантов — это реальные ошибки. Когда агент сделал что-то не то — задаёшь себе вопрос: «какое правило я считал само собой разумеющимся, которое модель нарушила?». Это правило — и есть кандидат в инварианты.
Поэтому первый рабочий набор инвариантов появляется после того, как агент уже работает и допустил несколько ошибок. Преждевременная попытка перечислить все возможные инварианты заранее обычно даёт переусложнённую и неработающую систему.
Два варианта:
Правило: всё, что можно проверить кодом — проверяй кодом. LLM-судья — только когда правда без него не обойтись. У него есть свои недостатки (сам может ошибаться, дорог, требует своих инвариантов).
«В system prompt написал «никогда не отправляй email без подтверждения» — этого достаточно». Не достаточно. LLM статистическая: 99 раз из 100 будет делать как сказано, на 100-м — нарушит из-за необычного входа. Если 1% ошибок — это списанные деньги или отправленные не тем письма, это катастрофа.
Инструкция в промпте + инвариант в коде = эшелонированная оборона. Просто инструкция — это «надеюсь, всё будет хорошо».
«Я добавлю строгих инвариантов на всё — будет максимально безопасно». Получишь систему, в которой агент постоянно блокируется на разумных действиях. Пользователь видит «не могу выполнить» 30 раз в день — и уходит.
Строгий инвариант — это «стоп-кран». Стоп-кранов в поезде немного. Они для редких критических ситуаций. На большинство «странностей» хватит мягкого инварианта (запиши в лог) или среднего (спроси пользователя).
Инвариант сработал — действие заблокировано — пользователь увидел ошибку — на этом всё. Ты не знаешь, что инвариант сработал, как часто, при каких входах.
Каждое срабатывание инварианта — это сигнал. Может быть, модель пытается сделать недопустимое из-за плохого промпта. Может быть, инвариант слишком жёсткий и блокирует разумное действие. Может быть, реальная атака. Без логов — ничего из этого не разобрать.
«Поставлю отдельную модель, которая проверяет ответы основного агента, и буду полагаться на неё». Окей, но судья сам тоже модель. Если у него нет своих чётких правил — он будет ошибаться так же, как основная модель.
LLM-судья работает, только если у него: (а) узкая чётко описанная задача проверки; (б) свой набор правил, по которым он судит; (в) логирование и периодическая проверка точности — на размеченных кейсах.
Возьми машину состояний, которую ты построил в Дне 13 (бот для бронирования или что-то своё). Задай себе вопрос: «что в этой системе никогда не должно произойти?». Запиши 5-7 инвариантов. Классифицируй каждый: строгий, средний, мягкий. Это даст тебе чувство, как смотрит на агента инженер с инвариантами в голове.
Открой банковское приложение или ChatGPT. Попробуй подсмотреть, какие инварианты у них есть. В банке: что приложение требует подтверждения? В ChatGPT: где появляется «я не могу это сделать»? У каждого продукта свой профиль инвариантов, и понимание чужих — отличный способ нарабатывать интуицию для своих.
Вспомни случай, когда LLM-ассистент сделал что-то не то: что-то отправил без спроса, что-то напридумывал, что-то пропустил. Запиши, какое правило он нарушил. Сформулируй это как инвариант — проверяемое утверждение. Это самое реальное упражнение, потому что инварианты в проде именно так и появляются.