Ricerca a fusione multi-modale: scegliere il retriever giusto per ogni query
> Una query come 'dove è definito parseConfig' vuole una ricerca diversa da 'come funziona l'auth'. Maguyva classifica l'intento, pesa di conseguenza quattro modalità di retrieval, e fonde i risultati con una Reciprocal Rank Fusion pesata.
Una query di ricerca non è una cosa sola.
“dove è definito parseConfig” vuole un simbolo esatto — una posizione precisa, veloce. “come funziona l’autenticazione” vuole significato — la distesa di codice correlato che spiega un concetto. “cosa si rompe se cambio questa funzione” vuole il grafo delle dipendenze. “trova la stringa ECONNREFUSED” vuole un match letterale, niente di furbo.
Grep è eccellente per i match letterali e utile per alcune cacce ai riferimenti, ma non è un grafo delle dipendenze e non capisce il significato. Gli embedding coprono il lato semantico, ma sono lo strumento sbagliato per stringhe esatte e analisi d’impatto. La maggior parte degli strumenti di ricerca sul codice sceglie un solo motore e fa vivere ogni query con quella scelta. Maguyva non sceglie. Capisce che tipo di domanda hai fatto, poi combina quattro retriever nella proporzione che quella domanda merita.
Quattro modalità
Sotto il cofano ci sono quattro modi indipendenti per trovare codice:
- semantica — ricerca vettoriale su embedding binari Voyage; trova codice per significato.
- testuale — matching a trigrammi; trova letterali, stringhe di errore, identificatori esatti.
- strutturale — query AST; trova definizioni, firme, e costrutti del linguaggio.
- a grafo — il grafo delle dipendenze; trova chiamanti, chiamati, e raggio d’impatto.
Ognuna eccelle su una classe diversa di domanda. Il trucco sta nel decidere quanto fidarsi di ciascuna per la query che hai davanti.
Classificazione dell’intento
Prima che parta qualsiasi retrieval, un classificatore leggero smista la query in una di sei categorie di intento, con un punteggio di confidenza. È deliberatamente economico — euristiche ordinate, vince il primo match — perché gira sul percorso critico e aggiunge solo un millisecondo o due:
- inizia con
def,class,func,import… → find_definition (confidenza 0,95) - “chi chiama”, “usi di”, “riferimenti a” → find_references (0,90)
- “impatto”, “raggio d’impatto”, “cosa dipende da” → impact_analysis (0,90)
- un
"string"tra virgolette o un token di errore cometraceback→ exact_match (0,85–0,90) - un identificatore
CamelCaseosnake_case→ find_definition (0,60–0,80) - “come”, “perché”, “spiega”, “architettura” → understand_code (0,75)
- niente corrisponde → understand_code, bassa confidenza (0,40)
Ogni intento porta un profilo di peso sulle quattro modalità. Questi sono i numeri reali:
| Intento | semantica | testuale | strutturale (AST) | a grafo |
|---|---|---|---|---|
| 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 |
Quindi “dove è definito parseConfig” pesa forte sull’AST (0,6). “come funziona l’auth” pesa sui vettori semantici (0,5). “cosa dipende da questo” è quasi tutto grafo (0,7). “trova ECONNREFUSED” è quasi tutto trigrammi (0,9), con il modello di embedding completamente spento — perché la similarità semantica è esattamente lo strumento sbagliato per una stringa esatta.
Il percorso rapido, e il percorso fuso
Quando il classificatore è sicuro — punteggio ≥ 0,85 — e la query è di tipo normale, Maguyva salta del tutto la fusione e instrada direttamente verso la singola modalità dominante. “dove è definito X” non ha bisogno di quattro retriever; ha bisogno dell’indice AST, subito. Quel percorso diretto viene riportato indietro come fusion_strategy: "direct".
Tutto ciò che è ambiguo passa attraverso la fusione. Le quattro modalità (o tre, sul preset predefinito) girano in parallelo, ciascuna restituendo la propria lista ordinata, e le combiniamo.
Reciprocal Rank Fusion pesata
Fondere retriever eterogenei è più difficile di quanto sembri: una similarità coseno di 0,82 e un punteggio a trigrammi di 137 e una centralità di grafo di 0,004 non sono sulla stessa scala, quindi non puoi semplicemente sommarli. La Reciprocal Rank Fusion aggira il problema scartando i punteggi grezzi e mantenendo solo il rank che ogni motore ha assegnato. Il contributo di un risultato da una modalità è:
contribution = weight × 1 / (k + rank + 1)
dove rank è la sua posizione nella lista di quella modalità e k è una costante di smoothing. I contributi vengono sommati tra le modalità per qualsiasi risultato che più di un motore ha trovato — l’accordo tra i retriever sale naturalmente in cima. Usiamo k = 40 sul preset predefinito e 60 su thorough (quick gira solo in modalità semantica, quindi la fusione lì non entra mai in gioco). Il lavoro originale su RRF è arrivato a k = 60 per il retrieval general-purpose; il nostro default è leggermente più aggressivo, il che dà un po’ più peso all’accordo tra le modalità nei risultati meglio classificati — e non consigliamo di regolarlo a mano.
Oltre a questo, i risultati portano un boost di importanza del grafo. Un hub — una funzione su cui si appoggia tutto il codebase — dovrebbe superare in classifica una foglia oscura anche a parità di rilevanza testuale, quindi moltiplichiamo ogni contributo per:
boost = min(1 + 0.3 × ln(1 + centrality), 1.5)
La centralità arriva dalle metriche PageRank/degree precalcolate dalla pipeline, e il boost è limitato a un massimo di 1,5× così che una funzione popolare non possa seppellire completamente una più rilevante ma oscura. Infine retrocediamo i risultati da percorsi vendor, build, e archive, e deduplichiamo al miglior chunk per file.
Cosa resta imperfetto
Il classificatore di intento è uno stack di regex, non un modello addestrato. Copre bene le forme comuni di una query — la decisione che lo ha introdotto ha registrato il tasso di risultati zero scendere da circa il 15% a meno del 5% — ma è euristico, e una query genuinamente ambigua finisce su understand_code e un blend a pendenza semantica. È un default sicuro, non uno furbo. Non lo abbiamo sostituito con un classificatore addestrato perché la versione economica è veloce e abbastanza buona, e perché un classificatore sicuro-ma-sbagliato è peggio di un fallback onesto. I pesi stessi sono priori scelti a mano, non appresi da dati di click che non raccogliamo.
Fai la domanda, non lo strumento
Un agente non dovrebbe dover sapere se ricorrere a grep o agli embedding o al call graph — dovrebbe fare la sua domanda in termini semplici e ottenere la risposta giusta. La fusione multi-modale è ciò che permette a find_symbol, alla ricerca semantica, e all’analisi delle dipendenze di stare dietro un’unica superficie di query: il sistema legge la forma della domanda e assembla silenziosamente il retriever giusto per essa. Il modello che valuta gli embedding conta, ma conta anche sapere quando non usarli. Scegliere lo strumento giusto per ogni query è un tipo di qualità a sé, ed è uno che preferiamo possedere noi piuttosto che scaricare su chi chiama.
// you bring the question. it brings the tools.
Letture correlate
Altro dal diario di costruzione di Maguyva
Perché abbiamo aggiornato la ricerca sul codice a voyage-4-large_
Abbiamo spostato i nostri embedding del codice su voyage-4-large — attualmente in cima alla classifica pubblica RTEB per il retrieval di codice. La versione onesta: il compromesso che facciamo, cosa indicizziamo davvero, e perché paghiamo per embedding premium.
Auto-miglioramento ricorsivo dei linguaggi: il grind della Code Intelligence su ~280 linguaggi_
Supportiamo la Code Intelligence per ~280 linguaggi. Nessun essere umano può controllarli a mano uno per uno. Così abbiamo costruito un loop di auto-miglioramento ricorsivo dei linguaggi — campionamento, LLM come giudice, correggi una cosa, rivalida — e lo facciamo girare con una flotta di agenti isolati finché l'estrazione non è davvero corretta, non solo verde.
Osservabilità degli agenti: hook, Alloy e Grafana_
Abbiamo collegato Claude Code e Codex a un unico stack Grafana con OpenTelemetry e Alloy, poi abbiamo usato trace e log per trovare e correggere i problemi di comportamento degli agenti alla fonte.