Стандартный семантический поиск находит нужный фикс по тексту баг-репорта лишь в 32–38% случаев. Это не ошибка конкретной модели эмбеддингов, а фундаментальное ограничение подхода: текст issue и текст pull request часто не имеют лексического или семантического пересечения. Экспериментальный бенчмарк на открытом репозитории YDB Яндекса показал, что обход рёбер графа причинных связей - GitHub-паттернов Closes, Fixes, Resolves - поднимает recall до 87–95%.
Метод не требует обучения моделей или сложной инфраструктуры. Достаточно распарсить ссылки между issue и PR, построить ориентированный граф и для каждого нового обращения искать связанные узлы через явные рёбра. Результат: ИИ-ассистент получает механизм извлечения релевантных фиксов, который работает там, где семантический поиск молчит. В материале - полная методология, цифры, влияние фильтрации ботов и пошаговая инструкция для запуска бенчмарка на своём репозитории.
Почему семантический поиск не справляется с поиском фиксов
Семантический поиск решает задачу близости текстов. Он хорошо находит документы, которые обсуждают похожие темы или используют схожие формулировки. Задача поиска фикса принципиально иная: нужно найти не похожий текст, а причинно-связанный артефакт. Issue описывает симптом, PR содержит исправление. Их тексты могут не пересекаться вообще.
Когда разработчик пишет issue «Падение сервиса при параллельной вставке в партиционированную таблицу», а PR называется «Рефакторинг блокировок в акторной системе», семантическая близость между этими документами нулевая. Никакая модель эмбеддингов не свяжет их по тексту. При этом PR действительно исправляет описанную проблему - связь существует на уровне фактов, а не слов.
Как устроен семантический поиск по issues и PR
Стандартный пайплайн выглядит так: тексты issue и PR векторизуются через эмбеддер - например, text-embedding-3-large или специализированную модель для кода. Затем для каждого issue ищутся k ближайших PR по косинусному расстоянию. Если нужный PR попал в топ-k выдачи, случай считается успешным.
Проблема возникает на этапе векторизации. Возьмём реальный сценарий из YDB: issue «Некорректное поведение TTL при удалении записей» и PR «Исправление логики очистки в TTL-компакшене». Семантически они близки - обе упоминают TTL, удаление, очистку. Модель, скорее всего, поставит этот PR высоко. Теперь другой случай: issue «Утечка памяти при длительных сессиях YQL» и PR «Добавление счетчика ссылок в контекст выполнения». Семантического пересечения нет. Эмбеддинг issue сгруппируется с другими issue про память и утечки, а PR - с PR про контексты выполнения и счётчики. Связь потеряна.
Цифры: 32–38% recall - почему это проблема для реальных проектов
Бенчмарк на корпусе YDB зафиксировал recall семантического поиска на уровне 32% для строгой метрики (нужный PR строго на первом месте) и 38% для top-5. Это означает, что в двух третях случаев разработчик или ИИ-ассистент не получает релевантный фикс автоматически.
На практике это выливается в ручной перебор. Разработчик открывает issue, затем вручную ищет связанные PR через историю коммитов, упоминания в комментариях или память коллег. ИИ-ассистент, лишённый доступа к графу связей, выдаёт нерелевантные PR и подрывает доверие к автоматизации. Для крупных репозиториев с тысячами issue и PR ситуация усугубляется: шум от нерелевантных выдач растёт, а время на поиск увеличивается линейно.
Даже современные эмбеддинги, обученные на кодовых корпусах, не улавливают паттерн «Closes #123», если он не развёрнут описательно. Ссылка на номер issue - это структурная, а не семантическая информация. Модель не знает, что «Fixes #456» в теле PR означает прямую причинную связь с issue номер 456. Она видит строку текста, которая семантически нейтральна.
Причинный recall: как граф связей меняет правила игры
Метод причинного recall использует информацию, которая уже есть в репозитории, но игнорируется семантическим поиском, - явные перекрёстные ссылки между issue и PR. GitHub автоматически связывает PR с issue, если в описании PR встречается ключевое слово Closes, Fixes или Resolves с указанием номера. Эта связь - жёсткий сигнал причинности: PR был создан для исправления конкретного issue.
Построение графа на этих связях превращает задачу поиска из задачи определения близости текстов в задачу обхода ориентированного графа. Узлы - issue и PR. Рёбра - ссылки Closes/Fixes/Resolves. Направление - от PR к issue (PR закрывает issue). Для нового issue поиск фикса сводится к проверке: существует ли входящее ребро от какого-либо PR.
GitHub-паттерн Closes/Fixes/Resolves как источник причинности
GitHub парсит описания PR и автоматически связывает их с issue при merge, если в тексте встречается один из паттернов:
Closes #123Fixes #123Resolves #123
Эта информация доступна через GitHub API в поле closing_issues для PR или через анализ тела PR регулярными выражениями. Для бенчмарка на YDB использовался прямой парсинг тела PR: извлечение всех упоминаний номеров issue после ключевых слов. Этот подход ловит даже те связи, которые GitHub не зарегистрировал автоматически - например, если PR был закрыт без merge или ссылка указана в нестандартном формате.
Пример из корпуса YDB: PR «Исправление гонки данных в курсоре запросов» содержит строку «Fixes #4521». Парсер извлекает связь PR → issue #4521. Issue #4521 называется «Падение узла при параллельном открытии курсора». Семантически тексты не пересекаются, но граф содержит ребро. Причинный recall находит фикс мгновенно.
Обход графа: от issue к фиксу за один шаг
Алгоритм состоит из четырёх шагов:
- Сбор данных. Через GitHub API выгружаются все issue и PR репозитория. Для каждого объекта сохраняются номер, заголовок, тело и состояние (открыт/закрыт).
- Извлечение ссылок. Тело каждого PR сканируется регулярным выражением на паттерны
(Closes|Fixes|Resolves)\s+#?(\d+). Извлекаются номера issue. Дополнительно анализируются комментарии к PR - иногда ссылки указываются там. - Построение графа. Создаётся ориентированный граф: узлы - issue и PR, ребро направлено от PR к issue, которую он закрывает. Для реализации использовалась библиотека networkx. Граф можно расширять транзитивными связями: если issue A ссылается на issue B, а PR закрывает B, то PR потенциально релевантен и для A.
- Поиск фикса. Для тестового issue проверяется наличие входящих рёбер. Если ребро есть - связанный PR считается фиксом. Если рёбер нет - метод честно сообщает, что явная связь не найдена.
Важный нюанс: граф строится один раз и может инкрементально обновляться при появлении новых PR. Сложность поиска - O(1) на один запрос после построения графа.
Результаты на YDB: рост recall с 32% до 95%
На корпусе YDB, содержащем несколько тысяч issue и PR, метод причинного recall показал следующие результаты:
| Метод | Recall@1 | Recall@5 |
|---|---|---|
| Семантический поиск (baseline) | 32% | 38% |
| Причинный recall (граф) | 87% | 95% |
Разрыв более чем вдвое. Причинный recall находит фикс в 87% случаев с первой попытки и в 95% - в топ-5. Оставшиеся 5–13% - это issue, для которых в репозитории нет явной ссылки Closes/Fixes/Resolves. Типичные причины: PR был создан до issue, связь указана нестандартным способом (например, «Related to #123»), или фикс был внесён в составе крупного PR без привязки к конкретному issue.
Влияние фильтрации ботов на метрики recall
Автоматические PR от ботов - dependabot, renovate, автоматизация релизов - часто содержат ссылки на issue, но не являются meaningful фиксами в контексте поиска прошлых исправлений. Dependabot создаёт PR с заголовком «Bump library X from 1.0 to 1.1» и телом «Fixes #5678», где #5678 - автосгенерированное issue о необходимости обновления зависимости. Такой PR формально связан с issue, но не несёт ценности для разработчика, который ищет исправление бага.
Бенчмарк на YDB показал, что включение ботовых PR завышает recall на 3–5 процентных пунктов. Цифра кажется небольшой, но она маскирует реальную картину: часть успешных нахождений - это автоматические обновления зависимостей, а не исправления логики. После фильтрации по автору (исключение аккаунтов, помеченных как боты) и по шаблонам заголовков (исключение паттернов «Bump», «Update dependency») recall снизился до честных 87–95%.
Рекомендация для самостоятельного запуска: всегда фильтровать ботов перед расчётом метрик. Критерии фильтрации зависят от репозитория. Минимальный набор - исключение PR от аккаунтов с флагом type: Bot в GitHub API и исключение PR, чьи заголовки соответствуют шаблонам автоматических обновлений.
Воспроизводимость: как запустить бенчмарк на своём репозитории
Метод воспроизводим на любом публичном репозитории GitHub. Для запуска потребуется Python 3.9+, GitHub API токен с правами на чтение публичных репозиториев и несколько сотен мегабайт дискового пространства для хранения выгруженных данных. Ниже - пошаговая инструкция.
Шаг 1: Сбор данных через GitHub API
Первый этап - выгрузка всех issue и PR. Используем библиотеку PyGithub для работы с API:
from github import Github
import time
g = Github("your_token_here")
repo = g.get_repo("owner/repo_name")
# Сбор issues
issues = []
for issue in repo.get_issues(state="all"):
issues.append({
"number": issue.number,
"title": issue.title,
"body": issue.body,
"state": issue.state,
"is_pull_request": issue.pull_request is not None
})
time.sleep(0.1) # соблюдаем rate limiting
# Сбор PR (включая тело)
pulls = []
for pr in repo.get_pulls(state="all"):
pulls.append({
"number": pr.number,
"title": pr.title,
"body": pr.body,
"state": pr.state,
"user_login": pr.user.login,
"user_type": pr.user.type
})
time.sleep(0.1)
Важно: GitHub API имеет лимит 5000 запросов в час для аутентифицированных пользователей. Для крупных репозиториев с десятками тысяч issue и PR выгрузка может занять несколько часов. Используйте sleep между запросами и обрабатывайте ошибку RateLimitExceededException с экспоненциальной задержкой.
Шаг 2: Построение графа связей и расчёт recall
После выгрузки данных строим граф:
import re
import networkx as nx
G = nx.DiGraph()
# Добавляем узлы
for issue in issues:
G.add_node(f"issue_{issue['number']}", type="issue", **issue)
for pr in pulls:
G.add_node(f"pr_{pr['number']}", type="pr", **pr)
# Извлекаем ссылки и добавляем рёбра
pattern = re.compile(r'(Closes|Fixes|Resolves)\s+#?(\d+)', re.IGNORECASE)
for pr in pulls:
if pr['body']:
matches = pattern.findall(pr['body'])
for match in matches:
issue_num = int(match[1])
if G.has_node(f"issue_{issue_num}"):
G.add_edge(f"pr_{pr['number']}", f"issue_{issue_num}")
# Расчёт recall
test_issues = [i for i in issues if not i['is_pull_request']]
correct = 0
total = len(test_issues)
for issue in test_issues:
node = f"issue_{issue['number']}"
# Проверяем входящие рёбра от PR
predecessors = list(G.predecessors(node))
pr_predecessors = [p for p in predecessors if G.nodes[p]['type'] == 'pr']
if pr_predecessors:
correct += 1
recall = correct / total
print(f"Recall: {recall:.2%}")
Этот код вычисляет recall@1 - долю issue, для которых найден хотя бы один связанный PR. Для recall@5 логика усложняется: нужно ранжировать несколько PR по дополнительным признакам (дата создания, близость заголовков), но базовая механика та же.
Шаг 3: Интерпретация результатов и типичные ошибки
Recall 100% недостижим. Всегда остаются issue, для которых нет явной ссылки: PR был создан до issue, связь указана нестандартно, фикс внесён без привязки, issue всё ещё открыт. Нормальный показатель для активного репозитория - 85–95%. Если recall ниже 70%, проверьте:
- Не фильтруете ли вы PR, которые были смержены без ссылки в теле, но со ссылкой в комментариях - парсите также комментарии к PR;
- Не исключены ли issue, преобразованные из PR - GitHub API возвращает их как issue с флагом pull_request;
- Корректно ли обрабатываются переоткрытые issue - иногда PR закрывает issue, затем issue переоткрывается и закрывается другим PR.
Для оценки качества полезно вручную проверить 20–30 случайных несвязанных issue. Это покажет, есть ли в репозитории системная проблема с документированием связей или метод работает на пределе возможностей данных.
Сравнение подходов: когда семантический поиск всё ещё нужен
Графовый метод требует наличия явных ссылок. Если разработчики не используют Closes/Fixes/Resolves, граф останется пустым и recall будет нулевым. Семантический поиск в таких условиях даст хотя бы 30%. Кроме того, существуют недокументированные связи: PR исправляет проблему, описанную в комментарии к другому issue, или фикс является побочным эффектом рефакторинга. Граф такие связи не улавливает.
Оптимальная стратегия - гибридный подход. Сначала применяется причинный recall: если ребро найдено, фикс возвращается мгновенно с высокой уверенностью. Если рёбер нет, запускается семантический поиск по корпусу PR. На корпусе YDB гибридный подход даёт следующие результаты:
| Метод | Recall@1 | Recall@5 |
|---|---|---|
| Только семантический поиск | 32% | 38% |
| Только причинный recall | 87% | 95% |
| Гибрид (граф + семантика) | 89% | 97% |
Прирост от добавления семантического поиска поверх графа скромный - 2 процентных пункта. Это объясняется тем, что большинство meaningful фиксов в YDB имеют явные ссылки. Для репозиториев с менее дисциплинированным процессом прирост может быть выше. В любом случае графовый метод покрывает основную массу случаев, а семантический поиск работает как страховка для крайних ситуаций.
Практический вывод: если вы внедряете ИИ-ассистента для работы с кодовой базой, начните с построения графа связей. Это дёшево, не требует обучения моделей и даёт максимальный прирост recall. Семантический поиск оставьте как fallback-механизм. Именно такой подход использован в системах памяти агентов, где память ИИ-агента предотвращает ложные отчёты за счёт проверки прошлых решений.
Метод причинного recall - это частный случай более общего принципа: структурные данные репозитория (граф ссылок, история коммитов, blame-информация) содержат сигнал, который семантические модели не извлекают. Аналогичный подход используется в многошаговом семантическом поиске в Amazon Bedrock, где LLM-планировщик разбивает сложные запросы на подзадачи и итеративно уточняет поиск. Разница в том, что для поиска фиксов граф уже существует - его не нужно строить итеративно, достаточно распарсить.
Для оценки качества поиска фиксов критически важна методология бенчмаркинга. Аудит бенчмарков AI показал, что до 12% вопросов в популярных наборах данных содержат ошибки. При построении собственного бенчмарка причинного recall проверяйте тестовую выборку вручную: убедитесь, что issue действительно исправлен связанным PR, а не просто упомянут. Это исключит завышение метрик из-за ложных связей.