Я дал четырем ИИ-ревьюерам 60 багов, про которые точно известно, что они баги
Про ИИ и код пишут много, и почти все эти тексты одного жанра: человек попробовал, ему понравилось или не понравилось, вот пара примеров. Чисел нет. Я честно искал статью, где кто-то взял бы набор дефектов с заранее известным ответом по каждому, прогнал через агента и посчитал. Не нашел.
Понятно почему. Такой набор взять неоткуда. Реальные баги из трекера давно починены и с большой вероятностью лежали в обучающей выборке. Синтетические баги пишет человек, а человек пишет такие баги, какие сам привык замечать, и меряет в итоге собственные слепые пятна.
Мне повезло, набор появился сам. Недели за две до этого я гонял мутационное тестирование по рабочему репозиторию и вручную разбирал выживших мутантов - механические правки кода, после которых ни один тест не упал. Каждого тогда пометил: дыра в тестах, эквивалент (поведение не поменялось, тесты правы) или спорно. Вышло 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 процентов багов” без интервала ни о чем не говорит, включая мою собственную, поэтому я и привожу три прогона.
Стенд простой: генератор кейсов, запускалка, парсер ответов, счетчик и судья, в сумме несколько сотен строк. Если у вас есть свои размеченные дефекты, повторить у себя - вечер работы. Хотелось бы, чтобы таких замеров было много и на разных стеках. Общих слов про ИИ-ревью уже хватает.
KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.