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

Что внутри (на 30 секунд):

  • Почему старая модель «опытный ревьюер читает PR на 200–400 строк» рвётся, когда AI вываливает в 5–10 раз больше кода за тот же день
  • 5 точек, на которые смотреть в первую очередь: контракт на стыках, соответствие методологии, крайние случаи, тесты на поведение, импорты
  • Что отдать машине: стиль, импорты, ArchUnit, спека-как-код
  • Слоёный процесс: pre-commit → AI-скиллы в CI → сравнение со спекой → человек последним, на 10 минут осмысленного обсуждения
  • 5 анти-паттернов и печатный чек-лист на 15 пунктов

Этот процесс работает в команде backend-кластера на 20+ инженеров уже полтора года. Под смежной публикацией про методологию + AI Alexey Tolmachev (Senior BSA, 14+ лет) написал в комментариях: «6× меньше дефектов после формализации границ контекстов до кодогенерации».

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

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

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

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

  • Объём. Ревьюер физически не успевает читать. Если он будет тратить столько же времени на каждую строку, ревью станет узким местом, и команда обойдёт его — стандартный паттерн «мержим без ревью» появляется быстрее, чем кто-либо это замечает.
  • Уверенный тон ≠ корректность. AI пишет код спокойным голосом «вот так и надо». Чем мощнее модель, тем убедительнее звучит галлюцинация. На code review это означает: глаза легко скользят по правильно выглядящему коду, который на самом деле не работает или работает не так.
  • Контекст не сохраняется между сессиями. Каждый PR пишется с чистого листа — AI не помнит, что в прошлом PR команда договорилась использовать определённый подход к nullable-значениям. Без явных правил поверх AI вы получаете локально корректный код, который глобально неконсистентен.
  • Покрытие смещается на успешный путь. AI отлично пишет успешный сценарий. Крайние случаи (null, пустая коллекция, конкурентный доступ, ошибка вниз по стеку) — выбираются хуже, потому что в обучающей выборке тоже хуже представлены.

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

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

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

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

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

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

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

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

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

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

  • Controller не содержит бизнес-логики, только маппинг и диспетчеризацию (см. Уровень 2)
  • Бизнес-правила сосредоточены в Handler-е, не размазаны по нескольким слоям
  • На Уровне 3 — инварианты в агрегате, не в Handler-е (см. Tactical Patterns)
  • События публикуются в той же транзакции, что и persist (Outbox, не «после save вызвали publish»)
  • Persistence через типизированные сгенерированные запросы, не через случайный объект из доменного слоя прямо в controller-е

Крайние случаи

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

  • Null / отсутствие данных. Что если хранилище вернуло «не найдено»? Что если входящий параметр отсутствует?
  • Пустая коллекция. Что если items.isEmpty()? Хождение по пустому списку даёт правильный ответ, но иногда 0 и приводит к делению на ноль или к «успешному» созданию пустого заказа.
  • Конкурентный доступ. Что если два запроса одновременно меняют один агрегат? Есть ли защита от потерянного обновления?
  • Ошибка вниз по стеку. Платёжный шлюз вернул 500. Что произойдёт — Retry? Circuit Breaker? Откат транзакции?

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

Тесты

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

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

func TestCreateOrder(t *testing.T) {
    svc, repo := newTestService(t)
    order, err := svc.CreateOrder(context.Background(), CreateOrderInput{
        CustomerID: "c-1",
        Items:      nil,
    })
    if err != nil {
        t.Fatal(err)
    }
    if order == nil { // проверили что объект не nil
        t.Fatal("expected order, got nil")
    }
    if !repo.saveCalled { // проверили что метод вызвался
        t.Fatal("expected Save to be called")
    }
}
it('should create order', async () => {
    const order = await service.createOrder(input);
    expect(order).not.toBeNull();        // проверили что объект не null
    expect(repository.save).toHaveBeenCalled(); // проверили что метод вызвался
});
def test_create_order():
    service, repository = build_test_service()
    order = service.create_order(CreateOrderInput(customer_id="c-1", items=[]))
    assert order is not None            # проверили что объект не None
    repository.save.assert_called()     # проверили что метод вызвался

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

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

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

Самое частое место галлюцинаций AI — выдуманные методы и пакеты. Популярные фреймворки имеют «правдоподобные» имена методов, которых на самом деле нет.

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

  • Все зависимости разрешаются (это IDE покажет, но AI часто пишет код в «промпте» без IDE)
  • Методы запросов к хранилищу существуют в используемой версии библиотеки
  • Версии библиотек совпадают с теми, что подключены в манифесте зависимостей

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

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

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

  • Стиль и форматирование — checkstyle, golangci-lint, prettier, ruff
  • Импорты — goimports, organize-imports, isort
  • Шаблонный код — кодогенерация или языковые средства (value objects, equals, toString)
  • Локальные имена переменных — если scope ≤ 10 строк, вкус ревьюера здесь не критичен; AI пишет адекватно
  • Trailing whitespace, line endings — git-hooks
  • Архитектурные инварианты — ArchUnit, dependency-cruiser или аналог (controller не зовёт repository, ядро не импортирует инфраструктуру и т.п.)

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

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

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

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

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

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

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

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

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

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

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

Слой 3 — сравнение со спецификацией

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

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

Слой 4 — человеческое ревью

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

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

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

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

Анти-паттерны ревью AI-кода

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

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

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

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

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

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

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

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

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

## При ревью (ревьюер)
- [ ] Контракт на стыках совпадает со спекой
- [ ] Controller не содержит бизнес-логики
- [ ] Транзакции расставлены там, где надо, и не там, где не надо
- [ ] Persistence через типизированные запросы, не прямой доступ из controller-а
- [ ] События публикуются в той же транзакции, что и save
- [ ] На Уровне 3 — инварианты в агрегате, не в Handler
- [ ] Зависимости разрешаются, версии совпадают (нет выдуманного API)
- [ ] Тесты проверяют конкретные значения, не any() / notNull
- [ ] AI-скиллы прошли в CI
- [ ] Сравнение со спецификацией совпало (если применимо)

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

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

Дальше

AI-инструменты — это рост пропускной способности. Без процесса, который с этим объёмом справляется, рост команды упирается в большой технический долг. Связка методология + спека-как-код + AI-скиллы для ревью держит планку качества при росте объёма.

Если ваша команда буксует на ревью — на странице услуг описаны четыре формата помощи: архитектурный аудит, внедрение Use Case Pattern, технический ментор, code review как сервис.