Почему ваше A/B тестирование LLM — это дорогой театр абсурда
Вы запускаете эксперимент. Claude 3.5 Sonnet против новой версии Claude 4.5 на Amazon Bedrock. Трафик делите 50/50. Ждёте две недели. Собираете метрики. Получаете p-value=0.06. Статистической значимости нет. Повторяете. Ещё две недели. Теперь p-value=0.04, но за это время пользователи изменили поведение, и результаты уже нерелевантны. Знакомо? Классическое A/B тестирование для LLM — это как измерять температуру комнаты термометром, который показывает результат через час.
Проблема глубже. Фиксированное разделение трафика игнорирует главное: разные модели лучше справляются с разными типами запросов. Одна блестяще генерирует код, другая — пишет маркетинговые тексты. Ваш единый эксперимент превращается в шум, где настоящий сигнал теряется. Вы платите за вызовы двух моделей, но не понимаете, какую и когда использовать. Это не эксперимент. Это ритуал.
На 18 марта 2026 года, когда выбор моделей в Amazon Bedrock включает не только Claude 4.5, но и обновлённые версии Titan Text G1, Jurassic-2 Ultra и другие, проблема выбора только усугубляется. Ручное тестирование всех комбинаций — путь в никуда.
MCP: не протокол, а контроллер хаоса
Model Context Protocol (MCP) — это не просто способ вызывать модели. Это система управления состоянием и контекстом. Вместо того чтобы напрямую дергать Bedrock API, вы общаетесь с MCP-сервером. Он знает, какие модели доступны, какие промпты к ним привязаны, и — ключевое — в каком эксперименте вы находитесь.
MCP становится единой точкой входа. Ваше приложение шлёт запрос. MCP, на основе динамической логики, решает, какую модель или промпт (вариант A/B теста) использовать для ЭТОГО конкретного пользователя и ЭТОГО конкретного запроса прямо сейчас. Это ломает парадигму статического сплит-теста.
Архитектура: сердце, мозг и память системы
Система состоит из трёх слоёв, которые работают как одна команда. Забудьте про монолитные скрипты.
1. Слой управления (Мозг)
AWS Lambda функция, которая реализует логику динамического назначения. Она принимает идентификатор пользователя и контекст запроса. Заглядывает в DynamoDB, чтобы понять, участвует ли пользователь в активном эксперименте. Если нет — назначает его по адаптивному алгоритму (например, многорукому бандиту). Если эксперимент завершён — выдаёт победивший вариант. Эта лямбда — дирижёр.
2. Слой исполнения (Сердце)
MCP-сервер, развёрнутый, например, в AWS ECS Fargate (бессерверные контейнеры). Он получает от «мозга» решение, какой вариант использовать. В его конфигурации описаны все варианты: «model_id: anthropic.claude-3-5-sonnet-20241022, prompt_template: A» и «model_id: anthropic.claude-4-5, prompt_template: B». Он формирует финальный промпт и вызывает нужную модель через Amazon Bedrock Runtime API. Все вызовы логируются для анализа.
3. Слой данных (Память)
Три таблицы DynamoDB:
- Experiments: активные эксперименты, их параметры (метрика, порог значимости).
- Assignments: кто, в каком эксперименте, какой вариант получил.
- Events: сырые события — результаты вызовов, пользовательские действия (клики, конверсии).
Собираем систему: от CloudFormation до первого запроса
Теория — это скучно. Давайте сделаем. Я разберу ключевые моменты, которые отличают рабочую систему от учебного примера.
1 Готовим инфраструктуру как код
Нельзя вручную настраивать Bedrock, IAM роли и таблицы. Используйте AWS CDK или Terraform. Вот фрагмент определения таблицы Assignments в CDK (TypeScript):
const assignmentsTable = new dynamodb.Table(this, 'AssignmentsTable', {
partitionKey: { name: 'userId', type: dynamodb.AttributeType.STRING },
sortKey: { name: 'experimentId', type: dynamodb.AttributeType.STRING },
billingMode: dynamodb.BillingMode.PAY_PER_REQUEST,
timeToLiveAttribute: 'ttl', // Автоматически удаляем старые назначения
});
// GSI для поиска назначений по эксперименту
assignmentsTable.addGlobalSecondaryIndex({
indexName: 'byExperiment',
partitionKey: { name: 'experimentId', type: dynamodb.AttributeType.STRING },
sortKey: { name: 'variantAssignedAt', type: dynamodb.AttributeType.STRING },
});
Обратите внимание на TTL и Global Secondary Index (GSI). Они критичны для производительности и стоимости на 18.03.2026. Без GSI агрегация данных по эксперименту убьёт вашу таблицу.
2 Пишем логику назначения (не просто random)
Лямбда «мозга» — это не random.choice(). Вот её ядро на Python, использующее алгоритм Томпсона сэмплирования (многорукий бандит) для адаптивного назначения:
import boto3
import random
import json
from scipy.stats import beta
dynamodb = boto3.resource('dynamodb')
experiments_table = dynamodb.Table('Experiments')
assignments_table = dynamodb.Table('Assignments')
def get_assignment(user_id, experiment_id, context):
# Получаем данные эксперимента и текущие агрегированные метрики
exp = experiments_table.get_item(Key={'experimentId': experiment_id})['Item']
variant_stats = exp.get('currentStats', {}) # Например: {'A': {'success': 120, 'total': 200}, 'B': {...}}
# Если данных мало, используем равномерное распределение
if not variant_stats or sum(v['total'] for v in variant_stats.values()) < 100:
chosen = random.choice(list(exp['variants'].keys()))
else:
# Томпсон сэмплинг: сэмплируем из бета-распределения для каждого варианта
samples = {}
for v_id, stats in variant_stats.items():
alpha = stats['success'] + 1
beta_param = stats['total'] - stats['success'] + 1
samples[v_id] = beta.rvs(alpha, beta_param)
# Выбираем вариант с максимальным сэмплированным значением
chosen = max(samples, key=samples.get)
# Сохраняем назначение
assignments_table.put_item(Item={
'userId': user_id,
'experimentId': experiment_id,
'variantId': chosen,
'variantAssignedAt': datetime.utcnow().isoformat(),
'ttl': int(time.time()) + 30*86400 # Удалить через 30 дней
})
return chosen
Это адаптивно. Система начнёт чаще отправлять трафик на вариант, который показывает лучшие результаты в реальном времени. Вы быстрее находите победителя и тратите меньше денег на заведомо проигрышные варианты. Для глубокого погружения в анализ экспериментов смотрите статью о сетевых эффектах в A/B тестах.
3 Конфигурируем MCP-сервер для Bedrock
MCP-сервер — это Python приложение. Его главная задача — абстрагировать вызовы моделей. Конфигурация в формате JSON, которая загружается при старте:
{
"experiments": {
"exp_prompt_optimization_2026": {
"variants": {
"A": {
"modelId": "anthropic.claude-3-5-sonnet-20241022",
"promptTemplate": "Ты — помощник. Отвечай кратко и по делу. Запрос: {user_input}"
},
"B": {
"modelId": "anthropic.claude-4-5",
"promptTemplate": "Ты — эксперт. Дай развёрнутый ответ с примерами. Запрос: {user_input}"
}
}
}
}
}
Сервер получает запрос вида { "experimentId": "exp_prompt_optimization_2026", "variantId": "A", "userInput": "..." }, подставляет входные данные в шаблон и вызывает нужную модель через boto3 client для Bedrock Runtime. Весь код оборачивается в логирование. Для развёртывания такого сервера в продакшн смотрите наш шаблон FAST для агентов на Bedrock.
4 Автоматический стоп-кран для экспериментов
Запускать эксперимент и ждать, пока кто-то его вручную проверит — преступление. Настраиваем AWS EventBridge Rule, которая раз в час запускает Lambda-анализатор. Она:
- Берёт все активные эксперименты из таблицы.
- Для каждого агрегирует последние события (успех/неудача) по вариантам.
- Проверяет условия остановки: (а) достигнута статистическая значимость (p-value < 0.05 по точному тесту Фишера), (б) достигнут максимальный размер выборки.
- Если условие выполнено — помечает эксперимент как завершённый, записывает победителя и может автоматически обновить конфиг MCP-сервера на использование победившего варианта по умолчанию.
Для тех, кто хочет отточить методику оценки LLM перед запуском таких систем, крайне рекомендую ознакомиться с принципами Evals Driven Development. Это сэкономит месяцы работы.
Ошибки, которые превратят вашу умную систему в глупую
Вы можете построить идеальную архитектуру и всё равно получить мусор. Вот что чаще всего ломается:
| Ошибка | Почему это проблема | Как исправить |
|---|---|---|
| Логировать только факт вызова, без контекста запроса | Не сможете понять, для каких типов запросов одна модель выигрывает, а для каких — другая. Упускаете персонализацию. | В событие записывайте эмбеддинг запроса (через Amazon Titan Embeddings) или его категорию. Анализируйте победы по сегментам. |
| Использовать одну метрику (например, длину ответа) для всех сценариев | Оптимизируете не за то. Для чата поддержки важна скорость ответа, для генерации кода — корректность. | Связывайте эксперимент с бизнес-метрикой. Используйте автоматические evals-агенты для оценки качества ответов. |
| Игнорировать «подглядывание» (peeking) | Если вы 10 раз проверите p-value по мере поступления данных, вероятность ложного обнаружения эффекта (type I error) взлетает с 5% до 40%. | Используйте последовательный анализ или фиксируйте размер выборки ДО старта. Алгоритмы вроде многорукого бандита более устойчивы к подглядыванию. |
| Не планировать стоимость | Claude 4.5 дороже Claude 3.5. Ваш адаптивный алгоритм может слишком быстро сконцентрировать трафик на дорогой модели, не дав шанса дешёвой. | Внесите поправку на стоимость в формулу выбора варианта. Оптимизируйте не просто конверсию, а конверсию на единицу стоимости. |
Что дальше? Экосистема, а не изолированный эксперимент
Умное A/B тестирование — не конечная точка. Это фундамент для системы динамического выбора моделей, о которой мы писали в статье про лидерборд моделей на Bedrock. Представьте: вместо двух вариантов в эксперименте у вас есть пул из 5-7 моделей (включая недавно добавленные в Bedrock). MCP-сервер, на основе накопленных исторических данных и контекста запроса, выбирает не между A и B, а из всего пула с вероятностями, обновляемыми в реальном времени.
Это уже не тестирование. Это адаптивная, самообучающаяся система маршрутизации запросов к ИИ. Она минимизирует затраты, максимизирует качество и автоматически интегрирует новые модели по мере их появления. И да, она построена на тех же компонентах: Bedrock, DynamoDB, MCP и несколько лямбд. Просто логика «мозга» становится сложнее.