Агент написал «готово», а фича не работает: почему так и как проверять работу AI-агента
Агент пишет «готово», потому что оценивает работу сам и видит только то, что проверил: обычно это юнит-тесты, которые не видят стыков между модулями, миграций, конфигурации и сквозного сценария. Лечится структурой, а не промптом: Definition of Done в AGENTS.md, обязательный E2E-прогон, скрипт верификации, который один имеет право поставить статус passing, и независимый оценщик с чистым контекстом.
Почему так происходит
Ситуация знакома всем, кто работает с агентами: агент написал «готово», а фича не работает. Он не врёт специально. Он сообщает о том, что сам успел проверить, и искренне уверен, что этого достаточно. Разрыв между заявлением и реальностью — свойство самой схемы «исполнитель сам оценивает свою работу», а не дефект конкретной модели.
Причин три. Первая — модели склонны к завышенной уверенности: уверенность ответа выше его точности. Вторая — агент оценивает свою работу в том же контексте, где её делал: он уже убедил себя, что решение верное, и просьба «будь объективнее» контекст не меняет. Третья, самая практическая, — агенту доступна только частичная проверка: юнит-тесты быстро зеленеют, а сквозной сценарий никто не запускал.
Хроника типичного ложного «готово»
Агенту поручили сброс пароля. Он меняет схему базы, добавляет эндпоинт, подключает шаблон письма, запускает юнит-тесты — всё зелёное. В чате появляется уверенное «Готово».
На деле произошло следующее:
- миграция упала на середине и оставила схему в несогласованном состоянии;
- шаблон письма есть, но почтовый сервис (SMTP) в окружении не сконфигурирован — письмо не уходит;
- сценарий «пользователь нажал ссылку → ввёл новый пароль → вошёл» ни разу не запускался.
Ни одну из этих проблем юнит-тесты поймать не могли: базу замокали, отправку письма замокали, а сквозного прогона не было вовсе.
Почему зелёные тесты не доказывают, что фича работает
Юнит-тест проверяет модуль в изоляции: зависимости заменены моками, внешний мир отключён. Это даёт скорость и точность, но и систематическую слепоту. Слепота — не баг тестов, а их дизайн. Зелёные юниты доказывают только то, что каждая деталь работает отдельно, а не то, что детали работают вместе.
Есть четыре класса дефектов, которые юнит-тест не видит в принципе:
| Класс бага | Пример | Почему юнит не ловит |
|---|---|---|
| Несоответствие интерфейсов | Один компонент отдаёт относительный путь, другой ждёт абсолютный | Каждый тестируется со своим моком, который «соглашается» на любой формат |
| Распространение состояния | Схема БД поменялась, а кэш ORM держит старую | В юните нет ни реальной базы, ни кэша |
| Жизненный цикл ресурсов | Файловые дескрипторы и соединения не освобождаются, память течёт на больших файлах | Тест работает на маленьких данных и завершается раньше, чем ресурсы кончаются |
| Окружение и конфигурация | Нет переменной окружения, не настроен SMTP, миграция не накатилась | Тест не запускает приложение в реальной конфигурации |
Уровни проверки вложены: всё, что ловят юниты, ловят и интеграционные тесты, а всё, что ловят интеграционные, ловит сквозной (E2E) прогон. Каждый следующий уровень видит строго больше. Поэтому право сказать «готово» должно быть только у верхнего уровня.
Как измерить проблему: verification gap
Прежде чем чинить, посчитайте. Возьмите последние 10 задач, закрытых агентом. По каждой ответьте на два вопроса: сказал ли агент «готово» и подтвердила ли это независимая проверка (вы сами прошли сценарий, E2E-тест зелёный). Доля расхождений — ваш verification gap.
Это число полезно по двум причинам. Оно показывает масштаб проблемы без споров «да вроде нормально работает». И оно даёт базовую линию: после каждого изменения в процессе вы пересчитываете его на похожем наборе задач и видите, помогло ли.
Как проверять по-настоящему: три слоя валидации
Удобная аналогия — автосервис. Машина проходит три поста, и пропустить пост нельзя.
- Диагностика (секунды): синтаксис, типы, линтер. Ловит опечатки и грубые ошибки.
- Стенд (минуты): юнит- и интеграционные тесты, приложение стартует. Двигатель работает, но машина ещё стоит на месте.
- Тест-драйв: сквозной сценарий так, как его проходит пользователь, — через API, браузер или CLI, с реальной базой и конфигурацией.
Правило прогрессии: нельзя ехать на тест-драйв, пока горят ошибки диагностики, и нельзя выпускать машину, пропустив тест-драйв. Зелёные юнит-тесты — это пройденный второй пост, а не третий.
Удобно свести всё в один вход:
# Makefile
check:
ruff check .
mypy app
pytest tests/unit tests/integration
e2e:
docker compose up -d db mailhog
alembic upgrade head
pytest tests/e2e
verify: check e2e
Здесь E2E поднимает реальную базу и тестовый почтовый сервер, накатывает миграции и проходит сценарий целиком. Упавшая миграция или ненастроенная почта проявятся сразу, а не у клиента.
Definition of Done в AGENTS.md
Агент не должен решать сам, что значит «готово». Критерий записывается в файл инструкций и формулируется проверяемо:
## Definition of Done
Фича готова, только если выполнены ВСЕ пункты:
1. make check завершился с кодом 0
2. make e2e завершился с кодом 0, сценарий фичи есть в tests/e2e
3. Миграции применяются на чистой базе и откатываются
4. Новые переменные окружения описаны в .env.example
5. В PROGRESS.md записано, что сделано и как проверено
НЕ являются признаком готовности:
- «код написан и выглядит правильно»
- «юнит-тесты проходят»
- «у меня локально работает»
Явный список того, что не считается готовностью, работает лучше, чем кажется: агент перестаёт останавливаться на первом зелёном прогоне.
Кто имеет право поставить «passing»
Следующий шаг — убрать статус готовности из рук агента совсем. Фичи хранятся в списке (например, feature_list.json) с полями «поведение», «команда проверки», «состояние». Перевести фичу в passing может только скрипт: он запускает общий make check, затем команду проверки конкретной фичи и при успехе фиксирует статус вместе с хешем коммита как доказательство. Не прошло — фича остаётся активной, никакого «почти получилось».
{
"id": "F04",
"behavior": "Пользователь получает письмо со ссылкой и меняет пароль",
"verification": "pytest tests/e2e/test_password_reset.py",
"state": "active"
}
Здесь есть ловушка: скрипт проверяет ровно то, что записано в поле verification. Если архитектурная проверка входит в Definition of Done, но не в команду фичи, дыра останется невидимой. Поэтому общий make check должен включать все обязательные проверки, а не только тесты.
Независимый оценщик
Самоотчёт исполнителя никогда не бывает независимым источником истины. Решение структурное — разделить роли:
- планировщик разворачивает требование в фичи с критериями приёмки и не пишет код;
- генератор реализует одну фичу и прогоняет тесты, но не решает, готово ли;
- оценщик выносит вердикт по чек-листу критериев.
Оценщик не должен видеть рассуждений генератора, иначе унаследует его уверенность. Он получает только требование, дифф и результаты прогонов. В Claude Code это удобно сделать сабагентом: у него свой чистый контекст. Ещё лучше, если оценщик — другая модель: у неё другие слепые пятна. Оценка «выглядит нормально» запрещена: каждый вывод должен ссылаться на файл, строку или лог.
Сообщения об ошибках, которые агент может исправить сам
Проверка полезна, только если её вывод ведёт к исправлению. «Test failed» или голый AssertionError агенту ничего не говорят. Хорошее сообщение содержит, что сломалось, почему это правило существует и как исправить:
ERROR arch-check: app/services/refund.py импортирует app/db/session.py
WHY: сервисы не обращаются к базе напрямую, только через репозитории
FIX: используйте RefundRepository из app/repositories/refund.py
Такой формат превращает проверку из запрета в подсказку: агент чинит нарушение без вашего участия.
Ручная проверка: где она всё ещё нужна
Автоматика не отменяет человека, но сужает его роль. Ручная проверка нужна там, где критерий трудно формализовать: внешний вид интерфейса, тексты, удобство сценария. Делайте её по чек-листу из критериев приёмки, а не «потыкать». Если вы второй раз пишете агенту один и тот же комментарий, это сигнал превратить его в автоматическую проверку: тест, правило линтера или grep-гвард в make check.
Как E2E меняет поведение агента ещё до прогона
Ценность сквозного теста не только в пойманных багах. Если агент заранее знает, что его работу проверит E2E-сценарий, он пишет код иначе: думает о контракте с соседним компонентом, а не только о своей функции, обрабатывает пути ошибок и не нарушает архитектурные границы, потому что их нарушение всплывёт в прогоне. Проверка работает как ограничение на входе, а не только как фильтр на выходе.
Есть условие: E2E осмыслен, когда у системы понятные границы. На архитектуре-спагетти сквозной тест докажет лишь, что спагетти работают, и не покажет, где нарушен замысел. Поэтому сначала закрепляют слои и правила зависимостей, а потом строят сквозные сценарии. Агенты охотно копируют существующие паттерны репозитория, включая плохие, так что ограничения лучше вводить с первого дня.
Типичные ошибки
- Просить агента «проверить внимательнее». Контекст тот же, оценка останется завышенной.
- Считать 100% покрытие гарантией. Покрытие юнитами не покрывает стыки.
- Рефакторить до подтверждения фичи. Порядок жёсткий: корректность → производительность → стиль.
- Мокать в E2E то, что ломается чаще всего. Базу, миграции и конфигурацию в сквозном тесте нужно запускать по-настоящему; внешние платные API можно подменить тестовым сервером.
Итог
Когда агент написал «готово», а фича не работает, проблема почти всегда в процессе проверки, а не в модели. Посчитайте verification gap, закрепите Definition of Done в AGENTS.md, сделайте E2E обязательным, отдайте статус готовности скрипту и добавьте независимого оценщика. Эта схема подробно разобрана на практике в курсе по харнес-инжинирингу.
Частые вопросы
Можно ли заставить агента самого запускать E2E-тесты?
Да, если команда записана в AGENTS.md как обязательная часть Definition of Done и агенту доступны нужные инструменты. Надёжнее дополнительно запускать её хуком или в CI, чтобы не зависеть от памяти агента.
E2E-тесты медленные, нужно ли гонять их на каждую правку?
Нет. Линт, типы и юнит-тесты — всегда, E2E — при изменениях, затрагивающих больше одного слоя, и перед объявлением фичи готовой.
Как проверять фронтенд, который агент сделал?
Сквозным тестом через браузер (например, Playwright) с кликами по реальному интерфейсу, а внешний вид — ручной проверкой по чек-листу или скриншотами, которые просматривает оценщик.
Достаточно ли код-ревью вместо оценщика?
Ревью человеком полезно, но оно дорогое и не масштабируется на часы автономной работы. Оценщик с рубрикой снимает очевидные проблемы до человека, а человек решает спорные вопросы.
Эта тема подробно разобрана с практикой в курсе «Harness Engineering: инженерия надёжности AI-агентов».
- Агент написал «готово», а фича не работает: verification gap и Definition of Done
- Все тесты зелёные, а фича сломана: E2E-тесты и архитектурные правила для AI-агента