Сравнение дообученных LLM: практика A/B эксперимента
Допустим, вы дообучили Llama 3.1 на 5000 демонстраций саппорта. ROUGE вырос на 7%, perplexity упала. Запускаете в прод — и escalation rate подскакивает на 15%. Знакомая ситуация. Лабораторные метрики не гарантируют улучшения в реальных условиях. Единственный достоверный способ — провести A/B-тест: разделить трафик, показывать часть запросов baseline, часть — candidate, и сравнить бизнес-метрики.
Наш опыт подтверждает: без корректного A/B-эксперимента дообучение часто оборачивается потерей времени и бюджета. Правильная методология позволяет за 2–5 недель получить статистически значимый ответ и принять решение о rollout или откате. Если вы хотите внедрить A/B-тестирование в свой MLOps-пайплайн, получите консультацию — мы поможем настроить эксперимент под ваш стек.
Например, в одном из проектов мы сравнили две версии fine-tuned Mistral 7B: контрольная (v1) и экспериментальная (v2). Результаты показали улучшение task completion rate на 7.6% при p-value < 0.01. Это позволило обосновать rollout и сэкономить бюджет на дальнейшее дообучение.
Как организовать эксперимент?
Сравниваются две версии: baseline (например, исходная GPT-4o) и candidate (fine-tuned на новых данных). Типичные сценарии:
- базовая модель против fine-tuned на том же архитектурном ядре;
- fine-tuned v1 против v2 (итерация датасета);
- две разные модели (Llama 3.1 8B vs Mistral 7B), обе дообучены;
- один и тот же inference с разными промптами.
Ключ — детерминированное разделение трафика. Реализуем через хэш от user_id:
import hashlib
import random
from typing import Literal
class ABRouter:
"""Детерминированная маршрутизация A/B по user_id"""
def __init__(self, experiment_name: str, split: float = 0.5):
self.experiment_name = experiment_name
self.split = split # Доля трафика для варианта B
def assign(self, user_id: str) -> Literal["control", "treatment"]:
"""Один пользователь всегда попадает в одну группу"""
hash_input = f"{self.experiment_name}:{user_id}"
hash_value = int(hashlib.md5(hash_input.encode()).hexdigest(), 16)
normalized = (hash_value % 10000) / 10000
return "treatment" if normalized < self.split else "control"
router = ABRouter("fine-tuned-v2-test", split=0.2) # 20% трафика на новую модель
# В обработчике запроса
def handle_request(user_id: str, prompt: str) -> str:
variant = router.assign(user_id)
if variant == "control":
response = baseline_model.generate(prompt)
model_version = "baseline"
else:
response = finetuned_model.generate(prompt)
model_version = "v2-finetuned"
log_event(user_id, variant, prompt, response, model_version)
return response
Какие метрики выбрать?
- Task completion rate — доля диалогов, где пользователь получил ответ без escalation. Первичная метрика, напрямую отражает бизнес-ценность.
- CSAT — оценка пользователем качества ответа (звёзды, лайк/дизлайк).
- Escalation rate — процент обращений, переданных оператору.
- Time-to-value — количество сообщений до решения задачи.
- Latency (P50, P95, P99) — чтобы не потерять в скорости.
Измеряйте их на одинаковом срезе запросов, исключая сезонность. Если модель показывает улучшение по первичной метрике, а latency выросла в пределах допуска — решение о rollout принимается.
Как определить статистическую значимость?
Недостаточно просто сравнить средние — нужен t-тест. Реализация на Python:
from scipy import stats
import numpy as np
def ab_significance_test(
control_outcomes: list[float],
treatment_outcomes: list[float],
alpha: float = 0.05
) -> dict:
"""
Двусторонний t-тест для проверки значимости разницы в метриках
control_outcomes: метрики группы A (например, task_completion = [1,0,1,1,...])
"""
t_stat, p_value = stats.ttest_ind(control_outcomes, treatment_outcomes)
control_mean = np.mean(control_outcomes)
treatment_mean = np.mean(treatment_outcomes)
relative_lift = (treatment_mean - control_mean) / control_mean * 100
return {
"control_mean": control_mean,
"treatment_mean": treatment_mean,
"relative_lift_pct": relative_lift,
"p_value": p_value,
"significant": p_value < alpha,
"sample_sizes": {"control": len(control_outcomes), "treatment": len(treatment_outcomes)}
}
# Расчёт необходимого размера выборки
def required_sample_size(
baseline_rate: float, # Текущая метрика (например, 0.75)
min_detectable_effect: float, # Минимальное значимое улучшение (например, 0.05)
alpha: float = 0.05,
power: float = 0.80
) -> int:
from statsmodels.stats.power import TTestIndPower
analysis = TTestIndPower()
effect_size = min_detectable_effect / (baseline_rate * (1 - baseline_rate)) ** 0.5
n = analysis.solve_power(effect_size=effect_size, alpha=alpha, power=power)
return int(np.ceil(n))
# Пример: baseline task_completion = 75%, хотим зафиксировать улучшение на 5%
n = required_sample_size(0.75, 0.05)
print(f"Нужно {n} запросов на каждую группу") # ~500–1000
Почему t-тест, а не только доверительные интервалы?
Традиционный подход с доверительными интервалами (check overlap) может вводить в заблуждение при малых выборках. t-тест даёт точный p-value, а при нормально распределённых метриках (task completion — бинарная, но для больших n t-тест робастен) он статистически корректен. В случае бинарных метрик можно дополнительно использовать z-тест для пропорций — мы в работе используем оба метода для перекрёстной проверки.
Кейс: A/B-тест саппорт-бота (из нашей практики)
Контекст: customer support бот на базе Llama 3.1 8B fine-tuned v1. Подготовлена версия v2 с улучшенным датасетом (+800 примеров, исправлены failure cases v1).
Эксперимент:
- Контроль (80% трафика): v1
- Тест (20% трафика): v2
- Длительность: 14 дней
- Размер выборки: 6200 диалогов на контрольную группу, 1550 на тестовую
Первичная метрика: task_completion_rate (пользователь решил вопрос без escalation). Вторичные: CSAT, escalation_rate, avg_turns_to_resolution.
Результаты:
| Метрика | v1 (control) | v2 (treatment) | p-value | Значимо? |
|---|---|---|---|---|
| Task completion | 71.3% | 78.9% | 0.0012 | Да |
| CSAT | 3.8 | 4.1 | 0.034 | Да |
| Escalation rate | 28.7% | 21.1% | 0.0008 | Да |
| Avg turns | 3.2 | 2.9 | 0.18 | Нет |
| Latency P95 | 2.1с | 2.3с | — | +10% |
v2 статистически значимо лучше по трём из четырёх метрик. Рост латентности P95 (+10%) приемлем. Принято решение о полном rollout.
Пошаговый план внедрения A/B-тестирования
Чтобы вы не упустили детали, вот чеклист этапов:
- Определите бизнес-метрику (например, task completion). Выберите одну первичную.
- Рассчитайте размер выборки с помощью power analysis (как в коде выше).
- Реализуйте маршрутизацию с детерминированным хэшем по user_id.
- Запустите эксперимент — не менее 1-2 недель, чтобы покрыть недельные циклы.
- Соберите данные — логируйте метрики в единую систему (MLflow, Weights & Biases).
- Проведите t-тест — проверьте p-value < 0.05.
- Примите решение: rollout, доработка или откат.
Что входит в нашу работу по A/B-тестированию?
- Разработка схемы эксперимента и выбор метрик.
- Реализация модуля маршрутизации с логированием.
- Интеграция с вашим MLOps-стеком (MLflow, LangSmith, Weights & Biases).
- Расчёт необходимого размера выборки и длительности теста.
- Анализ результатов и подготовка отчёта с рекомендациями.
- Пост-rollout мониторинг и, при необходимости, корректировка.
Мы также обучаем вашу команду самостоятельно запускать A/B-тесты в будущем.
Сравнение популярных инструментов
| Инструмент | Назначение | Интеграция | Открытый исходный код |
|---|---|---|---|
| LangSmith | Трекинг экспериментов LangChain | LangChain, Python | Да |
| Phoenix (Arize) | Observability LLM | OpenTelemetry | Да |
| MLflow | Универсальный трекинг | Python, REST API | Да |
| Weights & Biases | Таблицы, гистограммы | Python | Да |
Выбор зависит от текущего стека. Мы помогаем настроить оптимальную связку.
Сроки и экономия бюджета
- Настройка A/B инфраструктуры: 3–7 дней
- Эксперимент (достижение необходимого n): 1–4 недели
- Анализ результатов и принятие решения: 2–3 дня
- Итого: 2–5 недель
Сократите затраты на дообучение: закажите внедрение A/B-тестирования под ключ — мы настроим эксперимент, интегрируем с вашим MLOps-стеком и поможем интерпретировать результаты.







