Содержание, 6 разделов
Вот трасса одного провалившегося теста. Агенту в режиме планирования дают задачу: спроектировать кеш перед функцией getUser. В задании прямо сказано, что есть два варианта с настоящим компромиссом, кеш в памяти процесса, дешёвый, но несогласованный между инстансами, или Redis, согласованный, но с операционной зависимостью, и что больше ничего исследовать не нужно. Модель читает файл, смотрит каталог планов, пишет план на in-memory с «возможностью апгрейда до Redis» и вызывает plan_done. План аккуратный. Тест провален.
Провален он потому, что в задаче было решение, которое имел право принять только человек, а агент принял его сам. Ожидался вызов инструмента question с двумя вариантами и остановка. Вместо этого агент выбрал за пользователя и объявил работу готовой.
Последняя часть серии о том, как проверять такое: сделал ли агент то, что должен был, независимо от того, работает ли написанный им код. И почему это оказалось самым странным тестированием в моей практике.
Что измеряют бенчмарки и чего не измеряют
Стандартный способ оценивать кодовых агентов это SWE-bench: 2 294 реальных задачи из GitHub в двенадцати Python-репозиториях, успех определяется прохождением скрытых тестов. Когда его опубликовали в октябре 2023-го, лучшая модель решала 1,96% задач. Важно, что именно там измеряется. Система целиком: модель плюс обвязка, промпты, инструменты, цикл. Модель сама по себе там не измеряется. На лидерборде записи это пары «обвязка + модель», и одна и та же модель под разными обвязками даёт разные цифры. Вендоры публикуют результат как метрику модели, но и они фиксируют для этого какой-то минимальный скаффолд. Метрика при этом одна: задача решена или нет. Починил: зачёт, как именно, неважно.
Для меня как автора обвязки этого мало по двум причинам. Первая: когда система проваливает задачу, SWE-bench не говорит, кто подвёл, модель не поняла или обвязка её подставила. Вторая: он ничего не говорит о пути. Прочитал ли агент файл перед тем, как его править, или переписал из памяти. Запустил ли тест после починки, или объявил успех со слов. Спросил ли человека, когда решение было не его, или решил сам. Все три пути могут дать зелёный тест, а мне важен путь, потому что путь это и есть то, что я строю.
И второе, что бенчмарки результата скрывают, это нестабильность. Яо с соавторами в τ-bench показали это на агентах с инструментами: даже лучшие модели решали меньше половины задач, а если требовать, чтобы одна и та же задача решалась стабильно восемь раз подряд, метрика pass^8, успех падал ниже 25%. Агент, который проходит задачу в двух попытках из трёх, на одиночном прогоне даёт либо зелёный, либо красный, и ты не знаешь, какой из них тебе достался.
Контракт поведения
Поэтому у моего агента отдельный набор тестов, который я называю поведенческим. Сейчас в нём сорок два случая, каждый в своём файле, каждый проверяет один контракт. Тринадцать однотактных, где одного вызова инструмента хватает, чтобы всё решить: попросили прочитать файл, должен быть вызов read с ровно этим путём и никаких bash, write, edit. Двадцать девять многотактных, где проверяется порядок действий через несколько ходов: прочитал перед правкой, восстановился после ошибки, вошёл в режим планирования и вышел из него, запустил фоновую задачу и остановил её по идентификатору, делегировал субагенту.
Главное правило: доказательство только из наблюдаемого. Трасса инструментов и их аргументы, состояние файлов на диске, сигнал смены режима. Если задача просит агента сообщить факт, его ответ сверяется с независимо прочитанным состоянием. Прозы недостаточно никогда. Агент может написать «я запустил проверку, и она прошла», тест смотрит, был ли вызов bash с check.js после правки и что он вернул.
Пример контракта из середины набора. Фикстура из двух файлов: calc.js, где сумма реализована через вычитание, и check.js, который падает, если sum(2, 3) не равно пяти. Задание: проверка падает, найди причину, исправь, убедись. Тест требует: calc.js после работы содержит ровно a + b; оба файла были прочитаны, содержимое взято из них; после правки проверка была запущена и не сообщила об ошибке. Причём «не сообщила об ошибке» нельзя определить по коду выхода: node check.js; echo "exit=$?" завершается нулём, честно сообщая о провале в выводе. Пришлось искать вердикт в тексте результата.
Каждый случай помечен сигналами, и отчёт печатает их по отдельности: сколько прошло из скольких по «обоснованности аргументов», по «безопасности файловой системы», по «восстановлению после ошибки инструмента». Сводного балла нет намеренно. Число «35 из 38» ничего не говорит о том, где именно агент ошибается.
Три прогона или ничего
Из τ-bench я взял и правило про повторы. Каждый случай в режиме сертификации запускается три раза, в свежих сессиях, без общей истории. Зачёт только если прошли все три. Два из трёх это флаг нестабильности, который печатается отдельно и попадает в отчёт отдельным числом.
Насколько это важно, видно из собственных цифр. DeepSeek V4 Pro на одном и том же коммите, в двух сертификационных прогонах подряд, когда в наборе было ещё 38 случаев, дал 35 из 38 и 37 из 38, с шестью нестабильными случаями в первом. Один и тот же код, одна и та же модель, тот же день. Если бы я запускал по разу, в первый раз я бы «нашёл регрессию», а во второй «починил» её, ничего не меняя.
Отсюда рабочее правило: прежде чем назвать изменение регрессией или разрыв между моделями реальной находкой, три прогона. Разошлись: сначала трассы расходящихся попыток, выводы потом.
Модель или обвязка
Самое полезное, что дал этот набор, это способ отделять две причины провала, которые выглядят одинаково.
Агент вызвал не тот инструмент. Это модель не поняла задачу, или обвязка её подвела: описание инструмента невнятное, системный промпт склоняет не туда, протокол режима планирования сформулирован так, что его можно прочитать двояко? Симптом один, починки противоположные. Метод разделения простой. Держи обвязку неизменной и меняй модель: любая разница в поведении принадлежит модели, потому что всё остальное байт в байт то же. Держи модель и меняй одну строку в описании инструмента: разница принадлежит обвязке.
За два месяца этот метод нашёл больше дефектов обвязки, чем я ожидал, и ни один из них не был про модель.
Случай с починкой падающей проверки не проходил ни разу: модель читала код, правила его и только потом что-то запускала, так что «до» для сравнения не существовало. Оказалось, промпт про верификацию не говорил прямо, что прочитать код и воспроизвести баг это разные вещи. Сказал. И заодно обнаружил, что в самом тесте ключ toolsCalled был указан дважды, второй молча затирал первый, и требование bash пропадало. Тест был сломан так же, как и агент.
Инструмент write сообщал число строк как длину split("\n") и на содержимое READY\n отвечал «2 строки». Он говорил модели, что она записала пустую строку, которой не писала, и приглашал «починить» правильный файл. Заполненный tmpfs возвращал «Unknown system error -122» без имени: libuv не знает кода для превышения квоты, ошибка проходила мимо всех веток разбора и приходила модели без причины и без совета. Оба найдены только потому, что бенч гнал реальную модель по реальному диску.
Один случай требовал вызова write, а модель превращала PENDING в READY через edit, тот же результат лучшим инструментом, и проваливалась на имени. Тест проверял не то. Теперь проверяется содержимое файла.
И самое отрезвляющее. По сохранённым прогонам 31 провал лёг в нескольких сотнях миллисекунд от лимита времени случая: один случай шесть раз, другой пять, третий четыре. Попытки были незаконченными, без единой ошибки внутри, и чем медленнее модель, тем большую часть её оценки решал таймаут. Бенч, задуманный измерять поведение, измерял скорость. Лимит с 60 секунд поднят до 150. Случай, который заканчивается за 8, этого не заметил.
Цена зелёного
Есть у этих тестов свойство, которого не было ни у одних тестов до них: они стоят денег. Один сертификационный прогон это три с половиной миллиона токенов. На DeepSeek V4 Flash это семнадцать центов, на V4 Pro доллар семьдесят пять, на GPT-5.6 двадцать четыре цента. Не разорительно. Но npm test, у которого есть цена в долларах и который нельзя гонять на каждый коммит бездумно, меняет отношение. Каждый случай должен доказывать ровно один контракт и быть маленьким, а всё, что можно проверить детерминированно, терминальность plan_done, гейты записи, сохранность сессии при компакции, разбор рассуждений провайдера, живёт в обычных юнит-тестах и не тратит ни токена.
Была и попытка сделать больше: эксперименты с мутациями кода для оценки точности правок. Я их убрал. Они измеряли качество ответов модели, а набор существует для регрессий обвязки. Смешивать два вопроса в одном прогоне означало не ответить ни на один.
И ещё одно следствие, которое я не планировал. Таблица результатов по моделям это ответ на узкий вопрос: с этим системным промптом, этим набором инструментов, этим циклом и этим протоколом провайдера выбирает ли эта модель обоснованные вызовы, доводит ли многошаговые сценарии, уважает ли жизненный цикл режимов. Рейтингом интеллекта она не будет. Низкий балл это повод открыть трассу, вердикт модели из него не следует. Высокий и стабильный это основание поставить её в агент, и ничего больше.
Чем заканчивается серия
Девять частей назад я начал с трёх строк цикла и утверждения, что цикл это самая простая часть агента. Теперь понятно, где живёт сложность. В границах вокруг модели: в описаниях инструментов, которые читает недетерминированный собеседник; в сжатии контекста, которое уничтожает данные руками той же модели; в ролях, чья настоящая граница это список инструментов; в режиме планирования, который держится на парсере bash; в субагенте, который видит только задание; в памяти, которой нельзя верить ни при записи, ни при чтении; в демоне, который переживает своих клиентов и иногда самого себя.
Поведенческие тесты это способ держать все эти границы под наблюдением одновременно. Они не доказывают, что агент умён. Они доказывают, что при заданной модели агент прочитал перед правкой, проверил после починки, спросил, когда решение было не его, и остановился, когда его остановили. Это меньше, чем обещает слово «интеллект», и ровно столько, сколько мне нужно, чтобы отдать ему задачу и закрыть ноутбук.