- BrainTools - https://www.braintools.ru -
Я фулстек‑разработчик в TravelLine. Мы делаем единую систему для гостиничного предприятия, которая помогает отелям, санаториям и другим средствам размещения автоматизировать свои бизнес‑процессы. В этой статье я расскажу о том, как мы внедрили ИИ для генерации описаний PR/MR.
У нас два хостинга репозиториев и столько же билд‑серверов: одни команды живут в GitLab со сборкой в GitLab CI, другие — в Bitbucket Server (в компании его до сих пор зовут Stash) со сборкой в Jenkins. Общее у них одно: поле описания merge/pull request зачастую пустое или содержит только список commit messages.
Ревьюер открывает diff на несколько десятков файлов и не понимает, с чего начать.
Писать его руками — рутина, на которую часто забивают. При этом задача хорошо формализуется: на входе diff, на выходе текст по шаблону. Идеальная работа для LLM.
Так появился AI Describer — CLI‑инструмент, который запускается в пайплайне, берёт git diff относительно целевой ветки, отдаёт его LLM и публикует структурированное описание в MR/PR.
Готовый текст оборачивается в XML‑блок <ai-describer>.
## Что сделано
— Краткое описание изменений (1–3 пункта)
## Зачем
— Обоснование изменений и решаемые проблемы (1–3 пункта)
Бакелли и Бёрд, изучив ревью в Microsoft (ICSE 2013), показали: ключевой аспект ревью — понимание кода и самого изменения. Попутно там нашлось расхождение: главной мотивацией [2] ревью называют поиск дефектов, но в реальности комментарии про дефекты составляют небольшую долю.
А чего именно не хватает, видно из другой работы. Ко, ДеЛайн и Венолия (ICSE 2007) наблюдали за работой семнадцати разработчиков и смотрели, на каких вопросах те застревают. Хуже всего дело шло не с вопросом «что делает этот код», а с вопросами «почему он написан именно так» и «как он должен работать». Ревью приходилось откладывать: ответ знал только коллега, которого не было на месте.
Отсюда ровно две секции. «Что сделано» — это diff, поднятый на один уровень абстракции. «Зачем» отвечает на тот самый why‑вопрос, ответ на который лежит только в голове автора; чтобы секция не стала выдумкой, инструменту и понадобился Jira‑контекст.
Генерация срабатывает не всегда: автор может решить, что для этого MR/PR ИИ бесполезен.
поле пустое — генерируем;
в тексте есть маркер HELP_AI — обновляем (заденет только блоки);
текст написан человеком и маркера нет — не трогаем.
Тег <ai-describer> нужен для второго пункта: при перегенерации мы заменяем содержимое внутри тегов, а всё, что автор дописал руками вне этого блока, остаётся нетронутым. Разработчикам оказалось важно дополнять текст, который сгенерировал ИИ.
Первый прототип (сентябрь 2025) — ~1700 строк на Python, упакованные в docker‑образ. Задача была проверить гипотезу: работает ли это вообще, читаемы ли описания, полезны ли они. Выбор LLM пал на deepseek‑v3 — тогда это была самая дешёвая модель с приемлемым качеством. На пересказ изменений много «ума» не нужно.
Выявленные прототипом проблемы/недочёты:
объём описания. Если его не ограничивать, текст разрастается до нелепых размеров: читать пачку непричёсанного нейрослопа сложнее, чем саму вкладку «Diff»;
дистрибуция. Docker‑образ хорошо живёт в GitLab CI и плохо — в Jenkins‑джобах, которые собирают.NET‑проекты на Windows‑агентах. Тащить туда Python ради одной утилиты — слишком муторно для PoC;
язык не совпадал со стеком. Подавляющее большинство наших проектов написаны на.NET. Инструмент на C# может ставиться как dotnet tool рядом со сборкой, той же командой, тем же NuGet‑реестром, что и остальные внутренние пакеты;
засорение контекста. В изменениях попадаются технические файлы, которые разработчики не читают, но хранить в репозитории обязаны. В.NET это, к примеру, автогенерируемый код миграций — скармливать его LLM бессмысленно. В другом стеке наверняка найдётся свой аналог.
В марте 2026 инструмент был переписан на C# /.NET — за это спасибо Сергею, одному из самых толковых программистов, с которыми мне довелось работать.
|
Что |
Чем |
Обоснование |
|
Дистрибуция |
|
Ставится одной командой рядом с проектом, версионируется как обычный пакет, работает и на Linux‑раннерах GitLab, и на Windows‑агентах Jenkins |
|
Таргеты |
|
У команд стоят разные SDK — от старого.NET до нового |
В GitLab CI — один шаг в пайплайне:
ai-describer:
stage: ai_describe
image: <ваш-реестр>/dotnet/sdk:10.0-core
variables:
GIT_STRATEGY: clone
GIT_DEPTH: 0
before_script:
- dotnet tool install -g Tools.AiDescriber --source "<ваш-NuGet>"
script:
- tl-aidescriber
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
allow_failure: true
В Jenkins — отдельный стейдж: секреты приходят через withCredentials, а catchError гарантирует, что упавшая генерация описания не уронит сборку.
stage('Generate PR description') {
steps {
script {
catchError(buildResult: 'SUCCESS', stageResult: 'FAILURE') {
withCredentials([
usernameColonPassword(credentialsId: "bitbucket", variable: "BITBUCKET_CREDENTIALS"),
]) {
powershell '.\deployment\generate-pr-description.ps1'
}
}
}
}
}
Сам generate-pr-description.ps1:
$ErrorActionPreference = "Stop"
$scriptDir = Split-Path $MyInvocation.MyCommand.Path -Parent
$basePath = Split-Path $scriptDir -Parent
function Invoke-NativeCommand {
param([Parameter(Mandatory)][string]$Command, [string[]]$Arguments)
& $Command @Arguments
if ($LASTEXITCODE -ne 0) {
throw "$Command $($Arguments -join ' ') exited with code $LASTEXITCODE"
}
}
Push-Location $basePath
try {
Invoke-NativeCommand dotnet @('tool', 'restore')
# Передаём корень репозитория явно, не полагаемся на текущий каталог
$repoRoot = (git rev-parse --show-toplevel).Trim()
Invoke-NativeCommand dotnet @('tool', 'run', 'tl-aidescriber', '--workdir', $repoRoot)
}
finally {
Pop-Location
}
Две детали, на которых мы спотыкались. Invoke-NativeCommand нужен потому, что PowerShell не прерывает скрипт на ненулевом коде возврата нативного exe — $ErrorActionPreference на dotnet не действует. А корень репозитория передаётся явно через git rev-parse --show-toplevel: скрипты сборки меняют текущий каталог, и полагаться на него при поиске Git‑репозитория нельзя.
Аргументов почти нет: ветки, идентификатор merge/pull request и адреса API инструмент читает из окружения, которое билд‑сервер выставляет сам. Не‑секретные настройки — endpoint LiteLLM, модель, URL Jira — едут дефолтами внутри самого пакета, так что в репозитории проекта задавать необходимо только секреты.
Больше — не всегда качественнее. ИИ‑текст читать трудно, поэтому описание должно быть максимально ёмким и, желательно, с картинками или диаграммами. За несколько итераций я пришёл к ограничению в три пункта. Опирался на известный факт о рабочей памяти [4]: человек удерживает одновременно около 3–4 объектов (закон Миллера, уточнённый Коуэном до 4±1). Да, у этой гипотезы хватает критиков, но я всё же попробовал переделать шаблон под этот лимит. Не значит, что три — идеальная величина для всех: у вас она может оказаться другой. Общая же рекомендация такая — делайте описание лаконичным.
Первые интеграции идут не по плану. На старте почти каждое подключение упиралось в мелочь, которую было видно только на реальном пайплайне: неверно выставленные переменные, секрет не долетел до джобы, утилита не нашла идентификатор ветки. Совет: сразу просите доступ на запись в репозиторий подключаемой команды — иначе каждую правку придётся проводить через её разработчиков.
Ветка вместо PR. Jenkins выставляет переменные CHANGE_URL/CHANGE_BRANCH/CHANGE_TARGET только в сборках, порождённых из pull request. Если multibranch‑джоба собирает просто ветку — их нет. Пришлось искать PR самостоятельно: берём HEAD рабочей копии, через git ls-remote ищем среди ссылок refs/pull-requests/<id>/from, которые публикует Bitbucket Server, ту, что указывает на этот коммит, и дальше спрашиваем у API исходную и целевую ветки. Если PR не нашёлся — сообщение No open pull request found и код возврата 0. Если нашлось несколько (ветку предложили в разные целевые) — выбираем PR с наименьшим номером.
Один и тот же тег выглядит по‑разному. GitLab прячет незнакомые XML‑теги при рендеринге описания: <ai-describer> видно только когда редактируешь описание. Bitbucket Server так не умеет — теги торчат в тексте угловыми скобками прямо на странице. Пришлось написать своё расширение для браузера, которое стилизует этот блок в интерфейсе Stash.
Дороже — не всегда лучше. Тратить токены топовых моделей здесь незачем: deepseek‑v3 хватало за глаза. Фронтир не даёт профита — задача слишком простая, структура и объём описания строго ограничены промптом.
Кастом PoC‑проекта от энтузиастов. На первом этапе мы внедрялись копированием Python‑скриптов в репозитории команд, и дальше каждый правил их под себя. Вырос зоопарк решений: у всех свой взгляд и на генерацию текста, и на границу между ИИ‑частью и ручной. Мой совет: убедились, что вещь полезная, — поскорее делайте второй шаг и катите один общий инструмент на всех. Иначе от своего кастома люди отказываться не захотят, и его придётся тащить в платформенный проект. Пункт спорный: с другой стороны, именно это показало, что действительно важно командам.
Описание, построенное только по diff, честно отвечает «что изменилось», а «зачем» — по сути угадывает. Модель видит в диффе новое условие, но не знает, что причина — жалоба клиента в тикете.
Мы добавили три источника контекста, чтобы сделать описания более качественными.
Jira. Из ветки или заголовка PR извлекается ключ задачи, дальше — один запрос в Jira Data Center REST API v2 за summary и description. Аутентификация — Personal Access Token, Authorization: Bearer, требуется Jira DC 8.14+. Интеграция полностью опциональна: не задан URL или токен — пайплайн работает и без этого;
Файлы репозитория. Переменной AIDESCRIBER_CONTEXT_FILES перечисляются пути вроде ARCHITECTURE.md [5], docs/glossary.md [6], их содержимое подмешивается в промпт;
ai‑describer‑prompt — файл в корне анализируемого репозитория; то, что в нём написано, добавляется к системному промпту с наивысшим приоритетом. Команда может поменять формат, длину и стиль вывода под себя.
Инструкции и данные лежат в промпте раздельно. Системный промпт короткий — роль, язык вывода и правило приоритета, больше в нём ничего нет:
You are a code analysis expert with deep understanding of software engineering
principles. Your responses are always structured, accurate, and useful for
technical professionals. You write output only in Russian language and follow
all instructions precisely.
If repository-specific instructions are appended below under a "highest priority"
heading, treat them as the authoritative override: they take precedence over the
default structure, length, formatting and style rules given in the task, and you
MUST apply them even when they change the sections, the number of bullet points,
or the wording.
Структура и лимит длины живут в промпте задачи. Вот его существенная часть — та, из которой и берутся две секции и «не больше трёх пунктов»:
# Output Format
By default, write output in Russian using the following structure and headings
(unless repository-specific instructions override them):
## Что сделано
- Кратко, 1–3 пункта, что изменено/добавлено
## Зачем
- Кратко, 1–3 пункта, какую проблему решает и почему это нужно
# Constraints
- Write output only in Russian language
- Be specific and avoid general phrases
- By default, a maximum of 2–6 bullet points across both sections
- Use Markdown for formatting the output (## headers, - bullets)
Генерация описания — вспомогательный шаг, и красным пайплайн из‑за него становиться не должен. Основные способы отказа обрабатываются явно:
diff больше лимита в 2 млн символов — WARNING, описание не публикуется, сборка зелёная. Diff при этом не обрезается: либо он уходит в модель целиком, либо не отправляется вовсе — резаный посередине diff даёт описание хуже, чем его отсутствие;
Jira недоступна (401, 404, 429, 5xx, timeout, DNS, TLS) — описание генерируется без Jira‑контекста, а в лог уходит конкретный event‑код: JIRA_AUTH_FAILED, JIRA_UNREACHABLE, JIRA_RATE_LIMITED и так далее. Программист в логах CI видит, из‑за чего не подрубилась Jira;
PR смержили прямо во время сборки — обновлять описание некуда, это штатная гонка: тоже не роняем пайплайн.
Требование было простое: токены не должны утечь в логи CI, которые видят все.
PAT и API‑ключи не попадают в вывод ни в каком виде — ни в DEBUG, ни в тексте ошибок, ни в trace исключений;
Флаг AIDESCRIBER_JIRA_VERIFY_SSL=false скоупится только на Jira‑клиент. Общий тумблер «не проверять сертификаты нигде» — типичный способ незаметно расширить blast radius;
Секреты не лежат ни в репозитории, ни в appsettings.json — он едет внутри публикуемого пакета.
Сейчас инструмент работает в 5 командах, в том числе на флагманском продукте — ФБ, где им покрыто 4 репозитория. Поддержаны четыре сценария интеграции, на каждый есть своя инструкция: GitLab CI, Jenkins + dotnet CLI, Jenkins + MSBuild для проектов на.NET Framework, и Jenkins‑джобы, собирающие ветку без pull request.
Документация публикуется в Confluence прямо из репозитория отдельным шагом пайплайна — статьи нельзя редактировать в вебе, правки принимаются только через pull request. Это помогает держать инструкции в актуальном состоянии во времена ИИ.
Что это дало:
улучшило Developer Experience: текст появляется сам, а если автор написал своё — инструмент ничего не делает;
ревьюер видит намерение, а не только diff: блок отвечает на «зачем», потому что модель знает задачу из Jira.
Честно про ограничения: на очень больших MR качество падает — часть изменений выпадает, остальное превращается в общие фразы. Широкое контекстное окно спасает лишь отчасти.
Сейчас в моде агентские сессии — в них можно получить текст заметно качественнее, так что проекту остались редкие ручные и автоматизированные сценарии. Накопленный опыт [7] мы перенесли в новый репозиторий скиллов для агентской разработки: генерация описания ПР теперь живёт там.
Всем спасибо!
Bacchelli A., Bird C. Expectations, Outcomes, and Challenges of Modern Code Review [8]. ICSE 2013, pp. 712–721.
Ko A. J., DeLine R., Venolia G. Information Needs in Collocated Software Development Teams [9]. ICSE 2007.
Letovsky S. Cognitive processes in program comprehension [10]. Journal of Systems and Software, 1987.
Miller G. A. The Magical Number Seven, Plus or Minus Two. Psychological Review, 1956, 63(2), pp. 81–97.
Cowan N. The magical number 4 in short‑term memory: A reconsideration of mental storage capacity. Behavioral and Brain Sciences, 2001, 24(1), pp. 87–114.
Автор: Peregrine0
Источник [11]
Сайт-источник BrainTools: https://www.braintools.ru
Путь до страницы источника: https://www.braintools.ru/article/35984
URLs in this post:
[1] внимание: http://www.braintools.ru/article/7595
[2] мотивацией: http://www.braintools.ru/article/9537
[3] реакция: http://www.braintools.ru/article/1549
[4] памяти: http://www.braintools.ru/article/4140
[5] ARCHITECTURE.md: http://ARCHITECTURE.md
[6] glossary.md: http://glossary.md
[7] опыт: http://www.braintools.ru/article/6952
[8] Expectations, Outcomes, and Challenges of Modern Code Review: https://www.microsoft.com/en-us/research/publication/expectations-outcomes-and-challenges-of-modern-code-review/
[9] Information Needs in Collocated Software Development Teams: https://www.microsoft.com/en-us/research/publication/information-needs-in-collocated-software-development-teams/
[10] Cognitive processes in program comprehension: https://www.sciencedirect.com/science/article/pii/016412128790032X
[11] Источник: https://habr.com/ru/articles/1086306/?utm_source=habrahabr&utm_medium=rss&utm_campaign=1086306
Нажмите здесь для печати.