Про ИИ и код пишут много, и почти все эти тексты одного жанра: человек попробовал, ему понравилось или не понравилось, вот пара примеров. Чисел нет. Я честно искал статью, где кто-то взял бы набор дефектов с заранее известным ответом по каждому, прогнал через агента и посчитал. Не нашел.

Понятно почему. Такой набор взять неоткуда. Реальные баги из трекера давно починены и с большой вероятностью лежали в обучающей выборке. Синтетические баги пишет человек, а человек пишет такие баги, какие сам привык замечать, и меряет в итоге собственные слепые пятна.

Мне повезло, набор появился сам. Недели за две до этого я гонял мутационное тестирование по рабочему репозиторию и вручную разбирал выживших мутантов - механические правки кода, после которых ни один тест не упал. Каждого тогда пометил: дыра в тестах, эквивалент (поведение не поменялось, тесты правы) или спорно. Вышло 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 процентов багов” без интервала ни о чем не говорит, включая мою собственную, поэтому я и привожу три прогона.

Стенд простой: генератор кейсов, запускалка, парсер ответов, счетчик и судья, в сумме несколько сотен строк. Если у вас есть свои размеченные дефекты, повторить у себя - вечер работы. Хотелось бы, чтобы таких замеров было много и на разных стеках. Общих слов про ИИ-ревью уже хватает.