
На демо всё выглядит нормально. Промпт аккуратный, ответы связные, JSON парсится, никто не задаёт вопросов. Несколько прогонов спустя — всё ещё работает. Релизим.
Через неделю тот же самый запрос три раза подряд возвращает некорректный ответ. Не так, чтобы всё упало. Просто достаточно, чтобы сломать downstream парсер и разбудить кого-то в два часа ночи. Ничего драматичного. Просто отказ, который повторяется достаточно часто, чтобы начать мешать.
Этот сценарий возникает снова и снова не потому, что команды некомпетентны. Проверка “на глаз” кажется разумной. LLM-фича не падает с исключением, не даёт stack trace, не нарушает очевидных инвариантов. Таблицы expected output здесь не существует. Истину проверить нельзя — проверяют правдоподобие. Ответ звучит нормально, структура в целом держится, а edge cases выглядят академическими.
Есть и культурный зазор. CI вырос вокруг детерминированного кода: фиксированные входы, выходы, которые либо совпадают, либо нет. LLM-фичи выглядят как интерфейсы, но ведут себя как стохастические сервисы с памятью. В ответ команды делают то, что умеют: несколько ручных прогонов, пара промптов в стейдже, галочка “ок”. Это не халатность, а привычная реакция на систему, которая при первом контакте выглядит вежливой и стабильной.
Ломается всё не на демонстрации. Ломается на повторении, дрейфе и накоплении мелких изменений. В промпте сдвинули запятую. Обновилась модель. Temperature подкрутили с 0.2 до 0.4 “для живости”. Прогоните это пятьдесят раз — и картина меняется.
Парсер продолжает работать. Смысл — уже нет.
Инциденты обычно выглядят одинаково. Ответ формально “валидный”, но другой. Поле с ценой, которое было числом, превращается в строку с валютой. Refusal, который раньше почти не срабатывал, начинает появляться в 3–5% случаев для ранее зелёного промпта. Никакие алерты не срабатывают, если единственная проверка — “что-то вернулось”.
В этот момент команды обычно неосознанно пропускают важную вещь: определение behavioral contract. Не формальную спецификацию и не документ на двадцать страниц. А молчаливое соглашение о том, как система ведёт себя между прогонами. Downstream код предполагает, что числовые поля останутся числами, что отказы появляются только в определённых случаях, что denylist действительно запрещает. Контракт существует даже тогда, когда его никто не писал.
Неприятная часть в том, что поведение LLM меняется даже тогда, когда “ничего не сломалось”. Без baseline это просто не видно. Вчера schema validity была около 99%, сегодня — 94%. Вчера refusal rate был около 0.5%, сегодня — 4%. Это выглядит как шум, пока не выплывает в ночном запуске.
Внятный quality gate не пытается сделать модель детерминированной — этот поезд ушёл. Он признаёт вариативность, но всё равно проводит границы. Он фиксирует, какие изменения допустимы, а какие должны останавливать релиз. И тут неизбежно приходится выбирать, где в CI быть неудобным.
Что должен блокировать внятный gate
1. Структурную стабильность относительно baseline
На фиксированном наборе промптов валидация схемы не должна выходить за согласованный диапазон относительно последнего “зелёного” прогона. Падение с ~99% до 94% — это не шум, а изменение, которое требует объяснения.
2. Семантические инварианты под повторением
Числа остаются числами. Идентификаторы не меняют формат “тихо”. Refusal rate для разрешённых кейсов не пересекает потолок относительно baseline. Рост с ~0.5% до 4% — блокер, независимо от того, насколько вежливо сформулирован ответ.
3. Безопасность как жёсткое ограничение
Не должно быть ни одного срабатывания на запрещённые персональные данные, а уровень их сокрытия не должен быть ниже, чем в предыдущем релизе. Любые новые утечки блокируют релиз, даже если всё остальное “стало лучше”.
Речь не про личные привычки. Это требования, которые начинают неприятные разговоры. Кто-то должен объяснить, почему отклонения допустимы. Кто-то — решить, что в этот раз скорость важнее контроля.
Механика здесь скучная, и именно поэтому её часто откладывают. Предупреждения на небольшие отклонения, блокер на большие. Сравнение со старыми прогонами, а не с идеалом. Работа с распределениями, а не с одиночными примерами. Всё это стоит денег: токены, минуты CI, внимание людей. Блокирующий релиз из-за “какого-то процента” выглядит бюрократией ровно до тех пор, пока не начинает заполняться график on-call.
Это и есть реальный компромис. Жёсткие гейты замедляют команды и иногда блокируют безобидные изменения. Мягкие — сохраняют скорость и переносят риск вниз по цепочке. Только мониторинг выглядит гибко и современно, но предполагает, что кто-то смотрит и может обратить всё постфактум. CI гейты грубые, зато падают раньше, когда исправлять дешевле и объясняться проще.
Самое показательное — как спокойно индустрия принимает silent drift у LLM там, где она бы не приняла его ни у одной другой зависимости. Библиотека платежей, которая внезапно начала бы возвращать строки вместо чисел, была бы откачена за минуты. Модель, делающая то же самое, часто получает лишь тред в Slack.
Речь не о том, чтобы строить крепостные стены вокруг каждой LLM-фичи. Речь о том, что “выглядит нормально” перестаёт быть сигналом качества, как только появляется повторение. Мы продолжаем релизить демо в системы, которые живут по контрактам, а потом удивляемся, что контракт внезапно оказался важен.
Кто-то скажет, что такие гейты слишком заточены под текущие инструменты, что они нестабильны и будут мешать продукту, что лучше полагаться на ручной просмотр. Возможно. А возможно, дискомфорт и есть цена честности, потому что альтернатива — делать вид, что вайб можно считать стратегией регрессии.
Англоязычная версия опубликована в личном блоге.
Astrowalk
Какой кошмар...