Accueil / Articles

RAG pour tableau de fléchettes : Que faire lorsque Top-K renvoie le même morceau trois fois

La récupération top-k classique peut retourner des fragments presque identiques. Dartboard RAG sélectionne à la fois en fonction de la pertinence et de la diversité ; voici l’algorithme, le code ainsi que les cas d’utilisation.

Éditeur : REACTAPP.TOP11 min de lecture2616 mots

Sources: Dartboard RAG: When Top-K Returns Three Versions of the Same Chunk

La récupération top-k classique échoue silencieusement. Lorsqu’un corpus exprime la même chose en plusieurs endroits, les k fragments les plus proches pour une requête peuvent se révéler être un seul fragment qui le dit trois fois avec des mots différents.

Imaginez un rapport de 100 pages sur le changement climatique dans lequel cinq sections abordent chacune les gaz à effet de serre, sous des angles légèrement différents mais pour souligner le même point. Quelqu’un demande : « Qu’est-ce qui provoque le changement climatique ? » et l’index FAISS renvoie ceci :

Top 3:
1. "Greenhouse gases trap heat in the atmosphere, causing warming..."  (sim 0.91)
2. "Atmospheric greenhouse gases are the primary driver of climate change..."  (sim 0.89)
3. "The trapping of heat by greenhouse gases leads to rising temperatures..."  (sim 0.88)

Cela correspond à trois extraits et en réalité à un seul fait. L’utilisateur a demandé des causes, au pluriel, mais n’a reçu qu’une seule cause présentée à plusieurs reprises. Les extraits concernant la déforestation, l’agriculture industrielle et les changements d’affectation des sols se situaient aux positions 8, 12 et 17 respectivement, de sorte qu’aucun n’a atteint le contexte du modèle de langage. La réponse du modèle reflète son entrée : un paragraphe fluide sur les gaz à effet de serre, sans aucune mention des autres causes.

La recherche vectorielle n’est pas la cause du problème. Elle a fait ce qu’on lui avait demandé, en retournant les éléments les plus similaires selon le critère top-k. Le problème, c’est que la similarité ne prend pas en compte la redondance. Trois extraits peuvent être tous extrêmement proches de la requête tout en étant presque identiques les uns aux autres, et une simple méthode top-k retiendra tous les trois au détriment de matériaux différents mais pertinents.

Dartboard RAG modifie la règle de sélection. Alors que la récupération standard cherche les k extraits les plus proches de la requête, cette approche recherche des k extraits qui soient tous hautement pertinents et également suffisamment éloignés les uns des autres. Les résultats obtenus couvrent ainsi les parties pertinentes du corpus, et non plusieurs copies de sa meilleure correspondance.

Imaginez un tableau de fléchettes

Considérez la requête comme le centre du tableau et chaque extrait comme une fléchette déjà plantée dans ce dernier. Plus l’extrait est proche du centre, plus il est pertinent.

Chunks plotted by relevance to query
                    (closer to center = higher cosine sim)

                       ●●●         ← cluster of near-duplicate chunks
                      ●●●          (all about "greenhouse gases")
                      ● bull's-eye = QUERY

                                ●     ← chunk about deforestation
                                       (relevant but different topic)

                    ●           ●     ← chunks about agriculture, ocean carbon

                       ●  ●          ← chunks about historical climate


   STANDARD TOP-3 picks:           DARTBOARD TOP-3 picks:
   3 closest darts                 1 closest, then darts that are also
   = 3 darts in the same           good but spread across the board
     spot near bull's-eye          = better coverage of relevant content

La récupération ordinaire sélectionne les trois fléchettes les plus proches. La méthode du tableau de fléchettes choisit trois fléchettes à la fois proches et éloignées les unes des autres.

Le processus global

D’un point de vue architectural, il s’agit d’une récupération standard auquel on ajoute une sélection plus intelligente. Le stockage vectoriel et les embeddings restent exactement tels qu’ils étaient. La seule nouveauté est une étape de sélection gourmande qui décide quels des 15 candidats feront partie des 5 finaux.

Étape 1 : récupérer plus que nécessaire

Puisque l’algorithme ne peut pas choisir parmi des candidats qu’il n’a pas, il a besoin d’un ensemble plus grand que le nombre k final. La mise en œuvre de référence multiplie ce nombre par un facteur d’oversampling de 3 :

fetch_k = self.k * self.oversampling     # default: 5 × 3 = 15
candidates = vector_store.search(query_embedding, k=fetch_k)

Ainsi, on sélectionne 15 candidats au lieu de 5. FAISS les renvoie classés selon la similarité cosinus, ce qui place le candidat 0 le plus proche de la requête et le candidat 14 le plus éloigné. L’étape de sélection choisit ensuite le meilleur sous-ensemble de cinq en tenant compte à la fois de la pertinence et de la diversité.

L’étape suivante a besoin de l’embedding de chaque candidat et non seulement de son texte ; par conséquent, l’ensemble est récupéré sous forme d’objets Document complets accompagnés de leurs embeddings.

Étape 2 : créer deux matrices de distance

A partir des embeddings des candidats, on calcule deux matrices :

# Normalize all vectors so dot product = cosine similarity
query_norm = query_vec / np.linalg.norm(query_vec)
cand_norm = candidate_matrix / np.linalg.norm(candidate_matrix, axis=1, keepdims=True)

# Distance = 1 - cosine_similarity
query_distances = 1.0 - np.dot(query_norm, cand_norm.T)        # (1, N)
document_distances = 1.0 - np.dot(cand_norm, cand_norm.T)      # (N, N)
  • query_distances contient la distance entre la requête et chaque candidat. Une valeur faible indique une grande pertinence. Sa forme est (1, N).
  • document_distances contient la distance entre chaque paire de candidats. Une valeur faible signifie que ces candidats se ressemblent. Sa forme est (N, N).

La pertinence provient de la première matrice et la diversité de la seconde.

Étape 3 : transformer les distances en probabilités logarithmiques

Les distances brutes ne se combinent pas bien. L’écart entre 0,3 et 0,4 peut sembler significatif, mais une simple soustraction donne 0,1, ce qui le minimise. Ainsi, l’algorithme mappe les distances en probabilités logarithmiques sous un noyau gaussien :

def lognorm(dist, sigma):
    return -np.log(sigma) - 0.5 * np.log(2 * np.pi) - dist**2 / (2 * sigma**2)

Ce graphique représente le logarithme d’une densité gaussienne. En termes intuitifs, une petite distance correspond à une haute probabilité (ce qui est proche signifie probablement pertinent ou similaire), tandis qu’une grande distance correspond à une faible probabilité. La valeur sigma détermine la netteté de la courbe. Une petite valeur de sigma, avec une valeur par défaut de 0,1, produit des pics serrés qui réagissent fortement aux petites différences de distance. Une valeur plus élevée, comme 0,5, aplatisse la courbe afin que des distances similaires soient traitées de manière plus similaire.

Travailler dans l’espace logarithmique est important pour la stabilité numérique. Avec de petites distances et une petite valeur de sigma, les probabilités brutes se réduisent à des valeurs flottantes extrêmement petites qui tombent à zéro, tandis que les probabilités logarithmiques restent limitées et comparables.

Étape 4 : la boucle gourmande

C’est ici que réside réellement la logique du tableau de cible. La procédure se déroule comme suit :

  1. Prendre d’abord le fragment le plus pertinent. Aucun test de diversité n’est appliqué pour l’instant, car rien n’a encore été sélectionné comme point de comparaison.
  2. Conserver max_distances, qui enregistre pour chaque candidat sa plus grande distance de probabilité logarithmique par rapport à tout document déjà choisi. Une valeur élevée signifie une plus grande différence par rapport à la sélection actuelle.
  3. Pour chaque emplacement restant, évaluer chaque candidat non sélectionné en combinant un terme de pertinence (son degré de proximité avec la requête, issu de query_probs) et un terme de diversité (son éloignement par rapport au candidat déjà sélectionné le plus proche, issu de max_distances). La combinaison est diversity_weight × diversity + relevance_weight × relevance.
  4. Agréger les scores combinés à l’aide de logsumexp, méthode numériquement stable.
  5. Sélectionner le candidat ayant le score global le plus élevé, l’ajouter et mettre à jour max_distances.
  • Repetez jusqu’à ce que k candidats aient été sélectionnés.
  • Dans le code :

    # Step 1: pick most relevant first
    most_relevant_idx = np.argmax(query_probs)
    selected_indices = [most_relevant_idx]
    max_distances = doc_probs[most_relevant_idx].copy()    # diversity tracker
    
    # Step 2-6: iteratively add diverse + relevant chunks
    while len(selected_indices) < num_results:
        # For each candidate, compute "diversity from any selected"
        updated_distances = np.maximum(max_distances, doc_probs)
    
        # Combine relevance + diversity
        combined = (diversity_weight * updated_distances
                    + relevance_weight * query_probs[np.newaxis, :])
    
        # Aggregate per candidate (logsumexp for numerical stability)
        normalized = logsumexp(combined, axis=1)
    
        # Mask already-selected
        for idx in selected_indices:
            normalized[idx] = -np.inf
    
        # Pick the best
        best_idx = np.argmax(normalized)
        max_distances = updated_distances[best_idx]
        selected_indices.append(best_idx)
    

    L’idée principale est le score combiné. Chaque choix pèse la question « Est-ce pertinent ? » par rapport à « Est-ce différent de ce qui est déjà présent ? ». Le premier choix maximise la pertinence pure, tandis que chaque choix ultérieur équilibre les deux aspects.

    En termes simples, ce que fait la mathématique

    À chaque étape, le cycle se demande en fait : parmi les candidats encore non choisis, lequel améliore le mieux à la fois son score de pertinence et la couverture de la région pertinente ?

    Imaginez un graphique en dispersion 2D où l’axe des abscisses représente la pertinence et l’axe des ordonnées la distance par rapport au ensemble sélectionné. Le premier choix correspond au point le plus à droite. Par la suite, chaque autre candidat possède une valeur en ordonnée qui correspond à sa distance par rapport à ce premier choix.

    Pour le deuxième choix, vous souhaitez quelque chose situé bien à droite (rélevant) et également en hauteur (contrairement au premier). Comme les deux poids sont à 1,0, l’équilibre est égal, de sorte que la boucle sélectionnera un élément moins pertinent mais nettement plus différent.

    Relevance →
                Low              ●◯
    
                       ●         ●           ←  Standard top-k picks these 3
                                                  (highest relevance, regardless of diversity)
                        ●        ●
                           ●●●●●●  ●  ●  ●     ← Many similar high-relevance chunks
                High         (cluster)
    
    
                Diversity ↓
                from
                selected
                            ↓
                         ↓     ↓   ←  Dartboard picks 1 from cluster,
                                     then far-away ones with high relevance still
    

    La méthode top-k simple sélectionne les points les plus à droite, quel que soit le degré d’agglomération entre eux. Dartboard commence par choisir les points les plus à droite, puis s’étend délibérément vers d’autres régions pertinentes.

    Les deux poids

    relevance_weight = 1.0     # how much we care about chunks being close to query
    diversity_weight = 1.0     # how much we care about chunks being different from each other
    

    Tous deux sont réglés par défaut sur 1,0, ce qui signifie une importance égale. Leur modification modifie le comportement : augmentez la diversité si des doublons continuent d’apparaître, et augmentez la pertinence si des extraits hors sujet commencent à apparaître. Les valeurs par défaut conviennent à la plupart des corpus.

    Un exemple concret avec un corpus riche en doublons

    L’implémentation de référence dispose d’un paramètre duplicate_factor qui répète chaque extrait N fois, ce qui constitue un moyen utile de tester la diversité sur un corpus artificiellement dense. Définissez-le sur 5 et posez la question « Quelles sont les causes du changement climatique ? »

    La recherche des 5 meilleurs résultats standards donne ceci :

    1. "Greenhouse gases cause warming..."  (sim 0.91)
    2. "Greenhouse gases cause warming..."  (sim 0.91)  ← DUPLICATE
    3. "Greenhouse gases cause warming..."  (sim 0.91)  ← DUPLICATE
    4. "Greenhouse gases cause warming..."  (sim 0.91)  ← DUPLICATE
    5. "Greenhouse gases cause warming..."  (sim 0.91)  ← DUPLICATE
    
    Unique results: 1/5
    

    Les cinq résultats forment littéralement un seul ensemble, tous également proches de la requête ; ainsi, le LLM reçoit un même fait réexprimé cinq fois.

    Dartboard top-5 donne ce résultat :

    1. "Greenhouse gases cause warming..."  (highest relevance — wins first pick)
    2. "Deforestation reduces the carbon sink..."  (different chunk, still relevant)
    3. "Industrial agriculture emits methane..."  (third unique cause)
    4. "Land-use changes alter surface albedo..."  (fourth unique cause)
    5. "Fossil fuel combustion is the largest CO₂ source..."  (related to #1 but different angle)
    
    Unique results: 5/5
    

    Dès que le premier fragment est choisi, le terme de diversité pénalise chaque copie de celui-ci, car chacune présente une diversité nulle par rapport à la sélection. Le boucle cherche donc des contenus distincts dans les slots deux à cinq, et le LLM prend connaissance de cinq faits différents concernant les causes. C’est le meilleur scénario possible. Les corpus réels contiennent rarement des doublons exacts, ce qui atténue l’effet, bien que la même logique s’applique aux doublons partiels.

    Tout cela en une vingtaine de lignes

    def dartboard_select(query_emb, candidate_embs, k=5, sigma=0.1):
        # 1. Compute distance matrices
        query_dist = 1 - cosine(query_emb, candidate_embs)        # query→each
        doc_dist   = 1 - cosine(candidate_embs, candidate_embs)   # each→each
    
        # 2. Convert distances to log-probabilities
        query_probs = lognorm(query_dist, sigma)
        doc_probs   = lognorm(doc_dist, sigma)
    
        # 3. Pick most relevant first
        selected = [np.argmax(query_probs)]
        max_distances = doc_probs[selected[0]].copy()
    
        # 4. Iteratively add diverse + relevant
        while len(selected) < k:
            updated = np.maximum(max_distances, doc_probs)
            combined = updated + query_probs[np.newaxis, :]   # equal weights = sum
            scores = logsumexp(combined, axis=1)
            for idx in selected:
                scores[idx] = -np.inf       # don't re-select
    
            best = np.argmax(scores)
            max_distances = updated[best]
            selected.append(best)
    
        return selected
    

    La longueur est modeste. La difficulté réside dans la signification des calculs mathématiques et non dans le nombre de lignes. Une fois la règle claire (choisir le fragment le plus pertinent, puis privilégier ceux qui sont à la fois pertinents et différents de ce qui a déjà été choisi), le code devient facile à suivre.

    Paramètres à ajuster

    Deux paramètres méritent une attention particulière.

    • oversampling définit la taille de l’ensemble des candidats. La valeur par défaut de 3 signifie 15 candidats pour en retenir finalement 5. En augmentant cette valeur à 5 ou 10, on donne plus de matière au algorithme, mais cela ralentit le calcul des distances entre paires. Dans un corpus fortement dupliqué, un oversampling de 10 permet à dartboard d’identifier des fragments véritablement distincts plus en bas dans le classement.
    • diversity_weight doit augmenter si vos évaluations révèlent encore des doublons qui passent inaperçus, et diminuer si les résultats sont trop dispersés, avec des extraits hors sujet sélectionnés simplement parce qu’ils sont « différents ». La valeur par défaut de 1,0 est équilibrée, et des valeurs de 1,5 ou 2,0 conviennent bien pour des corpus très denses.

    sigma est plus délicat et nécessite rarement d’être modifié. La valeur par défaut de 0,1 fonctionne bien pour les distances cosinus sur les embeddings d’OpenAI. Si vous passez à un modèle d’embedding avec des échelles de distance différentes, vous pourriez avoir besoin de le recalibrer, bien que la plupart des utilisateurs puissent le laisser tel quel.

    Où cela aide et où cela ne sert à rien

    Dartboard convient à plusieurs situations.

    • Corpus contenant des quasi-dupliqués. De nombreuses sources traitant d’un même sujet, des documents de politique répétitifs, des archives de nouvelles avec une couverture chevauchante, des posts de forum indexés. La méthode standard top-k échoue fréquemment dans ce cas.
    • La couverture est aussi importante que la précision. Résumés de recherche, questions-réponses approfondis et analyses comparatives, où l’utilisateur souhaite avoir l’ensemble des informations pertinentes et non seulement la meilleure partie d’entre elles.
    • Ressources informatiques supplémentaires. Le coût additionnel provient du calcul de matrices sur l’ensemble des candidats, et non de requêtes supplémentaires vers un LLM. Cela est imperceptible pour k=5 avec 15 candidats, mais perceptible pour k=20 avec 60 candidats ou plus.

    Cela est moins utile dans de tels cas.

    • Corpus naturellement diversifiés. Si les segments sont déjà distincts sur le plan thématique, que ce soit grâce à la segmentation sémantique ou à des documents bien structurés, la méthode top-k produit déjà des segments variés ; Dartboard ajoute donc une charge inutile sans modifier le classement.
    • Questions très spécifiques. « Quel est le point d’ébullition du mercure ? » a une seule réponse pertinente. La logique de diversité peut choisir des segments moins pertinents dans le but de varier les résultats, ce qui nuit à la qualité de la réponse.
    • k très petit. Lorsque k vaut 1 ou 2, il n’y a peu de marge pour la diversification, donc la méthode top-k classique suffit.
    • Chaînes de traitement qui combinent déjà la fusion, le reclassement et le fenêtrage. Chacun de ces éléments traite des problèmes similaires à sa manière. Ajouter Dartboard à une telle chaîne de traitement ne sert généralement à rien et augmente la complexité.

    L’idée à retenir

    Le point intéressant de Dartboard RAG n’est pas la mathématique. C’est la prise de conscience que la pertinence seule n’est pas l’objectif adéquat pour la récupération top-k.

    La méthode standard top-k cherche à déterminer quels k éléments sont individuellement les plus pertinents. Cela traite chaque élément de manière indépendante et ignore ce qui se trouve déjà dans l’ensemble des résultats ; ainsi, lorsqu’il existe plusieurs éléments tout aussi pertinents, on les obtient tous même s’ils disent la même chose.

    L’objectif plus pertinent prend en compte l’ensemble : quels k éléments, pris ensemble, couvrent le mieux les besoins de la requête ? Il s’agit d’une optimisation différente, dans laquelle le score de l’élément k dépend des éléments 1 à k-1 et non seulement de sa propre pertinence. Les méthodes gourmandes comme Dartboard en sont des approximations réalisables.

    Cette approche se retrouve bien au-delà de la récupération d’informations :

    • Échantillonnage des données d’entraînement. Les techniques d’apprentissage actif utilisent un échantillonnage conscient de la diversité pour la même raison.
    • Recommandations. Sugerer trois variantes légèrement différentes d’un même produit ne sert à personne ; la diversité améliore donc la liste.
    • Recherche d’images. Dix photos presque identiques sont moins utiles pour l’utilisateur que dix images différentes et pertinentes.
    • Résumé extractif. Le choix des phrases doit tenir compte de la diversité, et non seulement de leur importance individuelle.

    La leçon générale est que, lors du choix d’un ensemble d’éléments, il convient d’évaluer les ensembles eux-mêmes et non les éléments individuels. Dartboard représente la version RAG de ce principe ; une fois que vous l’aurez compris, vous reconnaîtrez le même piège dans de nombreux autres processus du type « classer, puis prendre les k meilleurs ».

    Il y a un prix à payer : des calculs supplémentaires, et il est plus difficile d’expliquer pourquoi un bloc l’emporte sur un autre. La récompense, c’est une couverture plus complète, ce qui se traduit souvent directement par de meilleures réponses, surtout lorsque le corpus n’est pas organisé de manière ordonnée avec un bloc par fait distinct.