Назад к блогу

Бенчмарки для ИИ-ревью кода: как измерять качество автоматических проверок

Бенчмарки для ИИ-ревью кода: как измерять качество автоматических проверок

Создать бенчмарк для ИИ-ревью кода сложнее, чем кажется: нужно собрать эталонные находки, отделить реальные проблемы от правдоподобных и научиться честно сравнивать агентов, которые замечают разное. В статье разбирается устройство ReviewBench — от формирования корпуса и ground truth до метрик и лидерборда.

Автоматическое ревью кода легко объявить работающим — достаточно показать несколько полезных замечаний. Гораздо труднее построить измерение, которое не обманывает: где взять эталон, как отличить настоящую находку от правдоподобной, как сравнивать агентов, которые находят разное. Разбираем механику ReviewBench — от сборки эталонного набора до метрик и лидерборда.

Корпус и оси выравнивания

Корпус собран так, чтобы распределение по размеру PR было намеренно смещено в сторону средних и крупных изменений. Цель — уменьшить перепредставленность крошечных однострочных правок и сохранить более содержательные многопользовательские PR, где качество ревью особенно важно. При этом языковое распределение и распределение по размеру репозиториев напрямую повторяют GitHub, а размер PR смещён к средней и хвостовой части.

Полный набор содержит 219 PR из 187 различных репозиториев, и ни один репозиторий не доминирует: самый представленный даёт 10 PR, или лишь 4.6% корпуса. Доли по числу добавленных и удалённых строк в полном наборе: 50 или меньше — 17 PR (7.8%), 51–200 — 40 PR (18.3%), 201–500 — 44 PR (20.1%), 501–1,000 — 40 PR (18.3%), более 1,000 — 78 PR (35.6%).

Тестовая выборка из 25 задач отобрана как репрезентативная выборка полного набора: она сохраняет сочетание основных языков, размеров изменений и разнообразия репозиториев. Она состоит из 25 репозиториев. По языкам: 5 PR на TypeScript (20.0% против 31.1% в полном наборе), 4 на Python (16.0% против 18.7%), 3 на C# (12.0% против 11.4%), 3 на Go (12.0% против 8.7%), 1 на JavaScript (4.0% против 6.8%) и 9 на прочие языки (36.0% против 23.3%) — это Rust, Java, Jupyter Notebook, Kotlin, PHP, Ruby, Shell и Swift. По размеру изменений: 3 PR с 50 или менее строками (12.0% против 7.8%), 5 PR с 51–200 строками (20.0% против 18.3%), 5 PR с 201–500 строками (20.0% против 20.1%), 5 PR с 501–1,000 строками (20.0% против 18.3%) и 7 PR с более чем 1,000 строками (28.0% против 35.6%).

Находки golden set распределены по трём уровням серьёзности: High — 36 находок (10.2%), Medium — 134 (38.1%), Low — 182 (51.7%), всего 352 находки. По категориям наибольший вклад даёт Correctness (133 находки), затем Reliability (58) и Maintainability (44); далее Testing (35), Security (27), Documentation (21), Performance (12), API architecture (12) и Accessibility (10).

Как формируется ground truth

Кандидатные находки собираются из четырёх независимых источников: от реальных людей-ревьюеров, из проблем, выведенных по последующим коммитам автора, от детерминированных инструментов анализа и от нескольких передовых LLM разных семейств моделей. Источник находки не определяет её правильность. Все источники затем семантически дедуплицируются.

Комментарии людей-ревьюеров

Сначала все комментарии ревью извлекаются через GitHub API, затем фильтруются только написанные людьми — боты исключаются. Для каждого комментария извлекаются путь к файлу, диапазон строк, сообщение, автор, URL и время создания. Каждый комментарий верхнего уровня становится одной находкой, а ответы прикрепляются как контекст; ответ в ветке отбрасывается, если он не поднимает отдельную проблему. Комментарии уровня PR (не привязанные к конкретной строке) записываются как метаданные, но не извлекаются как находки, так как им нельзя назначить путь к файлу и диапазон строк.

Выбранный head-коммит

На каждый PR в манифесте фиксируется один head-коммит — коммит с наибольшим числом ревью-комментариев, представляющий состояние кода, привлёкшее наибольшее внимание ревьюеров. Вместе с base он задаёт review-time snapshot — состояние кода на момент, когда давалась ревью-обратная связь, до последующих фиксирующих коммитов. harness не должен показывать агентам коммиты после head.

Комментарии, привязанные к другим коммитам, не отбрасываются автоматически: они включаются, если описываемая ими проблема всё ещё присутствует на выбранном head. Приведение выполняется механически — для каждого комментария не на выбранном head вычисляется git diff между commit_id комментария и head манифеста для затронутого файла:

  • если файла на выбранном head нет — комментарий отбрасывается;
  • если строковый регион комментария вне diff (не изменён) — номера строк пересчитываются механически и комментарий включается;
  • если регион изменён — комментарий включается с best-effort пересчётом строк и помечается alignment: "code_changed".

Для помеченных code_changed классификатор определяет, сохранилась ли проблема: если код исправлен — метка FP, если проблема осталась — TP. Так выравнивание сохраняет комментарии из всей истории ревью, а вопрос валидности решает классификатор, а не отдельный шаг LLM-валидации при извлечении.

Вывод находок из follow-up коммитов

Сначала определяют, какие коммиты считаются follow-up: берут полную историю PR, находят коммит-голову на момент ревью из манифеста, и все коммиты после него — кандидаты. Если PR был слит, это коммиты между головой ревью и merge-коммитом; если закрыт или заброшен — все последующие коммиты в ветке PR. Затем оставляют только коммиты автора PR — коммиты других участников исключаются, так как представляют другой сигнал.

Для каждого оставшегося коммита вычисляют его собственный diff (не накопительный против базы), определяют изменённые файлы и диапазоны строк и проверяют, пересекаются ли эти регионы с diff'ом времени ревью. Кандидатами являются только изменения кода, входившего в ревьюируемое изменение; изменения в новых файлах или ранее не тронутых регионах — нет, это новая работа, а не исправления проверенного кода.

Для каждого коммита, меняющего проверенный код, LLM получает diff времени ревью по затронутым файлам (ограниченный релевантными хунками), diff follow-up коммита по тому же региону и человеческие комментарии ревью, привязанные к затронутым или близким строкам, если они есть. LLM должна определить, является ли изменение исправлением проблемы в проверенном коде или чем-то иным — новой функциональностью, стилистической чисткой, несвязанным рефакторингом; если не исправление, находка не выдаётся. Если это исправление, LLM описывает проблему, существовавшую в коде на момент ревью, так чтобы описание было понятно без просмотра исправления, и назначает путь к файлу и диапазон строк, привязанные к diff'у времени ревью, а не к строкам follow-up коммита.

Перед попаданием в корпус применяются три фильтра. Confidence gate: LLM просят оценить уверенность, что изменение действительно исправляет проблему, а не расширяет, рефакторит или полирует код; низкоуверенные выводы отбрасываются. Scope check: выведенная проблема должна находиться в коде, входившем в review-time diff; проблемы в окружающем контексте, которые автор попутно исправил, вне области вывода. Triviality filter: чисто пробельные, форматирующие изменения или изменения порядка импортов не выводятся как находки, даже если технически модифицируют проверенный код.

Детерминированные инструменты

На diff времени ревью запускаются линтеры, проверяющие типы и статические анализаторы — чтобы находить проблемы с высокой механической точностью. Кросс-языковые инструменты: Semgrep с наборами правил p/security-audit и p/bugs для безопасности и шаблонов ошибок, и CodeQL для глубокого анализа потоков данных, отслеживания заражения и безопасности. Для TypeScript и JavaScript: компилятор TypeScript (tsc --noEmit) даёт ошибки типов и недостижимый код, а ESLint ограничивается правилами поиска ошибок, включая категории possible-errors и possible-problems. Для Go: go vet находит несоответствия формата printf, недостижимый код и нарушения копирования блокировок; Staticcheck — стандартный статический анализатор для существенных проблем: поиска ошибок, устаревания, производительности; govulncheck — известные уязвимости в зависимостях. Инструменты, нацеленные в основном на стиль и форматирование, исключаются или ограничиваются, а при наличии существенных и стилистических правил инструмент настраивается только на существенное подмножество.

Стратегия разнообразия LLM-агентов

Стратегия строится по трём осям: модели, промпты и контекст. По моделям требуется использовать минимум 2–3 разные базовые модели, например Claude, GPT, Gemini, поскольку у каждого семейства свои паттерны внимания и слепые зоны, и находка, независимо всплывшая у двух разных семейств, считается более сильным свидетельством реальной проблемы. По промптам вместо одного широкого «review this PR» запускаются целевые проходы, каждый из которых акцентирует свою область: корректность и логические ошибки; обработка ошибок, граничные случаи и режимы отказа; безопасность и валидация ввода; конкурентность и управление состоянием; дизайн API и интерфейсные вопросы. По контексту проходы выполняются с разными уровнями: только diff; diff плюс окружающий контекст файла; diff плюс связанные файлы (вызывающие, вызываемые, тесты).

Для каждой комбинации (model, prompt, context) собирается вход из review-time diff, требуемого контекста, заголовка и тела PR, вызывается модель с целевым промптом, а вывод парсится в отдельные находки с путём файла, диапазоном строк и сообщением; каждая находка записывает, какая модель, вариант промпта и уровень контекста её породили. Цель — не сгенерировать больше находок за проход, а исследовать области пространства находок, которые один общий проход недоисследует.

Объём находок контролируется дедупликацией при построении golden set (находки из разных проходов, описывающие одну и ту же проблему, сворачиваются), классификацией одним классификатором Claude Sonnet 5, который помечает каждую находку TP/FP и назначает severity, категорию и прочие вспомогательные метки, причём низкокачественные или избыточные находки фильтруются метками, а не ограничением вывода производителей, и per-producer ablation, позволяющим измерить вклад каждой комбинации (model, prompt) и отбросить проходы с малой добавочной ценностью.

Смещение golden set в сторону моделей-производителей отслеживается по распределению производителей: для каждого PR фиксируется, какая доля golden TP пришла от каждого типа производителя (human, inferred, LLM, deterministic) и внутри LLM — от каждой модели. Если один LLM-производитель даёт больше целевого порога golden TP после дедупликации, это сигнал добавить более разнообразных производителей или понизить вес уникальных находок этого производителя. Определяющий тест — leave-one-out ablation: если удаление одного LLM-производителя существенно меняет recall кандидатного агента, значит golden set чрезмерно опирается на этого производителя.

Нормализация находок

Перед разметкой все находки независимо от производителя приводятся к общей схеме: многострочные сообщения сворачиваются в односоставные находки там, где это уместно, пути к файлам канонизируются относительно корня репозитория, а местоположения находок сохраняются. Находки не отбрасываются и не обрезаются только из-за того, что они частично или полностью выходят за пределы PR-диффа. Каждая находка записывает тип производителя producer и блок source со сведениями о происхождении; блок различается по типу производителя и обеспечивает прослеживаемость к исходному комментарию ревью или коммиту. Например, для детерминированного инструмента source содержит type, tool и rule_id.

Разметка и golden set

Рабочая группа старших инженеров итеративно составила руководство, которое определяет TP и FP качественно и через точные критерии. Находка считается TP, если она одновременно true, relevant и within scope of the review; иначе — FP. Руководство также перечисляет типичные режимы отказа FP: неверные утверждения, непроверяемые утверждения, наблюдения вне области, дубликаты, стилевые придирки ниже порога и советы, ухудшающие изменение.

Классификатор на основе Claude Sonnet 5 операционализирует эти правила: для каждой находки он получает саму находку и контекст PR (diff, окружающий код, заголовок и тело) и выдаёт решение TP/FP вместе с severity (уровнем серьёзности), category (категорией проблемы), scope (областью уместности наблюдения), difficulty (сложностью обнаружения), context required (объёмом контекста, нужным для суждения) и другими вспомогательными метками. Тот же классификатор используется для разметки корпуса и для несопоставленных кандидатных находок при оценке. Классификатор был hill-climbed на наборе разработки из человеческих находок, независимо размеченных старшими инженерами по TP/FP, severity, category, scope, difficulty, context required и другим измерениям; итерации по промпту и порогам решения максимизировали согласие с человеческими метками, при этом изменения промпта оценивались на примерах, не использованных для обоснования этой итерации.

Изначально ground truth корпус был размечен классификатором на базе Claude Sonnet 4.6, который присваивал TP/FP, severity, category, scope, difficulty, context required и другие вспомогательные атрибуты. Поскольку Sonnet 4.6 был позже признан устаревшим (deprecated), классификатор был обновлён до Claude Sonnet 5. Вывод классификатора не принимался как ground truth без проверки: люди провели аудит меток, исследовали расхождения с кодом и контекстом PR и исправили корпус, включая ручное исправление 47 находок, которые классификатор ошибочно пометил как TP. Согласие по TP/FP составило 96.6%, и каждое расхождение было проверено вручную; в некоторых случаях человеческая метка была неверной, например, когда сообщённая ошибка уже предотвращалась существующей защитой.

Golden set консервативен: люди оценивали серьёзность проблемы примерно в девять раз чаще выше, чем ниже, чем классификатор. Перед финализацией находки от разных производителей, относящиеся к одной и той же проблеме, дедуплицируются, чтобы не завышать знаменатель recall.

Метрики: grounded, augmented и Fβ

Grounded и augmented

grounded-метрики считают только находки, размеченные при построении корпуса: несовпавшие кандидаты исключаются из числителя и знаменателя. Поэтому grounded precision считает долю совпавших кандидатов, чья golden-находка помечена TP, а grounded recall — долю известных TP, которые агент нашёл.

Augmented-метрики дополнительно засчитывают кандидатов, не совпавших с golden-набором, но классифицированных как TP, и штрафуют классифицированных FP:

Augmented precision = (|M_TP| + |U_TP|) / |C|
Augmented recall = (|{g ∈ G_TP : ∃ c ∈ C raw-matched to g}| + |U_TP|) / (|G_TP| + |U_TP|)

Здесь M_TP — совпавшие кандидаты с TP-меткой, U_TP — несопоставленные кандидаты, классифицированные как TP, C — все находки кандидата, G_TP — известные TP. Augmented precision отвечает на вопрос, какая доля всех произведённых агентом находок полезна, а augmented recall дополняет золотые TP новообнаруженными TP самого агента.

Ключевое следствие: знаменатель augmented recall |G_TP| + |U_TP| зависит от агента, потому что его собственные новые TP расширяют знаменатель. Агент с многими одобренными классификатором новыми находками может получить высокий augmented recall даже при плохом grounded recall, поскольку |U_TP| входит и в числитель, и в знаменатель. Поэтому augmented recall не следует использовать как единственную основу для ранжирования агентов: grounded recall — сопоставимая метрика, а augmented recall и число новых TP дают дополнительный сигнал.

Сопоставление находок

Сопоставление семантическое: две находки совпадают, если описывают одну и ту же глубинную проблему, даже если их сообщения, диапазоны строк или формулировки различаются. Сначала находки группируются по пути файла, и внутри файла каждая находка кандидата сравнивается с каждой находкой golden set; находки из разных файлов никогда не сравниваются. Матчер на основе LLM получает для каждой пары путь файла, диапазон строк и сообщение и возвращает список соответствий: для каждой находки кандидата — возможно пустое множество индексов совпавших находок golden set.

Матчер порождает many-to-many соответствия: одна находка кандидата может совпасть с несколькими находками golden set, и несколько находок кандидата могут совпасть с одной находкой golden set. Для recall находка golden set считается покрытой, если хотя бы одна находка кандидата совпадает с ней в сырых соответствиях, и несколько кандидатов, совпавших с одной находкой golden set, не завышают recall — она считается один раз. Для precision кандидаты обрабатываются в порядке кандидатов: каждый кандидат заявляет не более одной находки golden set — первое незанятое совпадение в порядке golden set, наследует её метку и считается совпавшим. Кандидаты без незанятого совпадения считаются несопоставленными и проходят на независимую классификацию.

Классификация несопоставленных находок

Несопоставленные находки — это находки, у которых нет сырого совпадения с золотым набором, либо все их совпадения уже были заявлены для точности. Они классифицируются тем же классификатором Claude Sonnet 5, что и при разметке корпуса, что даёт решение TP/FP и те же вспомогательные метки. Формально множество несопоставленных находок определяется как U = C \ M, где C — все находки кандидата, а M — те, что заявили золотую находку для точности; внутри U выделяются U_TP (классифицированные как TP) и U_FP = U \ U_TP.

Агрегация по PR и длительность

Метрики по PR агрегируются двумя способами: macro-усреднение — среднее по PR, где каждый PR учитывается одинаково, а micro-усреднение — объединение счётчиков по всем PR до вычисления отношения. Оба способа приводятся, потому что отвечают на разные вопросы: macro отражает поведение на типичном PR, а micro — на типичной находке.

Длительность ревью измеряется и сообщается рядом с метриками качества, но не включается в расчёты precision, recall или Fβ. Для контейнерных оценок латентность каждого PR — это прошедшее реальное время вокруг вызова контейнера агентом в успешной попытке, записанное в миллисекундах как review_ms; оно исключает более ранние неудачные попытки, накладные расходы на повтор, подготовку репозитория, а также последующее сопоставление, классификацию и оценку. Для каждого раунда длительность — среднее арифметическое валидных измерений латентности по успешным PR, а лидерборд сообщает среднее арифметическое этих средних по раундам и их выборочное стандартное отклонение по трём раундам, причём каждый раунд взвешивается одинаково, а не объединяются все измерения PR по раундам. При отсутствии валидного review_ms конвейер отчётности откатывается к duration_seconds, сконвертированным в миллисекунды; это устаревшее значение имеет точность до целых секунд и включает повторы. Пропущенные измерения исключаются, а не считаются нулём; раунд без измерений не имеет значения длительности, а стандартное отклонение недоступно, когда доступно меньше двух средних по раундам.

Прогон участника и лидерборд

Тестовый прогон запускается на сайте и прогоняет тестовый набор из 25 pull request'ов (запросов на слияние), выдавая результат по каждому PR, чтобы участник мог исправить найденные проблемы; в лидерборд он не попадает. Финальный прогон выполняется на полном наборе из 219 PR в три раунда со свежим измерением, причём участник выбирает конфигурацию, а не конкретный прогон. Итоговый балл — среднее арифметическое баллов трёх прогонов, а сами три балла сохраняются для аудита и проверки разброса между прогонами.

Публикация строки в лидерборде возможна только через самостоятельный портал ReviewBench; локальные прогоны и локально сгенерированные метрики предназначены только для разработки и не могут быть опубликованы как результат лидерборда. При регистрации участник входит через GitHub и заполняет форму, указывая отображаемое имя, digest образа, метки конфигурации, URL API модели и имена нужных секретов; портал создаёт pull request с манифестом, а мейнтейнер проверяет и сливает его — именно этот merge является одобрением. Только финальный прогон, запущенный через этот self-service поток, может быть опубликован. Опубликованные баллы появляются в лидерборде, только если они превосходят текущий балл агента в лидерборде или если это первая запись агента.

Digest образа используется как точная ссылка на образ, при этом нельзя указывать тег или образ вне ghcr.io. Хост URL API модели разрешается автоматически, другие хосты нужно объявлять, иначе доступ блокируется: сеть блокируется для всего вне хостов egress из манифеста, и запуск сообщает о каждом отказанном хосте. Токен участника GHCR_PULL_TOKEN нужен только для приватного образа и должен быть классическим токеном GitHub с единственной областью read:packages.

Почему бенчмарк начинает врать

Известны несколько угроз валидности. Первая: golden set может быть смещён в сторону производителей. Нужно отслеживать распределение golden TPs по типам производителей (human, inferred, LLM, deterministic) и внутри LLM по моделям; если один LLM-производитель даёт больше целевого порога golden TPs после дедупликации, это сигнал добавить разнообразных производителей или понизить вес уникальных находок этого производителя. Leave-one-out абляция — решающий тест: если удаление одного LLM-производителя существенно меняет recall агента, golden set чрезмерно зависит от него.

Вторая: augmented recall зависит от агента, так как знаменатель |G_TP| + |U_TP| включает собственные новые TP агента, поэтому агент с многими одобренными классификатором новыми находками может получить высокий augmented recall даже при плохом grounded recall. Например, при |G_TP| = 10 и m = 5 агент с |U_TP| = 2 даёт 7/12 ≈ 0.58, а с |U_TP| = 100 — 105/110 ≈ 0.95 при одинаковой grounded-производительности. Ограничение: augmented recall не должен быть единственной основой для ранжирования агентов; при сравнении агентов grounded recall — метрика «яблоки к яблокам», а augmented recall и число новых TP (|U_TP|) дают дополнительный сигнал.

Третья: golden set может быть смещён в сторону определённых типов проблем. Инструменты, ориентированные на стиль и форматирование, могут искажать golden set в сторону педантичных замечаний, поэтому следует включать инструменты, нацеленные на существенные проблемы, и исключать или ограничивать стилевые.

Четвёртая: сам golden set может быть неполным. Комментарии, изначально оценённые как ложноположительные, оказывались реальными проблемами, которых не было в gold set. Из-за этого точность инструментов занижается, а полнота завышается, и это достаточно велико, чтобы изменить ранжирование.

Что из этого следует на практике

  • Эталон важнее метрики. Ground truth собирается из четырёх независимых источников, но правильность находки не зависит от её происхождения: TP — только если она верна, релевантна и нетривиальна. Любой источник даёт кандидатов, а не истину.
  • Выравнивание сохраняет историю, а не решает валидность. Комментарии из всей истории ревью приводятся к выбранному head-коммиту механически; сохранилась ли проблема — решает классификатор. Это разделяет две задачи: привести находку к нужному снимку кода и оценить её.
  • Вывод из follow-up коммитов консервативен по построению. Три фильтра (уверенность, область, тривиальность) отсекают расширения функциональности, попутные правки вне diff и чисто форматные изменения — иначе golden set засорился бы «исправлениями», не связанными с ревью.
  • Grounded recall — сопоставимая метрика, augmented recall — диагностика. Знаменатель augmented recall растёт от собственных новых TP агента, поэтому агент с плохим grounded recall может показать высокий augmented recall. Ранжировать агентов по augmented recall нельзя.
  • Смещение golden set проверяется абляцией. Распределение производителей отслеживается, а leave-one-out абляция показывает, не опирается ли эталон чрезмерно на одного производителя.
  • Неполнота эталона искажает обе метрики. Пропущенные в gold set реальные проблемы занижают точность и завышают полноту инструментов — эффект достаточно велик, чтобы изменить ранжирование.
  • Длительность не входит в качество. review_ms измеряется вокруг успешного вызова контейнера, исключая повторы и подготовку, и сообщается рядом с precision, recall и F_beta, а не внутри них.

Где смотреть в коде

Источники

Похожее