Зачем A/B, если есть offline-оценка

Логичный вопрос: мы же уже умеем сравнивать версии на датасете (offline + judge). Зачем ещё и эксперимент на людях? Потому что offline и A/B отвечают на разные вопросы, и второй — решающий перед раскаткой.

Offline-сравнениеA/B тест
На чёмФиксированный датасетЖивой трафик, реальные юзеры
МетрикаJudge/scorers на goldenПоведение: успех, retention, 👍, конверсия
ЛовитРегрессию на известных кейсахРеальный эффект на реальных людях
РольБыстрый фильтр «не хуже?»Финальное решение «раскатывать?»

Правильный порядок — воронка: offline отсеивает заведомо плохие версии дёшево и быстро (не гоняем в прод мусор), а прошедшие проверяем A/B-тестом на реальном трафике. Offline экономит эксперименты, A/B выносит вердикт. Именно поэтому этот урок идёт после offline-метрик и judge.

ℹ️ Главный враг A/B-теста — случайность

Метрики всегда колеблются: сегодня успех 71%, завтра 73% — даже без всяких изменений, просто из-за того, какие пользователи пришли. Увидеть «74% против 71%» и решить «новый промпт лучше» — классическая ошибка: разница может быть чистым шумом. Вся статистика A/B-теста — это машинерия, чтобы отличить реальный эффект от случайного колебания. Без неё ты подбрасываешь монетку и называешь это решением.

Постановка эксперимента

Надёжный A/B тест начинается не с кода, а с дизайна. Четыре решения до запуска — иначе результат будет неинтерпретируемым.

100%
колёсико — масштаб  ·  зажать и тянуть — перемещение
Схема A/B-эксперимента Трафик случайное деление 50% 50% CONTROL (A) старый промпт (baseline) TREATMENT (B) новый промпт (гипотеза) Одна метрика success rate / 👍 A: 71% B: 74% Стат-тест значимо? p < 0.05? 4 решения ДО запуска: ① гипотеза · ② ОДНА главная метрика · ③ размер выборки заранее · ④ длительность случайное деление · только одно изменение между A и B · не подглядывать до конца
  • ① Гипотеза. Конкретное, проверяемое утверждение: «новый промпт повысит task success rate». Не «сделаем лучше» — а что именно и на какой метрике.
  • ② Одна главная метрика (OEC — overall evaluation criterion). Ты сравниваешь по одной заранее выбранной метрике. Гнаться за десятью — путь к самообману (об этом ниже).
  • ③ Control и treatment. Пользователи случайно делятся на группы; между A и B меняется только промпт — иначе не поймёшь, что дало эффект. Control — текущий прод (baseline).
  • ④ Размер выборки и длительность — заранее. Сколько нужно данных и как долго крутить — считается до старта (следующий раздел), а не «пока не понравится».
✅ Меняй одну вещь за раз

Если в treatment ты поменял и промпт, и модель, и температуру — при любом результате не узнаешь, что сработало (или навредило). Классический принцип эксперимента: одно изменение между control и treatment. Хочешь проверить три идеи — три отдельных теста (или многовариантный тест с бóльшей выборкой), а не всё в кучу.

Статистическая значимость и p-value

Сердце A/B-теста — ответ на вопрос: «разница между A и B реальна или это случайность?». Инструмент — статистический тест, а его результат — p-value. Объясним без формул.

Представь, что на самом деле разницы нет (промпты одинаковы). p-value — это вероятность увидеть наблюдаемую разницу (или больше) чисто случайно, если бы разницы правда не было. Малый p-value = «такое совпадение слишком невероятно, значит разница, скорее всего, настоящая».

Гипотеза H0: «разницы нет» (промпты одинаковы)

p-value = P(увидеть такую разницу | H0 верна, т.е. эффекта НЕТ)

p = 0.60  → такое запросто бывает случайно      → НЕ значимо (не верим в эффект)
p = 0.03  → случайно почти не бывает            → значимо (эффект реальный)

Порог обычно α = 0.05:  p < 0.05 → «статистически значимо»

⚠️ p < 0.05 НЕ значит «вероятность 95%, что B лучше»
   и НЕ значит «эффект большой» — только «вряд ли это чистый шум»

Порог α = 0.05 — соглашение: готовы в 5% случаев принять шум за эффект (ложноположительный). Для бинарной метрики (успех/нет) считают, например, тест пропорций (z-test / хи-квадрат); для непрерывной (латентность, длина) — t-тест.

Проверка значимости для success rate (тест пропорций)
python
from statsmodels.stats.proportion import proportions_ztest

# control (A): 710 успехов из 1000 · treatment (B): 745 из 1000
successes = [745, 710]     # B, A
totals    = [1000, 1000]

stat, p_value = proportions_ztest(successes, totals)
print(f"p-value = {p_value:.4f}")   # напр. 0.0083

if p_value < 0.05:
    print("Разница значима — B, вероятно, реально лучше")
else:
    print("Не значимо — возможно, просто шум. Нужно больше данных или эффекта нет")

# ⚠️ значимость ≠ важность: 74.5% vs 71% значимо на 1000+1000,
#    но стоит ли эффект +3.5% усилий/рисков — отдельный продуктовый вопрос
⚠️ Значимость ≠ размер эффекта

Две ловушки. На огромной выборке значимым станет даже +0.1% — статистически реально, но практически бесполезно (эффект есть, но крошечный). И наоборот: на маленькой выборке даже большая разница может быть незначимой. Поэтому смотри и p-value (реален ли эффект), и размер эффекта с доверительным интервалом (насколько он велик). «Значимо» отвечает «не шум ли», но не «стоит ли оно того».

Сколько нужно данных

Частый провал: запустили тест, через день «B лучше!», раскатали — а на самом деле данных было слишком мало, и разница была шумом. Размер выборки считают заранее, исходя из трёх величин:

  • Baseline — текущее значение метрики (например, success 71%).
  • MDE (minimum detectable effect) — минимальный эффект, который хочешь уметь замечать (например, +2 п.п.). Чем меньше эффект хочешь ловить, тем больше данных нужно.
  • Мощность и α — обычно power 80%, α 0.05.
Грубая интуиция:
  хочешь ловить БОЛЬШОЙ эффект (+10%)  → нужно МАЛО данных
  хочешь ловить МАЛЫЙ эффект (+1%)     → нужно ОЧЕНЬ МНОГО данных

  редкое событие (конверсия 2%)        → нужно больше, чем для частого (успех 70%)

→ считай N ДО старта (power analysis), фиксируй длительность,
  и НЕ останавливай тест раньше, даже если «уже видно победителя»
❌ Peeking — подглядывание убивает статистику

Самая коварная ошибка: смотреть на p-value каждый час и остановить тест, как только он «пересёк 0.05». Так ты почти гарантированно поймаешь случайный момент, когда шум дал значимость — ложное открытие. Правило: зафиксируй N и длительность заранее и дождись конца. Хочешь останавливаться раньше легально — нужны специальные методы (sequential testing / поправки), а не «глазами по дашборду».

Rollout: безопасная раскатка

Тест показал: B значимо лучше. Не спеши включать его всем сразу — раскатывай поэтапно, чтобы поймать проблемы, которые A/B не увидел (редкие кейсы, всплеск latency/cost, деградация на определённом сегменте).

Поэтапный rollout (canary → gradual → full):

  1%  →  наблюдаем guardrails (error rate, latency, cost, quality)  →  ок?
  ↓
  10% →  дольше, шире аудитория                                     →  ок?
  ↓
  50% →  половина трафика                                           →  ок?
  ↓
  100% (полная раскатка)          ← в любой момент можно откатиться

Guardrail-метрики: даже если главная метрика ↑, откат если error/latency/cost/quality ↓

Ключевые элементы безопасной раскатки:

  • Canary — сначала крошечная доля (1–5%): ловим катастрофы малой кровью.
  • Guardrail-метрики — набор «предохранителей» помимо главной метрики: рост error rate, latency, cost, падение quality → автостоп/откат, даже если основная метрика выросла.
  • Быстрый откат (rollback) — возможность мгновенно вернуть control. Промпты, вынесенные из кода (prompt management из урока про Langfuse), позволяют откатиться без передеплоя.
  • Мониторинг всё время — дашборды из раздела Observability работают именно здесь.
🎯 Как всё связывается

A/B тест — это оркестровка всего раздела: метрику считаешь judge'ом/scorer'ом (offline-метрики, LLM-judge), guardrails и rollout смотришь на дашбордах (Observability), а сами версии промптов переключаешь через prompt management. A/B — не отдельный инструмент, а способ принять решение по данным, используя всё, что ты уже построил.

Типичные ошибки

❌ Ошибка 1: решать по двум числам без стат-теста

«74% > 71%, значит B лучше» — возможно, чистый шум. Всегда проверяй значимость и учитывай размер выборки, иначе раскатываешь случайность.

❌ Ошибка 2: peeking и ранняя остановка

Подглядывание за p-value и остановка «когда стало значимо» ловит случайные пики. Фиксируй N и длительность заранее, дожидайся конца.

❌ Ошибка 3: много метрик / много изменений

Смотришь 20 метрик — одна «значима» случайно (проблема множественных сравнений). Меняешь всё сразу — не знаешь, что сработало. Одна главная метрика, одно изменение.

❌ Ошибка 4: раскатка на 100% без canary и guardrails

Победа в A/B не гарантирует отсутствие редких катастроф. Раскатывай поэтапно с guardrail-метриками и возможностью мгновенного отката.

Шпаргалка

A/B тестирование промптов — всё в одном месте
text
A/B ТЕСТ = сравнение версий на ЖИВОМ трафике (финальный вердикт «раскатывать?»)
  воронка: offline отсеивает плохое дёшево → A/B выносит вердикт на реальных юзерах
  главный враг = СЛУЧАЙНОСТЬ (метрики колеблются сами по себе)

ПОСТАНОВКА (4 решения ДО старта):
  ① гипотеза (что именно, на какой метрике)
  ② ОДНА главная метрика (OEC)
  ③ control (старый) vs treatment (новый), случайное деление, ОДНО изменение
  ④ размер выборки + длительность — заранее

ЗНАЧИМОСТЬ / p-value:
  H0 = «разницы нет» · p = P(такая разница | эффекта нет)
  p<0.05 → значимо (вряд ли шум) · тест пропорций (бинарн.) / t-тест (непрерывн.)
  ⚠️ p<0.05 ≠ «95% что B лучше» · значимость ≠ размер эффекта (смотри ещё CI/effect size)
  большая выборка → значим даже мусорный +0.1%

РАЗМЕР ВЫБОРКИ: baseline + MDE (мин. эффект) + power(80%)/α(0.05) → power analysis
  меньше эффект/реже событие → больше N · считать ДО старта
  ❌ PEEKING: не смотреть p каждый час и не стопать «когда значимо» → ложное открытие

ROLLOUT (поэтапно): canary 1% → 10% → 50% → 100%, откат в любой момент
  GUARDRAILS: откат если error/latency/cost↑ или quality↓ (даже если гл. метрика ↑)
  prompt management → откат без передеплоя · дашборды → мониторинг

ГРАБЛИ: 2 числа без теста · peeking · много метрик/изменений · 100% без canary/guardrails

Практическое задание

Задание: спроектируй A/B тест промпта

  1. Сформулируй гипотезу для новой версии промпта агента из модулей 03–05: что меняешь и какую одну метрику ожидаешь улучшить.
  2. Опиши дизайн: как делишь трафик, что именно control и treatment, какое единственное изменение между ними.
  3. Возьми числа: A = 680/1000 успехов, B = 720/1000. Посчитай p-value (proportions_ztest). Значима ли разница? А если бы было по 100 наблюдений с той же долей?
  4. Объясни своими словами, что означает полученный p-value. Что он НЕ означает?
  5. Прикинь размер выборки: хочешь уверенно ловить эффект +2 п.п. при baseline 70%. Данных нужно много или мало? А для +10 п.п.?
  6. Спроектируй rollout: этапы раскатки и 3 guardrail-метрики, при падении которых ты откатишься, даже если главная метрика выросла.
  7. Со звёздочкой: объясни на примере, почему «подглядывание» (остановка теста, как только p<0.05) завышает долю ложных открытий. Как этого избежать?

Что дальше

A/B ловит, стал ли билд лучше в среднем. Но есть отдельная опасность — регрессия: правка, улучшив общее, тихо ломает то, что раньше работало. Защита от этого — regression testing на golden datasets: финальный урок раздела про то, как не дать качеству деградировать при каждом изменении промпта.