Мультимодальний пошук зі злиттям: обираємо правильний ретрівер для кожного запиту
> Запит на кшталт «де визначено parseConfig» потребує іншого пошуку, ніж «як працює автентифікація». Maguyva класифікує намір, відповідно зважує чотири режими пошуку і зливає результати за допомогою зваженого Reciprocal Rank Fusion.
Пошуковий запит — це не одна річ.
«де визначено parseConfig» хоче точний символ — одне точне місце, швидко. «як працює автентифікація» хоче сенс — розкидані пов’язані фрагменти коду, що пояснюють концепцію. «що зламається, якщо я зміню цю функцію» хоче граф залежностей. «знайди рядок ECONNREFUSED» хоче буквальний збіг, нічого хитрого.
Grep чудовий для буквальних збігів і корисний для деяких пошуків посилань, але це не граф залежностей, і він не розуміє сенсу. Ембеддинги покривають семантичний бік, але вони — неправильний інструмент для точних рядків та аналізу впливу. Більшість інструментів пошуку коду обирають один рушій і змушують кожен запит жити з цим вибором. Maguyva не обирає. Він з’ясовує, яке саме запитання ви поставили, а потім змішує чотири ретрівери в пропорції, якої заслуговує це запитання.
Чотири режими
Під капотом є чотири незалежні способи знайти код:
- semantic — векторний пошук за бінарними ембеддингами Voyage; знаходить код за сенсом.
- text — тріграмне зіставлення; знаходить літерали, рядки помилок, точні ідентифікатори.
- structural — запити AST; знаходить визначення, сигнатури та мовні конструкції.
- graph — граф залежностей; знаходить викликачів, викликаних та радіус впливу.
Кожен сильний у своєму класі запитань. Хитрість у тому, щоб вирішити, наскільки довіряти кожному для конкретного запиту перед вами.
Класифікація наміру
Перш ніж запуститься будь-який пошук, легкий класифікатор сортує запит в один з шести намірів, з оцінкою впевненості. Він навмисно дешевий — упорядковані евристики, перший збіг перемагає — бо працює на гарячому шляху й додає лише мілісекунду-дві:
- починається з
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)
Кожен намір несе профіль ваг серед чотирьох режимів. Ось реальні цифри:
| Намір | semantic | text | structural (AST) | graph |
|---|---|---|---|---|
| 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). «що залежить від цього» майже повністю graph (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×, щоб популярна функція не могла повністю поховати релевантнішу, але малопомітну. Насамкінець ми знижуємо ранг результатів зі шляхів vendor, build та archive, і усуваємо дублікати, залишаючи найкращий фрагмент на файл.
Що досі недосконале
Класифікатор наміру — це стос регулярних виразів, а не навчена модель. Він добре покриває поширені форми запитів — рішення, яке його впровадило, зафіксувало падіння частки нульових результатів приблизно з 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, а потім використали трейси й логи, щоб знаходити та виправляти проблеми в поведінці агентів у джерелі.