中文翻译暂不可用,显示俄语原文。

Реализовать 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
Инструмент для оценки faithfulnessRAGAS (Faithfulness) или кастомный judge-LLM (например, gpt-4o-mini)
База знаний (опционально, для ответов с фактами)Например, статьи Wikipedia по темам запросов

Если нет реального API или judge-инструмента — симулируем:

  1. Создать две версии промпта, заведомо одна лучше другой (например, v1 без инструкции check facts, v2 с явным требованием проверки).
  2. Вместо API использовать локальную модель через Hugging Face (например, Qwen2-1.5B).
  3. Для faithfulness написать простой эвристический скрипт: проверять, содержит ли ответ ключевые слова из эталонного ответа (ground truth). Для эталонных ответов взять первые абзацы из Wikipedia.
  4. Важно в таком случае метрика faithfulness будет приблизительной, но для учебной задачи достаточной.

3. Технологический стек

КомпонентИнструментыНазначение
Язык программированияPython 3.11+Основной код пайплайна
LLM APIopenai / anthropic / litellmГенерация ответов
A/B распределениеrandom (seed), UUIDДетерминированное направление запроса в v1/v2
Оценка faithfulnessRAGAS (опционально) или judge-LLMВычисление faithfulness для каждого ответа
Замер стоимостиtiktoken / litellm cost trackingПодсчёт токенов и cost на запрос
Статистический анализscipy.stats (ttest, mannwhitneyu)Сравнение метрик между группами
Логированиеloguru / structlogФиксация каждого запроса и метрик
Хранение результатовpandas DataFrame → CSV / ParquetДолговременная запись для анализа

4. Этапы выполнения

Этап 1: Подготовка тестового набора и промптов (1 час)

Действия

  1. Определить две версии промпта Пусть v1 — базовый:
    "Ответь на вопрос, используя контекст: {context}. Ответ:"
    v2 — улучшенная (с требованием точности):
    "Ответь на вопрос, используя только факты из контекста. Если ответа нет в контексте, скажи 'Не знаю'. Вопрос: {question}. Контекст: {context}. Ответ:"

  2. Собрать набор запросов (минимум 50). Каждый запрос — структура:

    {
      "id": "q001",
      "question": "Какая столица Франции?",
      "context": "Франция — страна в Западной Европе. Столица — Париж.",
      "ground_truth": "Париж"
    }
    

    Взять 20 простых (ответ есть в контексте), 20 сложных (требуется вывод), 10 без ответа (ответа нет в контексте). Можно синтезировать с помощью LLM.

  3. Подготовить скрипт загрузки — прочитать CSV или JSON с запросами.

ПолеТипПример
questionstr"Какая столица Франции?"
contextstr"Франция — страна. Столица — Париж."
ground_truthstr"Париж"
expected_answerablebooltrue

Ожидаемый результат этапа Файл test_queries.json с мин. 50 записями, две строковые переменные PROMPT_V1, PROMPT_V2 в коде.

Этап 2: Реализация A/B распределения и прогон (1.5 часа)

Действия

  1. Написать функцию 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}" для детерминизма.

  2. Реализовать цикл прогона

    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
        })
    
  3. Добавить логирование каждая итерация пишется в loguru.logger.info.

Ожидаемый результат этапа Сырой CSV raw_results.csv с колонками: query_id, version, answer, prompt_tokens, completion_tokens, cost, latency_sec.

Этап 3: Вычисление метрик faithfulness (1 час)

Действия

  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 как наиболее практичный (используем ту же модель, но отдельный вызов).

  2. Написать функцию 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
    
  3. Прогнать для всех результатов (можно параллельно, но не обязательно). Записать faithfulness в DataFrame.

Ожидаемый результат этапа Обогащённый CSV results_with_metrics.csv с колонкой faithfulness (float).

Этап 4: Статистический анализ и определение победителя (1 час)

Действия

  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']
    
  2. Проверить нормальность распределения метрик (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'])
    
  3. Сравнить средние cost Для cost обычно распределение скошено, используем Mann-Whitney U.

  4. Визуализировать boxplot для faithfulness и cost по версиям (matplotlib/seaborn).

  5. Определить победителя

    • Если p_value < 0.05 для faithfulness и v2 выше v1 → победитель v2.
    • Если p_value < 0.05 для cost и v2 дешевле → дополнительный плюс.
    • Если обе метрики не показывают значимой разницы → ничья.
  6. Документировать вывод в виде текста:

    Версия v2 показала статистически значимо более высокую faithfulness (mean=0.85 vs 0.72, p=0.003).
    Разница в cost незначима (p=0.45). Победитель: v2.
    

Ожидаемый результат этапа Файл ab_test_report.md с результатами анализа, графиками и финальным решением.

Этап 5: Документирование и воспроизводимость (30 минут)

Действия

  1. Упаковать весь код в единый скрипт run_ab_test.py, который принимает аргументы: --queries_file, --prompt_v1, --prompt_v2, --model.
  2. Зафиксировать seed для детерминированного разбиения.
  3. Добавить README с инструкцией по запуску.
  4. Запушить в 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 часа
Вычисление метрик faithfulness1 час
Статистический анализ и определение победителя1 час
Документирование и воспроизводимость30 минут
Итого~5 часов

Примечание: При первом выполнении может потребоваться +1 час на отладку judge LLM и асинхронного запуска.

9. Связанные вопросы из базы знаний

ВопросТема
153Метрики качества генерации (faithfulness, relevancy)
155Виды тестирования промптов (unit, regression, A/B)
160Проектирование промптов для стабильных ответов
162Статистическая значимость в ML-экспериментах
165Cost-оптимизация запросов к LLM
168RAGAS: библиотека для оценки RAG
172Детерминированное хеширование для сплитования трафика
180Judge LLM: использование одной LLM для оценки другой
185Асинхронные вызовы API в Python
195Экспорт результатов экспериментов в отчёты

10. Чек-лист самопроверки

  • Я создал две осмысленно различающиеся версии промпта (базовую и улучшенную).
  • Я подготовил тестовый набор из не менее 50 запросов с контекстом и ground truth.
  • Я реализовал детерминированное A/B-разбиение на основе хеша от ID запроса.
  • Я замерил cost (в долларах) для каждого запроса, используя прайсинг модели.
  • Я вычислил faithfulness, используя отдельный вызов judge-LLM или эвристику.
  • Я провёл статистический тест (выбрал подходящий) для обеих метрик.
  • Я визуализировал распределения метрик и проверил, нет ли выбросов.
  • Я написал итоговый отчёт с выводом о победителе и уверенностью в решении.
  • Я убедился, что код воспроизводим (фиксированный seed, случайные числа не используются).
  • Я закоммитил все файлы (код, данные, отчёт) в Git (если требуется).