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-сравнение — спорно, а ручная разметка не масштабируется. Если у вас есть рабочая практика — расскажите, обсудим.
Похожие статьи
- Как мы научили ИИ-агента на GigaChat Pro не путать живого клиента с ночным батчем
- Как мы настроили ИИ-агента для поиска в 1С: MCP-сервер вместо метаний между окнами
- Один вопрос — три темы: как мы научили ИИ-агента разбирать составные запросы
- Text2SQL для 340 таблиц: как мы собирали ИИ-агента для анализа корпоративных данных на GigaChat Pro




