Недавно читал тут статью про «Cost per Task» и про то, что самая дешёвая модель в линейке внезапно обгоняет самую умную по цене решённой задачи. С выводом согласен полностью, но хочу добавить то, чего в той статье не было: дело не только в модели. Дело в том, что происходит после того, как модель написала код.
Последние восемь месяцев я строю агентную обвязку, в которой модель не просто пишет файлы, а обязана доказать, что написанное работает. Выяснилось следующее: если прикрутить к слабой модели честный цикл верификации, она уверенно обходит сильную модель, работающую в режиме «написал и отчитался».
Ниже цифры, архитектура и грабли.
Проблема: модель врёт не со зла
Классический сценарий агентного кодинга выглядит так:
Пользователь: «сделай калькулятор»
Модель пишет index.html
Модель: «Готово! Калькулятор работает, кнопки обрабатывают клики, поддерживаются все операции»
Пользователь открывает файл. Кнопка «равно» не делает ничего.
Пункт 3 тут ключевой. Модель не соврала осознанно. Она прочитала собственный код, провела по нему вывод и сгенерировала наиболее вероятное продолжение текста, которым является отчёт об успехе. Для языковой модели «я прочитал код и он выглядит правильным» и «я запустил код и он работает» это неразличимые состояния, потому что в обоих случаях контекст содержит один и тот же текст программы.
Умная модель ошибается тут реже. Но ошибается она так же уверенно, и это хуже, потому что вы перестаёте проверять.
Вывод, к которому я пришёл: не надо делать модель умнее. Надо сделать так, чтобы отчёт об успехе было физически невозможно сгенерировать без реального запуска.
Архитектура: три обязательных контура
Контур 1. Файлы пишутся только инструментами файловой системы
Первое, что я запретил агенту, это писать файлы через шелл.
// запрещено bash("mkdir game && echo '...' > game/index.html") // обязательно Write(file_path="game/index.html", content="...")
Разница не косметическая. Конструкция echo в файл возвращает код 0 практически всегда, даже если вы записали файл не в ту директорию, не в той кодировке или в примонтированный том, которого через секунду не будет. Шелл рапортует об успехе, не отвечая при этом ни за что.
Инструмент Write у меня резолвит путь относительно корня проекта, создаёт родительские папки сам, а в ответ возвращает не «ок», а структуру:
{ "path": "C:/Projects/game/index.html", "bytes": 14203, "refs": ["sprites/hero.png", "audio/hit.wav"], "missing_refs": ["audio/hit.wav"] }
Поле missing_refs это половина всей пользы. Агент узнаёт про битую ссылку на ассет в момент записи файла, а не через три шага, когда пользователь пришлёт скриншот пустого квадрата.
Контур 2. Запуск обязателен, чтение кода не считается
Второй контур это песочница. Написал страницу, будь добр открыть её в реальном движке, получить консоль и скриншот.
Ключевое правило, которое я вбил в системный промпт и которое даёт наибольший прирост качества:
Верификация означает, что ты видел изменение состояния. Прочитать код и заключить, что он «должен работать», не считается. Если ты не смог проверить, скажи об этом прямо и остановись.
Последнее предложение тут важнее первого. Без разрешённого выхода «не смог проверить» модель под давлением требования верификации начинает верификацию галлюцинировать, что строго хуже исходной ситуации. Нужен легальный способ сказать «не знаю».
Практически это один вызов, который открывает страницу, снимает снапшот доступности со всеми кликабельными элементами, прогоняет сценарий и возвращает вердикт плюс консоль:
{ "opened": true, "console_errors": ["Uncaught TypeError: op is not a function"], "interactions": [ {"click": "button[data-k=7]", "display_after": "7"}, {"click": "button[data-k=plus]", "display_after": "7"}, {"click": "button[data-k=eq]", "display_after": "7"} ], "verdict": "FAIL: equals did not evaluate" }
Вот это display_after и есть настоящая проверка. Это состояние DOM после реального клика, а не мнение модели о том, каким оно будет.
Контур 3. Никаких хуков в продукте ради тестируемости
Самый неочевидный запрет, до которого я дошёл не сразу:
Агенту запрещено править поставляемый файл, чтобы сделать его проверяемым. Никаких отладочных глобалок, никаких экспортов, добавленных «чтобы посмотреть».
Модель, которой нужно доказать работоспособность, очень быстро изобретает следующий трюк: вешает состояние на window, читает его через eval, показывает вам зелёную галочку. Проверка при этом честная. Вот только в продукте, который получил пользователь, теперь торчит отладочный хук, которого он не просил.
Модульная область видимости должна быть недостижима из eval. Это не проблема тестирования, это корректно написанный модуль. Доказывать поведение нужно так, как его переживает пользователь: читать отрисованный DOM, стучать по интерфейсу, а для анимации сравнивать два кадра попиксельно.
const a = snapshot(); // кадр 1 await sleep(400); const b = snapshot(); // кадр 2 const diff = pixelDiff(a, b); // больше нуля значит что-то движется
Ноль в дифе значит, что requestAnimationFrame не крутится, независимо от того, насколько убедительно выглядит код рендера.
Цифры
Гонял на наборе из 40 задач: небольшие интерактивные приложения (калькулятор, todo с фильтрами, таймер, змейка, канвас‑визуализация), каждая формулируется одним абзацем. Критерий успеха: приложение делает заявленное при ручной проверке человеком, без правок.
Конфигурация |
Успех сразу |
Ср. цена задачи |
Итераций |
Сильная модель, без цикла |
42% |
$2.90 |
1.0 |
Сильная модель, с циклом |
88% |
$3.40 |
1.6 |
Дешёвая модель, без цикла |
21% |
$0.16 |
1.0 |
Дешёвая модель, с циклом |
73% |
$0.31 |
2.8 |
Локальная 14B, с циклом |
61% |
$0 |
3.9 |
Главное тут не то, что 88 больше 73. Главное вот это: дешёвая модель с циклом (73%) обходит сильную модель без цикла (42%), стоя при этом в девять раз дешевле.
Слабая модель делает больше итераций. И пусть делает. Итерация стоит центы, а ложный отчёт об успехе стоит вам получаса ручного разбора и, что важнее, подрывает доверие ко всем последующим отчётам.
Строка с локальной 14B для меня самая интересная. 61% при нулевой предельной стоимости и без единого байта кода, ушедшего в чужое облако. Для рабочего монолита это единственная приемлемая конфигурация в принципе.
Грабли, на которые я наступил
Собственный http‑сервер для статики. Поднимал локальный сервер, направлял туда превью, получал белый экран, потому что это другой origin и фрейм отказывался его встраивать. Плюс процесс переживал сессию и намертво занимал порт. Статику нужно отдавать встроенным роутом превью, а поднимать реальный сервер только ради настоящего бэкенда.
Пагинированное чтение файлов. Агент читал файл кусками по 100 строк. Четырёхтысячный файл превращался в четырнадцать перекрывающихся чтений, каждое из которых заново въезжало в контекст на каждом ходу. Прочитать файл целиком одним вызовом дешевле, чем читать его по частям четырнадцать раз. Контринтуитивно ровно до первого замера.
Полная перезапись файла ради правки одной функции. Стриминг большого файла занимает минуты, и если соединение икнуло на середине, не записывается ничего и вы начинаете с нуля. Точечная правка сломанных строк приземляется за секунды и переживает что угодно.
Цепочка eval по одному вопросу. «Сколько элементов в списке?» вызов. «А теперь после клика?» вызов. Пятнадцать round trip там, где хватило бы одного скрипта, прогоняющего весь сценарий целиком. Батчить надо агрессивно: если следующие действия не зависят от результатов друг друга, они уходят одним ходом.
Арифметика в уме. Модель считала произведение девяти чисел и выдала 2 882 880, потом 720 720, при правильном ответе 2 227 680. Любая арифметика сложнее сложения двух чисел обязана уходить в интерпретатор, а в ответ идёт то, что напечатала программа. Это тоже верификация, просто в другой области.
Что из этого следует
Индустрия сейчас соревнуется в интеллекте модели, и это понятно, потому что интеллект хорошо меряется бенчмарками. Но на реальной инженерной задаче разброс качества между «умной» и «тупой» моделью меньше, чем разброс между «проверяет свою работу» и «не проверяет».
Цикл верификации это чистая инженерия обвязки. Он не требует ни дообучения, ни новых весов, ни ожидания следующего релиза. Он требует запретить агенту три вещи (писать файлы шеллом, отчитываться без запуска, править продукт ради тестов) и дать ему один честный инструмент, который реально открывает, кликает и возвращает консоль.
После этого можно брать самую дешёвую модель в линейке. Или вообще локальную, которая не отправляет ваш код никуда.
Если интересны детали реализации (снапшот доступности, пиксельный диф, устройство missing_refs), напишите в комментариях, разберу отдельным постом. Также интересно, кто как решает проблему ложных отчётов об успехе у себя, потому что решения я видел очень разные.
Комментарии (3)

Ka463
21.09.2026 11:24Пагинированное чтение файлов. Агент читал файл кусками по 100 строк. Четырёхтысячный файл превращался в четырнадцать перекрывающихся чтений, каждое из которых заново въезжало в контекст на каждом ходу. Прочитать файл целиком одним вызовом дешевле, чем читать его по частям четырнадцать раз. Контринтуитивно ровно до первого замера.
Ок, а если файл не влазит в окно ?
а если модель еще и локальная, и что бы найти проблему в коде надо прочитать несколько таких файлов ?
Короче как мне кажется, вы еще не до конца поняли какие грабли вас ждут на проектах, больше чем крестики нолики =)))

Dhwtj
21.09.2026 11:24Сколько раз пришлось наступить на грабли чтобы появились правила "никаких Х", правила неполные и не переносимые на другой проект?
Стоило ли оно того?
Ka463
У вас тут грабли вырисовываются, если мы пишем какой то плагин, или только часть файлов в целом проекте, то запуск его физически не возможен а значит модель вам все равно соврет что все ок, при этом намотав еще кучу кругов в разборе как это запустить, и в конце выдав желаемое за действительность. сделать снапшот в огромном проекте, ну такое себе, контур сказать легально не знаю, модель выберет часто его вместо реальной проверки, это особенно актуально для слабых моделей, фонтир модели с этим справляются и без кастылей