В RetailCRM мы пишем тесты агентов на обычном pytest. Но с LLM быстро сталкиваешься с особенностью: один и тот же тест сейчас проходит, а при следующем запуске падает. Для таких проверок мы сделали llm-flaky — пакет, который повторяет тест и считает успешные попытки. У этого подхода есть полезная область применения и вполне конкретные ограничения.
Правка промпта меняет поведение программы
Ручное тестирование хорошо работает, пока агент маленький и его сценарии помещаются в голове. Можно открыть диалог, задать несколько вопросов, посмотреть на ответы. По мере роста агента проверять всё таким способом становится тяжело. Особенно когда часть бизнес-логики описана в промпте.
Допустим, мы уточнили, когда агент должен задавать дополнительный вопрос. На том примере, ради которого делалась правка, ответ стал лучше. Но инструкция читается целиком, и новое правило может повлиять на другие запросы. Поэтому рядом с новым примером нужно снова запустить старые сценарии: где данных достаточно, где они противоречат друг другу, где пользователь просит действие за пределами полномочий.
Тест сохраняет ожидание разработчика в форме, которую можно повторить после следующей правки. Для промпта это особенно полезно: удачный ответ в одном диалоге быстро забывается, а сценарий с явным условием успеха остаётся в репозитории.
Одна попытка даёт слишком мало информации
В обычной функции одинаковые входные данные обычно дают одинаковый результат. Ответ языковой модели может меняться между вызовами. Мы регулярно получали такие «мигающие» тесты и решили учитывать эту особенность явно.
В нашем исходном подходе порог — четыре успешные попытки при максимуме в пять запусков. Проверка допускает один неудачный результат. В статье про конкретный тест число 80% означает именно этот критерий приёмки. Пять попыток сами по себе не доказывают, что агент будет правильно выполнять 80% запросов пользователей.
Повторы одного сценария и разнообразие сценариев отвечают на разные вопросы. Первые показывают, насколько устойчиво воспроизводится нужное поведение. Второе помогает обнаружить случаи, которые вообще отсутствовали в наборе тестов. Сто повторений одного удобного запроса оставляют за кадром другую формулировку, чужую корзину или отсутствие нужной записи.
Как подключить llm-flaky
Пакет устанавливается через pip install llm-flaky. Нужные тесты помечаются @pytest.mark.llm. Плагин добавляет им правила повторного запуска и выводит сводный отчёт. Настройки и примеры есть в репозитории релиза 0.1.0.
Ниже схема теста классификатора. Фикстура agent здесь представляет агент из вашего проекта; её нужно реализовать отдельно. Само условие проверяет результат классификации, поэтому изменение формулировки ответа пользователю на него не влияет.
import pytest
@pytest.mark.llm
def test_order_status_intent(agent):
result = agent.classify("Где мой заказ?")
assert result.intent == "order_status"
Параметры можно задать при запуске:
pytest --llm-flaky-max-runs=5 --llm-flaky-min-passes=4
Тут есть деталь, которую легко пропустить. В коде плагина значение по умолчанию для min_passes равно max_runs − 1. Если поменять только максимум на десять, потребуется девять успехов. Поэтому нужный порог удобнее задавать обоими параметрами. Максимум запусков также задаётся переменной FLAKY_MAX_RUNS, у неё приоритет над аргументом командной строки.
Что именно считать успехом
До настройки повторов нужно разобраться с самим условием теста. Проверка наличия слова в ответе может пропустить смысловую ошибку. Агент способен написать «возврат оформлен», хотя вызов оформления завершился ошибкой. Если сценарий меняет данные, тесту нужен доступ к результату операции.
| Что проверяем | На что смотреть |
|---|---|
| Понимание намерения | Выбранное действие или класс запроса |
| Выбор товара | Артикул и выполнение всех заданных ограничений |
| Изменение данных | Состояние записи после выполнения инструмента |
| Доступ к чужой записи | Отсутствие запрещённого чтения и утечки в ответе |
| Формат ответа | Соответствие требуемой структуре, полям и значениям |
Это примеры того, как проектировать условия тестов. У каждого агента будет свой набор. Где есть точное правило, его стоит проверять напрямую: идентификатор должен совпасть, сумма должна сохраниться, запрещённый инструмент должен остаться невызванным.
Порог зависит от последствий ошибки
Четыре успеха из пяти — выбранная нами настройка для LLM-тестов. Применять её ко всем операциям подряд я бы не стал. Допустимая вариативность текста и разрешение списать деньги требуют разных критериев.
Для арифметики, проверки прав и ограничений изменения данных полезны обычные тесты с точным ожидаемым результатом. Для классификации или свободного ответа пригодятся повторные вызовы модели. Сценарии с необратимыми последствиями требуют отдельной защиты в самом приложении: права, лимиты, подтверждение операции, обработка повторного вызова.
Иначе можно получить красивый зелёный отчёт, который разрешает одну утечку из пяти. Условие теста должно соответствовать последствиям сбоя. Плагин только исполняет выбранные правила и считает результаты; ответственность за эти правила остаётся у команды.
Как читать отчёт после изменения агента
Я бы начинал с конкретных падений: какой запрос ушёл модели, какие данные она получила, какой инструмент выбрала и на каком условии остановился тест. Один красный сценарий может указывать на ошибку промпта, сбой API, нестабильную фикстуру или неверное ожидание самого теста. У этих причин разные исправления.
Пакет поддерживает запуск вместе с pytest-xdist, что помогает распараллелить проверки. При этом каждый тест должен получать независимое исходное состояние. Если несколько попыток меняют одну корзину, следующая попытка проверяет уже другую ситуацию, и смысл повторения теряется.
Для начала достаточно небольшого набора важных сценариев и явно заданного порога. Дальше каждую найденную ошибку можно превращать в новый тест. Так изменение промпта становится обычной частью разработки: есть ожидание, есть повторный запуск, есть конкретный результат, который нужно разобрать.
Материал может пригодиться коллегам? Отправьте им ссылку на статью.