Мультимодальный фьюжн-поиск: выбор правильного ретривера для каждого запроса
> Запрос вроде «где определён parseConfig» требует другого поиска, чем «как работает аутентификация». Maguyva классифицирует намерение, соответствующим образом взвешивает четыре модальности поиска и сливает результаты с помощью взвешенного Reciprocal Rank Fusion.
Поисковый запрос — это не одна вещь.
«где определён parseConfig» хочет точный символ — одно точное место, быстро. «как работает аутентификация» хочет смысл — набор связанного кода, который объясняет концепцию. «что сломается, если я изменю эту функцию» хочет граф зависимостей. «найти строку ECONNREFUSED» хочет буквальное совпадение, ничего умного.
Grep отлично справляется с буквальными совпадениями и полезен для некоторых поисков ссылок, но это не граф зависимостей, и он не понимает смысл. Эмбеддинги покрывают семантическую сторону, но это неверный инструмент для точных строк и анализа влияния. Большинство инструментов поиска по коду выбирают один движок и заставляют каждый запрос жить с этим выбором. Maguyva не выбирает. Он выясняет, какого рода вопрос вы задали, а затем смешивает четыре ретривера в той пропорции, которую заслуживает этот вопрос.
Четыре модальности
Под капотом есть четыре независимых способа найти код:
- семантический — векторный поиск по бинарным эмбеддингам Voyage; находит код по смыслу.
- текстовый — триграммное сопоставление; находит литералы, строки ошибок, точные идентификаторы.
- структурный — запросы к AST; находит определения, сигнатуры и языковые конструкции.
- графовый — граф зависимостей; находит вызывающих, вызываемых и радиус воздействия.
Каждый силён в своём классе вопросов. Хитрость в том, чтобы решить, насколько доверять каждому из них для конкретного запроса перед вами.
Классификация намерения
Прежде чем запускается любое извлечение, лёгкий классификатор сортирует запрос в одно из шести намерений, с оценкой уверенности. Он намеренно дешёвый — упорядоченные эвристики, побеждает первое совпадение — потому что он работает на горячем пути и добавляет всего миллисекунду-другую:
- начинается с
def,class,func,import… → find_definition (уверенность 0.95) - «кто вызывает», «использования», «ссылки на» → find_references (0.90)
- «влияние», «радиус воздействия», «что зависит от» → impact_analysis (0.90)
- строка в кавычках
"string"или токен ошибки вродеtraceback→ exact_match (0.85–0.90) - идентификатор
CamelCaseилиsnake_case→ find_definition (0.60–0.80) - «как», «почему», «объясни», «архитектура» → understand_code (0.75)
- ничего не совпало → understand_code, низкая уверенность (0.40)
Каждое намерение несёт профиль весов по четырём модальностям. Вот реальные цифры:
| Намерение | семантический | текстовый | структурный (AST) | графовый |
|---|---|---|---|---|
| find_definition | 0.2 | 0.1 | 0.6 | 0.1 |
| find_references | 0.1 | 0.2 | 0.2 | 0.5 |
| understand_code | 0.5 | 0.2 | 0.2 | 0.1 |
| find_similar | 0.4 | 0.3 | 0.2 | 0.1 |
| impact_analysis | 0.1 | 0.1 | 0.1 | 0.7 |
| exact_match | 0.0 | 0.9 | 0.1 | 0.0 |
Так что «где определён parseConfig» сильно опирается на AST (0.6). «как работает аутентификация» опирается на семантические векторы (0.5). «что зависит от этого» — почти целиком граф (0.7). «найти ECONNREFUSED» — почти целиком триграммы (0.9), при этом модель эмбеддингов полностью отключена — потому что семантическое сходство здесь ровно неверный инструмент для точной строки.
Быстрый путь и слитый путь
Когда классификатор уверен — оценка ≥ 0.85 — и запрос обычный, Maguyva полностью пропускает слияние и направляет запрос прямо в единственную доминирующую модальность. «где определён X» не нужны четыре ретривера; ему нужен AST-индекс, прямо сейчас. Этот прямой путь возвращается как fusion_strategy: "direct".
Всё неоднозначное проходит через слияние. Четыре (или три, в дефолтном пресете) модальности запускаются параллельно, каждая возвращает свой собственный ранжированный список, и мы их объединяем.
Взвешенное Reciprocal Rank Fusion
Слияние разнородных ретриверов сложнее, чем кажется: косинусное сходство 0.82, триграммная оценка 137 и графовая центральность 0.004 находятся не в одной шкале, так что их нельзя просто сложить. Reciprocal Rank Fusion обходит проблему, отбрасывая сырые оценки и оставляя только ранг, присвоенный каждым движком. Вклад результата от одной модальности равен:
contribution = weight × 1 / (k + rank + 1)
где rank — его позиция в списке этой модальности, а k — константа сглаживания. Вклады суммируются по модальностям для любого результата, который нашёл больше чем один движок — согласие между ретриверами естественным образом всплывает наверх. Мы используем k = 40 на дефолтном пресете и 60 на thorough (quick работает только в семантическом режиме, так что слияние там никогда не срабатывает). Оригинальная работа по RRF остановилась на k = 60 для поиска общего назначения; мы по умолчанию берём чуть более резкое значение, что даёт немного больше веса топовому согласию между модальностями — и мы не рекомендуем настраивать его вручную.
Поверх этого результаты несут буст по графовой значимости. Хаб — функция, на которую опирается вся кодовая база, — должен обгонять малоизвестный лист даже при той же текстовой релевантности, поэтому мы умножаем каждый вклад на:
boost = min(1 + 0.3 × ln(1 + centrality), 1.5)
Центральность берётся из предвычисленных метрик PageRank/степени вершины конвейера, а буст ограничен сверху 1.5×, чтобы популярная функция не могла полностью похоронить более релевантную, но малоизвестную. Наконец, мы понижаем результаты из путей вендоров, сборки и архивов и убираем дубликаты, оставляя лучший фрагмент на файл.
Что всё ещё несовершенно
Классификатор намерений — это стек регулярных выражений, а не обученная модель. Он хорошо покрывает распространённые формы запроса — решение, которое его ввело, зафиксировало падение доли нулевых результатов примерно с 15% до менее чем 5% — но он эвристический, и по-настоящему неоднозначный запрос проваливается в understand_code и смесь с уклоном в сторону семантики. Это безопасное значение по умолчанию, а не хитрое. Мы не заменили его обученным классификатором, потому что дешёвая версия быстрая и достаточно хорошая, а неверный, но уверенный классификатор хуже честного отката. Сами веса — это подобранные вручную априорные значения, а не выученные по данным о кликах, которые мы не собираем.
Задавайте вопрос, а не инструмент
Агенту не должно быть нужно знать, тянуться ли ему к grep, эмбеддингам или графу вызовов — он должен задать свой вопрос простыми словами и получить правильный ответ. Именно мультимодальное слияние позволяет find_symbol, семантическому поиску и анализу зависимостей находиться за одной поверхностью запроса: система считывает форму вопроса и тихо собирает под неё правильный ретривер. Модель, которая оценивает эмбеддинги, важна, но не менее важно знать, когда их не использовать. Выбор правильного инструмента под каждый запрос — это отдельный вид качества, и мы предпочитаем владеть им сами, а не перекладывать на вызывающую сторону.
// you bring the question. it brings the tools.
Похожие материалы
Ещё из журнала разработки Maguyva
Почему мы обновили поиск по коду до voyage-4-large_
Мы перевели эмбеддинги кода на voyage-4-large — модель, которая сейчас возглавляет публичный рейтинг RTEB по поиску кода. Честная версия: какой компромисс мы принимаем, что мы на самом деле индексируем и почему платим за премиальные эмбеддинги.
Рекурсивное самосовершенствование языков: шлифуем интеллектуальный анализ кода на ~280 языках_
Мы поддерживаем интеллектуальный анализ кода для ~280 языков. Ни один человек не может вручную проверить это. Поэтому мы построили цикл рекурсивного самосовершенствования языков — выборочная проверка, LLM в роли судьи, исправление одной вещи, повторная валидация — и прогоняем его флотом изолированных агентов, пока извлечение не станет по-настоящему верным, а не просто «зелёным».
Наблюдаемость агентов: хуки, Alloy и Grafana_
Мы подключили Claude Code и Codex к единому стеку Grafana через OpenTelemetry и Alloy, а затем с помощью трасс и логов находили и устраняли проблемы в поведении агентов прямо у источника.