/ Дневник курса / Урок 25

Выкатка новой версии агента: ступени, пороги и откат

Почему тестовый набор не ловит деградацию, во что обходится проверка новой версии на живом трафике и что делать, когда откатываться уже некуда.

  • release
  • canary
  • feature-flags
  • incident-management
  • llmops

В пятницу вечером системный промпт поправили — добавили пару абзацев о том, как оформлять в ответах суммы и валюту. Правка коснулась знакомого нам агента поддержки, того самого, что разбирает восемь тысяч обращений в сутки. Прогнали тестовый набор, получили зелёное, выкатили на всех. Кода при этом никто не трогал.

Через полчаса поддержка пишет, что агент путает валюту и называет клиенту с рублёвым счётом остаток в долларах. Сервис отвечает двухсотыми, задержка в норме, дашборды спокойны. С точки зрения мониторинга не произошло ничего. Ещё через час выясняется, что точного текста прежнего промпта никто не помнит, потому что правился он поверх, прямо в репозитории.

Канареечная выкатка, флаги, откат и разбор после аварии пришли из обычного бэкенда, и по отдельности каждый приём знаком. Разберём, что в них меняется, когда за флагом стоит не ветка кода, а языковая модель. Почему доля пройденных проверок плохо работает триггером отката, что зеркальная копия трафика делает с деньгами клиента и куда откатываться, если предыдущей версии модели у провайдера уже нет.

Дневник курса, урок 25. Здесь понадобится разобранное раньше: эвалы и четыре сигнала агента (урок 7) — чем вообще меряют качество; трассировка шагов (урок 19) — по ней потом ищут, что именно поехало; раскладка счёта агента (урок 22) — оттуда берётся сигнал стоимости. Пост читается отдельно: все термины вводятся заново.

Сквозным примером остаётся тот же чат-бот банка из учебного расчёта — 8000 обращений в сутки, диалог в пятнадцать ходов, $36 в сутки за вызовы моделей. Поток раскладывается на 30% вежливостей, 40% типовых вопросов и 30% сложных персональных. Выкатываем ту самую новую версию системного промпта.

Содержание
  1. Почему релиз агента ломается не так, как релиз кода
  2. Тестовый набор как ворота: где у него потолок
  3. Зеркало живого трафика и то, что оно не выбрасывает
  4. Первые живые пользователи и липкость, которая бьёт дважды
  5. Что годится в триггеры отката, а что нет
  6. Судья, который дрейфует сам
  7. Откат: перенос указателя вместо деплоя
  8. Куда откатываться, когда прежней версии больше нет
  9. Когда гейт всё-таки пропустил: дежурство и разбор
  10. Итог
  11. FAQ
  12. Источники

1. Почему релиз агента ломается не так, как релиз кода

Правка промпта не проходит ни через компилятор, ни через тайп-чекер, и ни один инструмент не скажет, что после неё агент начнёт писать суммы в долларах там, где раньше писал в рублях. Отличие от обычного релиза лежит глубже кода: поведение сервиса меняется по нескольким каналам сразу, и только один из них проходит через сборку.

   меняется деплоем              меняется без деплоя
   ┌──────────────┐         ┌──────────────────────────────┐
   │ код сервиса  │         │ текст промпта                │
   │ схемы данных │         │ версия модели у провайдера   │
   │ инструменты  │         │ тихое обновление той же      │
   └──────────────┘         │   версии на стороне вендора  │
          │                 │ дрейф самих обращений        │
          │                 └──────────────────────────────┘
          ▼                              ▼
   ловится тестами          не ловится ничем из перечисленного:
   и HTTP-кодами            сервис отвечает 200, качество падает
Рисунок — через сборку проходит только код, схемы и набор инструментов; текст промпта, версия модели, тихое обновление на стороне вендора и дрейф входящих обращений меняют ответы в обход деплоя, и HTTP-код при этом остаётся двухсотым.

Дрейфом дальше называется всякая тихая смена того, что мы считали неизменным. В посте таких три: обращения клиентов, версия модели у провайдера и та модель, которой мы измеряем качество ответов.

Второе отличие прячется в форме поломки. Обычный баг детерминирован, он воспроизводится, ловится в отладчике и чинится. У агента один и тот же запрос на одном вызове отрабатывает идеально, а на следующем сбивается. Anthropic в разборе своих сентябрьских инцидентов пишет об этом прямо. Поэтому «не воспроизводится» перестаёт быть аргументом в пользу того, что проблемы нет.

Третьим идёт накопление. Агент делает несколько шагов подряд, и неудачный выбор инструмента на первом шаге даёт неверное состояние на втором, а на третьем запускает цикл, который стоит денег. Одна и та же ошибка на одиночном ответе выглядит мелочью, а в цепочке из пятнадцати ходов вырастает в испорченный диалог.

Отсюда и весь дальнейший разговор. Раз изменение приходит помимо деплоя, а поломка не бросает исключения, проверять приходится не «работает ли», а «стало ли хуже». И проверять уже после того, как изменение вышло к людям, потому что раньше его просто не с чем сравнить.

2. Тестовый набор как ворота: где у него потолок

Перед выкаткой естественно прогнать набор проверок, который в обиходе зовут эвалами. Собирают его из реальных обращений, очищенных от персональных данных, с эталонными ответами экспертов, плюс отдельная стопка на инъекции и краевые случаи. Эвалы ловят грубую регрессию дёшево и быстро, и отказываться от них никто не предлагает. Потолков у них, однако, два. На том, что в наборе есть, оценка идёт по результату задачи целиком, и ошибка внутри цепочки шагов в неё не попадает. А сама деградация чаще вылезает на том, чего в наборе нет вовсе, потому что запросы туда придумали или отобрали инженеры.

В посте про наблюдаемость (урок 7) такой набор служил измерительным прибором, по которому видно, где агент ошибается и насколько. Здесь он смотрится с другой стороны. По нему принимают бинарное решение выкатывать или нет, и вопрос звучит иначе: можно ли на такой набор в этом решении опереться.

Самый честный ответ на этот вопрос дала команда, у которой ресурсы на эвалы лучшие в индустрии. В сентябре 2025 года Anthropic опубликовала разбор трёх наложившихся инцидентов. Баг маршрутизации от 5 августа отправлял часть запросов на серверы с неправильной конфигурацией, и сначала это задевало 0,8% запросов к Sonnet 4. Рутинное изменение балансировки нагрузки 29 августа расширило радиус, и в худший час 31 августа с неправильного пула обслуживалось уже 16% запросов к Sonnet 4. Хотя бы одно испорченное сообщение получили около 30% тех, кто пользовался Claude Code в те дни.

Симптомы выглядели как что угодно, только не как отказ: тайские иероглифы посреди английского ответа, синтаксические ошибки в сгенерированном коде. А вот что команда написала про свою проверку перед выкаткой.

The evaluations we ran simply didn't capture the degradation users were reporting, in part because Claude often recovers well from isolated mistakes.

Это и есть первый потолок, и устроен он у всех одинаково. Засчитывается ответ на задачу, а не каждый шаг к нему. Модель же хорошо оправляется от единичной ошибки внутри цепочки: испортила один шаг, на следующем исправилась, задача зачтена. Метрика показывает норму, а пользователь успел увидеть кривой промежуточный ответ и потерять минуту. В том же разборе команда признаёт, что опиралась на шумную метрику как на точную.

Наш банковский случай упирается во второй потолок. Правка про формат сумм не задевала в наборе ничего, потому что обращений с неоднозначной валютой счёта туда никто не положил: клиенты с двумя счетами в разных валютах при сборке никому не показались интересными. Качество набора тут ни при чём, такова его природа.

Действие, которым Anthropic закрыла этот пункт, звучит скучно и стоит дорого. Регулярные прогоны на своих системах команда пообещала сделать непрерывными и перенести на настоящий прод. Оценка переезжает с фиксированного набора на живой поток, и дальше весь пост про то, как этот переезд устроен по ступеням.

3. Зеркало живого трафика и то, что оно не выбрасывает

Первая ступень после набора выглядит бесплатной: пустить на кандидата копию настоящих обращений, ответы никому не показывать, а сложить в лог и сравнить с тем, что ответила текущая версия. Риска для клиента ноль, запросы настоящие. Механизм называется теневым режимом (shadow mode), и на уровне сети он давно готов, например, через зеркалирование трафика в Istio.

клиент ──► production v3.1 ──► ответ клиенту
              │
              └─ копия ──► candidate v3.2 ──► лог + судья
                                │             (сравнение пар по id)
                                │
                                └─ вызов инструмента ──► реальный мир
                                   ▲
                                   вот здесь протечка: ответ отброшен,
                                   действие совершено
Рисунок — теневая копия запроса уходит на кандидата, его ответ отбрасывается и попадает только в лог и на сравнение с ответом продакшена; вызовы инструментов при этом выполняются по-настоящему, и побочный эффект остаётся в мире.

Istio описывает семантику предельно ясно. Копию отправляют и забывают про неё, ответ на такой запрос сервис отбрасывает, а заголовок Host у зеркальной копии получает суффикс -shadow. Пропадает только ответ. Побочный эффект остаётся, и это первый капкан теневого режима. Если агент в цепочке шагов вызывает инструмент, который отправляет письмо или списывает комиссию, теневая копия сделает это второй раз. Сеть тут не поможет, она видит HTTP-запрос и ничего не знает о намерении.

Задача решается на уровне инструментов, и решается она не «отключить всё опасное». В работе Verified Tool Calls про ненадёжные вызовы инструментов проблема сформулирована шире зеркала. Фреймворки считают вызов атомарным, возвращающим успех или неудачу, а в реальности бывают таймауты уже после отправки, отложенная видимость результата и частично применённые изменения. Отвечают там же обёрткой с постусловиями, правилом «проверь, прежде чем повторить» и ключом идемпотентности (уникальный идентификатор операции, по которому повторный вызов с тем же ключом не выполняет действие второй раз, а возвращает результат первого). Для тени это означает, что инструменты с побочными эффектами либо замоканы, либо ходят в песочный контур, либо принимают ключ, по которому теневой вызов схлопывается с боевым.

Второй капкан стоит в настройках по умолчанию, и обходится он в живые деньги. В Istio доля зеркалируемого трафика задаётся отдельным полем, и документация про него предупреждает: «If this field is absent, all the traffic (100%) will be mirrored». Не указали процент — зеркалите сто процентов, то есть каждый запрос уходит в модель дважды и дважды оплачивается. Посчитаем на нашем агенте. Полное зеркало добавляет к $36 в сутки ещё столько же и превращает месяц обкатки в лишнюю тысячу долларов. Десятипроцентное зеркало даёт около $3,6 в сутки. Полсотни долларов за две недели уходит на само зеркало, работа судьи на парах ответов идёт сверху, и такую сумму подписывают спокойно. Отсюда и правило зеркалить 10–25% вместо всего потока, и держать режим ограниченным по времени. Одна-две недели, дальше зеркало либо сворачивают, либо переводят в живую выкатку.

Чего тень не покажет, так это реакции людей. Ответов кандидата никто не видит, значит нет ни повторных обращений, ни оценок, ни того, что клиент бросил диалог на третьем ходу. Всё поведенческое начинается только там, где кандидат отвечает по-настоящему.

4. Первые живые пользователи и липкость, которая бьёт дважды

Дальше кандидат начинает отвечать реальным людям, но не всем сразу: 1% трафика, через сутки 5%, потом 20%, 50% и только затем сто. Такую выкатку называют канареечной, по той малой группе, которая рискует первой. Смысл ступеней в том, чтобы при плохом раскладе кривые ответы достались одному проценту клиентов и на этом остановились. Ступень держат не меньше суток, иначе выборка накрывает только дневной пик и не видит ни ночного потока, ни утреннего.

У вопроса, кому именно попадать в канареечную долю, есть неочевидная цена. Стандартный ответ: липкое назначение (sticky routing), когда вариант закрепляется за пользователем по хешу его идентификатора и не меняется всю сессию. Без липкости клиент получал бы ответы попеременно от двух версий с разным стилем и разной логикой, и метрики смешивались бы внутри одного диалога.

Та же липкость оборачивается против вас, когда версия оказывается плохой. В разборе Anthropic это записано прямо. Клиент, чей запрос один раз попал на неисправный сервер, с большой вероятностью получал оттуда же и следующие ответы.

Ущерб не размазывается тонким слоем по всей аудитории, он концентрируется на тех, кто попал. Один процент пользователей получает не один странный ответ, а пятнадцать ходов подряд от сломанной версии. Для банковского агента это означает, что пострадавшие клиенты будут по-настоящему недовольны, зато их будет мало и все они известны поимённо, что пригодится при разборе.

Второе правило разбиения требует однородных когорт. Смешивать в одной выборке enterprise-клиентов и клиентов бесплатного тарифа нельзя: у них разный характер запросов, разные пороги стоимости и разные требования комплаенса, а среднее по двум несопоставимым группам не значит ничего.

И третье, о котором забывают чаще всего. Канареечная группа не изолирована от контрольной. Google SRE отмечает, что плохое поведение канареечной версии способно испортить и контрольную группу. Ответ канарейки меняет следующий запрос того же клиента, а этот следующий запрос уже уходит в контроль. У чат-агента с пятнадцатиходовым диалогом такая протечка особенно вероятна, ведь клиент, получивший невнятный ответ, тут же спросит иначе, и этот второй вопрос попадёт в общую статистику.

Соберём конвейер целиком. Между этапами стоит гейт, автоматическая проверка, которая пропускает кандидата дальше или возвращает назад.

этап 1  тестовый набор        прогон кандидата против текущей версии
   │
   ├─ гейт
   ▼
этап 2  зеркало 10–25%        1–2 недели, ответы в лог, риск нулевой
   │                          сравнение пар: качество, задержка,
   ├─ гейт                    стоимость, срабатывания guardrails
   ▼
этап 3  канарейка 1→5→20→50%  живые пользователи, ступень ≥ 24 ч,
   │                          пороги отката заданы до старта
   ├─ гейт
   ▼
этап 4  100% stable           мониторинг не выключается,
                              предыдущая версия остаётся тёплой ≥ 24 ч
Рисунок — релизный конвейер из четырёх этапов с гейтом между каждой парой: тестовый набор, зеркало на 10–25% трафика в течение одной-двух недель, канареечные ступени 1→5→20→50% по суткам минимум и полная выкатка с сохранением тёплой предыдущей версии не меньше суток.

5. Что годится в триггеры отката, а что нет

Пороги отката фиксируются до старта, иначе в момент инцидента начинается спор, считать ли текущие цифры регрессом, и спор этот выигрывает тот, кто громче. Вопрос в том, какая величина вообще годится на роль триггера. У Google SRE на это есть ответ в одну строку. Метрика, способная сильно скакать без всяких последствий для сервиса, вряд ли годится в канареечные. Формулировка мягкая, а крест она ставит на половине того, чем меряют агента.

Рядом стоит уже настоящее требование, атрибутируемость. Оно обязывает объяснять сдвиг метрики именно выкаченной правкой, а не посторонними факторами. Доля пройденных эвалов на живом трафике не проходит ни по рекомендации, ни по требованию. Она гуляет от состава входящих обращений, ведь в понедельник утром спрашивают не то же, что в субботу ночью. Гуляет от температуры генерации. Этим параметром задают разброс ответов, и на нуле модель каждый раз берёт самый вероятный вариант, а выше начинает допускать и менее вероятные. Гуляет от настроения судьи. Пятипроцентный провал такой метрики может не значить ничего, а может значить катастрофу, и по самому числу это неразличимо. У кода в этой роли работает HTTP 500, и безобидной версии у пятисотки нет.

Практический вывод из этого противоречия у SRE тоже есть, и он про порядок. На первой, самой маленькой ступени смотрят на самые внятные признаки беды, то есть на падения и отказы запросов, а шумные метрики подключают позже, на больших популяциях. Для агента жёсткая часть набирается легко: невалидный JSON в структурном ответе, исчерпанный лимит повторов, уход в запасную модель, скачок расходов на один запрос. Сюда же идут срабатывания guardrails — так называют проверки, которые стоят на входе и выходе модели и отсекают опасное. Всё это либо случилось, либо нет.

Теперь посчитаем, хватает ли нашему агенту трафика на шумную часть. Ориентир, который ходит по курсам, сформулирован для A/B-эксперимента: чтобы зафиксировать эффект в 5% на шумной метрике качества, нужно порядка десяти тысяч сессий на вариант против полутора тысяч у классического теста с кликами. Публичной методики за этим числом найти не удалось, и на канарейку оно переносится с оговоркой — там вопрос грубее, «не хуже ли», и размер выборки зависит от того, какую просадку мы согласны не заметить. Порядок величины тот же, и ощущается он сразу, стоит разложить его по нашим ступеням. Одно обращение клиента разворачивается в один диалог, он же одна сессия, так что дальше везде считаем сессии.

Ступень Доля Сессий кандидата за сутки Накоплено
день 1 1% 80 80
день 2 5% 400 480
день 3 20% 1600 2080
день 4 50% 4000 6080
день 5 100% 8000
Рисунок — при потоке 8000 обращений в сутки ступени 1→5→20→50% дают кандидату 80, 400, 1600 и 4000 сессий соответственно, а накопленным итогом к концу четвёртого дня — 6080 сессий, то есть выкатка заканчивается раньше, чем набирается выборка порядка десяти тысяч.

Выкатка кончится быстрее, чем наберётся статистика. И это ещё оптимистичный расчёт по всему потоку. Если внутри трети сложных персональных обращений нас интересует узкий класс — скажем, споры о списаниях, около 9% всего потока, — то в канареечном проценте это семь сессий за сутки. Семь. Любой вердикт по ним будет гаданием.

Отсюда три следствия, и приятных среди них нет. Первое касается разреза данных. Стратификация обязательна: запросы разбивают по типам до анализа, иначе регресс на редком классе тонет в общем среднем. Test Before You Deploy, работа про проверку чужой модели перед выкаткой, формулирует ту же мысль обобщённо: прицельная проверка в отдельных зонах риска вскрывает просадки, которые общие метрики пропускают. Второе касается размера продукта. Небольшому потоку честнее снизить требования к улавливаемому эффекту, чем делать вид, что канарейка что-то доказала.

Третье следствие чисто статистическое, и встроено оно в саму идею дашборда. Обычный критерий значимости держит вероятность ложной тревоги, пока применяется один раз на заранее заданном объёме выборки. А канарейку смотрят непрерывно и решение принимают в произвольный момент. Netflix описывает последствия без обиняков. От такого подглядывания ложных тревог набирается недопустимо много, доверие к канареечной проверке тает, а выкатки замедляются, потому что инженеры ищут причины несуществующих регрессов. Дежурный перестаёт верить алертам, и это худшее, что может случиться с гейтом.

Лечится это семейством методов, корректных при непрерывной проверке (anytime-valid): вместо порога по среднему сравниваются целиком распределения ответов, а доверительная граница строится так, чтобы её можно было смотреть в любой момент. На двух реальных канарейках Netflix такой подход поднял тревогу за секунды, тогда как классическому критерию на фиксированной выборке понадобилось бы полчаса, а то и больше.

Полчаса здесь — время Netflix на их потоке, и на восьми тысячах обращений в сутки фиксированный критерий копил бы выборку ещё дольше. Но масштаб понятен и по такой прикидке. За те же полчаса полной выкатки к клиентам успевает уйти около 170 сессий, и оценка эта скорее оптимистичная. Когда тревога приходит за секунды, счёт идёт уже не на выборку, а на скорость дежурного. Три минуты до аварийного выключателя стоят семнадцати сессий.

Отдельно стоит отказаться от соблазна сравнивать «до и после» вместо канарейки с контролем. SRE зовёт это канарейкой не по трафику, а по времени, и напоминает, что время само по себе крупнейший источник различий. Релиз в понедельник против выходных даёт разницу, которая не имеет к релизу никакого отношения.

Тут работает и дрейф самих обращений, первый из трёх, названных в начале поста. Состав входящего потока меняется без нашего участия: банк разослал письмо о смене тарифов, и в поддержку пошли вопросы про списания; у платёжного партнёра сбой, и полвечера агент отвечает на «почему не прошла оплата». Сравнение «до и после» припишет этот сдвиг выкаченной правке, и команда пойдёт искать несуществующий регресс. Канарейка с контролем от этого защищена: обе группы живут в одном потоке и в одном временном окне, так что общий для них сдвиг вычитается сам. Ради этого контрольную группу и держат живой.

6. Судья, который дрейфует сам

Допустим, шумную часть метрики мы всё-таки считаем, и считает её модель-судья (LLM-as-a-judge), то есть отдельный вызов, который читает пару ответов и выносит вердикт. Тогда возникает вопрос, который не задаёт почти никто: а что, если поехал судья, а не продукт?

Who Drifted: the System or the Judge?, работа про атрибуцию дрейфа в оценочных конвейерах, отвечает на него неприятно. Судья и сам живёт за чужим API, а тихое обновление его версии или правка оценочного промпта меняют то, как он ставит оценки. Поэтому каждая тревога двусмысленна. Провайдер тихо обновил версию модели-судьи, или кто-то из команды подправил формулировку в промпте оценки, и метрика качества просядет ровно так же, как от плохого кандидата. Откат сработает, версию агента вернут назад, а метрика не восстановится, потому что откатывали не то.

Масштаб проблемы измерен, и он больше, чем кажется. По умолчанию здесь берут тот же скользящий z-тест по потоку оценок, и подводит он там же, где подглядывание в канареечный дашборд из предыдущего раздела. Критерий, рассчитанный на однократную проверку, применяют к потоку и смотрят непрерывно. Из потоков, где никакого дрейфа нет вовсе, тревогу он поднимает на трёх из четырёх. Три четверти алертов ни о чём.

Различить два источника позволяет якорный набор (anchor set), фиксированная выборка примеров, размеченная людьми один раз; текущий судья переоценивает её с постоянным интервалом. Продукт на неё не влияет, ведь примеры не меняются. Значит, расхождение между вердиктами судьи и человеческой разметкой на якоре указывает на судью, а просадка на живом потоке при спокойном якоре указывает на продукт.

живой поток ──► судья ──► метрика поехала ──┐
                                            ├─► кто виноват?
якорный набор ──► тот же судья ──► сверка ──┘
   (размечен людьми,
    не меняется)      якорь спокоен  → поехал продукт
                      якорь поехал   → поехал судья
Рисунок — якорный набор проходит через того же судью, что и живой поток; если вердикты на неизменном якоре разошлись с человеческой разметкой, дрейфует судья, если якорь спокоен, а поток просел, дрейфует продукт.

Работает конструкция хорошо: тихое обновление версии судьи распознано как дрейф судьи в 60 прогонах из 60, без единой ошибочной атрибуции на продукт; более строгий промпт судьи атрибутирован верно в 110 случаях из 120. Обходится такой мониторинг дешевле, чем судить сильной моделью каждый элемент живого потока, так что якорь не бесплатен, но и не удваивает счёт.

Практический минимум, который отсюда следует, дешевле полной конструкции. Версия судьи и текст его промпта фиксируются наравне с версией агента, а их изменение проходит как обычный релиз, со своей канарейкой. Судья, который меняется молча, портит все метрики сразу, и сломанный он выглядит точно так же, как сломанный продукт.

7. Откат: перенос указателя вместо деплоя

Пороги пробиты, решение принято. Чем откатывают? Если новая версия промпта попала в прод правкой файла и деплоем, то откат оборачивается таким же деплоем, со сборкой, тестами и очередью в CI. Пятнадцать минут в лучшем случае, а в пятницу вечером как повезёт. Инженерный ответ на это старый: вариант должен уже быть в проде, просто выключенный, а включает и выключает его правка конфигурации.

У фича-флага для агента есть особенность, из-за которой обычный булев переключатель не годится. Строка model_version = "v3.2" бесполезна, потому что поведение задаёт связка целиком: промпт, отлаженный под одну модель, на другой ведёт себя иначе, а температуры 0,2 и 0,0 дают разные ответы на одном и том же промпте. Поэтому вариация флага указывает на полный объект конфигурации.

flags:
  support_agent:
    stable:
      provider: anthropic
      model: claude-3-5-sonnet
      prompt_id: support_v3.1     # ссылка на версию в реестре, не текст
      temperature: 0.2
    candidate:
      provider: anthropic
      model: claude-3-5-haiku
      prompt_id: support_v3.2
      temperature: 0.0
    rollout: { candidate: 5 }     # процент трафика, липко по user_id
    kill_switch: armed

Переключение такого флага меняет всё сразу и возвращает всё сразу, так что рассинхрона «новая модель со старым промптом» не возникает в принципе. Здесь же живёт аварийный выключатель (kill switch). Это одно значение в конфигурации, которое выводит фичу из работы сразу, без сборки и без выяснения, какая именно версия виновата. Когда его дёргали последний раз? На выключатель, который ни разу не пробовали, в момент аварии остаётся только надеяться, поэтому его проверяют регулярно и на трезвую голову.

Живут все три в одном месте. Зеркало из третьего раздела, канареечные проценты из четвёртого и аварийный выключатель — не три отдельных механизма, а три строки одной политики маршрутизации, и исполняет их один компонент на пути запроса: модельный шлюз. Он же обычно берёт четвёртую строку — деление трафика пополам между двумя рабочими вариантами.

                ┌────────────────────────────┐
   запросы ────►│ шлюз · политика маршрутов  │
   клиентов     ├────────────────────────────┤
                │ shadow: зеркало 10% ─► v3  │  копия, никто не видит
                │ canary: 5% сессий ─► v3    │  живые пользователи
                │ a/b: hash(user_id) 50/50   │  два рабочих варианта
                │ kill_switch: взведён       │  выключает всё разом
                └────────────────────────────┘
                              │
                              ▼
                    логи и трейсы ─► метрики ─► алерты ─► откат
                                        ▲                    │
                                        └────────────────────┘
Рисунок — четыре стратегии как четыре строки одной политики на модельном шлюзе: зеркало отдаёт кандидату копию 10% трафика, канарейка отдаёт ему 5% живых сессий, деление 50/50 по хешу идентификатора пользователя разводит два рабочих варианта, аварийный выключатель стоит поверх всех трёх. Шлюз пишет логи и трейсы, из них считаются метрики, метрики поднимают алерт, алерт ведёт к откату, а результат самого отката виден в тех же метриках, и на этой обратной стрелке автоматика легко зацикливается.

Последняя строка отличается от канареечной не механикой, а вопросом. Канарейка спрашивает «новая версия не хуже?» Кандидат один, и его либо доводят до ста процентов, либо откатывают. A/B-тест спрашивает другое — «какая версия лучше?» Оба варианта рабочие, у эксперимента есть измеримая гипотеза, а по итогам проигравший выключается тем же конфигом. Разбиение трафика у них общее, критерии остановки — разные.

Теперь про prompt_id. Во флаге лежит ссылка, а не текст. Промпты живут в отдельном реестре с версионированием, и оттуда же берётся техническая механика отката. Возьмём Langfuse, платформу, где промпты хранят и версионируют рядом с трассировкой вызовов. Версии там нумеруются автоматически при каждом сохранении, а до прода добирается та, на которой висит лейбл production, и по умолчанию SDK запрашивает именно её. Откат в документации описан буквально как перенос лейбла на предыдущую версию. Ни деплоя, ни ревёрта, ни пересборки: указатель переехал, следующий запрос читает старый текст.

Пара деталей превращает этот механизм из красивого в рабочий. Лейблы можно защитить, и тогда двигать production имеет право не всякий, а «случайно откатили прод в пятницу» перестаёт быть возможным сценарием. Есть и режим сравнения двух версий построчно, а рекомендация из той же документации звучит так: прогнать версии на датасете до переноса лейбла, а не после.

Дальше идут места, где откат формально сработал, а проблема осталась.

Откат возвращает версию, но не контекст. Диалоги, которые велись при плохой версии, остались в истории, и если агент читает переписку целиком, старая версия продолжит натыкаться на кривые реплики из прошлого.

Наивный автооткат осциллирует. Порог пробит — откатились; на старой версии метрика восстановилась — автоматика решает катить снова; пробили порог — откатились. Лечится паузой после отката и явным состоянием «заморожено до решения человека».

Наконец, причина может лежать вообще не в версии агента. Прежде чем откатывать, проверяют сторону провайдера. Код 429 означает, что мы упёрлись в лимит запросов, 529 — что перегружен сам провайдер. Туда же идут статус-страница и тихие изменения в самой модели.

Теперь собрано всё, чем та же пятница отличалась бы от описанной в начале. Промпт тот же, команда та же, отсечки те же четыре — меняется только то, что стоит между ними.

  • Пятница, вечер. Правку выкатывают не на всех: за флагом, на один процент трафика, пороги отката записаны до старта.
  • Через полчаса. Поддержка приносит ту же жалобу про валюту. Задела она восемьдесят диалогов, а не восемь тысяч.
  • Минутой позже. «А какой промпт был до этого?» — вопрос закрывается запросом в реестр: support_v3.1, на ней и висит лейбл production.
  • Ещё минута. Лейбл переезжает обратно, следующий запрос читает старый текст. Деплоя не было, вечер продолжается.

Честнее сразу сказать, чего конвейер здесь не сделал. Регресс он не обнаружил — обнаружила его та же поддержка и тем же способом. Путаница в валюте не пробивает ни один жёсткий порог: формат валиден, guardrails молчат, расходы на месте, а шумную метрику качества мы в триггеры не пустили сами. Обещание «поставили порог — откатились за восемь минут» описывает регресс, который порог пробивает, и таких меньшинство.

Что конвейер действительно поменял — цену ошибки и цену решения. Во что обходится один неверный ответ, мы считали отдельно, и эту величину конвейер не меняет никак. Он меняет множитель перед ней. Кривые ответы увидели восемьдесят клиентов, а не все восемь тысяч; прежняя версия нашлась в реестре, а не в чьей-то памяти; и возврат занял минуту вместо остатка вечера, потраченного на восстановление прежнего текста по кусочкам.

8. Куда откатываться, когда прежней версии больше нет

Вся конструкция предыдущего раздела держится на молчаливом допущении, что предыдущая версия существует и готова принять трафик. Внутри своего контура так и есть, ведь тёплую v(n−1) держат живой хотя бы сутки после полной выкатки, чтобы откат не упирался в холодный старт. Однако срок жизни версии модели назначает не ваша команда, а провайдер, и календарь этот стоит читать заранее.

Три слова из таблицы ниже стоит расшифровать сразу. GA, general availability, означает общедоступную версию модели, ту, что вышла из превью. Standard и provisioned — два способа платить за развёртывание, по факту запросов и за выкупленную заранее мощность.

Платформа Что обещано Что происходит в конце
Anthropic, собственное API не меньше 60 дней уведомления владельцам активных деплоев статус Retired: запросы к модели падают с ошибкой
Bedrock, Vertex AI собственные графики вывода, не совпадающие с календарём Anthropic одна и та же связка на двух платформах откатывается в разные версии
Microsoft Foundry, GA-модели дату вывода платформа проставляет сама при запуске модели, на 18 месяцев вперёд; замена объявляется за 90–120 дней standard-деплой апгрейдится автоматически, provisioned требует ручной миграции
Рисунок — три политики жизненного цикла: Anthropic даёт минимум 60 дней и затем возвращает ошибку, партнёрские платформы живут по своим графикам, Microsoft Foundry сама проставляет дату вывода GA-модели на 18 месяцев вперёд и автоматически апгрейдит standard-деплой на замену.

Последняя строка таблицы недооценена сильнее прочих. Автоматический апгрейд означает, что в какой-то момент ваш сервис начнёт работать на другой модели, а деплоя не будет. Со стороны инфраструктуры не сломается ничего. Сервис поднят, эндпойнт тот же, коды двухсотые. Изменится только то, как модель понимает ваш промпт.

Насколько сильно она может его понимать иначе? В замере How is ChatGPT's behavior changing over time? (2023) версия GPT-4 от марта отличала простые числа от составных с точностью 84%, а июньская на тех же вопросах давала 51%. Частично из-за упавшей восприимчивости к пошаговым рассуждениям в промпте. К моделям 2026 года эти конкретные проценты отношения не имеют, зато феномен они показывают в чистом виде: тот же промпт на другой версии даёт другой продукт. У GPT-3.5 на той же задаче, кстати, июньская версия оказалась лучше мартовской, так что дрейф не обязан быть деградацией, он просто есть.

Что с этим делают, кроме паники. Test Before You Deploy предлагает рамку из производственных контрактов (зафиксированных требований к поведению модели в вашем сценарии), проверок по категориям риска и гейтов совместимости, которые не пускают на новую версию, пока проверки не пройдены. Честности ради там же перечислено и нерешённое. Два вопроса из этого списка бьют прямо по нашей теме. Как задавать надёжные пороги в недетерминированных системах и как обнаруживать и объяснять дрейф модели, когда провайдер рассказывает о ней немногое. На оба вопроса и этот пост отвечает лишь наполовину.

Второй ход состоит в том, чтобы переписать промпт под новую модель. Работа Prompt Migration показывает на корпоративном поисковом сервисе, что структурная миграция промптов способна полностью вернуть надёжность приложения, потерянную из-за дрейфа модели. Вывод оттуда практический: у промптов есть жизненный цикл, а миграция требует своего стенда, где старая и новая связка гоняются бок о бок.

И следствие для нашего агента, скучное и обязательное: список «на какую версию откатываемся» имеет срок годности. Пересматривают его вместе с календарём вывода моделей, заранее, а не в тот вечер, когда откат понадобился. Рекомендация Microsoft по выбору цели миграции звучит так же приземлённо. Модели-кандидаты сравнивают на своём приложении, своих промптах и своих же типичных данных, публичных бенчмарков для этого мало.

9. Когда гейт всё-таки пропустил: дежурство и разбор

Ступени пройдены, кандидат на ста процентах, канарейка ничего не показала, а через неделю поддержка приносит жалобы. Инцидент с агентом начинается не так, как обычная авария: чаще всего ничего не падает. Ответы становятся хуже, счёт растёт, guardrails срабатывают чаще, а мониторинг доступности остаётся зелёным. Поэтому и шкала, и инструкции пишутся под деградацию качества, частный случай которой — «сервис лежит».

Тяжесть инцидента размечают шкалой уровней, которую называют severity. Нужна она ради одного: заранее договориться, кого и когда будят.

Уровень Что случилось Кого поднимаем
SEV-1 сервис лежит или деградация массовая немедленно, все роли, будим людей
SEV-2 просела часть функций или отдельный сегмент клиентов в рабочие часы
SEV-3 некритично, есть обходной путь в порядке очереди
Рисунок — три уровня severity и реакция на каждый: SEV-1 поднимает всех немедленно, SEV-2 разбирают в рабочие часы, SEV-3 уходит в очередь.

Спор об уровне посреди инцидента обходится дороже самой аварии, поэтому шкалу согласовывают и записывают заранее.

Роли на время инцидента не совпадают с должностями. Координатор держит общую картину и не чинит руками: как только он ушёл в консоль, инцидентом перестают управлять. Отдельный человек говорит с клиентами и стейкхолдерами, снимая эту нагрузку с тех, кто чинит. Технический ведущий устраняет поломку по инструкции. Кто из троих координатор, если в команде три человека и чинят все? Честного ответа на это у практики нет, зато есть требование назначить роль вслух до того, как начнётся авария.

Инструкцию называют runbook. Это документ на один класс инцидентов, и секций в нём пять: симптомы, диагностика, митигация, эскалация, восстановление. Будет он работать или пылиться, определяют несколько правил. Один runbook описывает один класс, и «деградация качества», «рост задержки», «отказ провайдера» живут в разных документах, потому что универсальный никто не читает. Внутри стоят исполнимые шаги. Формулировка «посмотреть логи» годится для обсуждения, а в инструкции пишут «открыть дашборд X, применить фильтр Y». И обновляется он вместе с релизом. Иначе документ описывает позапрошлую архитектуру и оказывается опаснее отсутствующего, потому что ему верят.

Поломку у агентного сервиса устраняют в своём порядке, и первый шаг здесь не тот, что в классическом бэкенде.

1. выключить флаг сломанной фичи        секунды, деплой не нужен
2. откатить связку на тёплую v(n−1)     промпт + модель + параметры разом
3. проверить провайдера                 лимит 429, перегрузка 529,
                                        статус-страница, тихий апгрейд
4. проверить расходы на запрос          финансовый инцидент выглядит
   и попадания в кеш                    как рост счёта при живом сервисе
Рисунок — четыре шага устранения по порядку: аварийный выключатель занимает секунды и не требует деплоя, откат возвращает всю связку целиком, третьим шагом проверяют сторону провайдера, четвёртым — расходы на запрос и попадания в кеш, потому что деньги деградируют отдельно от доступности.

Главная ценность всей конструкции в том, что она снимает груз решений. В три часа ночи, когда ошибиться легко и дорого, человеку нужен готовый план, который остаётся только исполнить.

Отбой ещё не конец инцидента. Конец наступает после разбора. Безвиновный разбор (blameless postmortem) отвечает на вопрос «как система позволила ошибке случиться» вместо «кто нажал», и дело здесь не в вежливости. Там, где ищут виноватого, детали замалчиваются, а вместо честного «неизвестно» появляется догадка, и догадка эта становится ложной корневой причиной. Разбор проводят в пределах двух суток, пока детали свежи. Каноническое описание практики есть у Google SRE, пересказывать его смысла нет.

У агентных инцидентов корневая причина почти никогда не сводится к одной строке кода. Типичная формулировка выглядит так: промпт изменили под новую модель, регресс проявился на редком классе запросов, которого не было в наборе, а канареечная выборка была слишком мала, чтобы его увидеть. Здесь сразу четыре фактора — процесс, технология, данные и контекст, — и виновного среди них нет. Оттого и задачи по итогам выходят процессными: расширить набор, добавить стратификацию, завести отдельный runbook, пересмотреть порог. В код из этого списка почти ничего не правится.

Вернёмся напоследок к разбору Anthropic, потому что он показывает ещё одну вещь, о которой в лекциях говорят реже. Откат бывает не мгновенным. Исправление выкатили 4 сентября, до Vertex AI оно доехало 16-го, до Bedrock — 18-го. Две недели на то, чтобы починенная версия дошла до всех платформ, где сервис живёт. А обнаружение заняло три недели, при том что жалобы в интернете команда видела и просто не умела связать их ни с одним из своих недавних изменений. Даже наша исправленная пятница с возвратом за минуту описывает хороший день — тот, когда жалоба пришла через полчаса и было кому её услышать. Типичный выглядит иначе, и растянут он не по вине конвейера: медленным оказывается не откат, а путь от «что-то не так» до «вот что именно».

Итог

  • Изменение поведения приходит не только с деплоем. Промпт, версия модели, тихое обновление на стороне вендора и дрейф самих обращений меняют ответы в обход сборки, и HTTP-код при этом остаётся двухсотым. Поэтому проверяют не «работает ли», а «стало ли хуже», и делают это на живом потоке.
  • Эвалы ловят грубую регрессию и упираются в два потолка. В наборе нет того, чего инженеры туда не положили, а на том, что есть, оценка идёт по результату задачи целиком. Разбирая сентябрьские инциденты 2025 года, Anthropic признала, что прогоны не поймали деградацию, о которой писали пользователи, отчасти потому, что модель хорошо оправляется от единичной ошибки внутри цепочки.
  • Зеркало отбрасывает ответ, но не побочный эффект. Istio отправляет копию и забывает про неё, а без указания процента копирует весь трафик, то есть удваивает счёт по умолчанию. Вызовы инструментов в тени закрывают на уровне самих инструментов, ключами идемпотентности и моками; сеть тут бессильна.
  • Доля пройденных эвалов в триггеры отката не годится. Она гуляет без всякого влияния на сервис, а такую метрику Google SRE в канареечные не рекомендует; атрибутировать её выкаченной правке тоже нельзя, и вот это уже прямое требование. На первой ступени смотрят жёсткие сигналы, шумные подключают на больших долях и считают методами, корректными при непрерывной проверке.
  • Трафика может физически не хватить. У агента поддержки с потоком 8000 обращений в сутки ступени 1→5→20→50% дают 6080 сессий кандидата за четыре дня, и выкатка кончится раньше, чем наберётся выборка. На узком классе в 9% потока канареечный процент даёт семь сессий в сутки.
  • Судью версионируют наравне с агентом. Скользящий z-тест по потоку оценок ложно срабатывает на 75% потоков без дрейфа, а якорный набор с человеческой разметкой различает дрейф судьи и дрейф продукта в 60 прогонах из 60.
  • У тёплой предыдущей версии есть срок годности. Anthropic даёт минимум 60 дней, дальше запросы падают с ошибкой; Foundry сама проставляет дату вывода общедоступной модели на 18 месяцев вперёд и апгрейдит standard-деплой автоматически. Откат в версию, которой больше нет, не работает.
  • Версия агента — это связка, а не строка в коде. Промпт, модель, параметры генерации, набор инструментов и политика проверок меняются вместе и возвращаются вместе. Всё остальное в посте — способы провести такую связку через прод по ступеням и вернуть назад одним движением, когда она не подошла.
  • Пятый столп приёмки закрыт. Наш агент поддержки выкатывается ступенями, откатывается переносом лейбла за секунды и на плохой день открывает инструкцию, но регресс за него по-прежнему замечают люди, и на поиск причины уходит больше времени, чем на сам возврат. Релизный контур был последним из пяти столпов приёмки. Наблюдаемость, данные, отказоустойчивость и деньги закрыты раньше, а в прод агент идёт по всем пяти сразу, потому что приёмка, пройденная на четыре пятых, не пройдена.

FAQ

Чем канареечная выкатка отличается от A/B-теста?

Целью. Канарейка ведёт единственного кандидата, метрики там работают предохранителем от регрессии, и исходов у неё ровно два, сто процентов трафика или возврат назад. В A/B-тесте оба варианта считаются рабочими, меряют их ради выбора победителя по заранее объявленной гипотезе вроде «короткий промпт поднимет релевантность и снизит задержку», а проигравший потом выключается тем же конфигом. Разбиение трафика по хешу пользователя устроено у них одинаково, зато останавливают их разные вещи. Канарейку останавливает пробитый порог, эксперимент — набранная статистика.

Почему доля пройденных эвалов плохо работает порогом отката?

Потому что она меняется без всякого влияния на сервис: от состава входящих обращений, от температуры генерации, от версии модели-судьи. Google SRE прямо пишет, что метрика, способная сильно скакать без последствий для сервиса, вряд ли годится в канареечные. А приписать её сдвиг конкретной выкаченной правке нельзя вовсе, потому что влияет на неё всё сразу, и это уже нарушение прямого требования об атрибутируемости. Рабочая схема: на малых долях трафика решают жёсткие сигналы (невалидный формат, исчерпанный лимит повторов, срабатывания guardrails, уход в запасную модель), шумную метрику подключают на больших долях и считают методами, допускающими непрерывную проверку.

Как гонять теневой трафик, если агент вызывает инструменты с побочными эффектами?

На уровне сети эта задача не решается. Зеркалирование выбрасывает ответ, а само действие уже совершено. Решают на уровне инструментов. Всё, что меняет мир, либо подменяют моками и прогоном вхолостую (dry-run), либо снабжают ключами идемпотентности, по которым повторный вызов с тем же ключом не выполняет операцию второй раз. Дополнительно помогают постусловия и правило «проверь состояние, прежде чем повторять»: реальные вызовы не атомарны, бывают таймауты после отправки и частично применённые изменения.

Сколько трафика нужно, чтобы канарейка что-то показала?

Зависит от того, какой размер эффекта вы хотите поймать и насколько шумна метрика. Ориентир, который приводят на курсах для A/B-эксперимента: около десяти тысяч сессий на вариант для эффекта в 5% на шумной метрике качества, против полутора тысяч у классического теста с кликами; это оценка, а не измеренная величина, и к канарейке она переносится приблизительно, потому что там проверяют не «лучше ли», а «не хуже ли на заданную величину». Практический вывод важнее числа: при потоке 8000 обращений в сутки ступени 1→5→20→50% дадут 6080 сессий за четыре дня, то есть выкатка закончится раньше набора выборки. Продукту с малым потоком честнее снизить требования к улавливаемому эффекту, чем делать вид, что канарейка что-то доказала.

Что делать, если провайдер выводит модель, на которую мы откатываемся?

Планировать заранее. Это работа по календарю, а не авария. Anthropic обещает минимум 60 дней уведомления владельцам активных деплоев, после чего запросы к выведенной модели падают с ошибкой; партнёрские платформы вроде Bedrock и Vertex AI живут по своим календарям, так что одна и та же связка на двух платформах откатывается в разные версии. В Microsoft Foundry дату вывода общедоступной модели платформа проставляет сама, на 18 месяцев вперёд, а деплой на тарифе standard апгрейдится на замену автоматически, так что поведение сервиса изменится без вашего участия. Список версий для отката пересматривают вместе с календарём вывода моделей.

Как отличить деградацию продукта от дрейфа модели-судьи?

Якорным набором: фиксированной выборкой примеров, размеченной людьми один раз, которую текущий судья переоценивает с постоянным интервалом. Продукт на неё не влияет, поэтому расхождение вердиктов на якоре указывает на судью, а просадка на живом потоке при спокойном якоре указывает на продукт. В опубликованных прогонах тихое обновление версии судьи распознавалось как дрейф судьи в 60 случаях из 60, тогда как обычный скользящий z-тест по потоку оценок даёт ложную тревогу на 75% потоков без всякого дрейфа. Минимальная версия той же дисциплины: версия судьи и текст его промпта фиксируются наравне с версией агента.

Источники

Числовые ориентиры из текста зависят от профиля нагрузки и состава корпуса. Проценты Anthropic описывают конкретный инцидент сентября 2025 года, замер 84% → 51% сделан на моделях 2023 года и говорит о самом феномене дрейфа, а не о сегодняшних версиях; оценка «десять тысяч сессий на вариант» дана для A/B-эксперимента и остаётся ориентиром курса без опубликованной методики, так что для своей метрики размер выборки считают отдельно. Календари вывода моделей меняются вместе с политиками платформ, поэтому перед планированием миграции сверьтесь с текущей страницей провайдера.