A/B-тесты и канареечный деплой
Новая модель лучше на валидации — но прав ли валидационный набор? Проверяем на живом трафике осторожно.
Канареечный деплой направляет малую долю трафика на новую модель; A/B-тест сравнивает две версии статистически на реальной бизнес-метрике.
Почему мало офлайн-метрики
Модель может выигрывать на валидации, но проигрывать в проде: валидационный набор не отражает живой трафик, бизнес-метрика отличается от технической (выше accuracy не всегда = выше выручка). Поэтому финальное решение принимают на реальном трафике, но без риска обрушить всё сразу.
Канареечный деплой
Новую версию выкатывают на маленькую долю (1%, 5%, 10%) трафика — это «канарейка». Если метрики и качество в норме — долю постепенно повышают до 100%. Если что-то не так — мгновенно возвращают весь трафик на старую версию. Имя — от шахтёрских канареек, предупреждавших об опасности.
95% --> [модель v6 (текущая)]
клиенты --> роутер
5% --> [модель v7 (канарейка)] <- следим за метриками
нормально? -> 5% -> 25% -> 50% -> 100%
проблема? -> вернуть на 0%, разбираться
A/B-тест
Здесь цель — не плавный выкат, а статистически достоверное сравнение. Трафик делят (часто 50/50) между версией A (контроль) и B (новая), и измеряют бизнес-метрику: конверсию, выручку, удержание. Решение принимают только когда разница статистически значима, иначе можно принять шум за улучшение.
Значимость своими руками
Покажем, почему нельзя верить «B чуть лучше» без проверки. Сравним конверсии и грубо оценим, отличаются ли они с учётом размера выборки.
import math
def conv_rate(success, n):
return success / n
def z_two_prop(s_a, n_a, s_b, n_b):
p_a, p_b = s_a / n_a, s_b / n_b
p = (s_a + s_b) / (n_a + n_b)
se = math.sqrt(p * (1 - p) * (1 / n_a + 1 / n_b))
return (p_b - p_a) / se if se > 0 else 0.0
# мало данных: B "лучше", но это может быть шум
print("Маленькая выборка:")
z1 = z_two_prop(20, 200, 26, 200)
print(f" A=10.0% B=13.0% z={z1:.2f} значимо(|z|>1.96)? {abs(z1)>1.96}")
# много данных: та же разница уже значима
print("Большая выборка:")
z2 = z_two_prop(2000, 20000, 2600, 20000)
print(f" A=10.0% B=13.0% z={z2:.2f} значимо(|z|>1.96)? {abs(z2)>1.96}")
Вывод:
Маленькая выборка: A=10.0% B=13.0% z=0.94 значимо(|z|>1.96)? False Большая выборка: A=10.0% B=13.0% z=9.40 значимо(|z|>1.96)? True
Одна и та же разница 10% против 13% на 200 наблюдениях — статистический шум (|z|<1.96), а на 20000 — уверенное улучшение. Вот почему A/B-тесту нужен достаточный размер выборки, а решение принимают по значимости, а не по «на глаз лучше».
Канарейка против A/B
| Канарейка | A/B-тест | |
| Цель | безопасно выкатить | статистически сравнить |
| Доля | растёт 1%→100% | фиксирована (напр. 50/50) |
| Решение | «не сломалось ли» | «значимо ли лучше» |
Как работает под капотом
Роутер (балансировщик, фича-флаг, service mesh) разделяет трафик по версиям и проставляет в лог версию модели. Метрики считаются раздельно по версиям (вот зачем версия в логе из прошлого раздела). Канарейка управляется автоматикой: правила «p99 в норме И качество не упало → повысить долю; иначе → откатить». A/B-тест требует случайного и устойчивого назначения (один пользователь всегда в одной группе) и достаточной длительности, чтобы набрать значимость.
Частые ошибки
- Сразу 100% трафика на новую модель. Любая скрытая проблема ударит по всем пользователям.
- Читать A/B-тест слишком рано. Подглядывание до набора выборки выдаёт шум за победу.
- Нестабильное назначение групп. Если пользователь скачет между A и B, результат загрязнён.
- Сравнивать по технической метрике вместо бизнес-метрики. Выше f1 ≠ выше выручка.
Итог
- Канареечный деплой безопасно выкатывает модель, повышая долю трафика и откатываясь при проблеме.
- A/B-тест статистически сравнивает версии на бизнес-метрике; решение — по значимости, а не «на глаз».
- Обоим нужен роутинг с версией в логах; A/B дополнительно требует устойчивого назначения групп и достаточной выборки.