A/B-тестирование дообученных моделей
A/B-тестирование LLM — процесс сравнения двух версий модели (baseline vs candidate) на реальном трафике или репрезентативной выборке запросов. Без A/B теста невозможно достоверно подтвердить, что дообученная модель лучше предыдущей в продакшн-условиях — лабораторные метрики (ROUGE, F1) не всегда коррелируют с реальной пользой.
Структура A/B теста для LLM
Типичные сравнения:
- Базовая модель (GPT-4o / Llama base) vs fine-tuned версия
- Fine-tuned v1 vs Fine-tuned v2 (итерация датасета)
- Модель A (Llama 3.1 8B) vs Модель B (Mistral 7B), обе fine-tuned
- Prompt v1 vs Prompt v2 на одной модели
Метрики A/B теста:
- Предпочтение пользователей (explicit: лайки/дизлайки, implicit: повторные использования)
- Task completion rate (пользователь получил нужный ответ с первого раза)
- Time-to-value (за сколько сообщений задача решена)
- Escalation rate (доля обращений, переданных человеку)
- Latency (P50, P95, P99)
Реализация маршрутизации трафика
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
Статистическая значимость
Недостаточно просто сравнить средние метрики — нужно проверить статистическую значимость разницы.
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
Практический кейс: 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 тестирования LLM
LangSmith (LangChain): интегрированное отслеживание экспериментов, comparison view. Phoenix (Arize): OpenTelemetry-based observability для LLM. MLflow: универсальный трекинг экспериментов. Weights & Biases: таблицы, гистограммы, LLM eval pipeline.
Сроки
- Настройка A/B инфраструктуры: 3–7 дней
- Эксперимент (достижение необходимого n): 1–4 недели
- Анализ результатов и принятие решения: 2–3 дня
- Итого: 2–5 недель







