Про ИИ и код пишут много, и почти все эти тексты одного жанра: человек попробовал, ему понравилось или не понравилось, вот пара примеров. Чисел нет. Я честно искал статью, где кто-то взял бы набор дефектов с заранее известным ответом по каждому, прогнал через агента и посчитал. Не нашел.
Понятно почему. Такой набор взять неоткуда. Реальные баги из трекера давно починены и с большой вероятностью лежали в обучающей выборке. Синтетические баги пишет человек, а человек пишет такие баги, какие сам привык замечать, и меряет в итоге собственные слепые пятна.
Мне повезло, набор появился сам. Недели за две до этого я гонял мутационное тестирование по рабочему репозиторию и вручную разбирал выживших мутантов — механические правки кода, после которых ни один тест не упал. Каждого тогда пометил: дыра в тестах, эквивалент (поведение не поменялось, тесты правы) или спорно. Вышло 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 процентов багов” без интервала ни о чем не говорит, включая мою собственную, поэтому я и привожу три прогона.
Стенд простой: генератор кейсов, запускалка, парсер ответов, счетчик и судья, в сумме несколько сотен строк. Если у вас есть свои размеченные дефекты, повторить у себя — вечер работы. Хотелось бы, чтобы таких замеров было много и на разных стеках. Общих слов про ИИ-ревью уже хватает.
ссылка на оригинал статьи https://habr.com/ru/articles/1077470/