Как ревьюить код, который написал AI
Объём AI-кода в 5–10 раз выше, обычный review-цикл размыкается. Что искать в первую очередь, что отдать автоматам, как масштабировать через AI-скиллы и spec-as-code. Антипаттерны, чек-лист команды на 15 пунктов.
Не «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:
- Аудит текущего review-процесса и кодовой базы — архитектурный аудит
- Внедрение AI-skills под ваш стек и стандарты — внедрение Use Case Pattern
- Async-ревью PR-ов с цитированием правил — code review как сервис