Startseite / Artikel

Dartboard RAG: Was tun, wenn Top-K dreimal denselben Teilinhalt zurückgibt

Die einfache top-k-Abfrage kann nahezu identische Abschnitte zurückgeben. Dartboard RAG wählt sowohl Relevanz als auch Vielfalt aus; hier ist der Algorithmus, der Code sowie Anwendungsfall.

Verlag: REACTAPP.TOP11 Min. Lesezeit2616 Wörter

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

Eine einfache top-k-Abfrage funktioniert nicht zuverlässig. Wenn ein Korpus dieselbe Aussage an mehreren Stellen enthält, können die k nächsten Passagen zu einer Abfrage aus einer einzigen Passage bestehen, die dieselbe Aussage in unterschiedlichen Worten dreimal wiederholt.

Stellen Sie sich einen 100-seitigen Bericht über den Klimawandel vor, in dem fünf Abschnitte jeweils die Treibhausgase behandeln – mit leicht unterschiedlicher Formulierung, aber demselben Kerninhalt. Jemand fragt „Was verursacht den Klimawandel?“, und der FAISS-Index gibt Folgendes zurück:

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)

Das sind drei Abschnitte, die im Grunde nur eine Tatsache darstellen. Der Nutzer fragte nach Ursachen im Plural und erhielt immer wieder nur eine Ursache. Die Abschnitte über Abholzung, industrielle Landwirtschaft und Veränderungen im Landnutzungsverhalten lagen auf den Plätzen 8, 12 und 17, sodass keiner von ihnen in den Kontext des LLM gelangte. Die Antwort des Modells spiegelt seine Eingabe wider: ein fließender Absatz über Treibhausgase ohne Erwähnung der anderen Ursachen.

Die Vektorsuche ist nicht der Schuldige. Sie hat getan, was ihr aufgetragen wurde, und die ähnlichsten Ergebnisse gemäß dem top-k-Prinzip zurückgegeben. Das Problem ist, dass Ähnlichkeit kein Konzept der Redundanz kennt. Drei Abschnitte können alle maximal der Anfrage ähneln und gleichzeitig nahezu identische Kopien voneinander sein, wodurch eine einfache top-k-Suche alle drei zurückgibt und dabei unterschiedliches, relevantes Material ausschließt.

Dartboard RAG ändert die Auswahlregel. Während bei der herkömmlichen Suche nach den k am Abfragetext nächsten Abschnitten gesucht wird, verlangt dieser Ansatz nach k Abschnitten, die jeweils hochrelevant sind und zudem weit voneinander entfernt liegen. Dadurch werden die relevanten Teile des Korpus abgedeckt und nicht mehrere Kopien der besten Übereinstimmung.

Stellen Sie sich ein Dartbrett vor

Betrachten Sie die Abfrage als Zielscheibe und jeden Abschnitt als einen bereits im Brett steckenden Pfeil. Je näher am Zentrum, desto relevanter ist der Abschnitt.

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

Die herkömmliche Abfrage sammelt die drei nächsten Pfeile. Die Dartboard-Methode sammelt drei Pfeile, die sowohl nah beieinander als auch verteilt sind.

Der gesamte Ablauf

Architektonisch handelt es sich dabei um eine Standardabfrage mit einer intelligenten Auswahl, die darauf aufbaut. Der Vektorstore und die Embeddings bleiben genau so, wie sie waren. Das einzige Neue ist ein gieriger Auswahlschritt, der entscheidet, welche der 15 Kandidaten in die endgültigen 5 gelangen.

Schritt 1: Mehr als nötig abrufen

Da der Algorithmus nicht zwischen Kandidaten wählen kann, die er nicht hat, benötigt er einen Pool, der größer als das endgültige k ist. Die Referenzimplementierung multipliziert dabei mit einem Oversampling-Faktor von 3:

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

Daher werden 15 Kandidaten statt 5 herangezogen. FAISS gibt sie nach kosinusähnlicher Ähnlichkeit sortiert zurück, wobei Kandidat 0 dem Abfragedatum am nächsten und Kandidat 14 am weitesten liegt. Im Anschluss wählt der Auswahlschritt den besten Teilmenge aus fünf Kandidaten aus, indem er Relevanz und Vielfalt miteinander abwägt.

Der nächste Schritt benötigt die Embeddings jedes Kandidaten und nicht nur seinen Text, weshalb der Datensatz als vollständige Document-Objekte mit angehängten Embeddings abgerufen wird.

Schritt 2: Erstellung zweier Distanzmatrizen

Aus den Kandidaten-Embeddings werden zwei Matrizen berechnet:

# 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 enthält die Entfernung vom Abfragedatumen zu jedem Kandidaten. Ein niedriger Wert bedeutet hohe Relevanz. Seine Form ist (1, N).
  • document_distances enthält die paarweisen Entfernungen zwischen jedem Paar von Kandidaten. Ein niedriger Wert bedeutet, dass diese Kandidaten einander ähneln. Seine Form ist (N, N).

Die Relevanz ergibt sich aus der ersten Matrix, die Vielfalt aus der zweiten.

Schritt 3: Umwandlung von Entfernungen in Log-Wahrscheinlichkeiten

Raue Entfernungen lassen sich nicht gut kombinieren. Der Unterschied zwischen 0,3 und 0,4 kann als bedeutend empfunden werden, doch eine einfache Subtraktion ergibt 0,1 und unterschätzt diesen Wert. Deshalb wandelt der Algorithmus die Entfernungen unter Verwendung eines Gaussian-Kerns in Log-Wahrscheinlichkeiten um:

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

Das ist das Protokoll einer Gaußschen Dichte. Im intuitiven Verständnis bedeutet eine kleine Entfernung eine hohe Wahrscheinlichkeit (nahe bedeutet wahrscheinlich relevant oder ähnlich), während eine große Entfernung zu einer niedrigen Wahrscheinlichkeit führt. sigma bestimmt außerdem, wie schmal die Kurve ist. Ein kleiner Wert für sigma, standardmäßig 0,1, erzeugt enge Spitzen, die stark auf geringe Entfernungsunterschiede reagieren. Ein größerer Wert wie 0,5 glättet die Kurve, sodass ähnliche Entfernungen ähnlicher behandelt werden.

Die Arbeit im Logarithmusraum ist wichtig für die numerische Stabilität. Bei kleinen Entfernungen und einem kleinen sigma schrumpfen die Rohwahrscheinlichkeiten auf winzige Werte, die unter den Nullwert abrutschen, während die logarithmierten Wahrscheinlichkeiten begrenzt und vergleichbar bleiben.

Schritt 4: die gierige Schleife

Hier befindet sich tatsächlich die Logik des Zielscheibensystems. Der Ablauf ist wie folgt:

  1. Nehmen Sie zunächst den relevantesten Teil. Ein Vielfaltstest ist noch nicht erforderlich, da noch nichts ausgewählt wurde, von dem abweichend geprüft werden soll.
  2. Bewahren Sie max_distances auf, das für jeden Kandidaten die größte Abstandswertung in Log-Wahrscheinlichkeit zu einem ausgewählten Dokument speichert. Ein höherer Wert bedeutet eine größere Abweichung von der aktuellen Auswahl.
  3. Für jeden verbleibenden Platz bewerten Sie jeden ungewählten Kandidaten, indem Sie einen Relevanzwert (wie nah er der Abfrage ist, aus query_probs) mit einem Vielfaltswert (wie weit er vom nächstgelegenen bereits ausgewählten Kandidaten entfernt ist, aus max_distances) kombinieren. Die Kombination lautet diversity_weight × diversity + relevance_weight × relevance.
  4. Fassen Sie die kombinierten Bewertungen mit logsumexp zusammen, da diese Methode numerisch stabil ist.
  5. Wählen Sie den Kandidaten mit der höchsten Gesamtbewertung aus, fügen Sie ihn hinzu und aktualisieren Sie max_distances.
  • Wiederholen, bis k Kandidaten ausgewählt wurden.
  • In 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)
    

    Die Kernidee ist die kombinierte Bewertung. Jeder Auswahlentscheid wird danach bewertet, inwieweit er „relevant ist“ im Vergleich dazu, ob er sich von dem bereits vorhandenen Unterscheidet. Die erste Auswahl maximiert die reine Relevanz, während jede weitere Auswahl beides ausbalanciert.

    Was die Mathematik im Überblick leistet

    In jedem Schritt fragt der Algorithmus im Grunde: Von den noch nicht ausgewählten Kandidaten, welcher verbessert am besten sowohl seine Relevanzbewertung als auch die Abdeckung des relevanten Bereichs?

    Stellen Sie sich einen 2D-Scatter-Diagramm vor, bei dem die Relevanz entlang der x-Achse und die Entfernung zur ausgewählten Menge entlang der y-Achse angegeben wird. Die erste Auswahl ist der Punkt ganz rechts. Danach hat jeder weitere Kandidat einen y-Wert, der seine Entfernung zu dieser ersten Auswahl darstellt.

    Zur zweiten Auswahl soll etwas ganz rechts liegen (relevant) und zugleich weit oben (im Gegensatz zur ersten). Da beide Gewichte bei 1,0 liegen, ist das Gegenübergewicht ausgeglichen, sodass der Loop einen Abschnitt wählt, der etwas weniger relevant ist, aber deutlich unterschiedlicher.

    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
    

    Die einfache top-k-Methode wählt die rechtsstehendsten Punkte, unabhängig davon, wie eng sie beieinander liegen. Dartboard wählt zunächst den rechtsstehendsten Punkt und breitet sich anschließend absichtlich auf andere relevante Bereiche aus.

    Die beiden Gewichte

    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
    

    Sowohl Werte gehen standardmäßig von 1,0 aus, was eine gleichmäßige Betonung bedeutet. Durch Änderung dieser Werte ändert sich das Verhalten: Erhöhen Sie den Wert für Vielfalt, wenn weiterhin Duplikate auftauchen, und erhöhen Sie ihn für Relevanz, wenn abweichende Abschnitte immer mehr Einfluss gewinnen. Die Standardwerte eignen sich für die meisten Korpora.

    Ein Beispiel mit einem korpusreichen Bereich an Duplikaten

    In der Referenzimplementierung gibt es einen Parameter duplicate_factor, mit dem jeder Abschnitt N Mal wiederholt wird – eine nützliche Methode, um die Vielfalt in einem künstlich dichten Korpus zu testen. Setzen Sie diesen Wert auf 5 und stellen Sie die Frage „Was verursacht den Klimawandel?“

    Die standardmäßige Top-5-Auswertung ergibt Folgendes:

    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
    

    Die fünf Ergebnisse bilden buchstäblich einen einzigen Block, alle sind gleichermaßen dem Suchbegriff nahe, sodass das LLM eine einmalige Tatsache fünfmal wiederholt erhält.

    Dartboard Top-5 zeigt Folgendes:

    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
    

    Sobald der erste Teil ausgewählt wurde, bestraft der Vielfaltsschlüssel jede Kopie davon, da jede im Verhältnis zur Auswahl eine Null-Vielfalt aufweist. Der Schleifenprozess sucht daher nach unterschiedlichem Inhalt in den Slots zwei bis fünf, und das LLM erhält somit fünf verschiedene Fakten zu den Ursachen. Das ist der beste Fall. Reale Korpora enthalten selten exakte Duplikate, wodurch die Wirkung milder ausfällt – doch dieselbe Dynamik gilt auch für nahezu identische Inhalte.

    Alles in etwa zwanzig Zeilen

    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
    

    Die Länge ist bescheiden. Die Schwierigkeit liegt darin, was die Mathematik bedeutet, und nicht in der Anzahl der Zeilen. Sobald die Regel klar ist (den relevantesten Teil auswählen und anschließend solche Kandidaten bevorzugen, die sowohl relevant sind als auch sich von dem bereits Ausgewählten unterscheiden), ist der Code leicht verständlich.

    Parameter, die angepasst werden sollten

    Zwei Einstellungen verdienen Aufmerksamkeit.

    • oversampling legt die Größe des Kandidatenpools fest. Der Standardwert von 3 bedeutet 15 Kandidaten, von denen schließlich 5 ausgewählt werden. Höhere Werte wie 5 oder 10 geben dem Algorithmus mehr Material zum Arbeiten, verlangsamen aber die Berechnung der Paarweiten. In einem stark duplizierten Korpus ermöglicht 10× Oversampling es Dartboard, wirklich einzigartige Teile weiter unten in der Rangliste zu finden.
    • diversity_weight sollte ansteigen, wenn bei Ihren Bewertungen immer noch Duplikate durchdringen, und sinken, wenn die Ergebnisse zu stark auseinanderliegen und ausschließlich themenfremde Abschnitte nur aufgrund ihrer „Unterschiedlichkeit“ ausgewählt werden. Der Standardwert von 1,0 ist ausgewogen, wobei Werte von 1,5 oder 2,0 für sehr dichte Korpora angemessen sind.

    sigma ist empfindlicher und muss nur selten geändert werden. Der Standardwert von 0,1 funktioniert angeblich bei Kosinus-Distanzen zu OpenAI-Embeddings. Wenn Sie auf ein Embedding-Modell mit anderen Distanzskalen wechseln, müssen Sie es möglicherweise neu kalibrieren, obwohl die meisten Nutzer es unverändert lassen können.

    Wo es hilft und wo nicht

    Dartboard eignet sich für verschiedene Situationen.

    • Korpora mit nahezu identischen Inhalten. Viele Quellen zu einem Thema, wiederholende Richtliniedokumente, Nachrichtenarchive mit überschneidender Berichterstattung sowie indizierte Forumbeiträge. Die herkömmliche top-k-Methode stößt hier regelmäßig an ihre Grenzen.
    • Berichterstattungskomplettheit genauso wichtig wie Präzision. Forschungsübersichten, ausführliche Fragen-und-Antwort-Seiten sowie vergleichende Analysen – hier möchte der Nutzer den gesamten Umfang relevanter Informationen und nicht nur einen besten Auszug.
    • Einsparung von Rechenleistung. Die zusätzlichen Kosten entstehen durch Matrixrechnungen auf der Liste der Kandidaten, nicht durch weitere Aufrufe von Large Language Models. Bei k=5 mit 15 Kandidaten ist dies kaum wahrnehmbar, bei k=20 mit 60 oder mehr Kandidaten hingegen deutlich spürbar.

    In diesen Fällen hilft es weniger.

    • Natürlich vielfältige Korpora. Wenn die Fragmente bereits thematisch unterschiedlich sind – sei es durch semantisches Fragmentieren oder gut strukturierte Dokumente – liefert top-k auf jeden Fall vielfältige Ergebnisse, und Dartboard verursacht zusätzliche Belastung, ohne die Rangliste zu verändern.
    • Sehr spezifische Fragen. „Wie hoch ist der Siedepunkt von Quecksilber?“ hat eine klare, richtige Antwort. Die Vielfaltsoptimierung könnte aus Gründen der Varianz schwächere Fragmente auswählen, was die Qualität der Antwort beeinträchtigen kann.
    • Kleinere Werte für k. Bei k gleich 1 oder 2 gibt es kaum Möglichkeiten zur Diversifizierung, daher reicht herkömmliches top-k aus.
    • Pipelines, die bereits Fusion, Neuranking und Fensterung kombinieren. Jeder dieser Ansätze bewältigt ähnliche Probleme auf seine eigene Weise. Das Hinzufügen von Dartboard zu einem bereits umfangreichen Pipeline-System hilft selten und erhöht lediglich die Komplexität.

    Die wichtigste Erkenntnis

    Der interessante Aspekt bei Dartboard RAG liegt nicht in der Mathematik. Es geht vielmehr um die Erkenntnis, dass Relevanz allein kein geeignetes Ziel für die top-k-Abfrage ist.

    Bei der herkömmlichen top-k-Methode wird ermittelt, welche k Elemente jeweils am relevantesten sind. Dabei werden alle Elemente unabhängig voneinander betrachtet und das, was bereits im Ergebnissatz enthalten ist, ignoriert. Wenn es mehrere gleich relevanten Elemente gibt, werden alle davon berücksichtigt – auch dann, wenn sie dasselbe aussagen.

    Ein besseres Ziel ist es, zu bestimmen: Welche k Elemente zusammen am besten das Bedürfnis der Abfrage decken? Das ist eine andere Optimierungsstrategie, bei der die Bewertung des k-ten Elements von den Elementen 1 bis k-1 abhängt und nicht nur von seiner eigenen Relevanz. Gierige Methoden wie Dartboard sind praktikable Annäherungen daran.

    Dieses Muster kommt auch außerhalb der Abfragereduzierung vor:

    • Stichprobenziehung aus Trainingsdaten. Aktive-Learning-Techniken verwenden aus demselben Grund stichprobenorientierte Verfahren, die auf Vielfalt achten.
    • Empfehlungssysteme. Das Vorschlagen von drei leicht abweichenden Varianten eines Produkts hilft niemandem, daher verbessert Vielfalt die Liste.
    • Bildsuche. Zehn nahezu identische Bilder dienen einem Benutzer schlechter als zehn unterschiedliche, relevante Bilder.
    • Ausziehende Zusammenfassung. Bei der Auswahl von Sätzen ist Vielfalt wichtig, nicht nur die Bedeutung einzelner Sätze.

    Die allgemeine Lektion ist, dass man bei der Auswahl von Elementen Sets bewerten sollte und nicht einzelne Einträge. Dartboard ist die RAG-Version dieses Prinzips – sobald man es verstanden hat, erkennt man denselben Fehler in vielen anderen „Ranken und anschließend die besten k auswählen“-Verfahren.

    Dafür gibt es einen Preis: zusätzliche Rechenleistung und es fällt schwerer zu erklären, warum ein bestimmter Teilbereich besser ist als ein anderer. Die Belohnung ist eine bessere Abdeckung, was sich oft direkt in besseren Antworten niederschlägt – insbesondere dann, wenn das Korpus nicht ordentlich so strukturiert ist, dass jeder einzelne Fakt in einem eigenen Teilbereich liegt.