Привет, Хабр! На связи Оля и Эрик из команды больших языковых моделей ecom.tech. Cегодня мы расскажем, как делали сервис для AI-ревью кода с встраиванием его в CI/CD пайплайн GitLab. Эта статья будет полезна тем, кто разрабатывает агентов для работы с кодом (и не только), а также кто хочет поближе познакомиться с ассортиментом граблей, на которые можно наступить при внедрении этого самого AI в производственные процессы.

А, собственно, зачем это ваше код-ревью?

Давайте честно: в современных реалиях код-ревью – уже давно не про то, как Вася ищет баги Пети и спасает прод, исправив один символ. За соблюдением синтаксиса следят линтеры, а corner кейсы покрыты тестами всех мастей.

И так тоже бывает
И так тоже бывает

Парадигма сместилась. Если раньше код-ревью было про то «как» написан код, то теперь оно больше про «почему» и «зачем». В процессе мы оцениваем:

  • Архитектуру. Не пытаетесь ли вы забить гвоздь микроскопом?

  • Инженерную зрелость. Является ли решение предсказуемым и устойчивым, даже если все тесты зеленые?

  • Безопасность. Не светите ли вы паролями и не оставляете ли неочевидные лазейки для злоумышленников?

  • Читаемость. Сможет ли Вася разобраться и поддерживать этот код, если завтра Петя на месяц уедет в леса Амазонки?

Но что точно осталось неизменным — это трудоёмкость. Ревью по-прежнему занимает время разработчиков (порядка 20% от рабочего времени), порой стопорит релизы и (что самое обидное) не всегда находит проблемы. Особенно если Петя пытается вмерджить 158 файлов в релизную ветку, а у Васи вчера был грипп, сегодня три встречи, а завтра — отпуск.

Поэтому изначальная постановка задачи звучала довольно амбициозно: «Сократить время ревью в командах разработки с помощью AI».

Космолёт, который не взлетел

На старте проекта, мы, как и полагается восторженным энтузиастам, планировали объять необъятное и сделать универсальный инструмент, который сможет оценивать бизнес-логику, качество кода, находить логические и системные ошибки, предлагать рефакторинг, а заодно варить кофе и чистить картошку. Назвали мы нашего чудо-бота Багослав Коммитьевич (как выяснилось, не случайно). 

Почему у нас не получилось и что пошло не так:

  • Глобальный контекст. Современные LLM-агенты хорошо справляются с генерацией кода, но хуже — с пониманием глобальной архитектуры и межмодульных связей. Без этого полноценная проверка крупных проектов просто невозможна (а зачастую приносит больше вреда, чем пользы).

  • Тепличные тесты. Первые версии AI-ревьюера мы тестировали на энтузиастах, которым было искренне интересно. Они готовили репозитории с искусственными ошибками, поэтому мы почти не проверяли поведение агента на живых проектах без ошибок (как оказалось – зря, ложные срабатывания строчили в нас аки из пулемета).

  • Зыбкость бизнес-критериев. Критерии для проверки бизнес-логики задавались кастомными промптами, т.е. создавались разработчиками под каждое новое ревью. Оказалось, что это совершенно нетривиальная задача (и довольно трудоемкая, так как каждый раз надо описывать словами, а что ты делал), и далеко не все формулировки оказываются понятными для агента.

  • Шум. В условиях нечетких формулировок и недостатка понимания структуры, агент начинал выдавать не совсем валидные комментарии (а временами совсем не валидные).

  • Психология. Полноценное ревью проекта — это почти как психотерапия: для эффективной работы нужен запрос от получателя. Если разработчик не готов к обратной связи, вмешательство AI только раздражает. Особенно когда человек хочет просто «окнуть» свои 5 строк (а получает 50 комментариев с рекомендациями по всему репозиторию).

Пример комментария от Багослава
Пример комментария от Багослава

Пересборка

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

1. Ревьюер – это инструмент для руководителей практик языка (Tech Leads / Team Leads). Он должен помогать в первую очередь им — поддерживать единые стандарты, отлавливать неочевидные антипаттерны и ошибки, которые не ловятся ни линтерами, ни тестами.
Например, при использовании локальных Map в качестве кэша, необходимо  проверять наличие механизма очистки или ограничения размера. А при обращении к БД уровень бизнес-логики должен избегать проблемы N+1 (когда вместо одного сложного запроса сервис делает N запросов в цикле).

2. Он работает по заранее подготовленным критериям. Их определяют как раз руководители практик, исходя из накопленного опыта проблем конкретного языка программирования.

3. Он интегрирован в GitLab и автоматически запускается в CI/CD. Без ручных триггеров — чтобы ревью происходило само, пока разработчик пьёт кофе.

4. Он минимально отвлекает разработчика. Агент оставляет комментарии только там, где находит проблемы по заданным критериям.

5. Он не находит проблемы там, где их нет.  И это, пожалуй, самое сложное.

Как научить LLM видеть код

Очевидно, что в рамках одного diff, закинутого в промпт, можно найти только проблемы этого diff – и то не все. Для полноценного вердикта нужен контекст – что это за проект, как он построен, куда ведёт изменение и какие части репозитория затрагивает. Живой ревьюер открывает файл целиком, смотрит определения, ищет usages, держит в голове структуру сервиса. У LLM ничего этого нет, пока вы явно не построите для неё такое окружение.

Итого «видение кода» — это не один промпт, а слоеный пирог. Разберем, что положить в контекст заранее, а что дать модели найти самой.

Дисклеймер: рекомендации построены исключительно исходя из нашего опыта.

Итак, что мы даем LLM сразу:

Diff. (Спасибо, кэп). Мы перепробовали несколько форматов и остановились на человекочитаемом — аналогичном тому, что видим в интерфейсе GitLab: плюсы/минусы, нумерация строк, тип изменений (новый файл, редактура старого).

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

Survey - общий обзор репозитория. Его формирует суб-агент, который обходит репозиторий с помощью ограниченного набора инструментов (list_directory и read_file). Он собирает: язык программирования, фреймворки и библиотеки; структуру проекта (многомодульный/монорепозиторий); конфиги форматтеров и линтеров; CI/CD; архитектурные заметки из README; переменные окружения; нестандартные правила оформления, а также особенности рабочих процессов и неочевидные подводные камни (что считать особенностями и «камнями» – отдано на откуп самой LLM). Получается что-то такое.

Критерий. Четко сформулированный специализированный критерий — путь к успеху.  В отличие от размытого «проверь, что изменения не нарушают работу кода», он помогает модели действовать целенаправленно, экономя время и токены. «Общие» формулировки размывают фокус и модель начинает искать все и сразу.

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

Системный промпт. Качественно проработанный, структурированный системный промпт, объясняющий правила игры: основные принципы, критические моменты, формат вывода, подробное описание инструментов с базовыми примерами. В ходе многочисленных экспериментов мы отказались от подробных примеров и жестких цепочек действий — агенту они скорее мешают и служат триггером для галлюцинаций (LLM может «выдумать» нужное на основе промпта, если данных в репозитории не хватает).

Все остальное LLM ищет уже сама с помощью MCP. Об этом расскажем чуть дальше.

Эволюция подходов: пилот

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

Модуль №1 — парсил diff и переводил в Gitlab-like формат.

Модуль №2 — строил карту релевантности между требованиями (aka критериями) и измененными файлами.

Модуль №3 — оценивал, достаточно ли текущего diff для вынесения вердикта по каждой релевантной паре «файл–критерий» или  нужно смотреть всю репу..

Модуль №4 — уточнял поисковые запросы: подключал языковые парсеры (на тот момент – библиотека grep-ast), определял, нужны ли импорты, связанные файлы или целые код-паттерны.

Модуль №5 — аккумулировал весь собранный контекст, передавал его LLM и возвращал вердикт – лаконичный SATISFIED или развернутый UNSATISFIED с обоснованием и привязкой к конкретным строкам.

Главной слабостью этого варианта была избыточность кодовой информации при одновременном недостатке информации структурной (просто дать дерево проекта вдогонку к валу найденных строк оказалось недостаточно). В итоге ревьюер вел себя как старательный junior-разработчик - справлялся с изменениями, локализованными в нескольких файлах, и начинал бессмысленно и беспощадно комментировать все подряд, как только в контекст попадали не связанные находки из других частей репозитория.

Эволюция подходов: первая продакшн версия

Структура кастомного критерия
Структура кастомного критерия

На следующей ступени эволюции мы решили отказаться от жесткой мультиагентности и предоставить ревьюеру больше свободы. Для этого мы подключили механизм tool calling и написали четыре базовых инструмента: read_file, list_directory, glob_search, grep_search. Агент получил возможность самостоятельно решать, когда и что искать, вместо того чтобы следовать предопределённой цепочке действий.

Также мы дали пользователям возможность максимально гибко настраивать критерии, задавая не только промпт, но и допустимые инструменты, разрешенные/ запрещенные расширения, паттерны названий файлов, область видимости в репозитории, а также делать критерий обязательным к исполнению или нет. Еще ввели параметр scope (diff или repository), который позволял переключать режим ревью между проверкой diff и общим анализом всей структуры репозитория.

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

Параллельно мы пересобрали внутреннюю механику. Ключевым нововведением стал Checklist State — внутренний менеджер-зануда, который заставлял LLM предварительно декомпозировать задачу на простые проверяемые шаги. Этим он помогал агенту не терять фокус, отслеживать прогресс и бороться с галлюцинациями: каждый пункт чек-листа требовал доказательств (или обоснования своего выбора).

Для нерелевантных критерию изменений мы добавили третий вариант вердикта наравне с SATISFIED/ UNSATISFIED – NOT APPLICABLE (если изменения в файле не имеют отношения к проверяемому критерию), который избавил LLM от необходимости выбирать между двух огней (как оказалось, LLM нервничают и ошибаются, когда им ставят условие “или-или”).

В итоге ревьюер научился работать с изменениями достаточно высокой сложности и адекватно обрабатывать требования по структуре репозитория. Однако проблема, которую мы так и не победили в этой версии – формирование глубокого понимания проекта помимо diff, шаг за шагом. На практике это выражалось в том, что вместе со вполне релевантными комментариями в «зоне влияния» diff агент регулярно оставлял замечания в самых неожиданных местах, не относящихся к измененной части. Так в нашем пайплайне появились три новых слоя: семантический поиск, MCP и LSP.

Эволюция подходов: финальная версия

Semantic Search: не точное совпадение, а смысл.

Схема семантического поиска
Схема семантического поиска

Идея была простой: дать модели возможность искать не строки, а смысловые блоки кода. Мы построили RAG-пайплайн: код репозитория индексируется в векторную БД, и поиск идет по семантической близости, а не по точному совпадению.

При запуске сервиса на каждом новом MR, из всего репозитория отфильтровываются кодовые и конфигурационные файлы. Их мы разбиваем на чанки с помощью библиотеки tree-sitter - извлекаем функции, классы, блоки импортов и другие значимые AST-узлы. Дальше векторизуем их (используем модель Qwen3-Embedding-4B) и сохраняем в PostgreSQL с расширением pgvector, которое реализует векторный поиск в концепции HNSW. Крупные блоки дробятся, а если AST по каким-то причинам не строится — используем fallback с простой разбивкой файлов по строкам с перекрытием. При повторном обращении к MR вектора пересчитываются инкрементально: только для изменённых файлов.

Семантический поиск используется дважды. Первый раз — на этапе «прогрева» ревью (об этом ниже) для оценки общей релевантности репозитория критерию. Получив критерий, агент генерирует c помощью LLM три поисковых запроса под него (HyDE запросы), ищет наиболее подходящие чанки, забирает топ-5 результатов, дедуплицирует их, объединяет и с помощью той же LLM использует для предварительной проверки релевантности. Второй раз - как обычный инструмент. Но здесь он скорее «запасной» вариант: агент прибегает к нему в основном, когда точный поиск через MCP-инструменты не дал результата.

MCP

Следующим шагом стал переход от рукописных инструментов к использованию готовых библиотек с протоколом MCP (Model Context Protocol). В первую очередь нас интересовали инструменты на базе tree-sitter (да, мы очень любим эту библиотеку), которые позволили бы более качественно осуществлять поиск по репозиторию и строить цепочки внутренних вызовов в проекте.

Забегая вперед – своего «идеального героя» мы так и не нашли. Протестировав три MCP-сервера на наших основных языках (Kotlin, Go, PHP, TypeScript, а также Ruby, Elixir и Python), мы убедились, что они покрывают далеко не весь заявленный функционал, а на каких-то языках работают вообще через раз. В итоге мы остановились на наиболее компактном и устойчивом варианте – msp_tree_sitter, несмотря на то, что построение цепочек вызовов он не поддерживает. Инструментов в нем немного (find_usage, search_code, analyze_code, check_errors), но благодаря гибким настройкам они дают LLM достаточно возможностей для навигации по коду. Архитектурно MCP поднят как отдельный прокси сервер, к которому ревьюер подключается по HTTP/SSE.

LSP

Поскольку вопрос структурной навигации не был покрыт полностью, мы приступили к освоению Language Server Protocol. Изначально мы планировали, что обращение к языковым серверам станет ещё одним инструментом, которым агент сможет управлять через tool calling. Но практика показала, что у ревьюера на эту тему свое мнение – без жесткой директивы использовать именно LSP, он предпочитает читать файлы целиком — это проще и привычнее. Так мы пришли к решению вынести сбор структурной информации на этап прогрева.

Мы подняли LSP-сервера для всех семи языков как отдельные прокси с первичным подключением по HTTP и дальнейшим взаимодействием через WebSocket. При старте ревью определяется язык репозитория, выбирается соответствующий сервер, инициализируется подключение (для Go дополнительно открывается go.mod и устанавливается таймаут на прогрев), после чего можно отправлять запросы.

На этапе прогрева для каждого изменённого файла мы отправляем несколько LSP-запросов и получаем:

·  Полную структуру измененной части — классы, функции, импорты, их иерархию.

· Семантические ссылки — это принципиальное отличие от текстового поиска: если вы ищете username, LSP найдёт не все строки с этим словом, а именно использование конкретного класса или переменной.

· Call hierarchy — кто и откуда вызывает этот код, как он встроен в общую картину проекта.

Собранная информация агрегируется в File Structure Map — структурированное представление диффа с привязкой к AST. И вот здесь случился важный инсайт: LLM оказалась гораздо более эффективной именно с таким «переваренным» контекстом, чем с сырым набором файлов и импортов.

Структура ML-блока

Итак, наше ревью состоит из трёх основных этапов: подготовки контекста, прогрева, и, собственно, самого ревью.

 Многоэтапная архитектура ревью
 Многоэтапная архитектура ревью

Сначала мы собираем контекст. Здесь клонируем репозиторий и векторизуем его, парсим diffs и обрабатываем до человекочитаемого формата, собираем survey репозитория. Все собранные части кэшируются и потом переиспользуются - если мы ревьюим MR повторно, то заново векторизуются/ обрабатываются только файлы, чей hash изменился при новом проходе.

Затем запускаем прогрев. Сначала надо оценить релевантность критерия репозиторию - ведь зачем проверять правильность обращения к БД в проекте, который их не использует? Это мы делаем с помощью семантического поиска – процесс описан выше. Если релевантность подтверждена, заполняем triage-matrix – матрицу релевантности критерия измененным файлам (даже если БД в репозитории есть, далеко не каждый измененный файл может относиться к этой функциональной части). Благодаря этому этапу отсева нам удалось сократить количество сканируемых пар «критерий-diff» на 40%.

Структура ML-блока
Структура ML-блока

Для отобранных в triage matrix дифов строим структурные карты FileStructureMap с помощью LSP – они становятся последней подготавливаемой частью нашего контента для LLM.

Далее вся собранная информация динамически собирается в промпты и запускается этап ревью.

И, наконец, ревью. Здесь мы позволяем LLM исследовать репозиторий в итеративном цикле, где она смотрит на контекст и собранные «доказательства» и решает – еще собрать дополнительную информацию с помощью инструментов или перейти к вынесению вердикта. Доступные LLM инструменты: read_file, read_many_files, list_directory, get_mr_diff (посмотреть содержимое других diff), semantic_search и 4 MCP-tools: find_usage, search_code, analyze_code, check_errors.

При исчерпании 85% итераций LLM получает friendly reminder, что неплохо бы и заканчивать этот углубленный ресёрч. Если после 100-й итерации она не остановилась - цикл прерывается и уходит на принудительное вынесение вердикта (это редкий сценарий и чаще связан с неточной формулировкой критерия). Также если LLM совершает 5 одинаковых ошибок подряд (например, открывает несуществующий файл) – цикл также прерывается, и агент переходит к вынесению вердикта с тем, что есть. Если же совершено 5 однотипных действий (например, открывается один и тот же файл) – LLM получает подсказку о смене инструмента.

Перед вынесением вердикта происходит обязательная суммаризация собранного контента – LLM получает задачу извлечь из всего объема информации найденные отклонения и ключевые находки. В итоге на финальный этап поступает не содержимое пары десятков файлов, а концентрированная структурная выжимка. В финале модель формирует вердикт в виде одного из трех тегов - SATISFIED, UNSATISFIED, NOT_APPLICABLE. Если тега нет, система принудительно запускает его извлечение. Теги SATISFIED и NOT_APPLICABLE не содержат комментариев (даже если LLM их добавила – агент их не парсит). Для UNSATISFIED LLM должна предоставить путь к файлу, описание нарушения и строки, в которых это найдено и на базе которых потом добавляются комментарии в Gitlab. Также в Gitlab уходит статистика об использованных инструментах и токенах и ссылка на логи на s3.

Большую часть времени наших изысканий сервис работал на LLM Qwen3-Coder-480B-A35B-Instruct – одной из самых сильных кодовых моделей на момент начала разработки сервиса. Сейчас мы переключили его на GLM 5.1 – и это дало приятную добавку к мощности и качеству. Все LLM мы разворачиваем локально на HGX (каждый HGX это 8 Nvidia H200) с помощью движка SGLang. В текущей реализации мы связали 2 HGX (на каждом крутится GLM 5.1) через умный роутер и распределяем между ними нагрузку по кэшу запросов. 

Вместо послесловия

Сегодня AI-ревьюер — это уже не эксперимент, а полноценный production-сервис, который ежедневно помогает командам разработки поддерживать высокие стандарты качества кода. На текущий момент система задействована более чем в 700 проектах и делает от 400 до 500 ревью в день. Среднее время одного ревью 10-15 минут - и это с учётом всех этапов: от прогрева до вынесения финального вердикта. 

Метрики нашего ревьюера в проде
Метрики нашего ревьюера в проде

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

Также мы рады сообщить, что с этим проектом мы вышли в финал премии Generation AI Awards 2026 в номинации «Лучшее применение генеративного AI для разработки и тестирования». Для нас это не только признание, но и подтверждение того, что мы движемся в правильном направлении.