ИИ-агент написал код и сам его проверил. Почему его «готово» ничего не доказывает

Агентская разработка ускоряет написание кода, но оставляет неудобный вопрос: кто проверяет самого агента? В Mad Duck мы разработали Gopnik - open source-навык, который не подтверждает результат, а пытается воспроизводимо его сломать.

Перед словом «готово» кто-то должен попытаться доказать обратное.
Перед словом «готово» кто-то должен попытаться доказать обратное.

Однажды агент реализовал фичу, увидел нужные строки в базе и активность в логах и сообщил, что всё работает end-to-end.

На самом деле учётные данные, которые сохраняла новая форма, не читались рантаймом. Поллер и исполнитель получали пустое значение. Настоящего сквозного прогона не было ни одного, хотя код был написан, тесты проходили, а отчёт звучал убедительно.

Агент не обязательно врал. Внутри выбранной им картины мира всё действительно сходилось:

  1. Он предположил, как должна работать фича.
  2. Написал реализацию из этого предположения.
  3. Выбрал проверки, которые подтверждали то же предположение.
  4. Прочитал зелёный результат как доказательство готовности.

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

Почему «проверь ещё раз» не решает проблему

В основе 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-библиотеки это может выглядеть так:

{ "language": "ru", "verification": { "artifact_kind": "library", "stage1": ["pytest -q", "ruff check ."], "stage2": [ "python3 -m build", "python3 -m venv /tmp/consumer && /tmp/consumer/bin/pip install dist/*.whl", "/tmp/consumer/bin/python examples/consume.py" ] } }

Так ловятся ошибки, невидимые внутренним тестам: файл не попал в wheel, экспорт забыли добавить, entry point указан неверно, а диапазон версий зависимости не соответствует реальности.

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

Проверка тоже должна пройти проверку

Есть ещё одна ловушка: агент придумал тест, запустил его и получил зелёный статус. Но способен ли этот тест вообще заметить дефект?

Gopnik требует хотя бы для несущих проверок показать чувствительность. Для этого можно:

  • запустить сценарий на старой ревизии;
  • безопасно мутировать вход;
  • подставить заведомо плохой пример;
  • временно отключить часть механизма;
  • воспроизвести известную сигнатуру сбоя.

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

Зелёная проверка, которая не умеет краснеть, - слабое доказательство.

Вердикт вместо победного отчёта

Обычный агентский отчёт часто выглядит так: «Реализация завершена, все тесты проходят». Из него непонятно, какую версию проверяли, что именно пытались сломать и что осталось за пределами теста.

Gopnik возвращает ограниченный вердикт с воспроизведением:

BLOCKER - скидка округляется дважды. Воспроизведение: POST /orders с {"discount": 0.1} Получено: 23.94 Ожидалось: 23.95 Проверенная ревизия: 8f31c2a Verdict: NOT READY

Контрпример означает NOT READY. После исправления прежний вердикт устаревает: проверку нужно повторить на новой ревизии.

READY тоже не означает «ошибок больше нет». Он означает только следующее: названная область проверена на указанной ревизии, обязательные проверки прошли, блокеров не найдено, чувствительность ключевых проверок показана, а всё недоказанное перечислено отдельно.

Три навыка и один рабочий цикл

В репозитории три навыка:

  • gopnik-setup один раз изучает проект, находит реальные команды проверки и сохраняет воспроизводимый маршрут в gopnik.json;
  • gopnik-critic атакует утверждение или предложенное решение до реализации;
  • gopnik атакует законченное изменение перед словом «готово».

Практический цикл может выглядеть так:

  1. Команда формулирует задачу и наблюдаемый критерий готовности.
  2. gopnik-critic пытается найти дыру в постановке или плане.
  3. Основной агент реализует изменение.
  4. gopnik строит матрицу, атакует код и поставленный артефакт.
  5. Вердикт с доказательствами возвращается в задачу или pull request.

Это не абсолютная независимость: роль всё ещё исполняет языковая модель. Но контекст проверки меняется с «подтверди мою работу» на «найди воспроизводимый контрпример», а формат отчёта не позволяет заменить наблюдение уверенностью.

Что получилось на практике

На крупном внутреннем проекте одна из первых проверок Gopnik нашла четыре класса дефектов. Их исправили, а контрпримеры превратили в регрессионные тесты.

В двух последовательных окнах примерно по 28 дней доля коммитов с пометками регрессий и хотфиксов снизилась с 8,9 до 3,9 на 100 коммитов без слияний.

ИИ-агент написал код и сам его проверил. Почему его «готово» ничего не доказывает

Это внутренняя метрика одного проекта, а не контролируемый эксперимент. Она не доказывает причинность и не обещает такой же результат другой команде. Для команды Mad Duck важнее оказался качественный эффект: слово «готово» стало требовать проверяемых доказательств, а найденные агентом контрпримеры начали пополнять тестовый набор.

Как попробовать

Gopnik распространяется по лицензии MIT. Для установки достаточно отправить агенту инструкцию из репозитория:

Прежде чем что-либо делать, прочитай эту полную raw-инструкцию только через stdout - не сохраняй её в файл - и точно следуй ей: https://raw.githubusercontent.com/madduck-tech/gopnik/main/docs/install.md

После настройки закончите изменение и попросите:

Прогони навык gopnik по этому изменению.

Gopnik не заменяет тесты, code review и инженерное решение о релизе. Он меняет более узкую, но важную вещь: право агента произнести «готово».

Когда coding agent в следующий раз скажет, что всё работает, не просите его просто проверить ещё раз. Спросите, как именно он пытался доказать обратное.

Источники

22