Как ревьюить код, который написал AI

Объём AI-кода в 5–10 раз выше, обычный review-цикл размыкается. Что искать в первую очередь, что отдать автоматам, как масштабировать через AI-скиллы и spec-as-code. Антипаттерны, чек-лист команды на 15 пунктов.

Статья внедрена в скилл AI-агента ucp-pattern-review / ucp-api-review / ucp-java-style-review Эталонная библиотека к статье usecase-pattern code review AI-сгенерированного кода

Не «AI это плохо/хорошо», а что конкретно искать в PR-е, который написан с Claude/Copilot/Cursor, что отдать автоматам и где ваш человеческий review-цикл рвётся под потоком сгенерированного кода.

Статья — продолжение «AI пишет код. Зачем тогда методология?». Там обсуждался общий контекст, без которого AI каждую сессию даёт другую версию правды. Здесь — конкретный pipeline review-а кода, который с этим контекстом или без него уже написан и приехал в PR.

Почему обычный code review здесь не работает

Старая модель: senior читает PR на 200–400 строк, оставляет 5–10 комментариев, junior правит, сливаем. Объёмы дневные — 2–4 PR на ревьюера.

С AI-инструментами та же команда вываливает в PR-ы в 5–10 раз больше кода за тот же день. Не потому что разработчики плохие — потому что Claude или Cursor реально ускоряют. И тут начинаются проблемы, на которые старая модель не была рассчитана:

1. Объём. Senior физически не успевает читать. Если он будет тратить столько же времени на каждую строку, review станет узким местом, и команда обойдёт его — стандартный паттерн «мержим без review» появляется быстрее, чем кто-либо это замечает.

2. Уверенный тон ≠ корректность. AI пишет код спокойным голосом «вот так и надо». Чем мощнее модель, тем убедительнее звучит галлюцинация. На код-ревью это означает: глаза легко скользят по правильно выглядящему коду, который на самом деле не работает или работает не так.

3. Контекст не сохраняется между сессиями. Каждая PR пишется с чистого листа — AI не помнит, что в прошлой PR команда договорилась использовать Optional.ofNullable, а не Objects.requireNonNullElse. Без явных правил поверх AI вы получаете локально корректный код, который глобально неконсистентен.

4. Покрытие смещается на счастливый путь. AI отлично пишет happy path. Edge-кейсы (null, пустая коллекция, concurrent доступ, ошибка вниз по стеку) — выбираются хуже, потому что в обучающей выборке тоже хуже представлены.

Из этого следует не «AI отменить», а поменять модель review под новый поток.

Что искать в первую очередь

В коде, написанном с AI, я смотрю на следующие точки в фиксированном порядке. Если первое сломано — дальше можно не идти, переписывать всё.

Контракт на стыках

API-сигнатура, public-методы доменных сервисов, события на шине — то, что видно соседним сервисам и команде. AI любит чуть-чуть менять контракт «ради удобства»: переименовать поле в DTO, поменять тип возврата с Optional на null, заменить void на boolean ради «удобной проверки». Такие правки молчаливо ломают всё, что снаружи.

Что проверяю:

  • Метод-сигнатура совпадает с тем, что описано в OpenAPI/AsyncAPI/спецификации
  • Имена полей в JSON совпадают со спекой (kebab-case URL, camelCase JSON в нашем стеке)
  • Возвращаемые исключения объявлены и обрабатываются ровно теми, кто должен
  • Если есть спецификация-как-код (см. Use Case спецификация) — поведение в коде совпадает с разделом «Commands» / «Queries»

Соответствие методологии

В нашем случае — Use Case Pattern и его уровни зрелости. AI без явных скиллов часто соскальзывает на «как принято в обучающей выборке», а это в среднем — толстый сервисный слой с бизнес-логикой в @Service.

Что проверяю:

  • Контроллер не содержит бизнес-логики, только маппинг и dispatcher.dispatch(uc) (см. Уровень 1)
  • Бизнес-правила в Handler-е под @Transactional, а не размазаны
  • На Tier C — инварианты в агрегате, не в Handler-е (см. Tactical Patterns)
  • События публикуются в той же транзакции, что и persist (Outbox, не «после save() вызвали publish()»)
  • Persistence через jOOQ generated, не через JPA-Entity-в-controller-е

Edge-кейсы

Самое типичное место, где AI спотыкается. Я прохожу мысленно по 4 сценариям:

  • Null / отсутствие данных. Что если БД вернула пустой Optional? Что если входящий параметр null (для Object) или 0 (для int)?
  • Пустая коллекция. Что если items.isEmpty()? Хождение по пустому списку даёт правильный ответ, но иногда даёт 0 и приводит к делению на ноль или к «успешному» создаванию пустого заказа.
  • Concurrent доступ. Что если две транзакции одновременно меняют один агрегат? Optimistic lock на месте?
  • Ошибка вниз по стеку. Платёжный шлюз вернул 500. Что произойдёт — Retry? Циркуит-брейкер? Откат транзакции?

Если в коде нет явных проверок и тестов на эти 4 точки — PR требует доработки, даже если happy path работает.

Тесты

Самая коварная категория. AI генерирует тесты, которые выглядят как тесты, но реально не проверяют поведение. Типичные галлюцинации:

@Test
void shouldCreateOrder() {
    var order = service.create(uc);
    assertNotNull(order);            // проверили что объект не null
    verify(repository).save(any());  // проверили что метод вызвался
}

Это не тест бизнес-логики, это «тест что код не упал». Реальный тест должен проверять:

  • Что в order правильные значения полей (соответствуют входу)
  • Что save вызвался с конкретными аргументами (не any()), и в правильное время (после, например, проверки баланса)
  • Что событие опубликовано — какое именно, с какими полями
  • Что при невалидном входе бросается ожидаемое исключение, не любое

Imports и зависимости

Самое частое место галлюцинаций AI — выдуманные методы фреймворков. Spring Data, Apache Commons, Guava — у всех есть «правдоподобные» имена методов, которых на самом деле нет.

Что проверяю:

  • Все imports разрешаются (это IDE покажет, но AI часто пишет код в «промптинге» без IDE)
  • Spring Data-методы существуют (findByCustomerIdAndStatus — может быть; findFirstByCustomerIdAndStatusOrderByCreatedAtDescThenByAmountAsc — скорее всего галлюцинация)
  • Версии библиотек совпадают с теми, что подключены в build.gradle

Что НЕ ревьюить вручную

Главное правило: если это можно проверить машиной — проверяет машина. Человеческое внимание дорого, тратьте его на то, что машина не может.

Отдаю автоматам:

  • Стиль и форматирование — checkstyle, ktlint, prettier
  • Импорты — IDE optimize-imports
  • Constructor / getter / equals / hashCode / toString — Lombok или generated, никто не пишет вручную с 2015 года
  • Локальные имена переменных — если scope ≤ 10 строк, вкус ревьюера здесь не критичен; AI пишет адекватно
  • Trailing whitespace, line endings — git-hooks
  • Boilerplate-импорты — линтер
  • Архитектурные инвариантыArchUnit (controller не зовёт repository, core не импортирует Spring и т.п.)

Если автоматы это уже ловят — в PR на ревью этих замечаний быть не должно. Если приходят — настройте pre-commit или CI правильно, не нагружайте человека.

Как масштабировать review

Объём не победить «более внимательным senior-ом». Нужен слоёный pipeline, в котором человек подключается последним, а не первым.

Слой 1 — pre-commit hooks локально

Разработчик коммитит → запускается локальный прогон AI-скиллов на конкретные файлы изменения. Если что-то нарушается, коммит блокируется до фикса.

Плюс: разработчик видит замечания до того, как открыл PR. Не нужен раунд «ревьюер написал — разработчик правит — снова ревью».

Минус: должно быть быстро (< 5 секунд). Не запускайте полный анализ на pre-commit, только diff.

Слой 2 — AI-skills в CI на каждой PR

При создании PR — Claude Code skills (ucp-pattern-review, ucp-api-review, ucp-java-style-review, ucp-ddd-tactical-review и т.д.) бегут по diff-у и комментируют автоматически. Каждое замечание цитирует код правила (например, JS-2.5, BR-C5, R-7) и ссылку на статью методологии.

Это не блокирует merge — это suggestions. Команда сама решает что принять. Но 80% «банального» замечания человеческий ревьюер уже не пишет — оно либо принято, либо обосновано отвергнуто разработчиком.

Готовый набор скиллов под Use Case Pattern — github.com/remodov/usecase-pattern-skills.

Слой 3 — spec-as-code сравнение

Если вы храните Use Case спецификацию в git рядом с кодом — можно автоматически проверять, что код реализует то, что в спеке. Команды из раздела «Commands» соответствуют контроллерам, бизнес-правила из «Business Rules» имеют тесты, события из «Domain Events» публикуются.

Расхождение — баг, который ловится в PR-обзоре, не «доработаем потом».

Слой 4 — человеческий review

К этому моменту до человека дошёл уже:

  • Очищенный от стиля и форматирования код
  • С автоматически проверенными правилами методологии
  • С тестами, которые прошли первичную AI-проверку
  • С соответствием спеке

Человек сосредотачивается на том, что машина пока не может: глобальная согласованность (вписывается ли решение в архитектуру в целом), бизнес-смысл (это вообще то, что нужно бизнесу?), trade-offs (выбран лучший из вариантов?).

10 минут на серьёзный PR в этом режиме — не «прочитал 800 строк и сказал ОК», а сфокусированное обсуждение трёх–четырёх архитектурных моментов.

Анти-паттерны review AI-кода

Они появляются естественно — никто не учит обратному, и под нагрузкой команда соскальзывает на удобные сокращения.

«Всё работает — мерджим». Тесты зелёные, локально запускается — значит ОК. На самом деле проверена только успешная ветка. Edge-cases, инварианты, конкурентные сценарии — никто не смотрел. Бомба замедленного действия.

«Не шарю в этом куске, доверяю AI». AI написал интеграцию с новой библиотекой, ревьюер не знает библиотеку, мерджит на доверии. Через месяц обнаруживается, что AI выдумал половину API, и работает оно только в happy-path-тесте, который тоже AI написал. Самый опасный паттерн — он растёт пропорционально мощности модели.

«Тест потом». AI генерирует код, разработчик торопится релизить, тест откладывает. К моменту, когда возвращается — забыл контекст, пишет тест по тому, что сейчас в коде, а не по тому, что должно быть в коде. Тест становится регрессионным от багов, а не защитой от них.

«AI заодно отрефакторил». AI получил задачу «добавь поле в DTO» и заодно переписал три соседних класса «для красоты». В PR — diff на 600 строк, из которых 20 относятся к задаче. Ревьюер либо тратит 2 часа, либо мерджит как есть. Чините на этапе промпта — учите команду писать AI узкие задачи.

«Один большой PR на спринт». AI быстро пишет — соблазн вкатить целую фичу в один коммит. Это рвёт review-цикл по объёму. Дробите, как делали бы при ручном написании — одна логическая единица = один PR.

Печатный чек-лист

Положите в docs/code-review-ai.md команды:

## Перед открытием PR (разработчик)
- [ ] Pre-commit прогон AI-скиллов прошёл без блокирующих замечаний
- [ ] Все edge-cases (null, empty, concurrent, error) имеют тесты
- [ ] Тест проверяет ПОВЕДЕНИЕ, не «не упало»
- [ ] PR содержит ровно одну логическую единицу — не «заодно отрефакторил»

## При ревью (ревьюер)
- [ ] Контракт на стыках совпадает со спекой
- [ ] Контроллер не содержит бизнес-логики
- [ ] @Transactional там, где надо, и не там, где не надо
- [ ] Persistence — jOOQ generated, не JPA-Entity-в-controller
- [ ] События публикуются в той же транзакции, что и save
- [ ] На Tier C — инварианты в агрегате, не в Handler
- [ ] Imports разрешаются, версии совпадают (нет hallucinated API)
- [ ] Тесты проверяют конкретные значения, не any() / notNull
- [ ] AI-skills прошли в CI
- [ ] Spec-as-code сравнение совпало (если применимо)

## Перед merge
- [ ] Все блокирующие замечания закрыты
- [ ] Не блокирующие — приняты или явно обоснованы
- [ ] Architecture tests (ArchUnit) зелёные

15 пунктов, делятся на три фазы: до PR, во время review, перед merge. Команда видит чек-лист и знает что от неё ждут.

Дальше

AI-инструменты — это рост throughput. Без процесса, который с этим throughput-ом справляется, growthу команды некуда деваться, кроме как в долгий tech-debt. Связка методология + spec-as-code + AI-skills для review держит планку качества при росте объёма.

Если ваша команда буксует на review — могу помочь поставить такой pipeline:

Обсудить задачу →