Site icon AppTractor

Как DoorDash создал систему проверки кода на основе ИИ, к мнению которой инженеры действительно прислушиваются

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

Мы хотели сделать что-то другое. Последние несколько месяцев мы постепенно разворачивали агента для code review внутри инженерной организации DoorDash. Ключевой проблемой в итоге оказалось внимание: помочь агенту сфокусироваться на тех частях изменений, которые действительно заслуживают пересмотра, и молчать, когда ему нечего полезного добавить.

Планка, которую мы для себя установили, была не в духе «находить проблемы». Вопросы были такими:

Вот к чему мы пришли.

Метрики

В типичную неделю агент ревьюит более 10 000 pull request’ов в 56 подключённых репозиториях: Go-бэкенд-сервисы, iOS- и Android-приложения, веб-фронтенды, инфраструктура, конвейеры данных. Многие ревью запускаются автоматически при открытии PR — инженерам не нужно отдельно запрашивать проверку. Агент работает на тех же PR, что и люди-ревьюеры, часто ещё до того, как человек успевает посмотреть diff.

Вот несколько метрик, которые мы отслеживаем:

Как мы к этому пришли

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

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

Во второй версии появились два параллельных review-агента общего назначения, каждый из которых видел изменение целиком. Они стали лучше находить архитектурные проблемы и ошибки на границах компонентов, потому что у них появился контекст, необходимый для понимания того, как части системы связаны между собой. Но у каждого ревьюера оказалось слишком много задач в рамках одной сессии: прочитать весь diff, проверить его на соответствие всем правилам, применимым к изменённому коду, проследить callers, проверить sibling-реализации и провалидировать каждое потенциальное замечание. Внимание распылялось по всему изменению, и реальные проблемы иногда терялись. Мы упускали критические issues не потому, что ревьюеры не умели их находить, а потому что система не умела определять, что именно заслуживает глубокого расследования.

В третьей версии перед ревьюерами появился «ведущий специалист» (lead scout) — и именно это изменение оказалось самым важным. Задача lead scout’а — ничего не верифицировать. Он просто читает diff и замечает вещи, которые выглядят подозрительно: «это удаление выглядит странно», «этот enum-case не обработан в sibling-файле», «этот error-path молча проглатывает ошибки». На выходе он формирует список направлений для расследования (investigation leads). После этого два deep-reviewer’а берут эти leads и начинают копать глубже: подтверждают те, которые действительно оказываются проблемами, и отбрасывают ложные.

Scout одновременно делает две вещи. Очевидная — создаёт список подозрительных мест в diff. Менее очевидная — отфильтровывает части изменений, которые не требуют пристального внимания. К моменту запуска продвинутых ревьюверов они уже не пытаются оценивать каждую строку. Они концентрируются на нескольких местах, где действительно может быть проблема, а остальная часть изменений остаётся лишь поддерживающим контекстом. Именно этот фокус позволяет им уходить достаточно глубоко, чтобы находить архитектурные проблемы, которые пропускала вторая версия.

Это гораздо ближе к тому, как код на самом деле ревьюят senior-инженеры. Они не пытаются исчерпывающе проверить каждую строку. Они замечают интуитивный сигнал или паттерн, который уже видели в неудачных случаях раньше, а затем углубляются именно в те части, которые заслуживают внимания. Разделение этапов «заметить» и «проверить» позволило нам глубже анализировать действительно важные вещи, не распыляя внимание на всё остальное.

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

На высоком уровне review pipeline теперь выглядит так:

Смысл был не в том, чтобы заставить агента ревьюить код точно так же, как человек. Люди-ревьюеры лучше понимают намерения, инженерный вкус и способны оценивать, приемлем ли тот или иной компромисс. Агент же лучше справляется с терпеливым, повторяющимся вниманием: проверкой удалений, трассировкой вызовов, сравнением sibling-реализаций и последовательным применением доменных правил.

Полезная система — это их комбинация, где AI покрывает те пути ревью, которые люди предсказуемо просматривают по диагонали.

Принцип дизайна: precision важнее recall

Самым важным решением при создании системы был отказ от оптимизации под принцип «поймать всё».

Типичный подход — показывать каждое потенциальное замечание, ранжировать их и оставлять фильтрацию человеку. Цена такого подхода предсказуема: спам в PR, выключенные уведомления, игнорируемые комментарии.

Мы пошли в противоположную сторону.

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

Из этого следуют несколько последствий:

Показатель принятия в 60% — результат именно этого компромисса. Мы лучше пропустим часть проблем, чем превратимся в шум. Ревьюер, которого замьютили, не находит ничего.

Вопросы, которые нас больше всего волнуют, редко бывают непонятны человеку в отрыве от контекста. Они сложны потому, что требуют правильного контекста в правильный момент: понимания того, какое удаление меняет контракт, какой enum имеет sibling-маппинги, какое доменное правило отсутствует в CI и какая правдоподобная проблема на самом деле безвредна.

Задача системы — принимать такие решения последовательно на тысячах несвязанных PR, не засыпая авторов предположениями.

Сфокусированный контекст важнее широкого

Стандартный подход к AI code review — загрузить в модель как можно больше контекста: весь AGENTS.md, полный diff, возможно README репозитория. А дальше надеяться, что модель заметит правильные вещи.

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

Инсайт, который изменил наши результаты: файлы AGENTS.md и CLAUDE.md пишутся для инженеров, создающих код, а не для инженеров, ревьюящих код. Они смешивают архитектурные рекомендации, setup-инструкции, шаблоны разработки и заметки о стиле в одном документе. Для написания кода это полезно. Для ревью — шумно.

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

Упрощённый профиль выглядит примерно так:

name: Data Integrity Review
description: >
  Review rules for changes where data shape may stay the same but meaning changes.
  Focuses on semantic migrations, cross-layer readers, and silent compatibility breaks.

rules:
  - id: FIELD_SEMANTIC_MIGRATION
    severity: critical
    description: >
      When a field's value-space meaning changes, all readers of that field
      must be audited. The field name and type may remain identical, making
      the change invisible in diffs. Check struct fields, storage columns,
      feature-flag contexts, analytics events, and URL parameters. During
      transition periods where old and new meanings coexist, callers must
      explicitly choose which value they need.
    evidence: >
      PR #48291 changed AccountId from a legacy integer string to a globally
      unique resource ID, breaking downstream analytics readers that still
      parsed it as an integer. Reverted in PR #48904. Pattern recurred in
      PR #50177 and PR #50632 when same-name accessors began returning
      different identifier semantics without updating all consumers.

Каждое потенциальное правило проходит через намеренно жёсткий фильтр, прежде чем попасть в профиль: если это уже ловит CI — правило отбрасывается; если LLM и так знает это из общего обучения — правило отбрасывается; если мы не можем указать конкретный файл и строку в кодовой базе как подтверждение — правило отбрасывается. В результате остаются только те review-знания, которые действительно специфичны для DoorDash — то, что заметил бы senior-инженер именно этой команды.

После этого включается routing. Когда PR затрагивает платежный шлюз-сервис-провайдер, агент загружает правила PSP, правила ядра платежей и правила монетарной безопасности  — и ничего больше. PR, затрагивающий пользовательский поток, получает совершенно другой набор правил. Агент, ревьюящий изменения в обнаружении фрода, фактически является другим ревьюером по сравнению с агентом, который проверяет изменения в ценах, потому что они опираются на разные инженерные доктрины.

Роутинг — одна из главных причин, почему уровень принятия остаётся стабильным на 56 очень разных репозиториях. Агент не применяет один универсальный стандарт. Он применяет именно тот стандарт, который важен для конкретного изменения.

Почему мы построили это сами

Мы создали систему самостоятельно, потому что хотели, чтобы review-агент глубоко понимал DoorDash, а не просто код в целом.

Владение системой даёт нам несколько важных преимуществ.

Глубокое понимание того, как мы работаем. Review-профили, описанные выше, возможны только потому, что мы полностью контролируем конвейер от начала и до конца. Мы можем кодировать доктрину, специфичную для репозиториев, историю инцидентов и те неявные знания, которые senior-инженеры держат в голове и которые почти никогда не попадают в публичную документацию.

Полный доступ ко всей кодовой базе, а не только к diff. Агент работает на удалённых VM с полным клоном репозитория. Это позволяет ему делать то же, что делает человек-ревьюер: трассировать вызовы по всему монорепозиторию, находить sibling-реализации, читать тесты, покрывающие изменяемый код, подтягивать контекст из любой части кодовой базы. Это также позволяет нам запускать современные системы управления кодом с полным доступом к файловой системе и инструментам. Большинство багов, которые действительно стоит ловить, живут не в diff. Они живут в зависимостях diff.

Гибкость в выборе вендоров. Ландшафт моделей, лежащих в основе, меняется очень быстро. Контроль над системой позволяет нам менять модели по мере появления лучших вариантов — от OpenAI, Anthropic, open source и других — и затем оценивать каждую из них на одном и том же наборе инцидентов, который мы используем для оценки собственных изменений. Архитектура агента намеренно сделана независящей от моделей.

Контроль стоимости. Сегодня одно ревью в среднем стоит около $3. Для класса проблем, которые мы пытаемся ловить, это уже разумная стоимость. Но более важный момент в том, что стоимость можно гибко настраивать. Поскольку workflow разбит на этапы, мы можем использовать более дешёвые модели для простых шагов, резервировать более сильные модели для тяжелых этапов верификации,  пропускать дорогие стадии на маловажных PR, использовать eval’ы и production acceptance, чтобы снижение стоимости не приводило незаметно к ухудшению качества.

Замыкание цикла от проверки к исправлению

Полезные комментарии к проверке по-прежнему создают работу. Та же система, которая находит проблемы, помогает замкнуть этот цикл. Когда агент публикует обнаруженную проблему или когда рецензент оставляет комментарий, любой может ответить в ветке запроса на слияние на GitHub, отметив агента и попросив внести изменения. Например: «Можете ли вы обработать проверку на nil здесь?» или «Разрешите конфликты слияния».

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

Несколько преимуществ для инженеров:

Речь не идет об отмене ответственности инженера. Речь идет об устранении механической передачи между «проверка обнаружила проблему» и «кому-то нужно прервать свою работу, чтобы исправить ее».

В чем реальная польза

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

Вот несколько примеров того, в чём он особенно хорош:

Удаления. Люди бегло просматривают удалённый код. Добавления выглядят опасно; удаления — как очистка. Но удаление поля структуры, флага конфигурации, поведения по умолчанию или метода интерфейса может незаметно изменить поведение во время выполнения, в то время как код всё ещё компилируется, а тесты проходят. Агент рассматривает каждое удаление как запрос: кто от этого зависел? Что раньше было верным, а теперь нет?

Переход между границами. Когда PR обновляет одну сторону границы, например, адаптер одного бренда, один из двух производителей или один обработчик перечисления, агент ищет соседние элементы, которые не были обновлены. Эти ошибки не проявляются в CI, потому что каждая сторона компилируется самостоятельно.

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

Уроки инженерной работы из продакшена

Некоторые уроки стали очевидны только после запуска этого на реальных запросах на слияние.

Счетчик шагов не является детектором прогресса. Один из ранних сбоев оказался крайне затратным: один и тот же запрос модели мог повторяться снова и снова, в то время как проверка не продвигалась. Ограничения максимального количества шагов не обнаруживали этого, потому что цикл не продвигал счетчик. Нашей первой защитой был единый общий тайм-аут для всей проверки, но это все равно позволяло одному зависшему агенту сжигать токены в течение 20 минут, прежде чем выполнение останавливалось. Мы перешли к более жестким мягким и жестким тайм-аутам для каждого агента. Мягкий тайм-аут прерывает работу агента и просит его прекратить расследование, отбросить все спекулятивные данные и вернуть только результаты, которые он уже проверил. Жесткий тайм-аут — это последний выключатель. Он превращает дорогостоящий зависший запуск в ограниченный результат, который все еще может быть полезен автору, вместо того, чтобы выбрасывать всю работу, выполненную агентом до того, как он завис.

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

Правильные находки всё ещё могут быть плохими комментариями. Общие сводные заметки, слабые формулировки типа «рекомендуется проверить» и дублирующиеся комментарии в повторных проверках подрывают доверие, даже если основная проблема реальна. Рабочие комментарии привязаны к изменённому файлу и строке, объясняют конкретное рискованное поведение и указывают автору, с чего начать. Если мы не можем определить этот пункт действий, система исключает проблему из встроенной проверки или отклоняет её.

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

Оценки — это цикл разработки.

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

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

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

Что дальше?

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

Источник

Exit mobile version