Месяц назад у меня сломался чат-бот техподдержки. Клиенты начали присылать скриншоты, на которых бот уверенно рекомендовал полностью удалить базу данных для "оптимизации". Я открыл код, прошёлся по тестам — всё зелёное. И вот в чём засада: привычные тесты для ИИ-приложений не ловят такие баги. Обычный unit-тест проверяет, вернул ли код ожидаемое значение. А здесь нейросеть может выдать десять разных ответов, и все будут "правильными" с точки зрения формата, но один из них — катастрофа.
Я разобрался, почему так происходит и какие инструменты реально помогают. Делюсь.
Почему ИИ-приложения сложно тестировать
Классическое тестирование построено на детерминизме. Функция принимает X, возвращает Y. Если вернула Y — тест прошёл. С ИИ-приложениями эта схема ломается по нескольким причинам.
Нет единственно правильного ответа. Запрос "напиши код для сортировки" может вернуть пузырьковую сортировку, быструю сортировку или merge sort — и все три корректны. Тест, который проверяет точное совпадение строки, всегда будет флапать. А тест, который проверяет только формат, пропустит дезинформацию.
Промпт — это тоже код. Модель ведёт себя по-разному в зависимости от формулировки, регистра, порядка инструкций. Изменил одно слово в системном промпте — и поведение уплыло. При этом в репозитории это обычно просто строка в конфиге, без версионирования и тестов.
Контекст пользователя. ИИ-приложение часто работает с данными из внешних источников: баз, API, документов. Тест, который проверяет ответ модели, зависит от содержимого этих источников. Если база изменилась — тест может начать падать не из-за модели, а из-за данных.
Стоимость и скорость. Вызовы LLM стоят денег и занимают секунды. Прогонять тысячи тестов на реальных моделях после каждого коммита — дорого и медленно. Нужно как-то балансировать между полнотой проверки и бюджетом.
Подходы, которые реально работают
Тестирование промптов через golden-датасеты
Суть простая: составляешь набор пар "запрос — эталонный ответ". Для каждого запроса прогоняешь текущую модель и сравниваешь вывод с эталоном. Но сравнивать тексты один-в-один нельзя — сломается на любом синониме.
Я использую два вида проверок. Первый — структурный: ответ содержит нужные поля, код компилируется, JSON валиден. Второй — через LLM-as-judge: отправляю оба ответа (эталонный и полученный) в ту же модель и прошу оценить семантическую эквивалентность по шкале от 1 до 5. Грубо, но работает для черновой проверки.
В RAG-системах хорошо заходит retrieval-тестирование. Берёшь векторную базу, подсовываешь запрос, проверяешь, достал ли ретривер нужные чанки. Можно заморочиться и написать assertion на cosine similarity, а можно просто проверить, что в топ-5 релевантных документов есть нужный.
Метрики и бенчмарки
Если у тебя ИИ-приложение генерирует текст, нужны метрики. BLEU и ROUGE — классика, но для задач с открытым ответом они часто коррелируют с качеством так себе. Человек читает текст и доволен, а BLEU показывает 0.3.
Для более вменяемой оценки я применяю g-eval — фреймворк, где модель сама оценивает вывод по заданным критериям. Задаёшь критерий вроде "ответ не содержит выдуманных фактов", модель оценивает от 1 до 5. Не идеально, но масштабируется лучше, чем ручная проверка.
Для задач классификации или извлечения — обычные метрики работают отлично. Precision, recall, F1 на тестовом датасете с разметкой дают чёткую картину.
Регрессионное тестирование на моделях-прокси
Тут всё просто: не гоняешь каждый коммит на GPT-4o. Берёшь более дешёвую и быструю модель — GPT-3.5, Llama 3 8B, — и на ней проверяешь, что ничего не сломалось. Дорогую и мощную модель запускаешь только на пайплайне CI перед релизом или раз в сутки.
Это не идеально: иногда баг проявляется только на более мощной модели. Но компромисс разумный. Однажды я поймал баг, который воспроизводился только на GPT-4o и полностью отсутствовал на GPT-3.5 — связано было с длиной контекста. Пришлось перестраивать пайплайн. Но такие случаи редкие.
Инструменты, которые стоит знать
Promptfoo — пожалуй, самый практичный инструмент. Задаёшь набор тестовых кейсов, прописываешь assertions, указываешь модели — и получаешь отчёт с оценками. Поддерживает множество LLM-провайдеров из коробки. Можно встроить в CI/CD и тормозить деплой, если качество просело.
LangSmith от LangChain — больше для отладки и мониторинга, чем для тестирования. Позволяет смотреть трассы вызовов, логировать промпты и ответы, собирать обратную связь от пользователей. Полезно на этапе, когда приложение уже в проде.
RAGAS —专门 для RAG-систем. Метрики retrieval quality и answer quality, ничего лишнего.
Evals от OpenAI — фреймворк для оценки моделей на своих задачах. Тяжеловат в настройке, но если у тебя узкоспециализированная задача — кастомный eval окупается.
Для тех, кто работает с агентами и tool-calling, отдельная головная боль — тестирование вызовов инструментов. Тут хорошо помогает логирование всех вызовов и replay-режим: берёшь запись реальной сессии и прогоняешь через новую версию кода, сравнивая последовательность действий.
Что я делаю на практике
Для своего последнего проекта — RAG-система для внутренней документации — выстроил трёхуровневую проверку.
Первый уровень — юнит-тесты на питоне. Проверяю, что парсер документов не падает на разных форматах, что ретривер возвращает чанки, что промпт валиден. Быстро, дёшево, не зависит от LLM.
Второй — интеграционные тесты на моделях-прокси через Promptfoo. Набор из 150 кейсов, проверка по структуре и базовой релевантности. Запускается на каждый пул-реквест, занимает около 15 минут и $2 на API-ключах.
Третий — еженедельный прогон на GPT-4o. Полный набор из 500 кейсов, включая длинные документы и сложные запросы. Результаты сравниваются с предыдущей неделей. Если метрики просели — разбираюсь, что изменилось в промпте или базе.
Между вторым и третьим уровнем ловлю основную часть багов. Пропустил только один серьёзный — модель на GPT-4o упорно hallucinated даты в документах, которые на более слабых моделях просто не генерировали. Пришлось добавить в промпт явную инструкцию и fact-checking вызов.
Что обычно упускают
Часто забывают тестировать граничные случаи. Пустой контекст — что ответит модель? Слишком длинный документ? Запрос на языке, которого нет в обучающих данных? Пользователь просит сделать что-то небезопасное — как отреагирует?
Я стал встраивать negative-тесты: отправляю明知故问 "плохие" запросы и проверяю, что система отклоняет или обрабатывает корректно. Для ИИ-приложений это критичнее, чем для обычного софта — модель может молча подыграть вредоносному запросу.
Второе — версионирование промптов. Промпт в конфиг-файле без истории изменений — это хаос. Я завёл простой чейнджлог в Notion: каждое изменение промпта — отдельная запись с датой, описанием и причиной. Когда метрики проседают — есть к чему вернуться.
Коротко
ИИ-приложения не тестируются как обычный софт, но и не летают вслепую. Трёхуровневая система — юнит-тесты на логику, интеграционные на моделях-прокси, периодические на флагманах — закрывает большинство проблем. Ключевое: golden-датасеты с хорошими assertions важнее покрытия кода. Красивый зелёный CI ничего не стоит, если тесты проверяют формат, а не содержание.
