Внешний бенчмарк причинного recall: проверяем память ИИ-ассистента на YDB Яндекса

от автора

Коротко, о чём речь, — для тех, кто пришёл по слову «Яндекс» и предыстории не знает. Когда ИИ-ассистент помогает чинить баг, самое ценное — не сочинить ответ с нуля, а вспомнить: похожий симптом уже разбирали, вот 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-agentcursordevincodexaider; в 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/