Агент выдал уверенно неверное число: как мы ловим такие случаи

Код, написанный агентом, прошёл тесты и повторное ревью, а на реальных выгрузках WB выдавал нулевое «Итого к оплате».

Я строю для своих проектов автономную систему агентов на Claude Code и Codex: задача приходит в Telegram, конвейер сам заводит ветку, пишет тесты, правит код, отдаёт работу критику и открывает PR. Ниже эпизоды с 18 по 21 сентября 2026 года: где агенты ошибались уверенно и какие барьеры это ловят.

Случай: ноль, который выглядел как число

Мой проект «До копейки» сверяет финансовые отчёты Wildberries и собирает разбор в Excel. Выдачу старой версии от 18 сентября разбирала отдельная сессия opus. Файл выглядел как отчёт, но строка «− расходы кабинета» в водопаде выплаты на деле прибавлялась. Рядом стояло «Товаров в минусе: 0» при шести отрицательных строках. Хранение почти совпадало с выручкой той же недели, типичный след чужой колонки. Внизу была фраза, что всё сходится со сводным отчётом WB до копейки, а самой сверки в файле не было.

Вторую версию писал агент на opus, с пересчётом из сырого xlsx и блокировкой выдачи при любом расхождении. Ревьюер нашёл три блокера, их исправили, повторное ревью это подтвердило.

19 сентября QA-агент прогнал эту версию на реальных выгрузках, и она не работала. Сверка читала выгрузку через openpyxl в режиме read_only. Файлы WB объявляют размер листа «A1» при десятках тысяч строк, библиотека верила и отдавала одну ячейку. Строк читалось ноль. Сверка блокировала каждый разбор, а «Итого к оплате» в резюме выходило нулевым. Синтетику для тестов писал тот же openpyxl с честным размером, и этот путь ни разу не выполнялся. Второй блокер: код склеивал все колонки со словом «дата», включая срок фиксации цены, окно недели растягивалось до девяти месяцев, в разборе появлялись три выдуманных пропуска недель.

Почему модель так делает

Модель не отличает «проверил» от «обычно так бывает». Недостающее она достраивает правдоподобным, и непосчитанный ноль в итоговой строке выглядит так же, как посчитанный.

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

Критик в свежем процессе

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

Сработало 20 сентября на карточке «убрать из оффера обещания, которые движок не доказывает», вроде «8 из 10 кабинетов». Критик дважды вернул работу и нашёл третий, незамеченный близнец обещания в закреплённом посте. Я нажал «Одобрить», со второго захода карточка дошла до PR.

Не сработало на «До копейки»: вторая версия прошла повторное ревью. Критик оценивает то, что ему показали, а показали синтетику.

Тестер до реализации

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

В тот же день тестер написал «НУЖЕН ВЛАДЕЛЕЦ» только потому, что Bash ему намеренно не выдан, и остановил карточку на пустом месте. Теперь звать владельца могут только implementer и критик. На другом прогоне стадия упала за 4 секунды: промпт начинался с «--- бриф проекта», и claude -p принял строку за флаг. Фейковый claude в тестах первую строку не разбирает. Тесты доказывают то, что автор себе представил, поэтому регрессия должна падать до исправления и проходить после. QA-агент проверял это через git stash.

План-критик

Дешёвая стадия между разбором задачи и тестами. Код не читает, получает карточку и мини-спеку, возвращает JSON: покрыто ли «готово когда», есть ли проверка на каждый критерий, влезает ли задача в пару часов. Неразборчивый ответ считается командой переписать.

Ночью 21 сентября карточка «витрина с тремя карточками проектов» остановилась здесь через 76 секунд. Файла с текстом стадии не видели, и критик написал: текст карточек выдумывать нельзя. Заодно нашёл противоречие: критерий требовал существующий демо-файл, а границы запрещали его создавать. Причина была в самой системе, стадии не видели нужный репозиторий. Дефект починили, карточку поставили заново с явными путями.

Промах 20 сентября: в пяти строках плана, которые приходят мне в Telegram, стояло «Что меняем: Не трогаю: …». Спеку обрезали до 6000 знаков для промпта критика, и мою карточку строили по той же обрезке. Раздел «План» выпал, строка съехала на «Границы». Теперь карточка читает спеку целиком.

Второе мнение другой моделью

На задачах уровня judgment после одобрения критика дифф читает Codex в режиме read-only и отвечает по JSON-схеме. Сошлись: PR с пометкой «два ревью». Разошлись: PR не открывается, я получаю обе позиции.

21 сентября агент прогнал настоящий Codex на диффе этого же этапа. Три круга, все с требованием правок, и все по делу. Telegram не ответил, а тревога о проде помечалась отправленной и терялась. Урезанный ответ {"verdict":"ok"} без замечаний засчитывался как согласие двух ревьюеров. Статус ok: null от systemctl превращался в «всё живо». Дыру в разборе ответа второго ревьюера нашёл сам второй ревьюер.

Четвёртый круг агент запускать не стал, цикл правок может идти бесконечно. Сквозной путь внутри живого моста ещё не прогнан. Для дизайна второе мнение выключено: Codex не видит картинок.

Проверка кодом вместо доверия

19 сентября сорванная стадия молча шла дальше, и «готово» получалось из ничего. Теперь ненулевой код возврата означает failed с причиной. 20 сентября агент заметил, что реестр молча превращает незнакомый статус paused в active и замороженный проект остался бы в работе. Теперь такой статус пишет ошибку. Вечером механик процитировал в отчёте маркер «НУЖЕН ВЛАДЕЛЕЦ», и конвейер принял цитату за вызов: мне пришла кнопка на пустом месте, я нажал «Одобрить», круг повторился. Теперь маркер считается только с начала строки. Сама карточка механика потом числилась живой, пока агент за ней наблюдал: её id находился в командной строке наблюдателя.

Предел есть: гейт команд смотрит на текст, команду из переменных он не увидит.

Done means saved

Работа закрыта, когда результат проверен или прямо назван verification_gap, отчёт лежит в репозитории, а перед коммитом проверено отсутствие секретов. 20 сентября implementer сам записал, что живой /status не смотрел. 19 сентября нашлась карточка с двумя записями «done» и разным результатом: исполнитель дописал её сам. Теперь исполнители работают в песочнице, финальная запись заменяется.

Не сработало на брифах: 20 сентября в брифе стояло «Последний прогон 20.09: 151 passed», а тестов уже было 181. Сверки брифов пока нет.

Журнал стоимости и лимитов

Каждая стадия пишет в журнал модель, стоимость, код возврата и причину маршрута. 20 сентября шесть карточек не дали ни одного PR. По журналу все 10 попыток implementer на opus оборвались с rc=1 на сумме от $1,05 до $1,26, успешные накануне стоили от $0,42 до $0,66. Причина системная.

Здесь и нашлось уверенно неверное число. Отчёт агента насчитал около $21 и советовал поднять долю бюджета implementer или резать карточки мельче. Реальных денег прогон не стоил: стадии идут по подписке, а total_cost_usd из CLI показывает прайс, а не списание. Теперь бюджетов два: время для подписки (implementer 900 секунд, остальные 300) и $3 на задачу для платных API. В карточке колонки «реально» и «условно». В тот же день те же шесть задач дали 6 PR при $0,00 реальных денег.

С лимитами Codex похоже. 20 сентября в код попали потолки 40 запусков за 5 часов и 400 за неделю, отчёт честно называл их взятыми с потолка. 21 сентября я сказал: не гадай, а изучи источники. Справка OpenAI о Codex таблицы лимитов не даёт, статья OpenAI о лимитах прямо оговаривает: «These are not fixed message limits», а на плане Pro пятичасового окна нет, что подтвердил и живой след сессии. Потолки удалены. Загрузка берётся из следа Codex, без свежего следа идёт оценка с пометкой estimated.

Что не помогло

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

Сколько стоит дисциплина

План-критик на понятной карточке: 17 секунд, условно около $0,056. Круг второго мнения: от 100 до 130 секунд и около 600 тысяч входных токенов, после трёх кругов недельное окно Codex показывало 0 %. Judgment-задача от моей кнопки до PR: 21 минута, $0 реальных денег. Провальный прогон 20 сентября стоил около 96 минут машинного времени и двух часов моего наблюдения. Метрики недели на 20 сентября: 26 задач, 13 PR, медиана до PR 15 минут, $0,00 реальных денег при ~$57 условной подписки. Тестов у конвейера 19 сентября было 87, утром 21-го их 324.

Самый дорогой ресурс здесь моё внимание. Одна задача 20 сентября прошла конвейер дважды и дала два PR, 21 минута впустую. Теперь новые карточки сравниваются с открытыми: у той пары перекрытие 0,97 при пороге 0,6.

Что поставить у себя за неделю

  1. Критика отдельным процессом: дифф, критерии и вывод тестов, которые запускал не исполнитель.
  2. Ответ ревьюера по схеме. Пустой или урезанный ответ означает «не получено», а не «ok».
  3. Код возврата стадии проверяет код, а не модель. Маркеры разбирать строго.
  4. Один настоящий входной файл в тестах рядом с синтетикой; регрессия падает до исправления.
  5. У каждого числа пометка «измерено» или «оценка», реальные деньги отдельно от прайса, угаданные потолки удалить.
  6. В конце отчёта агента список того, что вживую не проверено.

Дневник этой стройки веду в Telegram: «Нетник».