ИИ-агент написал код и сам его проверил. Почему его «готово» ничего не доказывает
Агентская разработка ускоряет написание кода, но оставляет неудобный вопрос: кто проверяет самого агента? В Mad Duck мы разработали Gopnik - open source-навык, который не подтверждает результат, а пытается воспроизводимо его сломать.
Однажды агент реализовал фичу, увидел нужные строки в базе и активность в логах и сообщил, что всё работает end-to-end.
На самом деле учётные данные, которые сохраняла новая форма, не читались рантаймом. Поллер и исполнитель получали пустое значение. Настоящего сквозного прогона не было ни одного, хотя код был написан, тесты проходили, а отчёт звучал убедительно.
Агент не обязательно врал. Внутри выбранной им картины мира всё действительно сходилось:
- Он предположил, как должна работать фича.
- Написал реализацию из этого предположения.
- Выбрал проверки, которые подтверждали то же предположение.
- Прочитал зелёный результат как доказательство готовности.
Одна ошибка прошла через весь цикл. Агент трижды согласился сам с собой.
Почему «проверь ещё раз» не решает проблему
В основе coding agent лежит языковая модель. Инструкции, история диалога, написанный код и вывод команд образуют один контекст, из которого модель генерирует следующий шаг.
Поэтому связная цепочка действий ещё не означает независимую проверку. Если ошибка уже попала в исходное рассуждение, она влияет и на реализацию, и на выбор тестов, и на интерпретацию результата.
Это похоже на две известные особенности LLM.
Первая - sycophancy, склонность соглашаться с позицией собеседника даже тогда, когда она неверна. Такое поведение наблюдали экспериментально: модель меняла ответ в сторону мнения пользователя вместо того, чтобы держаться фактов.
Вторая - ограниченность внутренней самокоррекции. Повторное рассуждение без нового внешнего сигнала не обязательно исправляет ошибку. Иногда модель повторяет прежний вывод, иногда меняет правильный ответ на неправильный.
Но проблема не только в моделях. Разработчики делают то же самое. Мы в Mad Duck тоже не раз писали тест на ожидаемых данных, получали ожидаемый ответ и считали гипотезу подтверждённой. Намного реже проверка начиналась с вопроса: какой наблюдаемый результат докажет, что наше решение не работает?
Зелёный тест - слабое доказательство
Ещё в 1969 году Эдсгер Дейкстра сформулировал асимметрию тестирования:
Тестирование программ может показать наличие ошибок, но никогда - их отсутствие.
Красный тест предъявляет контрпример: вот вход, вот фактический результат, вот нарушенное ожидание. Зелёный сообщает гораздо меньше: на выбранном входе и в выбранной среде сбой не найден.
Через десять лет Гленфорд Майерс превратил эту мысль в практический принцип: тестирование - это выполнение программы с намерением найти ошибку. Ценность теста определяется не тем, насколько красиво он зеленеет, а шансом обнаружить ещё неизвестный дефект.
Если перевести с академического на мытищинский начала нулевых, тест должен взять реализацию на понт. Не погладить её по голове, а попытаться обломать об колено и показать место, где она треснула.
Из этого принципа появился Gopnik.
Что такое Gopnik
Gopnik - open source-набор навыков для coding agents. Он работает с Claude Code и Codex и добавляет в процесс отдельную роль: не продолжать реализацию и не защищать уже сделанное, а атаковать утверждение «изменение готово».
Это не ещё один линтер, тестовый фреймворк или CI-сервис. Gopnik запускает те инструменты, которые уже есть в проекте, добавляет направленные пробы и требует связать вердикт с конкретной ревизией и реальной границей поставки.
Проверка состоит из трёх стадий.
Stage 0: сначала перечислить, что может сломаться
Самая опасная дыра появляется ещё до запуска тестов: агент проверяет только очевидные сценарии из собственного diff.
Gopnik сначала превращает задачу в матрицу поведения. Оси зависят от изменения:
- один объект или несколько;
- успех, отказ, отмена или повтор;
- валидное, пустое или повреждённое значение;
- зависимость доступна, отсутствует, запрещена правами или не отвечает;
- старая и новая ревизии;
- разные способы доставки и потребители.
Особенно полезны смешанные состояния. Например, обработка десяти задач, где восемь завершились, одна упала, а одна зависла. Баги чаще живут там, чем в сценариях «всё успешно» и «всё упало».
Для каждой существенной клетки нужен критерий отказа. Не «команда отработала», а конкретное наблюдение, которое отличает исправную систему от правдоподобного дефекта.
Stage 1: атаковать код внутри репозитория
На этой стадии агент запускает сборку, линтеры и штатные тесты, но не останавливается на них.
Он ищет:
- граничные и негативные входы;
- обе ветви fallback-механизмов;
- ошибки конфигурации и прав;
- все места вызова, если исправляется целый класс дефектов;
- путь потребления новых данных.
Последний пункт особенно важен. Если новая форма записывает токен в базу, недостаточно проверить HTTP 200 и наличие строки. Нужно найти код, который читает токен в рантайме, и доказать, что реальный исполнитель использовал именно новое значение.
Иначе мы проверили прокси, а не результат.
Stage 2: проверить не исходники, а поставленный продукт
Тесты внутри репозитория исполняются на территории разработчика: локальные импорты, фикстуры, моки, рабочее дерево. Пользователь встречает другой объект - собранный пакет, установленный бинарник, развёрнутый сервис или загруженный плагин.
Gopnik ищет первую границу, после которой артефакт перестаёт быть полностью подконтролен автору, и проверяет результат с другой её стороны.
- CLI: чистая установка, реальный вызов, проверка вывода и кода возврата.
- Библиотека: установка собранного пакета в отдельное окружение и импорт как у потребителя.
- API или сервис: запрос к развёрнутой точной ревизии и безопасный негативный сценарий.
- Веб-интерфейс: пользовательский сценарий в браузере, консоль, сеть и снимок результата.
- Плагин: установка в штатный хост, обнаружение, вызов и обновление.
- Миграция: запуск настоящим исполнителем на репрезентативной копии данных.
Для Python-библиотеки это может выглядеть так:
Так ловятся ошибки, невидимые внутренним тестам: файл не попал в wheel, экспорт забыли добавить, entry point указан неверно, а диапазон версий зависимости не соответствует реальности.
Ещё одно обязательное условие - проверяемая ревизия. Открывающаяся страница или здоровый эндпоинт ничего не доказывают, если на стенде могла остаться вчерашняя сборка.
Проверка тоже должна пройти проверку
Есть ещё одна ловушка: агент придумал тест, запустил его и получил зелёный статус. Но способен ли этот тест вообще заметить дефект?
Gopnik требует хотя бы для несущих проверок показать чувствительность. Для этого можно:
- запустить сценарий на старой ревизии;
- безопасно мутировать вход;
- подставить заведомо плохой пример;
- временно отключить часть механизма;
- воспроизвести известную сигнатуру сбоя.
Цель не в красном цвете как таковом. Нужно показать, что выбранный оракул отличает исправленное поведение от старого или неполного.
Зелёная проверка, которая не умеет краснеть, - слабое доказательство.
Вердикт вместо победного отчёта
Обычный агентский отчёт часто выглядит так: «Реализация завершена, все тесты проходят». Из него непонятно, какую версию проверяли, что именно пытались сломать и что осталось за пределами теста.
Gopnik возвращает ограниченный вердикт с воспроизведением:
Контрпример означает NOT READY. После исправления прежний вердикт устаревает: проверку нужно повторить на новой ревизии.
READY тоже не означает «ошибок больше нет». Он означает только следующее: названная область проверена на указанной ревизии, обязательные проверки прошли, блокеров не найдено, чувствительность ключевых проверок показана, а всё недоказанное перечислено отдельно.
Три навыка и один рабочий цикл
В репозитории три навыка:
- gopnik-setup один раз изучает проект, находит реальные команды проверки и сохраняет воспроизводимый маршрут в gopnik.json;
- gopnik-critic атакует утверждение или предложенное решение до реализации;
- gopnik атакует законченное изменение перед словом «готово».
Практический цикл может выглядеть так:
- Команда формулирует задачу и наблюдаемый критерий готовности.
- gopnik-critic пытается найти дыру в постановке или плане.
- Основной агент реализует изменение.
- gopnik строит матрицу, атакует код и поставленный артефакт.
- Вердикт с доказательствами возвращается в задачу или pull request.
Это не абсолютная независимость: роль всё ещё исполняет языковая модель. Но контекст проверки меняется с «подтверди мою работу» на «найди воспроизводимый контрпример», а формат отчёта не позволяет заменить наблюдение уверенностью.
Что получилось на практике
На крупном внутреннем проекте одна из первых проверок Gopnik нашла четыре класса дефектов. Их исправили, а контрпримеры превратили в регрессионные тесты.
В двух последовательных окнах примерно по 28 дней доля коммитов с пометками регрессий и хотфиксов снизилась с 8,9 до 3,9 на 100 коммитов без слияний.
Это внутренняя метрика одного проекта, а не контролируемый эксперимент. Она не доказывает причинность и не обещает такой же результат другой команде. Для команды Mad Duck важнее оказался качественный эффект: слово «готово» стало требовать проверяемых доказательств, а найденные агентом контрпримеры начали пополнять тестовый набор.
Как попробовать
Gopnik распространяется по лицензии MIT. Для установки достаточно отправить агенту инструкцию из репозитория:
После настройки закончите изменение и попросите:
Gopnik не заменяет тесты, code review и инженерное решение о релизе. Он меняет более узкую, но важную вещь: право агента произнести «готово».
Когда coding agent в следующий раз скажет, что всё работает, не просите его просто проверить ещё раз. Спросите, как именно он пытался доказать обратное.
Источники
- Towards Understanding Sycophancy in Language Models, Sharma et al., 2023.
- Large Language Models Cannot Self-Correct Reasoning Yet, Huang et al., 2023.
- The Art of Software Testing, Glenford J. Myers, 1979.
- Notes on Structured Programming, EWD249, Эдсгер Дейкстра, 1969.