Привет, Хабр! Я Полина Ященко, старший инженер по разработке ПО в YADRO. Мы с командой тестируем гипотезы и активно применяем искусственный интеллект, чтобы усовершенствовать процессы разработки. Так, недавно мы зарелизили AI-ревьюера — бота-помощника, который помогает искать проблемы в стиле и логике кода.
Мы разработали бота, чтобы упростить процесс ревью пулл-реквестов. В команде есть стажеры, которые совершают базовые ошибки, включая открытие очень больших PR, и иногда просто не хватало сил, чтобы вовремя их смотреть. Как мы выбрали модель и разрабатывали серверную часть, расскажу под катом. Отмечу, что наш бот не отличается высокой производительностью, зато отлично решает свою задачу — помогает инженерам находить и исправлять повторяющиеся ошибки.
Что за AI-ревьюер
AI-ревьюер — бот для пулл-реквестов (Pull Request, PR), который анализирует добавленный и измененный код и оставляет комментарии с предложениями по улучшению. Он находит проблемные места, касающиеся стиля, логики и конструкций кода. Ревьюеру остается только просмотреть запрос и проанализировать параметры, касающиеся дополнительных проблем: присутствие pytest-марки, соответствие автотеста его описанию в TestY TMS.
TestY TMS — это система управления тестами с открытым исходным кодом, которую разработали в YADRO. Команда TestY выпускает ежемесячную email-рассылку, в которой рассказывает об особенностях системы, а другие QA-инженеры YADRO делятся опытом тестирования в разных направлениях: от телекома до разработки СХД. Кстати, о нашем AI-ревьюере читатели рассылки узнали первыми. Подписывайтесь, чтобы не пропустить новый выпуск.
Как исследовали модели
Мы запланировали развернуть бот для работы с двумя репозиториями, в которых лежат автотесты. Нам нужно было проанализировать разные модели и определиться, какую будем использовать для бота, а также решить, как развертывать сервер с этой моделью.
Выбор модели
Исследование моделей проводилось от самых маленьких к самым большим — так мы определяли модель с минимальной нагрузкой и максимальными аналитическими способностями. Для работы на CPU-сервере модели использовались в формате GGUF-файлов, так как они лучше оптимизированы для быстрой загрузки и сохранения.
Обычно для использования локальной модели нужно скачать ее репозиторий, и уже в коде развертывать из него модель. У LLM есть свой GitHub под названием HuggingFace, там находятся репозитории моделей в оригинальной, несжатой версии.
У всех сжатых моделей есть свой параметр квантования. Квантование LLM — это процесс сжатия модели путем снижения ее параметров с высокой нагрузки до низкой. Каждый параметр модели задействует некоторое количество бит. Квантование уменьшает количество бит, нужное параметру, значит, уменьшает и размер модели, а также требования к оперативной памяти.
«Золотая середина квантования» — Q4_K_M. Это означает, что основная масса параметров модели хранится в четырехбитном формате со сжатием среднего качества. Именно такого формата GGUF-модели мы брали для исследования.
Имя модели | Размер репозитория (GB) | Размер GGUF-файла (GB) |
Microsoft/phi-2 | 5.5 | 1.7 |
Tinyllama | 2.2 | 1.1 |
Mistral | 15 | 4.2 |
Qwen3 | 6 | 2.81 |
Всем моделям для анализа мы давали ошибку автотестов, связанную с неверной версией библиотеки, над которой проводили тесты. В ней не было функции set_log_level:
AttributeError: /opt/sdk/libtboost_sdk.so: undefined symbol: set_log_level
Мы написали промт на английском и отправили его в несколько LLM. Для удобства перевела и промт, и ответы на русский язык.
Объясни, почему произошла ошибка:
AttributeError: /opt/tboost-sdk/libtboost_sdk.so: undefined symbol: tboost_set_log_level
Ответ phi-2:
Вот как выглядит лог-файл после выполнения этой команды:
[17563:0x7f49a08e4700] [ERROR] /opt/tboost-sdk/libtboost_sdk.so: undefined symbol: tboost_set_log_level
Не знаю, как пофиксить. Есть предложения?
Итог: сам придумывает какие-то логи, нет дельных советов.
Ответ Tinyllama:
Я пытался установить libtbost_sdk с помощью pip, но это не сработало. Я также пробовал переустановить библиотеку с помощью pip, но это тоже не помогло. Есть ли какие-нибудь предложения по решению этой проблемы?
Еще смешной ответ:
Надеюсь, это поможет!
Ответ mistral:
В данном случае сообщение об ошибке указывает на то, что функция tboost_set_log_level не определена в библиотеке libtboost_sdk.so. Это могло произойти, если вы не установили библиотеку с помощью pip install libtboost_sdk или если библиотека не была собрана с использованием встроенной библиотеки C-Python (python -m ctypes.build --module=libtboost_sdk).
Чтобы исправить эту проблему, убедитесь, что вы установили библиотеку с помощью pip install libtboost_sdk и что встроенная библиотека C была собрана с использованием встроенной библиотеки C-Python (python -m ctypes.build --module=libtboost_sdk).
Итог: правильные мысли, дельный ответ.
Ответ Qwen3:
Сообщение об ошибке указывает на то, что функция tboost_set_log_level не найдена в библиотеке libtboost_sdk.so. Это может быть вызвано несколькими причинами:
Библиотека
libtboost_sdk.soбыла собрана с использованием другой версии Python, чем та, которую вы используете. Вам необходимо убедиться, что обе библиотеки совместимы друг с другом.Библиотека
libtboost_sdk.soне установлена в вашей системе. Проверьте, установлена ли библиотека и правильно ли она настроена под Python.Библиотека
libtboost_sdk.soбыла собрана с использованием другой версии Python. Вам необходимо убедиться, что обе библиотеки совместимы друг с другом.
Итог: Qwen3 дает несколько возможных вариантов решения. Из-за того, что количество токенов на ответ было урезано, это не все ее предложения.
Из исследования моделей сделали такие выводы:
Microsoft/phi-2 и Tinyllama отвечали с переменным успехом. Иногда они либо придумывали свой traceback на основе переданного, иногда отвечали что-то, не относящееся к ошибке.
Mistral и Qwen3 хорошо разбирали ошибки и предполагали несколько возможных причин их возникновения.
Qwen3 лучше всего работает в очень плохих условиях. На маленьком не GPU-железе она способна давать хорошие ответы по базовым Python-ошибкам, а это решает нашу задачу.
Какую модель выбрали, расскажу чуть позже.
Выбор способа развертывания сервера
Следующий шаг — выбрать, как будем развертывать API-сервер, взаимодействующий с моделью. Ниже перечислила инструменты для создания API-сервера совместно с LLM, которые мы рассматривали.
ctransformers — Python-библиотека для развертывания LLM-моделей в формате GGUF. Похожа на известную Transformers, только эта создана для работы моделей на CPU. Не выбрана, так как требуется изучить много документации для развертывания LLM, а сама библиотека очень редко обновляется.
llama-cpp-python — Python-библиотека для интеграции моделей с OpenAI API. Не выбрана, так как предпочли llamafile, которая и так задействует эту библиотеку.
vLLM — Python-инструмент для развертывания OpenAPI-сервера с выбранной LLM. После его установки достаточно передать имя модели, далее инструмент сам скачает ее на машину и развернет готовый для взаимодействия API-сервер. Не выбран, так как для работы инструмента на CPU его нельзя скачать через pip. Его нужно собирать из исходного кода, что может вызвать дополнительные сложности, если серверу нужно будет переехать на другую машину. Даже с его CPU-версией инструмент работает и запускается, медленнее чем llamafile.
llamafile — пакет ПО, который помогает распространять и запускать модели одним файлом. Он задействует llama-cpp и разворачивает API-сервер, совместимый с OpenAI. Выбрали его, потому пакет легко разворачивается и запускается, а сам сервер отвечает достаточно быстро.
Результат
В качестве llamafile-модели мы выбрали Qwen3-Coder-4b — эта модель нацелена на анализ кода, что подходит нашей задаче. Она занимает минимальное количество ресурсов и адекватно отвечает на промпты с просьбой оценить код.
Размер модели — 4b, то есть 4 миллиарда параметров.
Параметры LLM — это числовые веса, миллиарды крошечных правил и рычагов, которые определяют, как нейронная сеть обрабатывает информацию, понимает язык и генерирует текст. Соответственно, чем больше параметров, тем более сложный текст может обработать модель. Среди Qwen3-llamafile были варианты с 1b, 4b, 30b. 30b требует очень много ресурсов — для нашей задачи столько не нужно. Мы остановились на 4b — этого вполне достаточно.
Анализ кода занимает от одной до восьми минут в зависимости от количества кода. Учитывайте, что мы ограничиваем ответ модели в 700 токенов, чтобы она не писала слишком длинные ответы.
Оптимальные параметры для работы модели:
Размер контекста модели (сколько токенов максимум она может запомнить) — 48 000. Этого хватит для самых больших файлов в 1000–1500 строк кода.
Температура — 0.3. Параметр отвечает за креативность и точность ответов, это значение — умеренная креативность.
RoPE Scaling — метод для работы с контекстом был выбран Yarn, так как он предназначен для обработки больших текстов, также для улучшения работы с большим кодом используются параметры
rope-freq-base=1000000иrope-freq-scale=0.1.Восемь CPU-потоков для вычислений.
Отключенная параллельная обработка вычислений для производительности. Это значит, что все запросы идут только в одном потоке.
Размер пропускной способности — 512 токенов в секунду.
mlock, no-mmap — блокируем модель в RAM, чтобы она не выгружала часть информации в своп, и мы предотвращали замедление при запросах к модели.
Так как модели нужно анализировать много кода, мы масштабируем обработку токенов. Rope-freq-scale заставляет модель думать, что 100 000 токенов — это на самом деле 10 000. Благодаря этому можно использовать параметры, обученные на коротком контексте, для работы с длинным, так как количество токенов для обработки находятся в знакомом для модели диапазоне. Однако в этом диапазоне их получается слишком много, модель может путать порядок слов и смешивать их друг с другом, поэтому мы также задействуем параметр rope-freq-base. Он увеличивает «точность» или «разрешение» этого диапазона, чтобы токены не сливались в кашу.
Разработка серверной и клиентской части
Когда мы только начинали разрабатывать AI-ревьюер, главными критериями были простота развертывания и возможность контролировать процесс «изнутри». Серверную часть разворачивали на виртуальной машине во внутреннем облаке в виде systemd-сервиса. Так мы могли просматривать логи через Journald и автоматически перезапускать сервис при перезагрузке машины или в случае непредвиденных падений.
В качестве модели мы использовали сжатый GGUF-файл, который запускался одной командой:
root/Qwen_Qwen3-4B-Q4_K_M.llamafile --server --nobrowser --host 0.0.0.0 --port 9000 -c 48000 -t 8 --temp 0.3 --batch-size 512 --parallel 1 --mlock --no-mmap --rope-scaling yarn --rope-freq-base 1000000 --numa distribute --rope-freq-scale 0.1
Процесс просто висел и ждал запросов. Удобно, эффективно, но были ограничения по размеру модели. Клиентская часть в той версии тоже была самописной и состояла из нескольких функций:
получение git diff;
определение параметров запроса (количества токенов, то есть длины ответа бота);
отправка запроса к серверу и получение ответа;
публикация ревью.
У алгоритма, который решал, какой код отправлять на ревью, не было «интеллекта». Мы отсекали код по числу строк: если в коде было от 10 до 20 строк, длина ответа ИИ составляла 300 токенов, если больше — 500 токенов. Код, в котором было мало строк, мы просто не отправляли.
Со временем стало понятно, что мощности модели Qwen3-4B недостаточно для глубокого анализа. Мы захотели использовать модель с бóльшим количеством параметров, но при этом не наращивать собственные вычислительные ресурсы. Так мы пришли к решению отказаться от своего сервера с виртуалками и перейти на платформу Dify. Сейчас мы используем ту же линейку моделей, но с расширенным набором параметров — Qwen3-30b-A3B-Instruct-2507. Модель развернута в Dify, где собран весь пайплайн: и промпты, и ИИ-часть.
Ключевое изменение: алгоритм принятия решений переехал из клиента в сервер. Раньше клиент сам решал, какой код посылать и какой длины должен быть ответ. Теперь клиентская часть просто отправляет git diff вместе с контекстом, а вс остальное делает ИИ через промпты: анализирует, стоит ли вообще ревьюить код, и определяет, насколько большим должно быть ревью.

В первой версии на маленькие фрагменты кода модель часто отвечала, что код «is correct», и объясняла, почему. Это было бесполезно: нам нужно находить проблемы, а не получать одобрение. Поэтому логику изменили: если модель считает, что код заслуживает ревью среднего или большого размера, она генерирует соответствующее по длине сообщение и отправляет его клиенту. Мелкие изменения либо игнорируются, либо получают минимальную обратную связь без ложного одобрения.
В итоге мы перешли от самодельного сервера с 4B-моделью и примитивной обрезкой кода к облачному пайплайну на Dify с моделью 30B. Клиент стал тоньше, сервер — умнее, а качество ревью выросло за счет того, что теперь не мы решаем, что и как анализировать, а сама модель.
Послать запрос к серверу тоже легко, так как это OpenAI API-сервер:
from openai import OpenAI client = OpenAI( base_url="http://server_ip:port/v1", api_key="not-needed" ) code_to_analyze = "your code is here" response = client.chat.completions.create( model="qwen3-coder", messages=[ {"role": "system", "content": "You are a helpful coding reviewer for Python."}, {"role": "user", "content": f"/nothink Advice how you can improve this code: {a}"} ], max_tokens=700 ) print(response.choices[0].message.content)
/nothinkв промте нужен, чтобы отключить thinking section, когда ИИ думает вслух. Для нашей задачи функциональность не нужна, а еще на нее тратятся токены.
Для ревью пулл-реквестов нужен алгоритм сложнее. Когда отправляем на проверку файл, AI-ревьюер фокусируется на всем содержимом, а не только на строках, которые нужно проверить. Если разработчик поменяет в PR только одну строку и отправит этот diff на сервер, тот не сможет это качественно проверить. Скорее всего, его комментарии будут не по делу.
Сейчас алгоритм работает так:
Смотрим git-diff каждого файла отдельно. Файлы не .py формата не рассматриваем.
Этот git-diff разбит на сегменты добавленных или удаленных строк. Мы смотрим только в добавленные.
Выделяем с помощью git контекст сегмента с добавленными строками, отправляем его AI-ревьюеру.
Отправляем контекст сегмента на анализ ИИ: ИИ определяет, следует ли ревьюить этот код, и насколько большое должно быть ревью.
ИИ, в зависимости от своего ответа, составляет ревью нужного размера.
Мы получаем ответ, постим его в Bitbucket и переходим к следующему сегменту в файле.
После получения ответа на все сегменты одного файла переходим к следующему файлу в пулл-реквесте.
Сейчас доработок требует именно клиентская часть:
Бот отвечает на английском, но иногда может ответить на русском, если увидит в коде этот язык. На качество ревью это не влияет, но нужно его определиться с языком для ответа, чтобы не путать пользователя.
Тестируем и дорабатываем его поведение, чтобы устранить непредсказуемость.
Proof of concept
В качестве PoC автоматизированного ревью PR в Bitbucket мы реализовали связку Bitbucket → Albert → Jenkins → AI → Bitbucket. Аналогичная схема используется и в для других автоматических проверок исходного кода в TATLIN.BACKUP.
Albert — это набор служебных сервисов и библиотек, написанных разработчиками YADRO на Python и объединенных в общую платформу. Набор представляет собой окружение для пользовательских сервисов, работающих в этой платформе. Через него можно отправлять уведомления в корпоративный мессенджер. В решении нашей задачи мы используем Albert как бота, который получает ответ от ИИ и дублирует его в пулл-реквест.

Пошаговое описание процесса
Bitbucket → при открытии PR срабатывает webhook → POST на endpoint сервиса Albert.
Albert фильтрует события: если репозиторий из списка — триггер сработал.
Albert вызывает Jenkins API (trigger job) с параметрами PR (repo, PR-id).
Jenkins запускает джобу-анализатор, та запускает Python-скрипт.
Скрипт: запрашивает diff по PR через Bitbucket API, парсит добавленные строки по файлам и выделяет для них контекст в файле
Скрипт отправляет код в AI-сервер, получает текст-ревью.
Скрипт постит комментарии в PR через Bitbucket API (по файлам и строкам).
С учетом того, что это не основная наша работа, на разработку бота у нас ушел месяц, и мы планируем его дорабатывать: добавим возможность общаться с ботом и обсуждать ревью, загрузим базу кода наших репозиториев. Конечно, последнее лишит его универсальности: не получится использовать AI-помощника для других проектов. Хотя команда разработчиков СХД уже создает похожий пайплайн для работы с их кодом на Rust.
Напишите в комментариях, если хотите узнать, как адаптировать наш инструмент для кода на другом языке.