Про ИИ и код пишут много, и почти все эти тексты одного жанра: человек попробовал, ему понравилось или не понравилось, вот пара примеров. Чисел нет. Я честно искал статью, где кто-то взял бы набор дефектов с заранее известным ответом по каждому, прогнал через агента и посчитал. Не нашел.
Понятно почему. Такой набор взять неоткуда. Реальные баги из трекера давно починены и с большой вероятностью лежали в обучающей выборке. Синтетические баги пишет человек, а человек пишет такие баги, какие сам привык замечать, и меряет в итоге собственные слепые пятна.
Мне повезло, набор появился сам. Недели за две до этого я гонял мутационное тестирование по рабочему репозиторию и вручную разбирал выживших мутантов - механические правки кода, после которых ни один тест не упал. Каждого тогда пометил: дыра в тестах, эквивалент (поведение не поменялось, тесты правы) или спорно. Вышло 60 размеченных случаев: 51 дыра, 8 эквивалентов, 1 спорный.
Это не идеальный эталон, но у него есть свойство, которого у публичных бенчмарков обычно нет. Разметку делал человек, знающий код, и делал ее до того, как вообще возникла мысль мерять на ней модели. Подгонять было не под что.
Что меряем
Стенд получился на 152 кейса. Движков четыре: codex, Claude Sonnet, Claude Haiku и локальная qwen 27B через ollama.
Режима два. В легком (называю его diff) агенту показывают однострочный дифф и файл целиком. То есть строка, которая изменилась, подсвечена, остается сказать, плохо это или нормально. В боевом (blind) агент видит только файл. Без подсказок, ищи сам.
Разница между этими двумя цифрами и была основным вопросом.
Дальше контроль, иначе замер ничего не стоит. Ревьюер, который на каждый файл выдает десять замечаний, найдет все дыры и будет выглядеть гением. Поэтому в стенд добавлены:
12 подсадных правок, которые физически не могут изменить поведение: одинарные кавычки заменены на двойные, лишние скобки вокруг условия, локальная const превращена в let. Находка на такой строке засчитывается как ложная тревога.
20 нетронутых продовых файлов. Любая находка тут - шум.
Агент работает в пустой директории с выключенными инструментами. Репозитория у него нет, тесты найти нельзя, на входе только текст файла. Иначе я бы мерял не умение читать код, а умение искать тесты.
Метрики две. Жесткая - попал ли агент в нужную строку с допуском плюс-минус две. Мягкая - правильно ли объяснил последствие. Вторую считает отдельная модель-судья, и насколько ей можно верить, разберу ниже, там есть что рассказать.
Результат
Движок |
Режим |
Дыры |
Верно объяснено |
Шум на чистых файлах |
Секунд на кейс |
|---|---|---|---|---|---|
codex |
diff |
98.0% |
94.1% |
- |
27 |
sonnet |
diff |
96.1% |
94.1% |
- |
12 |
haiku |
diff |
94.1% |
92.2% |
- |
8 |
qwen 27B |
diff |
86.3% |
86.3% |
- |
151 |
codex |
blind |
78.4% |
74.5% |
10/20 |
39 |
sonnet |
blind |
74.5% |
74.5% |
14/20 |
22 |
haiku |
blind |
74.5% |
74.5% |
12/20 |
45 |
qwen 27B |
blind |
68.6% |
66.7% |
5/20 |
638 |
Подсказка стоит примерно 20 процентных пунктов. Та же модель, тот же дефект, тот же промпт, меняется только одно: показали строку или нет. У codex 98 против 78, у Claude 96 против 74.
Почему я на этом останавливаюсь. Практически все, что пишут про ревью агентом, измерено в легком режиме, просто об этом не говорят. Агент в CI смотрит дифф пулл-реквеста - строки ему уже показали. Когда кто-то рассказывает, что “ИИ отлично ловит баги”, он почти наверняка описывает верхнюю половину таблицы. А задача “вот незнакомый файл, скажи, что в нем не так” - это нижняя половина, и там все заметно хуже.
На подсадных правках все четверо почти чисты: из 48 попыток (12 правок на 4 движка) ложная тревога случилась один раз. Форматирование от логики модели отличают надежно. Отличать “ломает” от “не ломает” - другая задача, и с ней сложнее.
Локальная модель выигрывает по шуму
Берем слепой режим и считаем не только попадания, но и все, что агент сказал мимо: находки на чистых файлах, находки не на той строке, находки на эквивалентных мутантах.
Движок |
Нашел дыр |
Ложных находок |
Сигнал к шуму |
|---|---|---|---|
qwen 27B локально |
35 |
29 |
1.21 |
sonnet |
38 |
44 |
0.86 |
haiku |
38 |
53 |
0.72 |
codex |
40 |
60 |
0.67 |
Бесплатная модель на домашней машине находит меньше всех, но по отношению полезного к мусору обходит все облачные. Она же единственная, у кого в легком режиме нет ни одного случая “попал в правильную строку, но соврал про причину”: 44 попадания из 44 судья засчитал. У codex, самого многословного, таких случаев больше всего.
Мое объяснение бытовое: qwen пишет коротко и не домысливает. Фронтирные модели умеют строить длинную правдоподобную историю про последствия. Когда история верная - отлично. Когда нет - вы получаете уверенный абзац про несуществующий баг, который кому-то придется читать и опровергать. Ревьюер, который в половине случаев молчит, обходится человеку дешевле, чем ревьюер, выдающий на каждый файл пару складных выдумок.
Платить за это приходится временем, и много: 638 секунд на файл против 39 у codex. Десяток файлов - полчаса. Для ночного прогона по репозиторию годится, для комментария в пулл-реквесте, который должен появиться через минуту, нет.
Весь замер целиком занял без малого сутки, и 72% этого времени ушло на локальную модель. Часть вины на мне: я параллельно пытался работать на том же ноутбуке, а память была почти целиком занята моделью, так что и она, и я тормозили. На выделенной машине было бы быстрее, но порядок цифр вряд ли поменялся бы.
Насколько этим цифрам можно верить
Самое полезное в замере оказалось не в том, кто победил. Полезнее было понять, сколько во всей этой конструкции шума. Мерял на трех уровнях.
Ревьюер сам с собой. Три полных прогона слепого режима, промпт идентичный, температура ноль. Дыры у sonnet: 38, 40, 38 из 51. У haiku: 38, 40, 40. Итоговое число почти не двигается, а вот по конкретным кейсам совпадение всего 88-90 процентов. Из 51 дыры модель стабильно находит 36, стабильно не видит 8-10, и еще 5-7 плавают от прогона к прогону.
Получается, у агента нет устойчивого мнения о конкретном месте в коде. Он ловит примерно постоянную долю дефектов, но каждый раз немного другую.
Из этого следует неудобная для практики вещь. Первая мысль - прогнать три раза и взять объединение. Работает, но не бесплатно: объединение трех прогонов поднимает находки с 38 до 41-43 из 51, и тем же движением поднимает шум на нетронутых файлах с 8 из 20 до 16 из 20. Пересечение трех прогонов шум не чистит совсем (те же 8 из 20) и при этом теряет дыры. Повторный прогон - это не бесплатная полнота, цена у него симметричная.
Судья сам с собой. Тот же промпт, тот же набор из 50 оценок, второй прогон: совпадение 96 процентов.
Судья с другим судьей. Sonnet против Opus на одной выборке: 85 процентов. Все расхождения в одну сторону.
Судья со мной. Взял 24 оценки, разложенные по всем комбинациям, и разобрал руками. Согласился с судьей в 19 случаях из 24. Из пяти расхождений четыре односторонние: судья строже меня и придирается к формулировке там, где ревьюер по сути прав.
Разница между 96 и 85 процентами говорит важное: основной шум в модели-судье - не случайность прогона, а вкус конкретной модели. Поменять судью на другого дороже, чем прогнать того же дважды.
И еще одна находка, после которой я на какое-то время остановился. В проверочном листе две строки оказались одним и тем же мутантом с почти дословно одинаковым ответом ревьюера, отличалась только обертка. Судья поставил одному correct, другому wrong. Он непоследователен не только между прогонами, но и внутри одного.
Поэтому в таблице выше первая цифра (попал в строку) - основной результат, а вторая (правильно объяснил) идет с оговоркой плюс-минус 4 процентных пункта и систематическим смещением в строгую сторону.
Две ловушки стенда
Обе не про модели. Обе выглядели как результат, и я был в шаге от того, чтобы так их и описать.
Первая - дрейф исходников. Номера строк я взял из прогона мутационного тестирования двухнедельной давности, а файлы читал из текущей ветки. За две недели четыре файла из выборки успели поменяться. Пять мутантов из 60 вклеились не туда, один превратился в синтаксический мусор, и агенты добросовестно сообщали про ошибку компиляции, которой в исходном мутанте не было.
Заметил случайно, когда полез смотреть, почему судья поставил wrong в пяти местах. Лечится закрепленным worktree на нужный коммит и сверкой куска кода с рабочим листом разметки. Не полез бы - в статье стоял бы красивый и неверный абзац про то, как модели путаются в типах.
Вторая - таймауты клиента, а не модели. У локальной модели стабильно падали кейсы, причем с нарастанием: сначала 4 из 72, потом 26 из 31. Напрашивался вывод, что 27B не тянет большие файлы. На деле все отказы приходили ровно на 301 секунде. Это дефолтный таймаут HTTP-клиента в Node, и их там два: на заголовки и на паузу между кусками тела. Локальная модель в слепом режиме на самом деле думает до 13 минут на файл. Переписал на голый node:http без таймаутов, все кейсы прошли.
Вывод из обеих историй один. В замере ИИ подавляющее большинство странных результатов - это ваш стенд. Прежде чем писать “модель не справилась”, проверьте, справился ли ваш код.
Чего этот замер не говорит
Один репозиторий, один стек: TypeScript, монорепа, Node. На другой язык не переносится.
Мутанты - не настоящие баги. Это механические поломки, и распределены они не так, как ошибки живого разработчика. Настоящий баг чаще размазан по трем файлам, а не сидит в одной строке.
Метка “эквивалент” у меня означает “тесты не заметят”, а не “последствий нет”. Классический пример - пустой блок catch:
--- a/libs/core/calllogs/src/lib/internal/telephony.service.ts +++ b/libs/core/calllogs/src/lib/internal/telephony.service.ts @@ -57,10 +57,7 @@ } this.logger.error(`Invalid response placing call: status=${response.status}`) return undefined - } catch (error) { - this.logger.error('Error placing call', error instanceof Error ? error.stack : String(error)) - return undefined - } + } catch (error) {}
Функция и раньше возвращала undefined, поведение не изменилось, но пропала строчка лога. Агенты это флагали, я формально засчитывал ложную тревогу, а по-хорошему они были правы. Часть их “шума” ложная только относительно моей же разметки.
Легкий режим дает агенту фору, которой в жизни почти не бывает.
Что я из этого вынес
Если выбираете инструмент ревью по чужим отзывам, спрашивайте, в каком режиме человек его пробовал. Разница между “показали строку” и “не показали” больше, чем разница между лучшей и худшей моделью в моем замере.
Полнота и шум - разные оси. Самая слабая по находкам модель оказалась самой полезной по соотношению. Если ревью читает живой человек, ложная тревога стоит дорого, и оптимум уезжает совсем не туда, куда его тащат маркетинговые бенчмарки.
Одиночный прогон агента не воспроизводится по конкретным местам. Цифра вида “агент нашел N процентов багов” без интервала ни о чем не говорит, включая мою собственную, поэтому я и привожу три прогона.
Стенд простой: генератор кейсов, запускалка, парсер ответов, счетчик и судья, в сумме несколько сотен строк. Если у вас есть свои размеченные дефекты, повторить у себя - вечер работы. Хотелось бы, чтобы таких замеров было много и на разных стеках. Общих слов про ИИ-ревью уже хватает.
Комментарии (8)

Innesiya
02.09.2026 07:18Про 301 секунду — это дефолтные таймауты Node на заголовки и на паузу между чанками, и они дают ровно такую картину: отказы копятся, а выглядит как «27B не тянет большие файлы». Отдельно зацепилась за нестабильность: 36 стабильных попаданий из 51 и 5-7 плавающих — это портрет одного движка. А объединение по разным движкам считали, не по прогонам одного? Если sonnet и qwen ловят непересекающиеся подмножества дыр, две дешевые модели по одному прогону могут дать соотношение сигнал/шум лучше, чем три прогона одной.

k41n Автор
02.09.2026 07:18Считал только по прогонам одного движка, вы правы, что это разные вопросы. Пошел посчитал по вашей схеме (типа sonnet+qwen), благо сырые ответы все лежат. Слепой режим, по одному прогону на движок:
комбинация дыр из 51 ложных сигнал/шум qwen один 35 29 1.21 sonnet один 38 44 0.86 qwen + sonnet 38 73 0.52 qwen + haiku 42 82 0.51 haiku + codex 44 113 0.39 sonnet, три прогона 41 140 0.29TL;DR: объединение двух движков действительно выгоднее трех прогонов одного. Любая пара при сопоставимой полноте дает 0.39-0.52 против 0.29 у sonnet на трех прогонах. Это ожидаемо: у повторного прогона шум почти весь новый, а находки почти все те же самые, так что вы доплачиваете за одно и то же.
Но! ни одна комбинация не бьет одиночный движок. qwen сам по себе дает 1.21, и это остается лучшим числом в таблице. Полнота по этой метрике всегда покупается дороже, чем стоит.
И отдельно про вашу пару, потому что там получилось прикольно. У sonnet и qwen подмножества не непересекающиеся, а вложенные: из 35 дыр, которые нашел qwen, ровно ноль таких, которых не нашел sonnet. Вааще ни одной. Поэтому пара qwen + sonnet дает те же 38 дыр, что и sonnet в одиночку, и ровно ничего, кроме удвоенного шума. Непересекающееся есть в других парах: у haiku с qwen 4 и 7 своих, у haiku с codex 4 и 6, там объединение хотя бы что-то приносит.
Если считать ложные находки без дублей (одна и та же строка, помеченная обоими движками, читается человеком один раз), числа подрастают, но порядок тот же: 0.62 у лучшей пары против 0.44 у трех прогонов sonnet.
Оговорка та же, что и везде: это по одному прогону на движок, а прогон не воспроизводится.

artalex
02.09.2026 07:18Так а какие именно модели-то использовались?

k41n Автор
02.09.2026 07:18claude-sonnet-5 и claude-haiku-4-5-20251001, через Claude Code CLI 2.1.258: claude -p --model sonnet / --model haiku, инструменты сняты --disallowedTools, работа в пустой временной директории.
gpt-5.6-sol, через codex-cli 0.152.0: codex exec --json --sandbox read-only.
qwen 27B локально: ollama 0.33.1, тег qwen3.8:27b (digest 22130167c4c2, архитектура qwen35, 27.3B, квантизация Q4_K_M), temperature 0, num_ctx 16384. Модель умеет 262k контекста, но в прогоне стоял именно 16k, и это часть объяснения, почему на больших файлах она проседала.
Судьёй во второй, мягкой метрике был claude-sonnet-5, и те же ответы отдельно прогонялись через gpt-5.6-sol и claude-opus-5, чтобы посмотреть, насколько судьи расходятся между собой. Расхождение есть, про него в статье отдельный раздел.
На самом деле, версии не прибиты гвоздями, я просто посмотрел что сейчас, но через месяц тот же sonnet может молча уехать на 5.1, например.
netricks
Не вполне понятна методика. Вопрос, который был задан агенту так и звучал, мол найди все баги, составь список, поясни, почему это баг?
k41n Автор
Нет, формулировка была поуже. Вот промпт целиком, как он уходил в слепом режиме (в легком сверху добавляется блок с диффом, остальное слово в слово):
Стиль, нейминг и отсутствие тестов запрещены прямым текстом, иначе замер шума превращается в фикцию: на любом продовом файле можно набрать десяток замечаний про нейминг и формально быть правым.
Строчка про пустой список тоже не для красоты. Без нее модель считает, что раз ее позвали, то найти обязана, и двадцать нетронутых файлов дают двадцать находок. С ней codex выдал 10 из 20, qwen 5 из 20.
Ответ строго JSON с номером строки, поэтому основную метрику (попал в строку плюс-минус две) считает скрипт, а не я. Трактовка начинается только во второй, мягкой метрике, и про то, насколько там можно верить судье, в статье отдельный раздел.
Ну и температура ноль, инструменты выключены, работа в пустой директории: ни репозитория, ни тестов агент достать не может.