← Все статьи

От «вроде работает» до MVP: тысяча и один тест

В процессе работы над ИИ-ассистентом для приёма заявок на закупку, который я делала в качестве выпускной работы на курсах по промпт-инжинирингу (тут отдельная статья об этом), я на собственном опыте выяснила: сделать почти готовый MVP оказалось в несколько раз быстрее, чем сделать его действительно работающим. И слово «почти» здесь оказалось самым дорогим.

Когда я начинала проект, мне казалось, что самая сложная часть — придумать архитектуру и заставить все компоненты работать вместе.

Но как выяснилось на практике, сам проект собрался быстро, дело вместе с Codex шло бодро. А потом я начала тестировать ассистента. Каждый новый набор выявлял новые недочёты, которые требовали исправления даже на уровне MVP. И вот тут я поняла, что такое настоящий проект.

Сначала всё выглядело прекрасно

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

Первые тесты на простых запросах были вполне адекватными.

Заявка на закупку офисных кресел оформилась. На бумагу — оформилась. На услуги — тоже. Вопросы по регламентам получили ответы в виде выдержек из документов. Круто! Вроде можно сдавать? Я подготовила сценарий для демонстрации для защиты проекта и устроила репетицию. А потом ещё штук пять репетиций с другими сценариями. И в процессе я несколько раз думала о том, чтобы всё снести и начать заново, поскольку казалось что заросли кода на Python, которые я только в общих чертах могу понять, разрослись уже до неприличия, и переписывание их будет только ухудшать работу ассистента. В этом и есть одна из реальных ловушек вайбкодинга для непрограммиста: проект быстро обрастает кодом, который ты умеешь читать и проверять только на уровне логики, но не способен переписать с нуля.

Тесты делаются для того, чтобы делать новые тесты

Итак, стоило перестать задавать боту удобные вопросы, как начали появляться странности.

Где-то он неправильно понимал единицу измерения.

Где-то слишком уверенно и неверно определял категорию.

В смешанном запросе на несколько разных позиций начинались приключения с разделением потребности.

Некоторые формулировки отправляли бота вообще не по той ветке диалога.

Мы в тандеме с ChatGPT и Codex исправляли одну группу ошибок, прогоняли тесты снова — и обнаруживали следующую. Постепенно количество автоматических тестов стало расти почти быстрее, чем сам проект, и там вроде всё было в порядке. Но при этом ручные тесты показывали, что останавливаться рано.

В какой-то момент у меня возникло ощущение, что цель моего проекта — успешно проходить всё новые тесты, которые мы сами же для него придумываем. И я начала подозревать, что всё это вообще заговор с единственной целью — израсходовать мои недельные лимиты.

Вот пример с категориями товаров и услуг. Сначала категории закупок определялись довольно традиционно: программные правила и набор известных признаков. Это отлично работает, пока пользователь заказывает что-то простое и ожидаемое.

А потом я зачем-то написала в заявке на закупку «промышленный вентилятор». И выяснилось, что бесконечно добавлять в словарь «вентилятор», «осушитель», «насос», «частотный преобразователь» — это не тот путь, которым надо идти. Заодно у меня появилось ещё одно полезное правило: всё-таки надо читать, что именно модель изменила после очередного промпта. Иногда она действительно чинит проблему. Иногда просто очень аккуратно подставляет табуретку под шаткую конструкцию.

Архитектуру поменяли. Известные случаи по-прежнему определялись программно, а незнакомые стали передаваться LLM. Но модель не может придумать новую категорию: ей разрешено выбирать только из закрытого классификатора. Результат проверяется, а пользователь должен его подтвердить.

Красиво.

Проверяем док-станцию для ноутбука.

Модель правильно предлагает «IT-периферия».

Я нажимаю «Да».

И бот спрашивает: «К какой категории относится товар?», хотя одновременно с этим сам же предлагает единственный вариант — «IT-оборудование».

В этот момент я решила, что «мне этот мир абсолютно понятен». Оказалось, что новую категорию правильно определял один участок системы, а старый классификатор потом ещё раз проверял весь текст, видел слово «ноутбук» и решал, что знает лучше.

Исправили.

Добавили тест.

Поехали дальше.

Потом сломался RAG. Хотя на самом деле не сломался

Следующим этапом я спросила уже по базе знаний:

К какой категории относится сетевое оборудование?

В базе ответ был. Поиск нужный документ находил. Но бот отвечал: «Недостаточно информации».

Разобрались.

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

То есть данные были найдены. А потом аккуратно потеряны.

После этого в RAG появился отдельный тип вопросов по классификатору.

Ещё несколько тестов.

«Посоветуй сериал» внезапно оказался заявкой на офисную мебель

Ближе к финалу я решила проверить безопасный отказ — бот должен идентифицировать всякую дичь, и не тратить токены (читай — мои нервы и деньги) на попытки её обработать.

Спрашиваю рецепт борща.

Бот корректно сообщает, что занимается внутренними закупками.

Отлично.

Потом задаю второй вопрос.

И внезапно оказываюсь в сценарии оформления заявки.

Начинаем разбираться.

Выясняется, что после постороннего вопроса бот сам вышел из контура с базой знаний и RAG, и перешёл в контур оформления новой заявки на закупку. Просто сценарий решил, что с него хватит.

Исправили: режим стал меняться только по явному действию пользователя, как я (но видимо только я) предполагала изначально.

Заодно нашли ещё одну особенность: в нейтральном режиме любой текст мог восприниматься как начало закупки.

«Посоветуй новый детективный сериал».

«Это товар или услуга?»

Чтобы окончательно убедиться, что бот действительно оформляет заявку, я решила не останавливаться.

Количество: один. Единица: штука. Характеристика: захватывающий сюжет. Стоимость: 100 рублей. Доставка: мне домой. Цель: посмотреть вечером.

Так в системе появилась зарегистрированная заявка на «офисную мебель» с характеристикой «захватывающий сюжет». Занавес. И да, я зарегистрировала её специально — мне нужно было убедиться, что бот действительно ушёл в сценарий оформления заявки, а не просто задал один странный вопрос.

Эту проблему тоже устранили.

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

Самый важный вывод

До этого проекта я воспринимала тестирование примерно как финальный штрих: сделали → проверили → исправили пару ошибок → готово.

Теперь я думаю наоборот: для ИИ-систем тестирование — часть проектирования.

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

При должном внимании и проведении тестов каждый сбой заставлял не просто исправить конкретную фразу, а смотреть шире: кто определяет режим? кто имеет право менять категорию? когда нужно доверять LLM? когда код обязан её остановить? что считается подтверждённым фактом?

И, пожалуй, именно эти вопросы для меня оказались самой полезной частью всего проекта.

Самого ассистента сделала достаточно быстро. А вот довести его до состояния, в котором я действительно готова была бы показать результат, потребовало намного больше итераций, чем я ожидала.

В итоге я получила не только работающего бота. Я научилась гораздо меньше радоваться одному правильному ответу и гораздо больше интересоваться тем, почему система его дала, что она запомнила и что сделает на следующем шаге. И, кажется, наконец поняла, почему хороший тестировщик иногда ценнее ещё одной новой функции.