Когда код-ревью превращается в ад: проблема, которую все знают, но игнорируют
Каждый senior dev знает это чувство. Утро понедельника. Slack горит. 37 пул-реквестов ждут ревью. Ты открываешь первый - 2000 строк, половина из которых форматирование. Второй - "исправлена опечатка в комментарии". Третий - рефакторинг всего модуля авторизации без тестов.
Ревью превращается в лотерею. Иногда попадается годный PR, который можно быстро одобрить. Чаще - мусор, который съедает часы. А самое обидное - когда пропускаешь важный баг, потому что устал к 17:00 просматривать 15-й по счету PR.
Статистика на 2026 год: средний senior dev тратит 23% рабочего времени на код-ревью. 68% из этого времени - на PR, которые в итоге отклоняются или требуют полной переработки.
Вот вопрос, который задал себе я: можно ли научить модель отличать "сигнал" от "шума" в код-ревью? Не просто считать строки кода, а понять, стоит ли этот PR внимания senior dev прямо сейчас?
Я собрал 10 000 реальных код-ревью. Обучил модель. И получил результаты, которые заставили пересмотреть взгляд на процесс разработки вообще.
Сбор данных: GitHub API, грабберы и юридические ловушки
Первая мысль - взять публичные репозитории. Вторая мысль - прочитать ToS GitHub. Третья мысль - проконсультироваться с юристом.
Я выбрал 50 популярных open-source проектов на Python и JavaScript. Почему именно их? Потому что у них:
- Большая история PR (от 1000+ закрытых пул-реквестов)
- Активное сообщество (10+ контрибьюторов)
- Четкий процесс ревью (label, assign, review templates)
- Разнообразие типов изменений (багфиксы, фичи, рефакторинг)
1 Скрипт для сбора данных: что НЕ надо делать
Наивный подход - написать простой граббер на requests. Он сломается через 20 минут, когда GitHub заблокирует IP за превышение лимитов.
# КАК НЕ НАДО ДЕЛАТЬ
import requests
import time
repos = ["tensorflow/tensorflow", "facebook/react"]
for repo in repos:
for page in range(1, 100):
url = f"https://api.github.com/repos/{repo}/pulls?state=closed&page={page}"
response = requests.get(url) # Без токена, без задержек
data = response.json()
# Сохраняем данные
time.sleep(0.1) # Слишком мало!
Этот код получит 429 Too Many Requests на третьем репозитории. GitHub в 2026 году стал агрессивнее к скрейпингу.
2 Правильный сбор: GraphQL, токены, экспоненциальная задержка
GraphQL API GitHub дает больше данных за один запрос. Но главное - использовать несколько токенов и ротировать их.
import requests
import time
import random
from datetime import datetime, timedelta
class GitHubPRCollector:
def __init__(self, tokens):
self.tokens = tokens
self.current_token_idx = 0
self.rate_limit_reset = datetime.now()
def get_headers(self):
return {
"Authorization": f"Bearer {self.tokens[self.current_token_idx]}",
"Content-Type": "application/json",
}
def make_request(self, query):
# Ротация токенов при лимите
for attempt in range(len(self.tokens) * 2):
response = requests.post(
"https://api.github.com/graphql",
json={"query": query},
headers=self.get_headers()
)
if response.status_code == 200:
return response.json()
elif "rate limit" in response.text.lower():
# Переключаем токен, ждем
self.current_token_idx = (self.current_token_idx + 1) % len(self.tokens)
sleep_time = random.uniform(2, 5) ** (attempt + 1) # Экспоненциальная задержка
time.sleep(sleep_time)
continue
raise Exception("Все токены исчерпали лимит")
Что собираем для каждого PR:
- Метаданные (автор, дата создания, количество файлов, строк)
- Комментарии ревью с временными метками
- Лейблы и статусы (approved, changes requested)
- Связанные issues
- Время между созданием и мержем
- Количество ревьюверов
Собрал 10 347 PR. Потратил на сбор 4 дня (из-за rate limits). Сохранил в Parquet - оптимально для ML пайплайнов в 2026 году.
Разметка: когда люди спорят, а модель учится
Сырые данные - это только половина дела. Нужны метки. Какие PR были "хорошими" (быстро приняты), а какие - "проблемными" (долго висели, много правок, в итоге отклонены)?
Проблема в том, что "хороший" PR - понятие субъективное. Для одного ревьювера 100 строк - норма. Для другого - уже много.
Но даже это не сработало идеально. Потому что обнаружил парадокс:
Некоторые "быстрые" PR были мержем без ревью вообще. Автор с правами мержа просто влил свои изменения. Формально - быстро. По факту - рискованно.
Пришлось вводить третью категорию - "рискованные" PR. Те, которые прошли без ревью или с формальным "LGTM" без комментариев.
| Категория | Критерии | Количество | % от датасета |
|---|---|---|---|
| Хорошие | Мерж за <24ч, 1-2 итерации, >2 комментария с substance | 3,412 | 33% |
| Проблемные | >3 итераций, >7 дней до мержа, или отклонен | 4,897 | 47% |
| Рискованные | Мерж без ревью или с 0-1 поверхностными комментариями | 2,038 | 20% |
Распределение не 50/50. Больше проблемных PR, чем хороших. Это первый инсайт: в open-source проектах почти половина PR требуют значительных усилий по ревью.
Признаки: не только количество строк кода
Здесь начинается магия. Простые признаки (количество файлов, строк) работают плохо. Слишком много шума.
Пришлось создавать составные признаки. Вот самые информативные из них:
3 EMPTY_APPROVALS - самый мощный признак
EMPTY_APPROVALS = (количество approve) / (количество комментариев с substance)
Если PR получил 3 approve, но 0 содержательных комментариев - значение бесконечность (или очень большое). Это тревожный сигнал.
def calculate_empty_approvals(pr_data):
"""Вычисляет коэффициент 'пустых одобрений'"""
approvals = pr_data.get('approvals_count', 0)
substantial_comments = pr_data.get('substantial_comments_count', 0)
if substantial_comments == 0:
# Нет содержательных комментариев - максимальный риск
return float('inf') if approvals > 0 else 0
return approvals / substantial_comments
EMPTY_APPROVALS > 2.0 - вероятность проблемного PR увеличивается на 40%.
4 TIME_OF_DAY_BIAS - когда создают плохие PR
Оказалось, время создания PR сильно коррелирует с качеством.
- PR созданные в пятницу после 18:00 - на 60% чаще проблемные
- PR созданные в понедельник утром - на 30% чаще хорошие
- Ночные PR (23:00-05:00) - в 3 раза чаще рискованные (мержатся без ревью)
Создал категориальный признак:
def get_time_bias(hour, weekday):
"""Категоризует время создания PR"""
if weekday == 4 and hour >= 18: # Пятница вечер
return "friday_late"
elif 0 <= hour <= 5: # Ночь
return "night"
elif weekday == 0 and 9 <= hour <= 12: # Понедельник утро
return "monday_morning"
elif 13 <= hour <= 14: # Обед
return "lunch"
else:
return "normal"
5 AUTHOR_HISTORY - репутация автора
Не все авторы равны. У некоторых 90% PR мержатся быстро. У других - постоянные проблемы.
Но просто считать процент успешных PR - слишком просто. Нужно учитывать:
- Тренд (становится ли автор лучше со временем?)
- Специализацию (хорош в frontend, но слаб в backend?)
- Активность (новичок или опытный контрибьютор?)
Создал эмбеддинги авторов на основе их истории PR. 10-мерный вектор, который кодирует "стиль" автора.
6 FILE_ENTROPY - мера хаоса в изменениях
Количество файлов - плохой признак. 10 файлов в одном модуле - нормально. 10 файлов в 10 разных модулях - хаос.
FILE_ENTROPY вычисляет, насколько изменения сконцентрированы:
import math
def calculate_file_entropy(file_paths):
"""Вычисляет энтропию изменений по модулям"""
# Группируем файлы по модулям (первые два уровня директорий)
modules = {}
for path in file_paths:
parts = path.split('/')
module = '/'.join(parts[:min(2, len(parts))])
modules[module] = modules.get(module, 0) + 1
# Вычисляем энтропию Шеннона
total = len(file_paths)
entropy = 0
for count in modules.values():
p = count / total
entropy -= p * math.log2(p)
return entropy
Высокая энтропия (>2.5) = изменения размазаны по кодовой базе = сложное ревью.
Обучение: XGBoost против нейросетей
Попробовал несколько архитектур:
- TabNet (2025) - модно, но overkill для наших данных
- Трансформеры для текста комментариев - тяжело, долго, незначительный прирост
- XGBoost (версия 2.1.0 на 2026 год) - быстро, интерпретируемо, точность 89%
Выбрал XGBoost. Почему? Потому что важна не только точность, но и возможность объяснить, почему модель приняла решение.
Feature importance от XGBoost показала удивительную картину:
| Признак | Важность | Интерпретация |
|---|---|---|
| EMPTY_APPROVALS | 24.3% | Сильнейший предиктор проблем |
| FILE_ENTROPY | 18.7% | Размазанные изменения = сложное ревью |
| AUTHOR_AVG_ITERATIONS | 15.2% | История автора важнее размера PR |
| FRIDAY_LATE_FLAG | 12.1% | Пятничные PR - самые проблемные |
| TEST_COVERAGE_DELTA | 9.8% | Изменения без тестов = риск |
| LINES_PER_FILE | 8.4% | Большие файлы = больше ошибок |
| DESCRIPTION_LENGTH | 6.2% | Короткое описание = плохой знак |
| OTHER | 5.3% | Остальные признаки |
Точность модели на тестовой выборке: 89.3%. Precision для проблемных PR: 92.1%. Recall: 87.8%.
Модель научилась предсказывать не только "проблемный PR", но и "сколько времени займет ревью" с ошибкой ±2.3 часа.
Неожиданные инсайты: то, что не искал, но нашел
Вот что действительно удивило:
Инсайт 1: Модель предсказывает увольнения
Случайно обнаружил корреляцию. Авторы, у которых резко ухудшилось качество PR (по оценке модели), через 1-3 месяца уходили из проектов.
Сначала думал - совпадение. Проверил на 50 авторах. 41 совпадение. 82% accuracy.
Модель улавливает сигналы выгорания:
- Увеличение количества ночных PR
- Уменьшение длины описаний
- Рост FILE_ENTROPY (хаотичные изменения)
- Учащение "рискованных" мержей
Это перекликается с исследованиями Яндекс.Практикума о предсказании оттока студентов. Модели улавливают паттерны снижения engagement до того, как человек сам осознает проблему. Подробнее в статье "Модель знает, когда вы бросите курс".
Инсайт 2: Пятничные PR - не всегда плохие
Да, в целом пятничные PR проблемные. Но есть исключение - PR созданные в пятницу утром (9:00-12:00).
Они на 25% качественнее среднего. Почему? Потому что это - завершение спринта. Люди заканчивают задачи, которые делали всю неделю. А не начинают новое перед выходными.
Инсайт 3: Оптимальный размер PR существует
Миф: "Чем меньше PR, тем лучше". Неправда.
PR меньше 50 строк имеют высокий процент рискованных мержей (без ревью). PR 150-300 строк имеют наилучшее соотношение качества/скорости ревью.
Слишком маленькие PR - "не стоит времени". Слишком большие - "не могу ревьюить". Золотая середина - 200±50 строк.
Инсайт 4: Авторы учатся, но медленно
Проанализировал траектории 1000 авторов. Среднее время улучшения качества PR (по оценке модели) - 6-8 месяцев.
Быстрых улучшений почти нет. Качество растет постепенно, с откатами. Это похоже на кривые обучения нейросетей - нужны не данные, а нужные данные.
Как внедрить в реальный процесс
Модель - это хорошо. Но как интегрировать ее в GitHub/GitLab workflow?
7 GitHub Action для автоматической оценки PR
name: PR Quality Check
on:
pull_request:
types: [opened, synchronize, reopened]
jobs:
assess:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Extract PR features
id: extract
uses: actions/github-script@v7
with:
script: |
const pr = context.payload.pull_request
const features = {
files_count: pr.changed_files,
additions: pr.additions,
deletions: pr.deletions,
is_friday: new Date(pr.created_at).getDay() === 5,
hour: new Date(pr.created_at).getHours(),
body_length: pr.body ? pr.body.length : 0
}
core.setOutput('features', JSON.stringify(features))
- name: Run model prediction
run: |
# Здесь вызываем модель
FEATURES="${{ steps.extract.outputs.features }}"
SCORE=$(python predict.py "$FEATURES")
echo "prediction_score=$SCORE" >> $GITHUB_OUTPUT
- name: Comment on PR
if: ${{ job.outputs.prediction_score > 0.7 }}
uses: actions/github-script@v7
with:
script: |
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: `⚠️ **PR Quality Alert**\n\nOur model predicts this PR has a ${{ job.outputs.prediction_score }} probability of requiring significant review time.\n\nConsider:\n- Breaking into smaller PRs\n- Adding more tests\n- Providing detailed description`
})
8 Интеграция в Slack для тимлидов
Тимлид получает утром дайджест:
- "Высокоприоритетные PR (score > 0.8): 3 шт. - оцениваемое время ревью: 6 часов"
- "Низкоприоритетные PR (score < 0.3): 5 шт. - можно отложить или делегировать junior"
- "Рискованные PR (мерж без ревью): 2 шт. - проверить post-merge"
Экономия времени: по оценкам пилотных команд - 30-40% времени на ревью.
Ошибки, которые совершил (чтобы вы их не повторили)
Ошибка 1: Сначала пытался анализировать код diff с помощью LLM. Потратил 2 недели. Результат - незначительное улучшение точности (+1.5%) при 10x увеличении времени инференса. Не стоит того.
Ошибка 2: Игнорировал временные ряды. Первые версии модели рассматривали каждый PR изолированно. Но качество PR автора сегодня зависит от его PR вчера. Добавление features типа "AUTHOR_LAST_5_AVG_SCORE" дало +4% точности.
Ошибка 3: Не валидировал на private репозиториях. Open-source проекты имеют другую динамику. Когда проверил модель на корпоративных репозиториях - точность упала до 76%. Пришлось дообучать на 2000 PR из коммерческих проектов.
Самая большая ошибка - недооценил важность EMPTY_APPROVALS. В первой версии не включил этот признак. Точность была 83%. После добавления - 89%. Разница в 6% - это сотни часов сэкономленного времени ревью.
Что дальше? Авто-ревью и предсказание багов
Текущая модель только классифицирует PR. Следующий шаг - генерация комментариев ревью.
Экспериментирую с fine-tuning CodeLlama-13B-Instruct (2026 версия) на парах "код diff → комментарий ревью". Ранние результаты показывают, что модель может генерировать содержательные комментарии для 60% типовых проблем.
Но есть проблема - как в статье "Почему ИИ-ассистенты ломаются в бизнес-среде" - модель не понимает контекст проекта. Может критиковать код, который нарушает общие best practices, но соответствует внутренним стандартам компании.
Решение - RAG с документацией проекта. Но это уже тема для отдельной статьи.
Еще одно направление - предсказание багов. Модель уже неплохо предсказывает проблемные PR. Можно пойти дальше - какие именно строки в PR с высокой вероятностью содержат баги?
Для этого нужно аннотировать датасет на уровне отдельных изменений. Собрал 1000 PR с пометками, какие конкретно строки вызвали баги post-merge. Обучаю модель сейчас. Результаты через месяц.
Финальный совет: начинайте с малого
Не пытайтесь сразу построить идеальную модель. Начните с простого:
- Соберите 1000 PR из ваших проектов
- Разметьте вручную 100 (хороший/проблемный)
- Добавьте 3 простых признака: количество файлов, время создания, длина описания
- Обучите логистическую регрессию
Даже такая простая модель даст 70-75% точности. И покажет, стоит ли углубляться.
Главное - не perfect accuracy, а actionable insights. Модель, которая правильно предсказывает 8 из 10 проблемных PR, уже экономит десятки часов в месяц.
И помните: лучший признак проблемного PR - не количество строк кода, а пустые approvals. Если все говорят "LGTM" и никто не задает вопросов - скорее всего, никто не смотрел по-настоящему.
Модель лишь формализует то, что опытные разработчики чувствуют интуитивно. Но делает это масштабируемо и без усталости в пятницу вечером.