Zum Hauptinhalt springen
Lektion 10 von 22 · Embeddings, RAG und semantische Suche

Bewerten Sie die Suchqualität

Lektion 10 von 22 Aktualisiert am 29. Juli 2026 Entwickler
Auf dieser Seite
  1. Lernziele
  2. Warum bewerten?
  3. Wichtige Metriken
  4. Erstellen Sie einen Evaluierungsdatensatz
  5. Vollständiger Evaluierungs-Pipeline
  6. Vergleichen Sie zwei Konfigurationen
  7. Zusammenfassung

Lernziele

  • Verstehen Sie die Standardmetriken zur Bewertung von Suchen
  • Erstellen Sie ein Evaluierungsdatensatz
  • Messen und verbessern Sie die Qualität Ihres Engines

Warum bewerten?

In vorherigen Lektionen wurden eine Reihe von Entscheidungen vorgestellt: Embedding-Modell, Anzahl der Dimensionen, Chunk-Größe und Hybrid-Gewichtung. Jede Entscheidung kann Ihr System verbessern oder verschlechtern. Subjektive Eindrücke aus dem manuellen Testen von drei Abfragen reichen nicht aus, um eine Entscheidung zu treffen – insbesondere, da eine Änderung oft eine Kategorie von Abfragen verbessert, während sie eine andere verschlechtert. Ohne Metriken fliegen Sie blind.

Wichtige Metriken

Recall@k beantwortet diese Frage: Unter den relevanten Dokumenten, wie viele erscheinen in den Top-k-Ergebnissen? Dies ist der Goldstandard für RAG, da ein Dokument, das im Kontext fehlt, vom Generierungsmodell niemals zitiert werden kann, egal wie gut es ist.

def recall_at_k(pertinents: set[str], resultats: list[str], k: int) -> float:
    """Proportion of relevant documents found in the top-k."""
    top_k = set(resultats[:k])
    trouves = pertinents & top_k
    return len(trouves) / len(pertinents) if pertinents else 0.0

Precision@k betrachtet das Inverse: Unter den k zurückgegebenen Ergebnissen, wie viele sind tatsächlich relevant? Dies ist am wichtigsten, wenn der Kontext, der an das LLM gesendet wird, begrenzt ist, da jedes off-topic-Dokument Platz einnimmt und die Aufmerksamkeit des Modells verwässert.

def precision_at_k(pertinents: set[str], resultats: list[str], k: int) -> float:
    """Proportion of relevant results in the top-k."""
    top_k = resultats[:k]
    trouves = sum(1 for r in top_k if r in pertinents)
    return trouves / k if k > 0 else 0.0

MRR (Mean Reciprocal Rank) misst den Rang des ersten relevanten Ergebnisses. Ein Dokument an erster Stelle erhält 1, an zweiter Stelle 0,5 und an fünfter Stelle 0,2. Priorisieren Sie diese Metrik für Suchleisten, die für Menschen gedacht sind, die selten über die ersten drei Zeilen hinausschauen.

def mrr(pertinents: set[str], resultats: list[str]) -> float:
    """Reciprocal rank of the first relevant result."""
    for i, r in enumerate(resultats):
        if r in pertinents:
            return 1.0 / (i + 1)
    return 0.0

NDCG@k (Normalized Discounted Cumulative Gain) geht einen Schritt weiter, indem er die binäre Relevanz aufgibt: Er akzeptiert Abstufungen der Relevanz und ermöglicht es Ihnen, zwischen einem Dokument zu unterscheiden, das die Frage genau beantwortet, und einem, das dies nur anspricht. Der Logarithmus im Nenner wendet eine progressive Abschreibung basierend auf dem Rang an, und die Normalisierung gegenüber der idealen Rangfolge skaliert alles zwischen 0 und 1.

import numpy as np

def ndcg_at_k(relevances: list[float], k: int) -> float:
    """NDCG with graded relevance scores."""
    dcg = sum(
        rel / np.log2(i + 2) for i, rel in enumerate(relevances[:k])
    )
    ideal = sorted(relevances, reverse=True)[:k]
    idcg = sum(
        rel / np.log2(i + 2) for i, rel in enumerate(ideal)
    )
    return dcg / idcg if idcg > 0 else 0.0

Erstellen Sie einen Evaluierungsdatensatz

Keine dieser Formeln ist nützlich ohne Ground Truth. Ein Evaluierungsdatensatz verknüpft jede Abfrage mit der Liste der Dokumente, die erscheinen sollten, und optional mit denen, die definitiv nicht erscheinen sollten – letztere sind nützlich zur Überwachung bekannter Falschpositiver.

import json

# Dataset structure
dataset_eval = [
    {
        "requête": "comment installer Python sur Windows",
        "pertinents": ["doc_install_python", "doc_setup_env"],
        "non_pertinents": ["doc_cuisine"]
    },
    {
        "requête": "configurer nginx comme reverse proxy",
        "pertinents": ["doc_nginx_proxy", "doc_nginx_config"],
        "non_pertinents": ["doc_python_web"]
    },
]

# Save
with open("eval_dataset.json", "w") as f:
    json.dump(dataset_eval, f, ensure_ascii=False, indent=2)

Dreißig gut gewählte Paare, die die verschiedenen Arten von Fragen abdecken, die Ihr Engine tatsächlich erhält, sind mehr wert als dreihundert massenproduzierte. Priorisieren Sie das Abrufen aus Ihren Suchprotokollen: Dies sind die tatsächlichen Formulierungen Ihrer Benutzer, nicht die, die Sie sich vorstellen.

Generieren Sie Testabfragen mit einem LLM

Auf einem großen Korpus wird das manuelle Schreiben unpraktisch. Also drehen wir das Problem um: Anstatt nach Dokumenten zu suchen, die eine Frage beantworten, fragen wir ein Modell, welche Fragen ein gegebenes Dokument beantworten kann. Das Dokument ist per Konstruktion relevant für diese Fragen, und Sie erhalten Paare in Minuten.

from openai import OpenAI

client = OpenAI()

def generer_requetes_test(document: str, n: int = 3) -> list[str]:
    """Generate n queries that this document should answer."""
    response = client.chat.completions.create(
        model="gpt-5.6-terra",
        messages=[{
            "role": "user",
            "content": (
                f"Voici un document :\n\n{document}\n\n"
                f"Génère {n} questions auxquelles ce document répond. "
                f"Retourne uniquement les questions, une par ligne."
            )
        }]
    )
    return response.choices[0].message.content.strip().split("\n")

Diese Methode hat einen Blindpunkt, den Sie kennen müssen: Generierte Fragen verwenden oft das exakte Vokabular des Dokuments, wodurch sie einfacher als echte Benutzerabfragen sind und Ihre Scores künstlich aufblähen. Verwenden Sie sie, um zwei Konfigurationen gegeneinander zu vergleichen, niemals, um ein absolutes Qualitätsniveau zu deklarieren.

Vollständiger Evaluierungs-Pipeline

Der Evaluierer führt jede Abfrage aus dem Datensatz auf dem Engine aus, berechnet alle Metriken für mehrere Werte von k und mittelt dann den gesamten Satz.

class EvaluateurRecherche:
    def __init__(self, moteur_recherche):
        self.moteur = moteur_recherche

    def evaluer(
        self, dataset: list[dict], k_values: list[int] = [1, 3, 5, 10]
    ) -> dict:
        """Evaluate the engine on a complete dataset."""
        resultats = {f"recall@{k}": [] for k in k_values}
        resultats.update({f"precision@{k}": [] for k in k_values})
        resultats["mrr"] = []

        for item in dataset:
            # Run search
            res = self.moteur.rechercher(item["requête"], k=max(k_values))
            ids_resultats = [r["id"] for r in res]
            pertinents = set(item["pertinents"])

            # Calculate metrics
            resultats["mrr"].append(mrr(pertinents, ids_resultats))
            for k in k_values:
                resultats[f"recall@{k}"].append(
                    recall_at_k(pertinents, ids_resultats, k)
                )
                resultats[f"precision@{k}"].append(
                    precision_at_k(pertinents, ids_resultats, k)
                )

        # Average
        return {
            metrique: round(np.mean(scores), 4)
            for metrique, scores in resultats.items()
        }

# Usage
evaluateur = EvaluateurRecherche(moteur)
metriques = evaluateur.evaluer(dataset_eval)

print("Résultats d'évaluation :")
for metrique, score in metriques.items():
    print(f"  {metrique}: {score}")

Das Verfolgen mehrerer Werte von k ist an sich lehrreich: Ein hoher recall@10 gepaart mit einem schlechten recall@1 signalisiert, dass die richtigen Dokumente gefunden werden, aber schlecht gerankt sind – ein Reranking-Problem, kein Indexierungsproblem.

Vergleichen Sie zwei Konfigurationen

Die folgende Funktion setzt zwei Engines gegen denselben Datensatz und zeigt die Metrik-Lücke pro Metrik an. Wiederholen Sie diesen Schritt vor jeder Produktionsbereitstellung, auch für eine Änderung, die trivial erscheint.

def comparer_configs(moteur_a, moteur_b, dataset, noms=("A", "B")):
    """Compare two search engines."""
    eval_a = EvaluateurRecherche(moteur_a).evaluer(dataset)
    eval_b = EvaluateurRecherche(moteur_b).evaluer(dataset)

    print(f"{Métrique:<20} {noms[0]:<10} {noms[1]:<10} {Delta:<10}")
    print("-" * 50)
    for metrique in eval_a:
        delta = eval_b[metrique] - eval_a[metrique]
        signe = "+" if delta > 0 else ""
        print(f"{metrique:<20} {eval_a[metrique]:<10.4f} "
              f"{eval_b[metrique]:<10.4f} {signe}{delta:<10.4f}")

Lesen Sie Deltas vorsichtig auf einem kleinen Datensatz: Eine Lücke von 0,02 über dreißig Abfragen ist wahrscheinlich Rauschen, während ein Rückgang von 0,10 im recall@5 ein klares Signal ist, nicht zu deployen.

Zusammenfassung

  • Recall@k, Precision@k, MRR und NDCG sind die Standardmetriken
  • Erstellen Sie einen Evaluierungsdatensatz mit (Abfrage, relevante Dokumente)-Paaren
  • Automatisieren Sie die Abfragegenerierung mit einem LLM
  • Vergleichen Sie Konfigurationen systematisch, bevor Sie bereitstellen