Реализовать A/B тестирование промптов
ТЕХНИЧЕСКОЕ ЗАДАНИЕ: Реализовать A/B тестирование промптов
1. Цель задачи
Научиться проектировать и проводить A/B-тестирование двух версий промпта (v1 и v2) для LLM-системы в production-подобных условиях. Сравнить метрики faithfulness (фактологическая точность) и cost (стоимость токенов) для каждой версии, собрав достаточную статистику для принятия решения. Определить победителя (версию, которая статистически значимо лучше по ключевым метрикам).
Ключевой результат Воспроизводимый пайплайн A/B-теста с детерминированным распределением трафика 50/50, фиксацией метрик и статистическим выводом о том, какая версия промпта побеждает с уверенностью ≥95%.
2. Исходные данные
| Что нужно | Откуда взять |
|---|---|
| Две версии промпта (v1 и v2) | Составить самостоятельно или взять из существующего проекта |
| Набор тестовых запросов (не менее 50 штук) | Собрать из логов, синтезировать или использовать датасет (например, SQuAD для фактологических вопросов) |
| LLM API (OpenAI, Anthropic или локальная модель) | Ключ API / локальный endpoint |
| Инструмент для оценки faithfulness | RAGAS (Faithfulness) или кастомный judge-LLM (например, gpt-4o-mini) |
| База знаний (опционально, для ответов с фактами) | Например, статьи Wikipedia по темам запросов |
Если нет реального API или judge-инструмента — симулируем:
- Создать две версии промпта, заведомо одна лучше другой (например, v1 без инструкции check facts, v2 с явным требованием проверки).
- Вместо API использовать локальную модель через Hugging Face (например, Qwen2-1.5B).
- Для faithfulness написать простой эвристический скрипт: проверять, содержит ли ответ ключевые слова из эталонного ответа (ground truth). Для эталонных ответов взять первые абзацы из Wikipedia.
- Важно в таком случае метрика faithfulness будет приблизительной, но для учебной задачи достаточной.
3. Технологический стек
| Компонент | Инструменты | Назначение |
|---|---|---|
| Язык программирования | Python 3.11+ | Основной код пайплайна |
| LLM API | openai / anthropic / litellm | Генерация ответов |
| A/B распределение | random (seed), UUID | Детерминированное направление запроса в v1/v2 |
| Оценка faithfulness | RAGAS (опционально) или judge-LLM | Вычисление faithfulness для каждого ответа |
| Замер стоимости | tiktoken / litellm cost tracking | Подсчёт токенов и cost на запрос |
| Статистический анализ | scipy.stats (ttest, mannwhitneyu) | Сравнение метрик между группами |
| Логирование | loguru / structlog | Фиксация каждого запроса и метрик |
| Хранение результатов | pandas DataFrame → CSV / Parquet | Долговременная запись для анализа |
4. Этапы выполнения
Этап 1: Подготовка тестового набора и промптов (1 час)
Действия
-
Определить две версии промпта Пусть v1 — базовый:
"Ответь на вопрос, используя контекст: {context}. Ответ:"
v2 — улучшенная (с требованием точности):
"Ответь на вопрос, используя только факты из контекста. Если ответа нет в контексте, скажи 'Не знаю'. Вопрос: {question}. Контекст: {context}. Ответ:" -
Собрать набор запросов (минимум 50). Каждый запрос — структура:
{ "id": "q001", "question": "Какая столица Франции?", "context": "Франция — страна в Западной Европе. Столица — Париж.", "ground_truth": "Париж" }Взять 20 простых (ответ есть в контексте), 20 сложных (требуется вывод), 10 без ответа (ответа нет в контексте). Можно синтезировать с помощью LLM.
-
Подготовить скрипт загрузки — прочитать CSV или JSON с запросами.
| Поле | Тип | Пример |
|---|---|---|
| question | str | "Какая столица Франции?" |
| context | str | "Франция — страна. Столица — Париж." |
| ground_truth | str | "Париж" |
| expected_answerable | bool | true |
Ожидаемый результат этапа Файл test_queries.json с мин. 50 записями, две строковые переменные PROMPT_V1, PROMPT_V2 в коде.
Этап 2: Реализация A/B распределения и прогон (1.5 часа)
Действия
-
Написать функцию assign_version(user_id или request_id):
import hashlib def assign_version(request_id: str) -> str: # стабильное хеширование, 50/50 hash_val = int(hashlib.md5(request_id.encode()).hexdigest(), 16) return 'v1' if hash_val % 2 == 0 else 'v2'Примечание: используем request_id = f"run-{i}" для детерминизма.
-
Реализовать цикл прогона
import openai, time results = [] for i, query in enumerate(queries): version = assign_version(f"test-{i}") prompt = PROMPT_V1 if version == 'v1' else PROMPT_V2 # заменить {question} и {context} в prompt filled_prompt = prompt.format(question=query['question'], context=query['context']) start = time.time() response = openai.chat.completions.create( model="gpt-4o-mini", messages=[{"role": "user", "content": filled_prompt}], max_tokens=200 ) elapsed = time.time() - start answer = response.choices[0].message.content usage = response.usage cost = (usage.prompt_tokens * 0.00015 + usage.completion_tokens * 0.0006) / 1000 # пример цен results.append({ 'query_id': query['id'], 'version': version, 'answer': answer, 'prompt_tokens': usage.prompt_tokens, 'completion_tokens': usage.completion_tokens, 'cost': cost, 'latency_sec': elapsed }) -
Добавить логирование каждая итерация пишется в loguru.logger.info.
Ожидаемый результат этапа Сырой CSV raw_results.csv с колонками: query_id, version, answer, prompt_tokens, completion_tokens, cost, latency_sec.
Этап 3: Вычисление метрик faithfulness (1 час)
Действия
-
Выбрать способ оценки faithfulness
- Вариант A (RAGAS): использовать ragas.metrics.Faithfulness — требует ответ, контекст и список сгенерированных утверждений. Может потребовать дополнительного API вызова.
- Вариант B (Judge LLM): отправить ответ и контекст в отдельный промпт-судья:
judge_prompt = """Оцени faithfulness ответа по шкале 0-1. Ответ: {answer}. Контекст: {context}. Выведи только число от 0 до 1.""" score = float(ask_llm(judge_prompt)) - Вариант C (Эвристика): по ground_truth — точное совпадение или ROUGE-L. Используется, если нет доступа к дополнительному LLM.
Выбрать вариант B как наиболее практичный (используем ту же модель, но отдельный вызов).
-
Написать функцию evaluate_faithfulness(answer, context):
def evaluate_faithfulness(answer: str, context: str) -> float: prompt = f"""Context: {context}\nAnswer: {answer}\nIs the answer faithful to the context? Answer only a number between 0 and 1.""" response = client.chat.completions.create( model="gpt-4o-mini", messages=[{"role": "user", "content": prompt}], temperature=0.0, max_tokens=5 ) try: return float(response.choices[0].message.content.strip()) except: return 0.0 -
Прогнать для всех результатов (можно параллельно, но не обязательно). Записать faithfulness в DataFrame.
Ожидаемый результат этапа Обогащённый CSV results_with_metrics.csv с колонкой faithfulness (float).
Этап 4: Статистический анализ и определение победителя (1 час)
Действия
-
Разделить данные по версиям
import pandas as pd from scipy.stats import ttest_ind, mannwhitneyu df = pd.read_csv('results_with_metrics.csv') v1 = df[df.version == 'v1'] v2 = df[df.version == 'v2'] -
Проверить нормальность распределения метрик (Shapiro-Wilk) для faithfulness и cost. Если p>0.05 — t-test, иначе Mann-Whitney U.
from scipy.stats import shapiro stat_v1, p_v1 = shapiro(v1['faithfulness']) stat_v2, p_v2 = shapiro(v2['faithfulness']) use_parametric = p_v1 > 0.05 and p_v2 > 0.05 test = ttest_ind if use_parametric else mannwhitneyu stat, p_value = test(v1['faithfulness'], v2['faithfulness']) -
Сравнить средние cost Для cost обычно распределение скошено, используем Mann-Whitney U.
-
Визуализировать boxplot для faithfulness и cost по версиям (matplotlib/seaborn).
-
Определить победителя
- Если p_value < 0.05 для faithfulness и v2 выше v1 → победитель v2.
- Если p_value < 0.05 для cost и v2 дешевле → дополнительный плюс.
- Если обе метрики не показывают значимой разницы → ничья.
-
Документировать вывод в виде текста:
Версия v2 показала статистически значимо более высокую faithfulness (mean=0.85 vs 0.72, p=0.003). Разница в cost незначима (p=0.45). Победитель: v2.
Ожидаемый результат этапа Файл ab_test_report.md с результатами анализа, графиками и финальным решением.
Этап 5: Документирование и воспроизводимость (30 минут)
Действия
- Упаковать весь код в единый скрипт
run_ab_test.py, который принимает аргументы:--queries_file,--prompt_v1,--prompt_v2,--model. - Зафиксировать seed для детерминированного разбиения.
- Добавить README с инструкцией по запуску.
- Запушить в Git (опционально).
Ожидаемый результат этапа Репозиторий/папка со скриптом, примером данных, отчётом.
5. Критерии приемки (Definition of Done)
- Разработан скрипт A/B тестирования, который принимает две версии промпта и набор запросов.
- Распределение трафика 50/50 детерминированное и воспроизводимое.
- Для каждого запроса замерены: ответ, prompt_tokens, completion_tokens, cost, latency.
- Метрика faithfulness вычислена для каждого ответа (через judge LLM или эвристику).
- Проведён статистический тест (t-test или Mann-Whitney) для faithfulness и cost.
- Сгенерирован отчёт с визуализацией и заключением о победителе.
- Все данные (сырые, с метриками, отчёт) сохранены в файлы.
- Код не использует жёстко зашитые ключи API (использует переменные окружения).
- Набор тестовых запросов содержит минимум 50 запросов с указанием ground_truth и контекста.
- При повторном запуске с теми же параметрами результаты идентичны (детерминизм).
6. Ожидаемый результат
Основной артефакт — Python-скрипт run_ab_test.py, который:
- Принимает два промпта (v1, v2) и файл запросов;
- Запускает A/B тест;
- Сохраняет результаты в CSV;
- Вычисляет faithfulness;
- Выполняет статистический анализ;
- Формирует отчёт
ab_test_results.md.
Дополнительные артефакты:
raw_results.csv— сырые ответы и стоимость;results_with_metrics.csv— с колонкой faithfulness;ab_test_report.md— итоговый отчёт с таблицами метрик (средние, std, p-value) и boxplot (в виде ссылки на PNG или ASCII-графика при возможности).
Содержимое отчёта:
- Общее количество запросов по версиям;
- Средние и медианные faithfulness и cost;
- Результат статистического теста (p-value и какой тест использован);
- График распределения faithfulness;
- Итоговый вывод: какая версия победила.
7. Возможные сложности и их решение
| Сложность | Решение |
|---|---|
| Неточная оценка faithfulness judge-LLM (низкая согласованность) | Использовать тот же judge-промпт для всех ответов, temperature=0; или применить агрегацию по 3 запускам. |
| Высокая стоимость прогона 50+ запросов (до $0.2 за прогон) | Использовать самую дешёвую модель (gpt-4o-mini), уменьшить max_tokens. |
| Асинхронность — долгий последовательный прогон | Добавить asyncio с asyncio.gather, но следить за rate limits. |
| Неравномерное количество запросов в группах из-за сбоев | Использовать retry-логику (например, tenacity) и логировать ошибки отдельно. |
| Проблема множественного сравнения (faithfulness + cost) | Не поправлять p-value, т.к. это исследовательский тест, но в отчёте явно указать, что тестируются две гипотезы. |
| Отсутствие ground truth для эвристической оценки | Использовать только judge-LLM или RAGAS; если нет — ограничиться качественным анализом. |
8. Бюджет времени (оценка)
| Этап | Время |
|---|---|
| Подготовка тестового набора и промптов | 1 час |
| Реализация A/B распределения и прогон | 1.5 часа |
| Вычисление метрик faithfulness | 1 час |
| Статистический анализ и определение победителя | 1 час |
| Документирование и воспроизводимость | 30 минут |
| Итого | ~5 часов |
Примечание: При первом выполнении может потребоваться +1 час на отладку judge LLM и асинхронного запуска.
9. Связанные вопросы из базы знаний
| Вопрос | Тема |
|---|---|
| 153 | Метрики качества генерации (faithfulness, relevancy) |
| 155 | Виды тестирования промптов (unit, regression, A/B) |
| 160 | Проектирование промптов для стабильных ответов |
| 162 | Статистическая значимость в ML-экспериментах |
| 165 | Cost-оптимизация запросов к LLM |
| 168 | RAGAS: библиотека для оценки RAG |
| 172 | Детерминированное хеширование для сплитования трафика |
| 180 | Judge LLM: использование одной LLM для оценки другой |
| 185 | Асинхронные вызовы API в Python |
| 195 | Экспорт результатов экспериментов в отчёты |
10. Чек-лист самопроверки
- Я создал две осмысленно различающиеся версии промпта (базовую и улучшенную).
- Я подготовил тестовый набор из не менее 50 запросов с контекстом и ground truth.
- Я реализовал детерминированное A/B-разбиение на основе хеша от ID запроса.
- Я замерил cost (в долларах) для каждого запроса, используя прайсинг модели.
- Я вычислил faithfulness, используя отдельный вызов judge-LLM или эвристику.
- Я провёл статистический тест (выбрал подходящий) для обеих метрик.
- Я визуализировал распределения метрик и проверил, нет ли выбросов.
- Я написал итоговый отчёт с выводом о победителе и уверенностью в решении.
- Я убедился, что код воспроизводим (фиксированный seed, случайные числа не используются).
- Я закоммитил все файлы (код, данные, отчёт) в Git (если требуется).