Code review с AI: что проверять, когда код стало слишком легко писать

AI удешевил написание кода, но не понимание. Review смещается с синтаксиса на intent, scope, архитектуру и вопрос, должен ли этот код вообще существовать.

04.09.2026 · 17 мин чтения

За последние пару лет писать код стало заметно проще. Причём не в привычном смысле, когда появился новый framework и пять файлов теперь можно заменить одним. Изменилась сама стоимость производства кода. Я могу описать задачу агенту, уйти читать другой pull request и через несколько минут получить реализацию, тесты, миграцию и ещё пару файлов, которые никто особенно не просил.

На первый взгляд это выглядит как довольно однозначный рост производительности. Если разработчик раньше тратил несколько часов на изменение, а теперь получает его за двадцать минут, значит разработка должна ускориться примерно в ту же сторону. На практике быстро выясняется, что скорость написания кода была только одной частью системы.

Код всё ещё нужно понять.

Нужно проверить, что он решает правильную задачу, не ломает старую, нормально вписывается в архитектуру, не дублирует уже существующую реализацию, не добавляет лишнюю зависимость и вообще стоил того, чтобы появиться в проекте. Потом его ещё кто-то будет поддерживать, изменять и однажды разбирать ночью во время инцидента.

AI сильно удешевил generation, но почти не удешевил understanding. И чем больше кода мы можем произвести за единицу времени, тем заметнее эта разница.

Поэтому мне кажется, что code review в эпоху AI становится не менее важным, а наоборот. Просто проверять теперь нужно немного другие вещи.

Код перестал быть естественно дорогим

Раньше у написания кода существовал довольно полезный встроенный тормоз. Чтобы добавить новый слой abstraction, разработчику нужно было этот слой придумать, написать интерфейсы, реализацию, тесты и потом самому разбираться с последствиями. Стоимость решения хотя бы иногда заставляла остановиться и подумать, действительно ли новый код нужен.

С агентом этот ограничитель практически исчезает. Попросить создать ещё один service, adapter или mapper почти ничего не стоит. Если первая реализация не понравилась, можно за минуту получить вторую. Потом третью. Сгенерировать пятьсот строк теперь иногда проще, чем внимательно прочитать пятьдесят уже существующих.

И это постепенно меняет codebase.

Исследования AI-assisted development уже показывают довольно логичный эффект: разработчики создают больше pull requests и изменений, а нагрузка на review начинает расти вслед за ними. Отдельные исследования GitClear также указывают на рост duplication и снижение повторного использования существующего кода в AI-heavy workflows. Причина здесь, на мой взгляд, довольно понятная. Для модели локально завершить задачу новым кодом часто проще, чем разобраться, какую существующую abstraction лучше расширить или какие строки вообще можно удалить.

У разработчика хотя бы иногда возникает лень писать ещё один класс.

У AI такой лени нет.

Поэтому первый вопрос на review теперь всё чаще должен звучать не «правильно ли написан этот код?», а «почему этот код вообще появился?»

Я бы начинал review не с первой строки diff

Когда открываешь аккуратный pull request, довольно естественно сразу пойти сверху вниз по изменениям. Особенно если diff хорошо оформлен, методы небольшие, переменные понятно названы и рядом лежат тесты. AI как раз очень хорошо умеет создавать такое ощущение законченности.

Я бы с agent-generated изменениями делал наоборот и сначала вообще не смотрел подробно на реализацию. Сначала нужно восстановить исходную задачу.

Что мы хотели изменить? Какие acceptance criteria? Где должна была находиться граница изменения? Что специально не должно было поменяться? Какие существующие ограничения системы нужно сохранить?

Допустим, задача была исправить обработку timeout во внешнем API, а pull request меняет двенадцать файлов, добавляет abstraction для всех внешних clients и одновременно переписывает retry policy. Возможно, это действительно хорошая идея. Но объяснить необходимость такого scope должен автор изменения, а не reviewer после получаса чтения кода.

С AI эта проблема встречается особенно часто, потому что агенту легко поручить что-нибудь вроде:

исправь проблему и заодно приведи код в порядок.

Через несколько минут мы получаем смесь bug fix, refactoring и новой архитектуры в одном diff. Всё может даже работать. Только review такого изменения становится гораздо сложнее, потому что невозможно отделить необходимую часть от улучшений, появившихся по пути.

Поэтому перед чтением реализации я бы проверял три вещи: intent, scope и necessity.

Если с ними нет ясности, разбирать детали пока рано.

Самый полезный вопрос: этот код вообще нужен?

AI почти всегда предлагает что-то написать. Это естественно: его попросили решить задачу кодом, и он генерирует код.

Но хорошее инженерное решение довольно часто заключается не в добавлении новой реализации.

Иногда нужная функция уже существует в соседнем модуле. Иногда достаточно поменять одну существующую abstraction. Иногда новый класс появился только потому, что модель не нашла старый. Иногда условие можно выразить конфигурацией. А иногда правильное изменение вообще уменьшает количество кода.

Вот это я сейчас стараюсь отдельно проверять в AI-generated diff.

Допустим, агент добавил новый:

final class UserAccessValidator
{
    public function canViewInvoice(User $user, Invoice $invoice): bool
    {
        return $invoice->userId() === $user->id();
    }
}

Локально класс выглядит нормально. Название понятное, метод маленький, тест написать несложно.

Но потом оказывается, что в проекте уже есть InvoiceAccessPolicy, где находится вся остальная логика доступа к invoice. Новый код не ошибочный в привычном смысле. Он даже может прекрасно работать.

Проблема в том, что его не должно было существовать.

Именно такие изменения мне кажутся одним из самых неприятных свойств AI-кода. Модель может написать очень качественную локальную реализацию, которая делает систему хуже целиком.

Поэтому во время review я бы отдельно искал:

  • существующую логику, которую можно было использовать;
  • почти одинаковые abstractions;
  • новые классы с одной простой функцией;
  • повторно реализованные helpers;
  • новые слои, которые появились только ради текущей задачи;
  • места, где изменение существующего кода заменили добавлением нового.

С появлением AI принцип less code is usually easier to maintain никуда не исчез. Просто теперь следовать ему стало сложнее, потому что дополнительный код практически бесплатен в момент генерации.

Локальная правильность ещё не означает хорошее изменение

Большая часть автоматических проверок хорошо отвечает на локальные вопросы. Компилируется ли проект? Проходят ли тесты? Есть ли type errors? Нарушены ли правила static analysis?

Это полезно и желательно запускать до человеческого review.

Но самые дорогие ошибки обычно находятся уровнем выше.

Представим задачу:

пользователь не должен иметь доступ к invoice другого tenant.

AI может написать:

$invoice = $invoiceRepository->find($invoiceId);

if ($invoice->tenantId() !== $currentTenant->id()) {
    throw new AccessDeniedException();
}

Функционально проверка есть.

Но архитектурное правило системы может заключаться в том, что объекты другого tenant вообще никогда не должны покидать repository boundary:

$invoice = $invoiceRepository->findForTenant(
    $invoiceId,
    $currentTenant->id()
);

Оба решения могут пройти тесты. Первое даже выглядит очевиднее.

Но второе сохраняет системный invariant: данные другого tenant не появляются выше определённого слоя приложения.

Модель редко знает такие вещи сама по себе. Даже если дать ей хорошую документацию, внутри mature codebase существует огромное количество договорённостей, которые никогда нормально не были записаны. Команда просто знает, где должна происходить авторизация, как оформляются transactions, какие сервисы могут обращаться друг к другу и какие данные запрещено передавать между bounded contexts.

Поэтому human review всё сильнее смещается в сторону архитектурного контекста.

Не только «работает ли этот метод», а «на правильном ли уровне системы он вообще находится».

Business invariants стоит проверять отдельно

Есть ещё один класс правил, которые особенно легко пропустить: business invariants.

Например:

Invoice нельзя редактировать после settlement.

Payment operation должна быть idempotent.

User одного tenant никогда не видит данные другого.

Order после отправки нельзя вернуть в status draft.

Balance нельзя уменьшить ниже определённого лимита.

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

Причём happy path будет работать.

Допустим, нужно добавить повтор платежа после network timeout. Агент реализует retry, тесты проходят, API отвечает правильно. Но если внешний provider успел обработать первый запрос и просто не вернул response, второй вызов создаст двойную операцию.

Код синтаксически правильный.

Test suite может быть зелёным.

Задача вроде выполнена.

Но нарушен главный invariant payment system.

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

Happy path AI пишет заметно охотнее

Если попросить модель реализовать API endpoint, основной сценарий обычно получается довольно быстро. Запрос валидный, объект существует, внешний сервис отвечает, база доступна — всё хорошо.

Гораздо интереснее посмотреть, что происходит вокруг.

Что будет при timeout?

Что если внешний сервис ответит дважды?

Что если transaction откатится после успешного HTTP call?

Что произойдёт при concurrent request?

Можно ли повторить операцию?

Что будет с частично записанными данными?

Как система поведёт себя при пустом ответе?

Есть ли ограничение на retries?

Логи не содержат sensitive data?

Именно failure paths часто отличают реализацию «работает у меня» от кода, который спокойно проживёт несколько лет в production.

AI здесь немного усиливает старую человеческую проблему. Разработчики и без него любят писать happy path первым. Просто теперь этот happy path появляется настолько быстро и выглядит настолько законченно, что возникает желание принять его как практически готовое решение.

Я бы поэтому после понимания основной логики специально переключался в режим разрушения:

Что должно произойти, чтобы этот код перестал работать?

И потом искал это место в реализации.

Error handling иногда выглядит лучше, чем работает

У AI есть ещё одна неприятная привычка: он любит сделать код устойчивым хотя бы визуально.

Например:

try {
    return $client->request();
} catch (\Throwable $e) {
    $logger->warning($e->getMessage());

    return null;
}

Ошибка обработана.

Приложение не падает.

Всё аккуратно.

Только теперь вызывающий код вообще не понимает, чем отсутствие результата отличается от network failure. Ошибка фактически замаскирована.

Или агент добавляет:

catch (\Exception $e) {
    // fallback
}

и возвращает старое значение, потому что это помогает тестам и делает сценарий «надёжнее».

Такие конструкции особенно важно проверять не по форме, а по семантике. Нужно ли здесь вообще ловить exception? Кто должен принимать решение о retry? Может ли вызывающий код безопасно продолжить выполнение? Не превращаем ли мы реальную проблему в трудноуловимый неправильный state?

GitClear в исследованиях AI-heavy codebases отдельно отмечал рост error-masking patterns. Меня это не особенно удивляет: для модели обработанный exception выглядит как более законченная реализация, чем exception, который сознательно пробрасывается выше.

Но production code не обязан выглядеть спокойным.

Иногда правильное поведение системы — громко упасть.

Dependencies теперь нужно проверять руками

С зависимостями появился отдельный класс риска.

AI совершенно спокойно может предложить библиотеку, которая выглядит реалистично, имеет нормальное название и решает именно нашу задачу. Иногда библиотека действительно существует. Иногда существует, но давно не поддерживается. Иногда API уже изменился. А иногда package вообще придуман.

Для application code я бы поэтому считал новую dependency отдельным предметом review.

Нужна ли она вообще?

Существует ли package?

Кто его поддерживает?

Когда был последний release?

Какая лицензия?

Сколько дополнительных transitive dependencies он притягивает?

Нельзя ли решить задачу стандартной библиотекой или уже используемым package?

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

В mature system новая dependency — это не просто одна строка в composer.json или package.json. Это ещё один внешний компонент, который потом кто-то должен обновлять, проверять на vulnerabilities и учитывать при миграциях.

Поэтому я бы не принимал решение о dependency только потому, что агент уверенно написал инструкцию установки.

Тесты, написанные AI, тоже написал AI

Одна из самых обманчивых вещей — pull request, где AI одновременно сделал реализацию и написал под неё красивые тесты.

Психологически это выглядит сильно. Есть код, есть coverage, всё зелёное. Кажется, что задача закрыта со всех сторон.

Проблема в том, что реализация и тесты могли родиться из одной и той же неправильной предпосылки.

Допустим, требование звучало так:

скидка действует до конца дня 10 сентября.

AI понял это как:

expires_at < 2026-09-10 23:59:59

и написал тест именно под это поведение.

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

Поэтому AI-generated tests я бы review'ил почти как production code.

Что именно доказывает этот тест?

Он проверяет requirement или просто повторяет реализацию?

Есть ли negative case?

Проверены ли boundaries?

Что произойдёт за секунду до и после границы?

Есть ли failure scenario?

Если production code и tests полностью написал один агент в одном контексте, наличие тестов само по себе ещё довольно слабое доказательство правильности.

Хороший тест должен быть независимым источником требований, а не объяснением к сгенерированной реализации.

AI-specific smells

Со временем у AI-generated изменений начинают появляться довольно узнаваемые запахи.

Первый — duplication. Агент не нашёл существующую логику и сделал свою.

Второй — лишняя abstraction. Для одной операции появляется interface, implementation, factory и ещё mapper между ними, потому что архитектурно это выглядит солидно.

Третий — слишком много defensive code. Проверки ситуаций, которые по контракту вообще невозможны, nullable там, где значение обязательное, try/catch вокруг всего подряд.

Четвёртый — комментарии, объясняющие очевидное:

// Check if user exists
if ($user === null) {

Пятый — новый utility function рядом с практически идентичным старым.

Шестой — deprecated или просто нехарактерный для проекта pattern. Модель знает огромное количество кода из разных эпох и иногда совершенно уверенно приносит решение десятилетней давности в современную codebase.

Седьмой — слишком универсальное решение. Ticket просил одну конкретную возможность, а агент построил framework на случай следующих десяти.

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

Всё детерминированное лучше проверить до человека

При этом human reviewer точно не должен вручную компенсировать все недостатки AI.

Если ошибку можно найти автоматически, её желательно найти автоматически.

До review должны пройти:

formatter
lint
type checking
unit tests
integration tests
static analysis
security scanning
dependency scanning

В зависимости от проекта сюда добавляются architecture tests, API compatibility checks, migration checks и другие deterministic gates.

Нет никакого смысла тратить человеческое внимание на неправильный формат или очевидный type error, если это может за секунду проверить машина.

Мне нравится разделять verification на два слоя.

Первый отвечает на вопросы, которые можно формализовать:

Компилируется?

Проходят тесты?

Нарушено правило static analysis?

Есть известная vulnerability?

Изменился API contract?

Второй требует judgment:

Это вообще правильное решение?

Нужен ли этот код?

Хорошо ли выбрана граница?

Соответствует ли изменение архитектуре?

Стоит ли такой trade-off сложности?

И чем больше routine implementation уходит к AI, тем важнее становится второй слой.

По сути reviewer постепенно меньше проверяет написание кода и больше проверяет инженерное решение.

Размер pull request теперь нужно ограничивать сознательно

Есть ещё одна проблема, которая раньше частично регулировалась скоростью самого разработчика.

Большой pull request долго писать.

Поэтому изменения хотя бы иногда естественным образом дробились на части.

Agent может за один запуск изменить двадцать файлов, добавить тысячу строк и вполне убедительно объяснить, зачем понадобилась каждая из них. Его скорость вообще никак не связана со способностью reviewer эту тысячу строк понять.

Cognitive bandwidth человека не вырос вместе с производительностью генерации.

Поэтому правило небольших PR в AI-development становится даже важнее.

Я бы не позволял скорости агента определять размер изменения.

Если задача состоит из трёх независимых шагов, лучше сделать три reviewable changes, даже если AI технически может реализовать всё за одну сессию.

Большой diff опасен не потому, что много строк само по себе плохо. Проблема в количестве состояний и решений, которые reviewer должен держать в голове одновременно. Чем больше scope, тем проще пропустить маленькое предположение, которое потом окажется главным источником проблемы.

В итоге одна из самых полезных оптимизаций AI-workflow может оказаться довольно скучной: генерировать быстро, merge'ить маленькими кусками.

Может ли AI сам проверять AI

Конечно.

И это уже довольно полезно.

Можно отдать pull request другому агенту, попросить найти ошибки, проверить security, посмотреть missing tests или сравнить изменение с описанием задачи. GitHub, Sonar и другие инструменты уже активно развивают автоматический AI review.

Я бы просто не путал такой review с независимым доказательством корректности.

Модель, которая проверяет код другой модели, всё ещё подвержена примерно тем же ограничениям. Она может не знать скрытый business invariant, пропустить архитектурное ограничение или согласиться с очень убедительно выглядящим неправильным решением.

Поэтому AI reviewer хорошо работает как дополнительный слой.

Например pipeline может выглядеть так:

AI пишет изменение
        ↓
tests / static analysis
        ↓
AI first review
        ↓
human review
        ↓
merge

AI может бесплатно найти часть очевидных проблем до того, как pull request попадёт человеку. Это полезно.

Но я бы не строил:

AI написал
↓
AI одобрил
↓
production

если изменение имеет хоть сколько-нибудь заметную цену ошибки.

Probabilistic verification одного probabilistic output другим probabilistic system — не совсем тот уровень уверенности, который мне хотелось бы иметь для критичного кода.

Кто считается автором такого изменения

После нескольких месяцев работы с coding agents меня всё меньше интересует вопрос, кто физически написал конкретную строку.

Если я дал агенту задачу, он написал класс, я проверил изменение и отправил его в production — для системы не имеет никакого значения, сколько процентов текста набрал я руками.

Гораздо важнее другое: кто принял решение, что этот код можно merge.

AI может предложить решение. Может написать реализацию. Может добавить тесты и сам провести первый review.

Но ответственность за изменение пока всё равно остаётся на стороне команды.

И мне кажется, это полезная граница.

Потому что иначе появляется довольно удобная психологическая ловушка:

это не я написал, это AI.

Production от этого легче не становится.

Если изменение сломало платежи, архитектуру или данные пользователя, источник autocomplete уже мало кого волнует.

Поэтому я бы относился к AI-generated code примерно как к коду очень быстрого разработчика, который знает огромное количество технологий, никогда не устает, но не живёт внутри нашей системы и иногда совершенно уверенно придумывает детали.

Ему можно делегировать производство.

Нельзя автоматически делегировать judgment.

Что я теперь проверяю в AI-generated PR

Если свести всё к короткой последовательности, мой порядок примерно такой.

Сначала я проверяю задачу и scope. Что собирались изменить и почему diff получился именно таким.

Потом смотрю на необходимость нового кода. Можно ли было использовать существующую реализацию, не появилось ли duplication и не строим ли мы abstraction ради одного случая.

Дальше уже архитектура и business invariants. На правильном ли уровне находится логика, не сломаны ли boundaries, не нарушено ли правило системы, которое агент мог просто не знать.

После этого failure paths и security: ошибки, retries, transactions, concurrency, permissions, sensitive data.

Отдельно смотрю новые dependencies и tests. Особенно внимательно — тесты, которые были сгенерированы одновременно с реализацией.

Всё, что можно проверить deterministic способом, стараюсь вообще не обсуждать на human review. Для этого есть CI.

И только после этого остаётся обычная работа с читаемостью, naming и небольшими улучшениями реализации.

Получается примерно такой порядок:

Intent
↓
Scope
↓
Necessity
↓
Architecture
↓
Business invariants
↓
Failure paths
↓
Security
↓
Dependencies
↓
Tests
↓
Implementation details

Мне нравится, что syntax и formatting здесь находятся где-то вообще за пределами списка.

Так, наверное, и должно быть.

Вместо заключения

AI не отменил code review. Он просто изменил экономику вокруг него.

Код теперь можно производить значительно быстрее, чем команда способна его внимательно понять. Если продолжать относиться к review как к проверке строк, очень легко получить ситуацию, где pipeline генерирует всё больше аккуратного, протестированного и формально правильного кода, а система при этом постепенно становится сложнее.

Поэтому самая важная часть review смещается вверх.

Не «правильно ли написан этот if».

А почему появился новый сервис.

Почему изменение затронуло восемь файлов.

Почему нельзя было использовать старую abstraction.

Какой invariant сохраняет эта проверка.

Что произойдёт при частичном отказе.

И кто готов сказать, что именно это изменение действительно стоит оставить в системе.

В каком-то смысле код снова становится дешёвой частью разработки.

Дорогой частью остаётся понимание.

И чем лучше AI научится писать код, тем больше инженерной работы, кажется, будет находиться именно там.

Олег Пацай

Инженерный лидер — AI, архитектура и сложные системы

Проектирую и развиваю сложные программные системы: от архитектуры и инженерных практик до интеграции AI в разработку.

GitHub LinkedIn Telegram Связаться

Давайте делать сложное понятным.

Архитектура, инженерное лидерство и AI в разработке — когда система слишком важна, чтобы её упрощать, и слишком дорогая, чтобы ею не владеть.

LinkedIn Telegram Email