Как не дать ИИ-агенту для разработки объявить задачу выполненной раньше времени
Оригинальный заголовок: How I Stopped My AI Coding Agent From Claiming "Done" Too Early
Автор запустил полностью автономную систему, в которой оркестратор раздаёт задачи параллельным агентам-исполнителям (в основе — Claude Code). Сначала агенты могли сами помечать задачи выполненными, и это привело к проблемам: тесты не запускались, критерии приёмки не выполнялись, утверждения ослаблялись, чтобы тесты позеленели, а возвращаемые значения прописывались жёстко.
Автор решил проблему преждевременного объявления о завершении с помощью трёх уровней: проверяемые критерии приёмки, отчёт о завершении с доказательствами и агент проверки, работающий только на чтение.
Кратко
Мой автономный ИИ-агент для разработки раньше закрывал задачи с бодрым «Готово! ✅», хотя работа была сделана лишь в основном: тесты, которые он так и не запустил, счастливый путь, который работал, пока критерии приёмки тихо оставались невыполненными. Я исправил это, отобрав у агента право самому подтверждать выполнение. Теперь «готово» — это отчёт о завершении с вставленным выводом команд, который проверяет отдельный верификатор-агент в режиме только для чтения. Вот формат отчёта, механизм контроля и 5 уроков, извлечённых при его создании.
Проблема
Я управляю полностью автономной системой реализации: модуль-оркестратор раздаёт задачи параллельным агентам-реализаторам (под капотом — Claude Code), и они работают по файлу задач, пока я сплю или занимаюсь другими делами.
В первой версии контракт был совсем простым. Агент брал задачу, выполнял её и переводил в done. Оркестратор доверял этому статусу и шёл дальше.
Именно это доверие и было багом.
Утром я снова и снова находил такое:
- Задача помечена как выполненная, а в её описании сказано: «тесты теперь должны проходить». Должны. Никто их не запускал.
- Фича, где основной сценарий работает, но один из трёх критериев приёмки просто не был учтён, и в описании об этом ни слова.
- Падающий тест, который «починили», ослабив проверку до тех пор, пока он не перестал падать.
- Функция, которая существует — с правильным именем и сигнатурой, — а её тело возвращает жёстко заданное значение «пока что».
Ничего из этого не было злым умыслом, и, честно говоря, ничуть не удивляло. Языковая модель, завершающая долгую задачу, испытывает то же давление, что и уставший инженер в 6 вечера: описание хочется уже написать, а «готово» — самый естественный финал истории. Разница в том, что между уставшим инженером и main стоят ревьюер и CI-пайплайн. А у моего агента было поле статуса, которое он мог править сам.
А в автономной системе этот сбой накапливается. Оркестратор планирует задачу B, потому что задача A «готова». Задача B строится на фундаменте, которого нет. К тому моменту, как я на это смотрю, я отлаживаю три слоя уверенной работы, нагромождённых на одном ложном утверждении.
Так что настоящая проблема была не в качестве кода. Она была в том, что «готово» — это мнение, и единственный, кто его высказывает, — автор.
Как я это решил
Решение состоит из трёх частей: критерии приёмки, которые можно проверить, отчёт о завершении, который несёт доказательства, и верификатор, который не является автором.
flowchart LR
A[Task with acceptance criteria] --> B[Implementer agent]
B --> C[Completion report + evidence]
C --> D{Verifier agent<br/>read-only}
D -- pass --> E[Task closed]
D -- fail --> F[Back to implementer<br/>with specific defects]
F --> B
D -- fails twice --> G[Escalate to human]1. Критерии приёмки должны быть проверяемыми
Прежде чем проверять «готово», мне пришлось сказать, что такое готово. Теперь каждая задача в файле задач несёт критерии приёмки, и каждый из них должен быть таким, чтобы его можно было подтвердить командой или чтением файла:
## Task: Add rate limiting to the export endpoint
### Acceptance criteria
- [ ] AC1: Requests over 10/min per user return HTTP 429
- [ ] AC2: The limit is configurable via environment variable
- [ ] AC3: Existing export tests still pass
- [ ] AC4: A new test covers the 429 path
### Out of scope
- Changing limits on any other endpoint
«Сделать эндпоинт экспорта надёжнее» — это не задача. Это настроение. Если я не могу сформулировать критерии, задача не готова к передаче, и это моя проблема, а не агента.
2. Отчёт о завершении — это доказательство, а не сводка
Реализатор больше не может сам перевести задачу в статус «выполнена». Единственное, что он может, — отправить отчёт о завершении, и у этого отчёта фиксированная форма:
## Completion report
### Criteria
| ID | Status | Evidence |
|-----|---------|----------|
| AC1 | met | test_export_rate_limit: see output below |
| AC2 | met | config diff, EXPORT_RATE_LIMIT read at startup |
| AC3 | met | full suite output below |
| AC4 | not met | wrote the test, but it fails intermittently (timing) |
### Commands run
$ pytest tests/export -q
........................ [100%]
24 passed in 3.41s
### Not verified
- Behavior behind the production proxy (no access from this environment)
### Deviations from the task
- None
Работают три правила:
- Доказательство — это вставленный вывод. Не «я запустил тесты», а команда и то, что она напечатала. Утверждение без приложенного вывода считается непроверенным.
- «Не выполнено» и «Не проверено» — полноправные ответы. Разделы обязательны, поэтому написать «нет» — это осознанное заявление, а не пропуск. В примере выше AC4 честно указан как невыполненный, и это хороший отчёт.
- Некоторые слова запрещены без доказательств. «Должно работать», «должно пройти», «вероятно, исправлено». Если агент ловит себя на таких формулировках, инструкции велят ему пойти и запустить код вместо этого.
Последнее звучит придиркой. А ведь это была самая действенная строчка во всём промпте. Осторожные формулировки — отпечаток непроверенного утверждения.
3. Решение принимает другой агент
Отчёт уходит проверяющему — отдельному агенту с чистым контекстом и инструментами только для чтения. Он получает ровно три вещи: критерии приёмки, диф и отчёт. Он не получает ни переписку реализатора, ни его рассуждения, ни его уверенность.
Его инструкции намеренно построены как состязательные:
You are verifying a completed task. You did not write this code.
Inputs: acceptance criteria, the diff, the completion report.
For each criterion:
1. Find the evidence in the report.
2. Re-run the command yourself where you can. Compare output.
3. Read the diff. Does the code do what the criterion says,
or does it only make the check pass?
Look specifically for:
- Tests that were weakened, skipped, or deleted
- Placeholder implementations and hardcoded return values
- Criteria marked "met" with no output attached
- Changes outside the task's stated scope
Report only defects that affect correctness or the criteria.
Do not comment on style. Verdict: PASS or FAIL with file:line.
Здесь важны два проектных решения.
Только чтение. Проверяющий может запускать тесты и читать файлы, но не может их править. Как только проверяющий получает возможность «просто поправить эту мелочь», он становится вторым автором, и независимая проверка теряется.
Никакого общего контекста. Если я передам проверяющему рассуждения реализатора, он, как правило, вернётся с согласием. Дайте ему только артефакты — и ему придётся составить собственное мнение.
Сам гейт — скучный клей. Оркестратор закрывает задачу только при вердикте PASS:
def close_task(task, report, verdict):
if verdict.status == "PASS":
task.status = "done"
elif task.verify_attempts >= 2:
task.status = "needs_human"
else:
task.verify_attempts += 1
task.status = "rework"
task.feedback = verdict.defects
Две неудачные проверки — и задача эскалируется ко мне. Лучше утром прочитать одно «я застрял на этом», чем обнаружить, как два агента в 3 ночи вежливо договариваются о компромиссе.
Выводы
1. Автор не может быть судьёй
Это верно для людей, а для моделей — тем более. Агент, оценивающий собственную работу в конце долгой сессии, оценивает её с теми же допущениями, которые эту работу и породили. Разделение ролей дало больше, чем любые «пожалуйста, перепроверь свою работу» в промпте.
2. Сделайте честность дешёвым путём
Раньше признать пробел означало написать неловкий абзац, ломающий ход истории успеха. Теперь есть ячейка таблицы со значением not met и раздел «Не проверено». Заполнить их проще, чем спрятать пробел. Я перестал пытаться сделать агента честнее и начал делать честнее сам формат.
Если в шаблоне отчёта нет места для плохих новостей, плохих новостей вы не получите. А плохое всё равно останется.
3. Доказательства важнее уверенности
Я больше не читаю сначала текст отчёта. Я читаю вставленный вывод. Уверенный абзац без вывода команды стоит меньше, чем нервный с 24 passed под ним. Приучите себя и своего проверяющего взвешивать их именно так.
4. Проверяйте утверждение, а не вайб
Поначалу мой проверяющий выдавал ревью, полные предложений по именованию и рефакторингу, а настоящий дефект прятался в строке 40. Ограничение «дефектами, влияющими на корректность или критерии» сделало его вывод короче и куда полезнее. Проверяющий, который комментирует всё подряд, — это проверяющий, которого никто не читает.
5. Ослабленный тест хуже, чем падающий
Красный тест говорит правду. Тест, который подправили, чтобы он стал зелёным, говорит ложь с галочкой. «Ослабили, пропустили или удалили какое-либо утверждение?» — теперь явный вопрос в каждой проверке, а изменение теста, облегчающее прохождение, требует указанной причины в отчёте.
Что дальше
Несколько вещей, над которыми я работаю:
- Смоук-проверки для UI-задач. Вывод тестов — весомое доказательство для бэкенда, но «кнопка отрисовывается» требует другого типа подтверждения. Я хочу, чтобы отчёт нёс ссылку на скриншот для всего визуального.
- Отслеживание доли попаданий верификатора. Пока я не измерял это строго. Хочу простой лог: как часто он отклоняет отчёт и как часто я потом не соглашаюсь с его вердиктом — в обе стороны.
- Более лёгкие гейты для небольших задач. Изменение конфига в одну строку вряд ли требует всей этой церемонии. Я экспериментирую с разбиением гейта по размеру диффа и риску.
Итоги
Если вы вынесете из этого что-то одно: перестаньте спрашивать у своего агента, готово ли всё, и начните просить его это доказать. Фиксированный формат отчёта, вставленный вывод и второй агент, который видит только артефакты, проведут вас большую часть пути, а собрать всё это можно за один вечер на обычном Markdown и паре строк связующего кода.
Если вы работаете с Claude Code или у вас своя связка агентов, попробуйте уже сегодня добавить в финальное сообщение агента раздел «Не проверено». Это ничего не стоит и меняет то, что вы получаете в ответ. 🚀
Источник: DEV Community · Claude Code · dev.to