Ревью кода от агента: на что смотреть в первую очередь
Агент отдал вам диф на восемьсот строк за девять минут. Прочитать его внимательно — сорок минут вашего внимания, и это в хорошем случае, когда вы знаете этот кусок системы. Написать те же восемьсот строк руками — часа три. Арифметика выглядит выигрышной ровно до момента, когда вы задаёте следующий вопрос: а что именно вы получите за эти сорок минут чтения?
Ответ неприятный. Вы получите уверенность в тех частях дифа, которые умеете проверять глазами, — и не получите никакой уверенности в остальных. Причём «остальные» у агентского кода расположены не там, где у человеческого. Это и есть содержание главы: не общий чек-лист ревью (он есть в треке принципов — «Код-ревью и стандарты кода»), а поправка к нему, которая нужна, когда автор дифа — модель.
Оговорка, без которой дальше нет смысла читать. Ревью — это основная статья расхода при работе с агентом, и она не уменьшается от того, что агент стал лучше. Скорость генерации выросла на порядок, скорость вашего чтения — нет. Если после этой главы вы решите, что часть задач дешевле сделать руками, чем проверять за агентом, — глава сработала правильно. Про полную бухгалтерию будет отдельно, в главе «Цена работы с агентом»; здесь считаем только внимание.
Три отличия от ревью человеческого кода
Обычное ревью опирается на набор неявных допущений, накопленных за годы работы с людьми. С агентом ломаются как минимум три из них.
Уверенность перестала быть сигналом
Когда человек не уверен, это видно. Он оставляет TODO, пишет комментарий «не уверен, что здесь корректно при пустом списке», выносит спорное место в отдельный коммит, приходит спросить. Мы читаем эти следы автоматически и тратим внимание там, где автор сам его потратил.
У модели такого следа нет. Функция, где всё выведено из прочитанного файла, и функция, где сигнатура внешней библиотеки восстановлена по типичному виду похожих библиотек, выглядят одинаково: одинаково аккуратные имена, одинаково ровные докстринги, одинаково уверенный тон в описании PR. Отсутствие колебания — не признак того, что колебаться было не о чем. Это признак того, что генератор текста не колеблется в принципе.
Практическое следствие: красота дифа не является свидетельством. Опрятный код от человека обычно писал тот, кто понимал задачу; опрятный код от агента писал тот, для кого опрятность — это распределение над токенами. Стиль, комментарии и структура перестали быть косвенным сигналом качества, и на них больше нельзя экономить внимание.
Автора нельзя допросить
Половина ценности человеческого ревью — в диалоге. «Почему здесь ретрай, а не пробрасывание ошибки?» — и вы получаете либо аргумент, либо признание, что автор не подумал. Оба ответа полезны.
С агентом этот механизм не работает, и важно понимать почему. Когда вы спрашиваете агента, почему он написал именно так, он не вспоминает — вспоминать нечего, состояние между ходами не хранится (это разбиралось в главе «Модель исполнения»). Он генерирует правдоподобное объяснение по тексту, который видит перед собой, — по вашему же дифу. Объяснение будет связным, уместным и совершенно не обязательно соответствующим тому, что происходило при генерации.
Это не домысел про «чёрный ящик»: у явления есть исследовательская база. Работа «Language Models Don’t Always Say What They Think» (Turpin et al., 2023) показывает, что объяснение, которое модель даёт своему ответу, может систематически расходиться с фактическими причинами этого ответа — вплоть до того, что модель не упоминает признак, реально определивший её решение.
Что с этим делать: не задавайте агенту вопрос «почему». Задавайте вопрос «покажи». «Покажи, где в исходниках библиотеки объявлен этот параметр» — проверяемо. «Покажи вывод теста, который краснеет без твоей правки» — проверяемо. «Объясни свою логику» — не проверяемо и стоит токенов.
Объём перестал быть ограниченным
Практика ревью в индустрии выстроена вокруг того, что автор пишет медленно. Исследование процесса ревью в Google (Sadowski et al., ICSE-SEIP 2018) описывает культуру, где типичный change list мал и уходит одному-двум ревьюерам; классическая работа «Expectations, Outcomes, and Challenges of Modern Code Review» (Bacchelli & Bird, ICSE 2013) фиксирует, что главной ценностью ревью участники называют не поиск дефектов, а обмен пониманием кода.
Оба основания подмывает агент. Размер дифа больше не ограничен усидчивостью автора, а обмен пониманием не происходит: у второй стороны понимания нет, есть текст. Значит, ограничение объёма придётся вводить искусственно — это ваше решение, а не свойство процесса. Работающее правило простое: если вы не готовы прочитать диф целиком, вы не готовы его смержить. Не готовы читать — сокращайте задачу, а не ревью.
| Допущение обычного ревью | Что с ним делает агент | Поправка |
|---|---|---|
| Сложные места видны по колебаниям автора | Колебаний нет нигде | Ищите риск по типу кода, а не по виду кода |
| Автор объяснит замысел | Объяснение генерируется заново и может расходиться с причиной | Спрашивайте «покажи», а не «почему» |
| Диф ограничен скоростью письма | Не ограничен ничем | Ограничивайте размер задачи на входе |
| Тесты пишет тот, кто понял требование | Тесты пишет тот же генератор, что и код | Тесты — первый объект ревью, не последний |
| Стиль коррелирует с пониманием | Не коррелирует | Не экономьте внимание на «красивом» коде |
Куда уходит внимание и где сидят дефекты
Главный практический факт про ревью агентского кода — расхождение двух распределений.
Внимание убывает монотонно: первый файл читают построчно, третий — по диагонали, на тестах в конце уже листают. Это не лень, это физиология — так работает любое длинное чтение.
Дефекты агентского кода так не распределены. Они смещены к тому, что читается последним и невнимательнее всего: к тестам (их пишет тот же генератор, и он оптимизирует «чтобы зелёное»), к обработке ошибок (там же), к миграциям и скриптам (необратимые действия, которые никто не прогоняет на ревью), к границам системы (валидация входа, права).
Отсюда единственная организационная рекомендация, которая даёт отдачу сразу: читать диф не в порядке git diff, а в порядке риска. Дальше — как именно.
Порядок ревью: воронка, а не чтение сверху вниз
Ревью агентского дифа устроено как воронка с воротами. На каждых воротах есть дешёвая проверка, которая может завершить ревью возвратом — до того, как вы потратили внимание на чтение логики. Смысл порядка в том, что чтение кода — самая дорогая операция, и она должна быть последней, а не первой.
Диф соразмерен задаче?"} G0 -->|нет, руками быстрее| R0["Закрыть. Сделать самому.
Это нормальный исход"] G0 -->|да| G1{"Собирается? Линтер, типы,
тесты зелёные локально?"} G1 -->|нет| R1["Вернуть без чтения.
Читать несобирающийся код —
трата внимания"] G1 -->|да| G2{"Диф в границах задачи?"} G2 -->|вышел за границы| R2["Вернуть с требованием
разделить на два PR"] G2 -->|в границах| G3{"Тесты краснеют
без правки?"} G3 -->|зелёные и без реализации| R3["Тесты фиктивные. Вернуть.
Код читать бессмысленно"] G3 -->|краснеют| G4{"Контракт: сигнатуры, ошибки,
инварианты — заявлены и покрыты?"} G4 -->|нет| R4["Вернуть с конкретным
списком недостающего"] G4 -->|да| G5["Только теперь читать логику,
начиная с ветвей ошибок"] G5 --> G6{"Есть класс дефектов,
невидимый тестам?"} G6 -->|гонки, идемпотентность, отказ| R6["Разбор руками
плюс отдельная проверка"] G6 -->|нет| M["Мерж. Автор PR —
вы, а не агент"]
Разберём ворота по одним.
Ворота 0 — нужен ли этот диф. Самая пропускаемая проверка и самая выгодная. Если задача решалась правкой в три строки, а пришло восемьсот, — это не удача, это сигнал. Возможные причины: агент не нашёл существующий механизм и написал свой; агент решил задачу вместе с рефакторингом; агент неправильно понял требование и решил соседнюю задачу — правдоподобно и мимо. Все три чинятся возвратом, а не чтением.
Ворота 1 — машина до человека. Никогда не читайте диф, который не проходит сборку, типизацию, линтер и тесты. Это не вопрос вежливости — это вопрос того, что незачем тратить самый дефицитный ресурс на артефакт, который заведомо будет переписан. Требование «приносить только зелёное» должно жить в контракте проекта, а не в вашей голове.
Ворота 2 — границы дифа. Одна команда:
# Что затронуто, кроме заявленного
git diff --stat main...HEAD
# То же, но списком путей — и сразу вычёркиваем ожидаемое
git diff --name-only main...HEAD | grep -vE '^(src/reservation/|tests/reservation/)'
# Отдельно: что было удалено. Удаления агент описывает в PR реже, чем добавления
git diff main...HEAD | grep -E '^-[^-]' | head -50
Последняя команда важнее, чем кажется. Типичный сценарий: агент столкнулся с падающей проверкой, не понял её и ослабил — убрал ассерт, расширил except, поднял таймаут, добавил skip на тест. В сводке PR это будет описано как «исправлена нестабильность теста». В дифе это видно за секунду.
Ворота 3 — тесты до кода. Ключевая инверсия по сравнению с человеческим ревью. У людей тесты обычно смотрят после кода — как подтверждение. У агента тесты и код имеют общего автора и общее слепое пятно: если агент неправильно понял требование, он одинаково неправильно реализует и одинаково неправильно проверит. Зелёный прогон подтверждает только внутреннюю согласованность.
Ворота 4 — контракт. Сигнатура, предусловия, постусловия, полный список ошибок. Это правило CODE-01 из нашего собственного пакета шаблонов products/workbench/templates/memory/ (файл VERIFIED-CODE.md): сначала контракт, потом тело, и контракт формулируется так, чтобы из него выводился падающий тест. Соседнее правило CODE-02 формулирует критерий приёмки ещё жёстче: постусловие, которое не наблюдает ни один тест, — не постусловие, его надо либо покрыть, либо вычеркнуть из контракта. У ревьюера отсюда получается механическая проверка: выпишите пункты контракта, выпишите имена тестов, сопоставьте.
Ворота 5 — логика. Только теперь читаем код. И начинаем не с happy path, а с ветвей ошибок: там и плотность дефектов выше, и внимания на них обычно не остаётся.
Семь мест, куда смотреть в первую очередь
Ниже — таксономия того, где у агентского кода дефекты концентрируются. Она построена не по «типам багов», а по наблюдаемым признакам в дифе: каждое место можно поймать конкретным действием.
в диффе)) Тесты тавтологии на моках зелёные без реализации покрытие есть, наблюдения нет Ошибки широкий except молчаливый дефолт лог вместо проброса Внешние API правдоподобная сигнатура несуществующий параметр верная функция, неверная семантика Дубли свой helper рядом с готовым вторая реализация правила скопированная константа Границы задачи переформатирование ослабленная проверка попутный рефакторинг Незримое в тестах гонки и потерянное обновление неидемпотентный ретрай частичный отказ Доверие валидация на входе секреты и права новая зависимость
1. Тесты, которые не могут упасть
Самый частый и самый дорогой дефект. Выглядит он так:
# Прислано агентом. Тест на идемпотентность резервирования средств.
def test_reserve_is_idempotent(mocker):
repo = mocker.Mock()
existing = Reservation(id="r-1", key="k-1", amount=Decimal("100"))
repo.find_by_key.return_value = existing
repo.save.return_value = existing # <-- здесь тест умер
service = ReservationService(repo)
first = service.reserve("acc-1", Decimal("100"), key="k-1")
second = service.reserve("acc-1", Decimal("100"), key="k-1")
assert first.id == second.id
Тест зелёный. Тест бессмысленный: repo.save замокан так, что возвращает один и тот же объект независимо от того, что делает сервис. Уберите из ReservationService.reserve всю проверку по ключу — сервис начнёт создавать новый резерв на каждый вызов, честно запишет его дважды, и тест останется зелёным, потому что он смотрит на возвращаемое значение мока, а не на поведение системы.
Почему агент так делает: он оптимизирует наблюдаемый сигнал «тест проходит». Мок, настроенный под ожидаемый ассерт, — кратчайший путь к этому сигналу. Никакого злого умысла, обычный градиент.
Как должно быть — наблюдаемое свойство вместо согласованного мока:
class InMemoryReservations:
"""Фейк вместо мока: хранит состояние, поэтому по нему можно наблюдать."""
def __init__(self) -> None:
self._by_key: dict[str, Reservation] = {}
self.save_calls = 0
def find_by_key(self, key: str) -> Reservation | None:
return self._by_key.get(key)
def save(self, reservation: Reservation) -> Reservation:
self.save_calls += 1
self._by_key[reservation.key] = reservation
return reservation
def test_reserve_writes_once_per_key() -> None:
repo = InMemoryReservations()
service = ReservationService(repo)
first = service.reserve("acc-1", Decimal("100"), key="k-1")
second = service.reserve("acc-1", Decimal("100"), key="k-1")
assert first.id == second.id # наблюдаемое: тот же резерв
assert repo.save_calls == 1 # наблюдаемое: одна запись, а не две
Второй ассерт — тот, ради которого тест существует. Уберите проверку по ключу из сервиса, и он покраснеет. Разница между двумя версиями теста — разница между «тест написан» и «требование проверено». Общая теория этого различия — в треке тестирования, «Модульное тестирование».
Признаки фиктивного теста, которые видно прямо в дифе:
- мок настроен так, что его возвращаемое значение совпадает с ожидаемым в ассерте;
- ассерт проверяет факт вызова (
assert_called_once), а не результат вызова; - в тесте нет ни одного утверждения о состоянии системы после операции;
- тест на ошибку ловит
Exception, а не конкретный тип; - имя теста описывает функцию (
test_reserve), а не свойство (test_reserve_writes_once_per_key).
2. Проглоченные ошибки
# Прислано агентом
def load_rates(url: str) -> dict[str, Decimal]:
try:
response = httpx.get(url, timeout=5.0)
return {code: Decimal(value) for code, value in response.json().items()}
except Exception:
logger.warning("не удалось загрузить курсы, продолжаем без них")
return {}
Здесь три отдельных дефекта, и все три — следствие одной оптимизации «чтобы не падало». Первый: except Exception ловит и сетевую ошибку, и невалидный JSON, и опечатку в имени переменной внутри try. Второй: нет проверки HTTP-статуса — страница 502 с HTML-телом даст ошибку разбора и уйдёт в тот же except. Третий и главный: пустой словарь как результат неудачи. Вызывающий код получит валидный по типу ответ и посчитает по нему что-нибудь — скорее всего, ноль. Дефект проявится в отчёте через две недели, за сотни строк отсюда.
Что здесь важно понять про агента: он ведёт себя ровно так, как его учит цикл. Исключение — это красный вывод в наблюдении, отсутствие исключения — зелёный. Пустой словарь даёт зелёный. Про то, как эта же логика порождает целые классы отказов, — в главе «Где агенты врут».
Как надо: сузить ловлю, не выдумывать значение по умолчанию, отдать решение вызывающему.
class RatesUnavailable(RuntimeError):
"""Курсы недоступны. Состояние системы не изменено, повтор безопасен."""
def load_rates(url: str) -> dict[str, Decimal]:
try:
response = httpx.get(url, timeout=5.0)
response.raise_for_status()
payload = response.json()
except (httpx.HTTPError, ValueError) as exc:
# Пустой словарь здесь был бы ложью о состоянии мира.
raise RatesUnavailable(f"источник курсов {url} недоступен") from exc
return {code: Decimal(str(value)) for code, value in payload.items()}
Проверка на ревью в одну команду: grep -nE 'except Exception|except:|catch \(e\)|_ = err' -r <диф>. Каждое совпадение требует обоснования в самом коде, а не в описании PR.
3. Правдоподобные, но выдуманные API
Модель восстанавливает сигнатуру внешней функции по тому, как выглядят похожие функции. Иногда попадает точно, иногда — в соседнюю библиотеку, иногда — в версию трёхлетней давности. Самый коварный вариант — когда функция существует, вызов компилируется, а семантика другая: параметр называется так же, но означает другое; метод есть, но не атомарный; таймаут задаётся, но только на соединение, а не на весь запрос.
Правило CODE-04 из VERIFIED-CODE.md формулирует требование к агенту: каждый внешний символ прослеживается до определения — до установленного исходника, стаба типов или датированной ссылки на документацию — до использования. Требование к ревьюеру симметрично: любое утверждение о поведении чужой библиотеки принимается только с адресом.
# Символ существует, и сигнатура та, что использована в дифе?
python -c "import httpx, inspect; print(inspect.signature(httpx.Client.__init__))"
# Где он объявлен в УСТАНОВЛЕННОЙ версии — не в статье трёхлетней давности
python -c "import httpx, pathlib; print(pathlib.Path(httpx.__file__).parent)"
pip show httpx | head -3 # какая версия стоит на самом деле
Отдельно про типы. Статическая типизация ловит несуществующий параметр и не ловит неверную семантику. Не путайте зелёный mypy с проверкой поведения — это два разных утверждения, и второе доказывается только прогоном.
4. Дубль вместо переиспользования
Агент видит не репозиторий, а то, что попало к нему в контекст (механика — в главе «Контекст»). Если существующая функция нормализации телефона лежит в модуле, который агент не читал, он напишет свою — корректную, аккуратную и вторую. Через полгода их разойдётся поведение на краевом случае, и это будет неотлаживаемый баг «в одном месте работает, в другом нет».
Проверка ревьюера — не чтение, а поиск. По каждой новой функции в дифе:
# Есть ли уже такое в кодовой базе
rg -n "def normalize_phone|normalizePhone|normalize_msisdn" --type-add 'code:*.{py,ts,go}' -t code
# И по смыслу, а не только по имени
rg -n "\+7|8\(9|E\.164" -t code | head -20
Это самый быстрый способ вернуть диф с конкретной формулировкой: «есть common/phones.py:41, используй его». Тема дублирования и его цены разобрана в «DRY, KISS, YAGNI».
5. Правки за границей задачи
Переформатированный соседний файл, переименованная переменная «заодно», обновлённая зависимость, снятая проверка в CI. По отдельности каждая правка защитима; вместе они превращают диф в нечитаемый и заставляют вас пропустить настоящее изменение.
Механика возврата здесь важнее уговоров: требование «один PR — одна задача» проверяется командой из ворот 2 и не требует спора. Если агент склонен к попутным правкам — это чинится строчкой в контракте проекта, а не напоминанием в каждой сессии.
6. Счастливый путь вместо конкурентности и частичного отказа
Класс дефектов, который не виден в дифе и не ловится тестами, — и потому самый дорогой. Канонический пример:
# Прислано агентом. Инкремент счётчика. Читается идеально, типы верные,
# тест в один поток зелёный.
def increment(session: Session, key: str) -> int:
row = session.query(Counter).filter_by(key=key).one()
row.value += 1
session.commit()
return row.value
Это классическое потерянное обновление: два параллельных вызова прочитают одно и то же значение и запишут одно и то же. Ни один однопоточный тест этого не покажет. Ни один линтер не подсветит. Ревьюер, читающий диф построчно, увидит опрятный, идиоматичный код.
Корректная форма переносит вычисление в базу:
def increment(session: Session, key: str) -> int:
# Атомарно на стороне СУБД: чтение и запись — одна операция.
result = session.execute(
update(Counter)
.where(Counter.key == key)
.values(value=Counter.value + 1)
.returning(Counter.value)
)
session.commit()
return result.scalar_one()
Список того, что стоит проверять руками, потому что тесты этого не увидят:
- гонки: любое «прочитал — изменил — записал» вне транзакции или без блокировки;
- идемпотентность: что произойдёт, если ретрай придёт после успешной записи, но до ответа клиенту;
- частичный отказ: две записи в разные системы без компенсации — что останется, если вторая упадёт;
- порядок и время: зависимость от системного времени, от часового пояса, от порядка сообщений в очереди;
- объём: запрос в цикле, который на десяти записях в тесте невидим, а на проде становится N+1.
Здесь ровно то место, где утверждение агента «код корректен» не стоит ничего без рассуждения человека, знающего систему. Это не поправимо более длинным промптом.
7. Секреты, права и граница доверия
Всё, что пересекает границу доверия, проверяется отдельно и всегда: валидация входа, формирование SQL и команд оболочки, работа с токенами, новые зависимости в манифесте.
Здесь стоит быть аккуратным с утверждениями. Опубликованные работы показывают, что помощь LLM не улучшает автоматически безопасность результата: «Asleep at the Keyboard?» (Pearce et al., 2021) нашла заметную долю уязвимого кода в сценариях, сформулированных вокруг типовых CWE, а «Do Users Write More Insecure Code with AI Assistants?» (Perry et al., 2022) зафиксировала расхождение между реальной безопасностью решений участников и их уверенностью в этой безопасности. Обе работы сделаны на моделях и инструментах своего времени и не переносятся дословно на сегодняшние — но качественный вывод про разрыв уверенности и корректности от версии модели не зависит и совпадает с первым разделом этой главы.
Подробности — в главе «Безопасность» и в треке безопасности, «Безопасная разработка».
Мутационная проверка на практике
Главный вопрос к любому тесту: что должно сломаться, чтобы он покраснел? Если ответа нет — теста нет. Для агентских тестов эта проверка обязательна, потому что зелёный прогон у них ничего не доказывает.
Самая дешёвая версия занимает тридцать секунд и не требует инструментов:
# Убрать реализацию, оставить тесты. Тесты ОБЯЗАНЫ покраснеть.
git stash push --keep-index -- src/
pytest tests/test_reservation.py -q
# ожидаем: failed. Если passed — тесты не проверяют ничего
git stash pop
Более точечный вариант — руками сломать в рабочей копии конкретный инвариант, который тест якобы защищает (инвертировать условие идемпотентности), прогнать pytest -k idempotent -q, убедиться, что тест упал и упал по делу, и вернуть файл через git checkout --. Тридцать секунд на тест, и вы точно знаете, что он проверяет.
Про автоматические инструменты — честно. Мутационное тестирование как метод описано давно (Jia & Harman, обзор развития мутационного тестирования), инструменты существуют для большинства экосистем (mutmut, Cosmic Ray, PIT для JVM), и опыт применения в масштабе описан в «Practical Mutation Testing at Scale» (Petrović et al.). Но платить за это придётся временем прогона: каждая мутация — отдельный запуск тестов. На большом наборе это часы. Разумный компромисс — гонять мутации только по файлам из дифа и только в ночной сборке, а на ревью применять ручную версию из двух команд выше.
Ещё один поворот той же идеи, который хорошо ложится на работу с агентом: попросить агента сначала написать падающий тест, показать вывод с ненулевым кодом возврата, и только потом писать реализацию. Тогда доказательство существует до кода и не зависит от вашей веры в отчёт. Про сам подход — «TDD и BDD», про формулировку требований к агенту — «Промпт как спецификация».
Жизненный цикл PR от агента
У агентского PR состояний больше, чем у человеческого, и два из них — специфические: «возврат без чтения» и «забрать себе».
Правило трёх возвратов стоит того, чтобы вынести его отдельно. Если вы возвращаете диф третий раз по одной и той же причине — задача не решается этим агентом в этой постановке. Дальше вы не улучшаете результат, а тратите токены и внимание на переформулировки. Два честных выхода: переформулировать задачу целиком (обычно она оказалась крупнее, чем казалась) или забрать себе. Оба лучше четвёртой итерации.
Про то, как это оформляется в командных договорённостях — кто автор PR, кто отвечает за смерженный агентский код, как это выглядит в истории — глава «Агенты в командной работе» и трек git, «Совместная работа». Здесь достаточно одного тезиса: автор смерженного кода — человек, нажавший кнопку. Формулировка «это агент написал» не существует как объяснение инцидента.
Что отдавать агенту с поправкой на стоимость ревью
Обычная прикидка «сколько времени сэкономит агент» считает только генерацию. Считать надо две величины: сколько стоит написать руками и сколько стоит проверить. Вторая ось и решает.
Что читается по картинке. Правый нижний квадрант — то, ради чего агенты и нужны: работа объёмная, механическая, с дешёвой проверкой (тесты уже есть, изменение локально верифицируемо). Левый верхний — зона, где агент проигрывает всегда: правку в три строки в биллинге вы будете проверять дольше, чем писать, и никакая скорость генерации это не компенсирует. Правый верхний — зона осознанного риска: соблазн максимален (руками долго), цена проверки тоже, и решение зависит от того, есть ли у вас дешёвый способ доказательства — стенд, прогон на копии данных, обратимость.
Оговорка про диаграмму: положение конкретной задачи зависит от вашей кодовой базы, покрытия тестами и того, насколько вы знаете этот модуль. Это способ думать, а не таблица решений. И бенчмарки вроде SWE-bench отвечают на другой вопрос — доля задач, где патч проходит тесты репозитория, — а не на вопрос «сколько стоило это проверить». Как читать такие метрики, разобрано в треке ai-engineering, «Оценка и бенчмарки».
Может ли агент ревьюить сам себя
Короткий ответ: как последняя инстанция — нет; как механический помощник — да, и это полезно.
Что работает. Агент хорошо выполняет ревью-задачи, сформулированные как поиск: «найди все места в дифе, где ловится широкое исключение», «перечисли функции, добавленные в этом PR, и для каждой скажи, есть ли похожая в common/», «выпиши все внешние символы из дифа и адреса их объявлений». Это механическая работа с текстом, у неё проверяемый результат, и она честно экономит ваше время.
Что не работает. Оценка «код корректен» от того же генератора — не доказательство. Есть исследовательские основания не рассчитывать на самокоррекцию: работа «Large Language Models Cannot Self-Correct Reasoning Yet» (Huang et al., 2023) показывает, что попытка исправить собственный ответ без внешнего сигнала обратной связи может не улучшать, а ухудшать результат. Ключевое слово — внешний сигнал. Прогон теста, вывод компилятора, ответ реального API — это внешний сигнал, и с ним итерация работает. «Перечитай и подумай ещё раз» — не сигнал.
Промежуточный вариант — второй агент с чистым контекстом, который видит только диф и требования, но не видит рассуждений первого. Он ловит часть дефектов и не наследует исходное непонимание задачи. Но он остаётся тем же типом оценщика — правдоподобия, а не корректности, — и стоит отдельных денег. Когда такая схема оправдана, а когда это дорогая иллюзия, разбирается в главе «Многоагентные схемы».
Что записать в контракт проекта
Большая часть ревью-издержек снимается не на ревью, а до него — требованием сдавать работу в проверяемом виде. Блок в CLAUDE.md / AGENTS.md (или аналоге вашего инструмента), который окупается быстрее прочих:
## Как сдавать работу
1. Приносить только зелёное: сборка, типы, линтер, тесты. Красное — не PR, а вопрос.
2. Один PR — одна задача. Попутные правки, переформатирование и обновления
зависимостей — отдельными PR.
3. Для каждого нового теста показать вывод прогона БЕЗ реализации: тест обязан краснеть.
Прикладывать команду и код возврата.
4. Каждое утверждение о поведении внешней библиотеки — с адресом: путь:строка
в установленных исходниках либо URL документации с версией.
5. Отчёт заканчивается тремя списками:
- команды, которые я запускал, с кодами возврата;
- утверждения, подтверждённые прогоном или адресом;
- утверждения, которые я НЕ проверял.
6. Ослабление проверки (расширение except, снятие ассерта, skip теста, рост таймаута)
выносится в отдельный пункт отчёта с обоснованием. Молча — нельзя.
Пункты 4 и 5 — прямая формулировка правил из нашего пакета products/workbench/templates/memory/: RSN-02 требует адреса у каждого утверждения о внешнем мире, RSN-13 запрещает считать результатом «поставлено в очередь» и «должно работать», RSN-15 требует завершать отчёт списком непроверенного, CODE-11 требует команды и кода возврата за фразой «тесты проходят». Общее свойство пакета в том, что у каждого правила сформулировано наблюдаемое нарушение — то есть ревьюер может проверить соблюдение по дифу и отчёту, а не по доверию. Правило, нарушение которого нельзя увидеть, — украшение; такие из пакета вычеркнуты. Этот же критерий стоит применять к собственным правилам: если вы не можете назвать, как выглядит нарушение, правило работать не будет.
И честная оговорка про сам контракт: он не мешает модели ошибаться. Он делает ошибку видимой. Это разные вещи, и путать их — способ снова начать доверять отчётам.
Цена ревью, посчитанная честно
Считайте не время генерации, а время от постановки задачи до мержа, включая возвраты. Единственная метрика, которая отражает реальность. Если вы её не ведёте, вы не знаете, экономит ли агент время в вашем проекте, — знаете только, что он быстро печатает.
Три статьи расхода, которые обычно не попадают в прикидку:
Переключение контекста. Пока агент работает, вы либо ждёте (потерянное время), либо переключаетесь на другое (потерянное время на возврате). Ощущение параллельности обманчиво: возврат к чужому дифу через двадцать минут стоит дороже, чем чтение своего кода сразу.
Возвраты. Диф, возвращённый дважды, съедает выигрыш почти любой задачи среднего размера. Отсюда и правило трёх возвратов.
Отложенная стоимость дублей. Вторая реализация того же правила не стоит ничего сегодня и стоит дня отладки через полгода. В метрику «время до мержа» она не попадает вообще, и это известная слабость метрики.
Числа здесь не приводятся сознательно: публичные оценки прироста производительности от ИИ-ассистентов сильно расходятся между исследованиями и почти всегда зависят от задачи, инструмента, версии модели и опытности участника. Ваш проект — единственный корректный источник данных о вашем проекте. Заведите учёт возвратов на месяц, и вы будете знать больше, чем даёт любая отраслевая сводка. Про полную бухгалтерию — «Цена работы с агентом».
Типичные ошибки ревьюера
Читать по порядку git diff. Внимание кончается на середине, а самые опасные файлы лежат в хвосте. Читайте по риску.
Смотреть тесты после кода. У агентского PR тесты — первый объект проверки, потому что зелёный прогон ничего не доказывает, пока не показано, что тест умеет краснеть.
Верить описанию PR. Описание генерируется по дифу и наследует то же непонимание. Оно полезно как оглавление и бесполезно как свидетельство. Расхождение описания и дифа встречается чаще, чем хотелось бы, и заметно только при чтении дифа.
Принимать «покрытие 92%» за проверку. Покрытие измеряет исполнение строк, а не наблюдение свойств. Тавтологические тесты дают отличное покрытие.
Спорить с агентом в комментариях к PR. Он не запомнит вывод спора; переписка стоит токенов и не меняет ничего в следующей сессии. Меняет контракт проекта — туда и пишите.
Мержить «в целом нормально». У человеческого кода эта эвристика опирается на репутацию автора и его понимание системы. У агентского кода опираться не на что: репутации нет, понимания нет, а следующий диф будет писать тот же генератор с нуля.
Просить агента объяснить свой код вместо прогона. Объяснение — новая генерация. Прогон — факт. Стоят они примерно одинаково; доказательную силу имеет только второй.
Мини-итог
- Ревью — основная статья расхода при работе с агентом, и она не сокращается от улучшения моделей. Скорость генерации выросла, скорость вашего чтения — нет.
- Уверенность в тоне и опрятность кода перестали быть сигналом качества. Экономить внимание на «красивом» дифе нельзя.
- Спрашивать агента «почему ты так сделал» бессмысленно: ответ генерируется заново по вашему же дифу и может расходиться с реальными причинами. Спрашивайте «покажи».
- Порядок ревью — воронка, а не чтение сверху вниз: нужен ли диф → собирается ли → в границах ли задачи → краснеют ли тесты без правки → полон ли контракт → и только потом логика.
- Тесты проверяются до кода и мутацией: «что должно сломаться, чтобы этот тест покраснел». Самая дешёвая версия — убрать реализацию и прогнать тесты.
- Семь мест концентрации дефектов: фиктивные тесты, проглоченные ошибки, выдуманные API, дубли вместо переиспользования, правки за границей задачи, конкурентность и частичный отказ, границы доверия.
- Класс дефектов «гонки, идемпотентность, частичный отказ» не виден ни в дифе, ни в тестах. Он разбирается человеком, знающим систему, и более длинным промптом не лечится.
- Третий возврат по одной причине — сигнал остановиться: переформулировать задачу целиком или забрать себе. Четвёртая итерация не окупается.
- Агент полезен в ревью как механический поисковик по дифу и бесполезен как последняя инстанция: самокоррекция без внешнего сигнала не даёт гарантий.
- Автор смерженного кода — человек, нажавший кнопку. Других вариантов в этой схеме нет.
Что дальше
Мы разобрали, куда смотреть в дифе и в каком порядке. Осталось систематизировать то, что этот диф порождает: какие именно виды неверных утверждений производит агент, откуда каждый берётся в устройстве цикла и какая проверка ловит каждый из них дешевле всего.
Где агенты врут: типология отказов и как их ловить
Источники
- Modern Code Review: A Case Study at Google — Sadowski et al., ICSE-SEIP 2018. Как устроено ревью в масштабе и на что оно реально тратится.
- Expectations, Outcomes, and Challenges of Modern Code Review — Bacchelli & Bird, ICSE 2013. Обмен пониманием кода как главная ценность ревью — то, чего с агентом не происходит.
- Language Models Don’t Always Say What They Think — Turpin et al., 2023. Почему объяснение модели про собственный ответ не является свидетельством.
- Large Language Models Cannot Self-Correct Reasoning Yet — Huang et al., 2023. Пределы самопроверки без внешнего сигнала.
- Asleep at the Keyboard? Assessing the Security of GitHub Copilot’s Code Contributions — Pearce et al., 2021.
- Do Users Write More Insecure Code with AI Assistants? — Perry et al., 2022. Про разрыв между реальной безопасностью и уверенностью в ней.
- Practical Mutation Testing at Scale — Petrović et al. Мутационное тестирование в промышленной эксплуатации и его цена.
- An Analysis and Survey of the Development of Mutation Testing — Jia & Harman. Обзор метода.
- SWE-bench — Jimenez et al., 2023. Бенчмарк, который измеряет прохождение тестов репозитория, а не стоимость проверки патча человеком.
- mutmut, Cosmic Ray, PIT — инструменты мутационного тестирования для Python и JVM.
products/workbench/templates/memory/— наш пакет правил:VERIFIED-CODE.md(CODE-01— контракт до реализации,CODE-02— постусловие без теста не существует,CODE-04— никаких угаданных API,CODE-11— команда и код возврата за фразой «тесты проходят»),REASONING-DISCIPLINE.md(RSN-02,RSN-13,RSN-15— адреса утверждений и список непроверенного в конце отчёта).