Порівняння донавчених 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-стеком і допоможемо інтерпретувати результати.







