Passer au contenu
cd /blog

Recherche par fusion multimodale : choisir le bon moteur de récupération pour chaque requête

[Recherche][Architecture]

> Une requête comme « où est défini parseConfig » appelle une recherche différente de « comment fonctionne l'authentification ». Maguyva classe l'intention, pondère en conséquence quatre modalités de récupération, puis fusionne les résultats avec une Reciprocal Rank Fusion pondérée.

Une requête de recherche n’est pas une chose unique.

« où est défini parseConfig » veut un symbole exact — un emplacement précis, vite. « comment fonctionne l’authentification » veut du sens — l’ensemble du code apparenté qui explique un concept. « qu’est-ce qui casse si je modifie cette fonction » veut le graphe de dépendances. « trouver la chaîne ECONNREFUSED » veut une correspondance littérale, rien de plus subtil.

Grep excelle pour les correspondances littérales et se révèle utile pour certaines chasses aux références, mais ce n’est pas un graphe de dépendances et il ne comprend pas le sens. Les embeddings couvrent le versant sémantique, mais ce sont le mauvais outil pour les chaînes exactes et l’analyse d’impact. La plupart des outils de recherche de code choisissent un seul moteur et font vivre chaque requête avec ce choix. Maguyva ne choisit pas. Il détermine quel type de question vous avez posée, puis mélange quatre moteurs de récupération dans la proportion que cette question mérite.

Quatre modalités

Sous le capot, il existe quatre façons indépendantes de trouver du code :

  • sémantique — recherche vectorielle sur des embeddings binaires Voyage ; trouve le code par le sens.
  • textuelle — correspondance de trigrammes ; trouve les littéraux, les chaînes d’erreur, les identifiants exacts.
  • structurelle — requêtes AST ; trouve les définitions, les signatures et les constructions du langage.
  • par graphe — le graphe de dépendances ; trouve les appelants, les appelés et le rayon d’impact.

Chacune excelle sur une classe de question différente. L’astuce consiste à décider dans quelle mesure faire confiance à chacune pour la requête que l’on a sous les yeux.

Classification d’intention

Avant tout lancement de récupération, un classificateur léger trie la requête dans l’une de six intentions, avec un score de confiance. Il est délibérément peu coûteux — des heuristiques ordonnées, la première correspondance l’emporte — car il s’exécute sur le chemin critique et n’ajoute qu’une milliseconde ou deux :

  • commence par def , class , func , import … → find_definition (confiance 0,95)
  • « qui appelle », « usages de », « références à » → find_references (0,90)
  • « impact », « rayon d’impact », « qu’est-ce qui dépend de » → impact_analysis (0,90)
  • une chaîne entre guillemets "string" ou un jeton d’erreur comme tracebackexact_match (0,85–0,90)
  • un identifiant CamelCase ou snake_casefind_definition (0,60–0,80)
  • « comment », « pourquoi », « expliquer », « architecture » → understand_code (0,75)
  • rien ne correspond → understand_code, faible confiance (0,40)

Chaque intention porte un profil de pondération réparti sur les quatre modalités. Voici les chiffres réels :

Intention sémantique textuelle structurelle (AST) graphe
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

Ainsi, « où est défini parseConfig » s’appuie fortement sur l’AST (0,6). « comment fonctionne l’authentification » s’appuie sur les vecteurs sémantiques (0,5). « qu’est-ce qui dépend de ceci » est presque entièrement du graphe (0,7). « trouver ECONNREFUSED » est presque entièrement du trigramme (0,9), le modèle d’embedding étant entièrement désactivé — parce que la similarité sémantique est précisément le mauvais outil pour une chaîne exacte.

Le chemin rapide, et le chemin fusionné

Quand le classificateur est confiant — score ≥ 0,85 — et que la requête est ordinaire, Maguyva saute entièrement la fusion et route directement vers l’unique modalité dominante. « où est défini X » n’a pas besoin de quatre moteurs de récupération ; il lui faut l’index AST, tout de suite. Ce chemin direct est renvoyé sous la mention fusion_strategy: "direct".

Tout ce qui est ambigu passe par la fusion. Les quatre modalités (ou trois, sur le préréglage par défaut) s’exécutent en parallèle, chacune renvoyant sa propre liste classée, et nous les combinons.

Reciprocal Rank Fusion pondérée

Fusionner des moteurs de récupération hétérogènes est plus difficile qu’il n’y paraît : une similarité cosinus de 0,82, un score de trigramme de 137 et une centralité de graphe de 0,004 ne sont pas sur la même échelle, donc on ne peut pas simplement les additionner. La Reciprocal Rank Fusion contourne le problème en jetant les scores bruts et en ne conservant que le rang attribué par chaque moteur. La contribution d’un résultat pour une modalité est :

contribution = weight × 1 / (k + rank + 1)

rank est sa position dans la liste de cette modalité et k est une constante de lissage. Les contributions sont sommées entre modalités pour tout résultat trouvé par plus d’un moteur — l’accord entre moteurs de récupération remonte naturellement vers le haut. Nous utilisons k = 40 sur le préréglage par défaut et 60 sur thorough (quick tourne uniquement en sémantique, donc la fusion n’y intervient jamais). Les travaux originaux sur la RRF ont retenu k = 60 pour la récupération généraliste ; notre valeur par défaut est légèrement plus tranchante, ce qui donne un peu plus de poids à l’accord entre modalités bien classées — et nous ne recommandons pas de la régler manuellement.

En plus de cela, les résultats portent un bonus d’importance de graphe. Un nœud central — une fonction sur laquelle s’appuie toute la base de code — devrait devancer une feuille obscure même à pertinence textuelle égale, donc nous multiplions chaque contribution par :

boost = min(1 + 0.3 × ln(1 + centrality), 1.5)

La centralité provient des métriques PageRank / degré précalculées du pipeline, et le bonus est plafonné à 1,5× pour qu’une fonction populaire ne puisse pas complètement enterrer une fonction obscure plus pertinente. Enfin, nous rétrogradons les résultats issus des chemins vendor, build et archive, et nous dédupliquons pour ne garder que le meilleur segment par fichier.

Ce qui reste imparfait

Le classificateur d’intention est un empilement d’expressions régulières, pas un modèle appris. Il couvre bien les formes courantes de requête — la décision qui l’a introduit a enregistré une chute du taux de résultats vides d’environ 15 % à moins de 5 % — mais il reste heuristique, et une requête réellement ambiguë retombe sur understand_code et un mélange à dominante sémantique. C’est un choix par défaut sûr, pas malin. Nous ne l’avons pas remplacé par un classificateur entraîné, parce que la version peu coûteuse est rapide et suffisamment bonne, et parce qu’un classificateur qui se trompe avec assurance est pire qu’un repli honnête. Les pondérations elles-mêmes sont des a priori choisis à la main, pas appris à partir de données de clics que nous ne collectons pas.

Poser la question, pas choisir l’outil

Un agent ne devrait pas avoir à savoir s’il faut recourir à grep, aux embeddings ou au graphe d’appels — il devrait poser sa question en termes simples et obtenir la bonne réponse. La fusion multimodale est ce qui permet à find_symbol, à la recherche sémantique et à l’analyse de dépendances de se tenir derrière une seule surface de requête : le système lit la forme de la question et assemble discrètement le bon moteur de récupération pour elle. Le modèle qui note les embeddings compte, mais savoir quand ne pas les utiliser compte tout autant. Choisir le bon outil pour chaque requête est une forme de qualité à part entière, et c’est une responsabilité que nous préférons assumer plutôt que de la reporter sur l’appelant.

// you bring the question. it brings the tools.

Lectures associées

Encore plus du journal de bord Maguyva