
Коротко, о чём речь, — для тех, кто пришёл по слову «Яндекс» и предыстории не знает. Когда ИИ-ассистент помогает чинить баг, самое ценное — не сочинить ответ с нуля, а вспомнить: похожий симптом уже разбирали, вот issue, вот причина, вот PR с фиксом. Вопрос ровно один — достаёт ли ассистент это прошлое решение из памяти. Мы это померили на своём корпусе тикетов и получили две цифры. Обычный семантический поиск (по смыслу слов) находит нужный прошлый фикс по симптому лишь в 38% случаев; обход явных причинных связей «issue → закрывший его PR» — в 87%. То есть сходство слов упирается в треть, а явные связи поднимают recall в разы.
Упрёк в комментариях к первой статье был предсказуем — «ни одного бенчмарка на чужих данных»: это ваш корпус, ваш домен, ваша культура ведения задач. Справедливо. Поэтому держите внешний бенчмарк — тот же скрипт, без единой подгонки, прогнанный по чужому большому репозиторию, к которому мы не имеем отношения: ydb-platform/ydb, открытая СУБД YDB от Яндекса.
В нём 10 285 issue и 37 067 PR, C++, английский язык, инженерная культура крупной компании. Ничего не настраивали под него — тот же demo/causal-recall-eval.mjs, та же разметка регулярным выражением, ноль вызовов LLM.
Метод, в двух абзацах
Эталонная разметка не выдумывается: это стандартный GitHub-паттерн Closes / Fixes / Resolves #N в теле PR. Он одновременно и пара «проблема → её фикс», и ребро графа. Извлекается голым regex — риска «модель неправильно поняла причинность» нет по построению.
Запрос — заголовок issue, то есть симптом, как его описал человек. Целевой PR почти всегда сформулирован иначе, поэтому подсказки в запросе нет: по словам симптома надо достать другой по словам узел-фикс. Метрика — chain-hit: доля запросов, где нужный PR оказался в выдаче. Две руки: (a) обычный семантический recall и (b) обход причинных рёбер от тех же засевов. Разница — вклад графа поверх сходства слов.
Что пришлось учесть в чужом корпусе
Первый прогон вскрыл особенность YDB, которой нет у нас: 53% свежих PR создаёт бот ydbot — механические синки из внутренней Arcadia. Они не ссылаются на issue и не несут смысла для recall, но занимают окно выгрузки: на 4000 свежих PR нашлось всего 47 пригодных пар.
Поэтому сделали два прогона. Первый — «как есть», на свежем срезе. Второй — на том же порядке (сначала свежие), но пропуская механических авторов, чтобы корпус состоял из живых PR людей и ИИ-агентов. Фильтруется только ydbot и github-actions; PR от copilot-swe-agent остаются — это настоящий агент, его работа в корпусе нужна.
Результаты
|
Прогон |
Корпус |
Gold-пар |
(a) семантика |
(b) причинный recall |
Вклад графа |
|---|---|---|---|---|---|
|
Свежее окно, как есть |
8 000 узлов (05.06–22.07.2026) |
47 |
36.2% |
95.7% |
+59.6 п.п. |
|
Без механических PR |
19 000 узлов (PR 09.2025–07.2026, issue с 12.2024) |
293 |
32.1% |
94.5% |
+62.5 п.п. |
|
Наш корпус (статья 1) |
4 299 узлов |
1 684 |
38.0% |
87.0% |
+49.0 п.п. |
Основной прогон — второй: 293 пары, все цели достижимы (целевой PR лежит в графе), 95%-е доверительные интервалы 32.1% [27.0–37.6] и 94.5% [91.3–96.6]. Первый прогон на 47 парах дал ту же картину с ожидаемо широкими интервалами (36.2% [24.0–50.5] и 95.7% [85.8–98.8]) — то есть фильтрация ботов картину не создала, а лишь сузила разброс.
Обратите внимание на направление отклонения от нашего корпуса: baseline на YDB ниже нашего (32.1% против 38.0%), причинный recall — выше (94.5% против 87.0%). Оба сдвига объясняются одним и тем же: корпус в 4.4 раза больше нашего, дистракторов больше, и сходство слов работает хуже; а рёбра Closes #N в YDB чище — issue и закрывающий PR связаны один-к-одному, тогда как у нас в корпусе гуще «братья-issue» одного модуля, на которых обход иногда садится.
Что это значит — и чего не значит
Переносится не число, а закономерность: семантика сама достаёт прошлое решение примерно в трети случаев, явные причинные связи поднимают это в разы. Baseline на YDB (32.1%) оказался рядом с нашим (38.0%) — при полностью другом языке, домене и культуре тикетов. Это и есть главный результат: не «у нас получилось 87», а «треть — это потолок сходства слов, и он воспроизводится у чужих людей».
Чего замер не показывает — честно: он не про latency и не про масштаб миллионов узлов; он держится на явных ссылках Closes #N (там, где их не пишут, этот бонус вы теряете); и он не измеряет качество самих рёбер отдельной цифрой — ложное ребро уводит обход в сторону, то есть шум recall занижает, а не завышает.
Побочное наблюдение: агенты уже коммитят — и это только прямой след
Раз уж корпус был под рукой, тем же детерминированным способом — регулярками по автору и телу PR, без LLM — посчитали следы ИИ. Их удобно разделить на два вида.
Прямой след — явная метка авторства, ложных срабатываний почти нет:
-
143 PR созданы аккаунтами-агентами (детектор ловит
copilot-swe-agent,cursor,devin,codex,aider; в YDB это преимущественно агент GitHub Copilot); -
22 PR несут трейлер соавторства или строку «Generated with Claude Code».
Косвенный след — использование без метки, и его в цифре нет по построению: внутренние инструменты следов на GitHub не оставляют, а при синке из внутреннего монорепозитория (Arcadia) трейлеры теряются. Поэтому доля «есть след ИИ» в свежем окне из 4000 PR — всего 7 (0.2%) — это заведомо нижняя граница, а не оценка сверху.
И там же, в том же корпусе, видно, что проблема памяти не выдумана, — работа возвращается: 173 PR с revert в заголовке, 2 067 issue со словом flaky и 356 — с regression, плюс 778 групп практически одинаковых по смыслу заголовков (3 514 PR). Это иллюстрация с поправками (часть повторов — легитимная рутина, часть flaky-issue заводится автоматически), но порядок величины один: одну и ту же работу переоткрывают даже в дисциплинированном инфраструктурном проекте. Именно её и должна ловить память ассистента.
Почему это будет только острее
Тему уже меряют публично — и теми же словами. Руководитель «Поиска и ИИ» Яндекса называет в интервью Forbes минимизацию галлюцинаций «критически важной инженерной работой», приводит долю фактических ошибок новой модели — 14.1% — и добавляет, что полностью убрать их нельзя из-за вероятностной природы сетей. Наш предмет — ровно та часть этой ошибки, что возникает не от слабого рассуждения, а от отсутствующего факта, который лежит в собственной истории проекта и не достаётся поиском. Тот же тезис Яндекс формулирует и сам — «LLM без поиска — генератор галлюцинаций». Пока агенты пишут всё больше кода, а исключить галлюцинацию арифметикой нельзя, вопрос «что ассистент помнит о прошлых разборах» из теоретического становится ежедневным.
Воспроизведение — без нашего кода
npm-пакет пока не опубликован — готовим его, — и предложить «склонируйте и прогоните» я пока не могу. Поэтому даю то, чего достаточно, чтобы собрать замер заново на своём стеке и при желании опровергнуть наш результат: нашу разметку целиком — 293 пары, выложены гистом 04-ydb-gold-pairs.csv — и метод в шести пунктах. Это не жест доброй воли — по построению в замере нет ни одной обученной или подогнанной детали, вся сложность в правилах, а они умещаются в шесть пунктов. Ни одного вызова LLM тут нет, только эмбеддинги и обход графа.
1. Корпус. Публичный, забирается двумя командами:
gh pr list --repo ydb-platform/ydb --state all --limit 24000 \ --json number,createdAt,author,title,body > ydb-prs.jsongh issue list --repo ydb-platform/ydb --state all --limit 9000 \ --json number,createdAt,title,body > ydb-issues.json
2. Окно и фильтр. Сортируем по createdAt, свежие первыми. Из PR выбрасываем механических авторов — regex ^(ydbot|app/github-actions|app/dependabot|app/renovate)$ — и берём первые 11 000; из issue берём первые 8 000. Важно: корпус и разметка строятся из одного и того же окна, иначе часть целей физически вне графа и промах говорит о границах выгрузки, а не о качестве поиска.
3. Узлы. Один тикет — один узел, текст: issue #N: <заголовок, 120 симв.>\n<тело, 300 симв.> (для PR — PR #N: …). Эмбеддинг — Xenova/paraphrase-multilingual-MiniLM-L12-v2, 384 измерения, метрика — косинус. Модель обычная, локальная; на любой другой мультиязычной числа поедут, а картина — нет (это и есть предмет проверки).
4. Разметка (gold). Для каждого PR ищем в теле \b(?:clos|fix|resolv)\w*\s+#(\d+) — это GitHub-паттерн автозакрытия. Цель обязана быть issue из окна (иначе fix #123 про соседний PR даст ребро PR→PR); заголовок issue не пустой. Пара = «заголовок issue → номер PR», запрос — заголовок issue, обрезанный до 120 символов.
5. Метрика. chain-hit при равном бюджете выдачи — 32 узла в обеих руках: (a) семантика — top-32 по косинусу плюс расширение по рёбрам похожести на глубину 2; (b) причинный recall — обход рёбер «issue → закрывший PR» на глубину 3 от тех же засевов. Попадание — целевой PR оказался в выдаче. Разница между (b) и (a) и есть вклад графа.
6. Сверка. Нашу разметку выкладываем целиком: 04-ydb-gold-pairs.csv — 293 пары, колонки issue, pr, query, pr_title. Если ваша разметка совпала с нашей, а числа разошлись — расхождение в реализации поиска, и это самое интересное, что может случиться с этой статьёй; напишите. Если не совпала сама разметка — сверяйте окно и фильтр из пунктов 2 и 4.
Перед публикацией мы прогнали замер заново только по этому CSV — без исходных json-выгрузок, беря пары прямо из файла, — и получили ровно те же числа: семантика 94 из 293 (32.1%), причинный recall 277 из 293 (94.5%), вклад графа +62.5 п.п. То есть файл выше самодостаточен: он и есть предмет проверки, а не иллюстрация к ней.
И главное: наведите то же правило на свой репозиторий. Ваше число, а не наше, отвечает на вопрос, сколько ваш ассистент теряет на поиске прошлых решений; наши 32% против 94.5% — лишь демонстрация процедуры на корпусе, к которому мы не имеем отношения.
Статья 4 в серии по опыту разработки vecmory. Предыдущие: 1 — «Агент, пойманный на вранье его же памятью», 2 — «Почему мы не написали ещё один Bad CaRMa», 3 — «Строили ANN руками — когда это того стоило, а когда нет».
ссылка на оригинал статьи https://habr.com/ru/articles/1065112/