1. Главная
  2. Блог
  3. A/B-тесты промптов в продакшене: как понять, что ИИ-агент реально стал лучше

A/B-тесты промптов в продакшене: как понять, что ИИ-агент реально стал лучше

A/B-тесты промптов в продакшене: как понять, что ИИ-агент реально стал лучше

Утро, я ещё не успел допить кофе. В общий чат прилетает сообщение от менеджера по работе с ключевыми клиентами: «Ребят, агент в письме поставщику назвал нашего клиента поставщиком, а себя — клиентом. Всё перепутал». Днём ранее мы выкатили «улучшенный» промпт для генерации деловых писем. На стейдже он давал ответы заметно структурнее, на ручных примерах — лучше. На проде через пару дней — два испорченных контакта и одно неловкое объяснение по телефону.

После этого инцидента мы выкинули практику «поменяли промпт — задеплоили — посмотрели глазами на десяток выводов». И построили инфраструктуру A/B-тестов промптов для нашего ИИ-агента. Дальше — как именно, на каких метриках, и какие три ошибки мы успели совершить, пока всё не заработало.

Почему «на глаз» вообще не работает

Промпт — это не функция с детерминированным выходом. Даже с temperature=0 российские LLM (как и любые другие) дают разброс между запусками: токенизация контекста, кеши провайдера, обновления модели на стороне поставщика. Когда вы смотрите глазами на десяток ответов, вы видите случайную выборку из распределения. Двух удачных примеров достаточно, чтобы вы поверили в новый промпт. Двух неудачных — чтобы откатили хороший.

Пример из нашей практики. Промпт-кандидат A для классификации намерения клиента в B2B-переписке давал на ручной выборке из тридцати писем ощутимо лучшую точность, чем B. Разница казалась «существенной». Когда мы прогнали оба на тысячах реальных писем с разметкой от менеджеров, оказалось: цифры почти сравнялись, а разница вписывалась в шум. Решение «выкатываем B» было бы выбросом монетки.

Без статистики на промптах вы катаете монетки. Особенно если строите не одиночный ИИ-ассистент, а полноценного ИИ-агента, который сам ходит по API, дергает CRM и что-то пишет реальным людям от лица компании. Тут любая незаметная деградация промпта тянет за собой цепочку внешних действий — и заметить её постфактум сильно дороже, чем при простой болталке в чате.

Кейс: оптовый поставщик и резюме входящей почты

Заказчик — крупный оптовый поставщик, десятки менеджеров в коммерческом отделе. У них ИИ-агент на GigaChat Pro каждое утро резюмирует входящие письма за ночь: что просили, какой объём, какой срок, есть ли обещанные документы. Менеджер открывает CRM и видит готовую карточку.

До нас процесс выглядел так: полдня у менеджера уходило только на разбор ночной и утренней почты. После внедрения первой версии агента разбор укладывался в короткое утреннее «пробегусь глазами и поправлю». Экономия по коммерческому отделу ощутимая, за счёт неё проект и жил.

Через несколько месяцев возникла боль: менеджеры всё чаще правили резюме руками. Разбор показал — агент путает «отгрузка» и «оплата», особенно когда в письме обсуждаются обе. Мы написали новый промпт. И вот тут возникла дилемма: как понять, что новый промпт действительно лучше, а не просто «звучит лучше»?

Архитектура: трафик на два промпта одновременно

Решение в лоб — раскладывать входящий трафик между версиями промпта в фиксированной пропорции. Не A потом B, а A и B одновременно на разных письмах. Это убивает временные искажения: понедельник у нас всегда грязный, пятница — короткие письма, в конце месяца — закрытие отгрузок.

Конфигурация хранится в Postgres, изменения подхватываются без рестарта:

class PromptVariant(Base):
    __tablename__ = "prompt_variants"
    id: Mapped[int] = mapped_column(primary_key=True)
    task: Mapped[str]  # "email_summary", "intent_classify"
    name: Mapped[str]  # "v3.1-baseline", "v3.2-explicit-roles"
    template: Mapped[str]
    weight: Mapped[int]  # доля трафика, 0..100
    active: Mapped[bool]

def pick_variant(task: str, request_id: str) -> PromptVariant:
    variants = active_variants(task)
    # хешируем request_id, чтобы один и тот же запрос
    # при повторе попал в ту же ветку — для идемпотентности
    bucket = crc32(request_id.encode()) % 100
    cumulative = 0
    for v in variants:
        cumulative += v.weight
        if bucket < cumulative:
            return v
    return variants[-1]

Каждый вызов модели пишет в llm_calls таблицу: id запроса, имя варианта, модель, латентность, число токенов, стоимость, сам ответ. Это даёт нам сырой материал для оценки задним числом. Если ИИ-агент по API дальше идёт в CRM или в почтовый сервис, туда же логируем идентификатор варианта — чтобы можно было связать промпт с итоговым бизнес-действием, а не только с текстом ответа.

Метрики качества без gold-набора

Главная сложность: у нас нет «правильных ответов» на боевых данных. Мы не можем заранее разметить огромный корпус писем. Поэтому используем три суррогатных сигнала.

Первый — частота правок менеджером. Если резюме менеджер открыл и сохранил без изменений — это сигнал «ок». Если переписал значимую часть текста — «плохо». Мы храним diff между сгенерированным и финальным текстом, считаем расстояние Левенштейна. Метрика грубая, но устойчивая на больших объёмах.

Второй — явный thumbs-down. В интерфейсе кнопка «резюме плохое» с обязательным комментарием. Жмут её лишь несколько процентов пользователей, но эти проценты — золото для понимания, что именно сломалось.

Третий — LLM-as-a-judge. Раз в сутки отдельный сервис прогоняет случайную выборку пар «исходное письмо — сгенерированное резюме» через GigaChat Pro в режиме оценщика по шкале 1–5 по четырём осям: полнота, точность, отсутствие галлюцинаций, чёткость структуры. Промпт оценщика заморожен — это важно, иначе мы будем сравнивать яблоки с грушами на разных днях.

После пары недель прогона по вариантам v3.1 и v3.2 у нас накопился достаточный объём: десятки тысяч писем в каждой ветке. Картина сложилась в пользу v3.2 сразу по всем трём сигналам — и по частоте правок, и по thumbs-down, и по средней оценке judge. Важно: мы принципиально не бежали смотреть на первые сотни писем. На таком объёме разница ещё гуляет так, что легко перепутать сигнал с шумом и наделать выводов.

Дальше считаем доверительный интервал на разность пропорций. Для thumbs-down 95% CI не пересекал ноль — разница статистически значима. Выкатываем v3.2 на 100%.

Где мы наступали на грабли

Ошибка номер один — забыли про распределение трафика по клиентам. Один заказчик присылал большую часть всего объёма писем. Когда мы катали 50/50 по запросам, его специфика доминировала: сравнение фактически превращалось в «промпт A на этом клиенте против промпта B на этом же клиенте плюс шум остальных». Пришлось хешировать не request_id, а (client_id, день) — чтобы каждый клиент видел обе версии равномерно по дням, а не «понедельник — A, вторник — B».

Ошибка номер два — экономия на латентности. Промпт-кандидат v3.3 был длиннее почти на тысячу токенов и давал чуть лучшие метрики качества. Но средняя латентность заметно выросла, а стоимость на запрос ощутимо подросла. По совокупности v3.3 проиграл v3.2: разница в качестве не покрывала роста счёта за модель. Теперь у нас в дашборде A/B-теста рядом с качеством висят latency P95 и стоимость токенов — иначе есть шанс проглядеть, что новый ИИ-агент стал чуть умнее, но заметно дороже и медленнее.

Ошибка номер три — игнор сезонности. Мы выкатили промпт за пару дней до закрытия квартала, когда характер писем меняется: все спешат отгрузить, тон жёстче, чисел больше. Метрики «подскочили» — менеджеры просто не успевали править и жали «сохранить» на всём подряд. Через неделю качество просело обратно. Сейчас минимальное окно прогона — десять рабочих дней, и оно покрывает «понедельник–пятницу» и хотя бы один цикл закрытия недели.

Что мы получили на горизонте полугода

За несколько месяцев мы прогнали больше двух десятков промпт-экспериментов на трёх задачах: резюмирование писем, классификация намерения, генерация черновика ответа. Примерно треть закончилась выкаткой нового промпта на 100%, ещё треть — откатом, остальное — гибридом. Гибрид — это когда новый промпт лучше на одном сегменте клиентов, а старый — на другом. Теперь у нас сегментированный роутинг: агент модели ИИ выбирает промпт в зависимости от типа заказчика и характера письма.

Если бы мы катили все эти версии «на глаз», по моей оценке мы бы протолкнули на прод большую часть из них. И примерно у половины выкаченных потихоньку деградировало бы либо качество, либо стоимость. А заметили бы мы это уже через жалобы клиентов, а не через дашборд.

A/B-тесты промптов — не «нашлёпка для серьёзных команд». Это инфраструктура, без которой любая итерация на LLM-системе превращается в рулетку. Тем более если вы собираете не разовую подсказку в чате, а агента, который каждый день молча делает работу за десятки живых людей. Стоимость кодовой части — пара дней инженера: таблица вариантов, роутер, дашборд по сырым логам. Возврат — отсутствие историй вроде «агент перепутал клиента с поставщиком в письме, и нам потом полчаса всё объясняли».

Отдельно про готовые ИИ-агенты «из коробки». Мы их пробуем и продолжаем смотреть, что появляется на рынке. Но всегда с одной поправкой: как только такого агента встраиваешь в реальный бизнес-процесс с деньгами и людьми, промпты и обвязку под него всё равно приходится дорабатывать под конкретного заказчика. А значит, A/B-контур нужен и для готовых решений — иначе непонятно, стало ли после доработки лучше или мы просто уговорили себя, что стало.

Хороший вопрос, на который у нас пока нет идеального ответа: как тестировать промпт, который радикально меняет структуру вывода (например, был обычный текст — стал JSON со схемой)? Edit distance тут бесполезен, judge-сравнение — спорно, а ручная разметка не масштабируется. Если у вас есть рабочая практика — расскажите, обсудим.

Похожие статьи

Хотите так же?

Обсудим, как это внедрить у вас

Опишите задачу — подберём ИИ-решение под ваш процесс и бюджет: что сделать агентом, какие нужны данные и интеграции, с чего начать пилот.