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

Evaluierung einer RAG-Pipeline

Lektion 15 von 22 Aktualisiert am 29. Juli 2026 Entwickler
Auf dieser Seite
  1. Lernziele
  2. Die zwei Achsen der Evaluierung
  3. Metriken für die Generierung
  4. Komplettes Evaluierungsframework
  5. Metriken-Dashboard
  6. Zusammenfassung

Lernziele

  • Trennte die Evaluierung von Abruf und Generierung
  • Implementieren Sie automatisierte Metriken für die Qualität von RAG
  • Erstellen Sie ein vollständiges Evaluierungsframework

Die zwei Achsen der Evaluierung

Eine RAG-Pipeline kann auf zwei sehr unterschiedliche Arten versagen, und die Verwechslung dieser beiden führt zu Wochen an Entwicklungszeit. Entweder hat der Schritt des Abrufs (retrieval) die richtigen Dokumente nicht gefunden – in diesem Fall ist die Antwort falsch, und zwar aus Gründen, die nichts mit dem Generierungsmodell zu tun haben; die Metriken aus Lektion 10 diagnostizieren dies. Oder die richtigen Dokumente waren vorhanden, aber der Schritt der Generierung ist aus dem Ruder gelaufen: Das Modell hat Details erfunden oder eine andere Frage beantwortet als die, die gestellt wurde.

Wenn ein Benutzer meldet, dass „die Antwort falsch ist”, wird er Ihnen nicht sagen, welche der beiden Ursachen vorliegt. Daher gilt die Regel: Messen Sie beide Komponenten unabhängig voneinander mit unterschiedlichen Metriken und optimieren Sie erst dann eine Komponente, nachdem Sie identifiziert haben, welche unterperformt.

Metriken für die Generierung

Treue (Faithfulness) beantwortet die wichtigste Frage in der Produktion: Ist die Antwort vollständig durch die bereitgestellten Dokumente gestützt, oder hat das Modell eigene Interpretationen hinzugefügt? Anstatt nach einem globalen Score zu fragen – der von Richtern oft zu großzügig vergeben wird – lassen Sie den Richter die Antwortthese für These aufthese durchgehen und jede als gestützt, nicht gestützt oder widerlegt klassifizieren. Letztere Kategorie ist die wertvollste: Eine These, die der Quelle widerspricht, signalisiert einen weit schwerwiegenderen Fehler als eine einfache Unterlassung.

from openai import OpenAI

client = OpenAI()

def evaluate_faithfulness(
    question: str,
    answer: str,
    contexts: list[str]
) -> dict:
    """Evaluates whether the answer is faithful to the contexts."""
    full_context = "\n\n---\n\n".join(contexts)

    response = client.chat.completions.create(
        model="gpt-5.6-terra",
        messages=[{
            "role": "user",
            "content": (
                f"Evaluate the faithfulness of this answer relative to the "
                f"source documents.\n\n"
                f"Source documents:\n{full_context}\n\n"
                f"Question: {question}\n\n"
                f"Answer: {answer}\n\n"
                f"For each claim in the answer, indicate:\n"
                f"- SUPPORTED: if confirmed by the documents\n"
                f"- UNSUPPORTED: if it does not appear in the documents\n"
                f"- CONTRADICTED: if it contradicts the documents\n\n"
                f"End with a global score from 0 to 1."
            )
        }],
        temperature=0
    )
    return {"evaluation": response.choices[0].message.content}

Eine Antwort kann vollständig treu sein, aber dennoch nutzlos: Sie könnte einen Abschnitt kopieren, der sich mit etwas anderem befasst. Das ist, was die Relevanz der Antwort misst, diesmal im Vergleich der Antwort allein zur Frage, ohne die Dokumente. Der Prompt legt eine explizite Skala fest – 1,0 für eine vollständige Antwort, 0,7 für eine teilweise aber nützliche Antwort, 0,3 für tangential verwandte Inhalte und 0,0 für abwegige Themen –, weil ein Richter, der lediglich aufgefordert wird, „von 0 bis 1 zu bewerten”, von einem Lauf zum anderen instabile Scores liefert.

def evaluate_relevance(question: str, answer: str) -> float:
    """Relevance score of the answer relative to the question."""
    response = client.chat.completions.create(
        model="gpt-5.6-terra",
        messages=[{
            "role": "user",
            "content": (
                f"On a scale of 0 to 1, how well does this answer "
                f"address the question asked?\n\n"
                f"Question: {question}\n"
                f"Answer: {answer}\n\n"
                f"Criteria:\n"
                f"- 1.0: complete and precise answer\n"
                f"- 0.7: partial but useful answer\n"
                f"- 0.3: tangentially related\n"
                f"- 0.0: off-topic\n\n"
                f"Return only the numeric score."
            )
        }],
        temperature=0,
        max_tokens=10
    )
    try:
        return float(response.choices[0].message.content.strip())
    except ValueError:
        return 0.0

Dann gibt es die Kontextpräzision, die die beiden Achsen verbindet: Von den fünf Dokumenten, die Sie in den Prompt eingespeist haben, wie viele wurden tatsächlich verwendet? Wenn die Antwort auf eine Frage zu Urlaubsrichtlinien nur auf einem der fünf Chunks basiert, haben die anderen vier den Kontextbudget verbraucht und Rauschen hinzugefügt. Daher fragen Sie den Richter nach jedem Kontext einzeln, mit einer einfachen Ja- oder Nein-Antwort, und berechnen Sie den Anteil, der nützlich war.

def evaluate_context_precision(
    question: str,
    contexts: list[str],
    answer: str
) -> dict:
    """Evaluates which contexts were useful for the answer."""
    results = []

    for i, ctx in enumerate(contexts):
        response = client.chat.completions.create(
            model="gpt-5.6-terra",
            messages=[{
                "role": "user",
                "content": (
                    f"Was this document useful for answering "
                    f"the question?\n\n"
                    f"Question: {question}\n"
                    f"Document: {ctx[:500]}\n"
                    f"Generated answer: {answer}\n\n"
                    f"Answer YES or NO."
                )
            }],
            temperature=0,
            max_tokens=5
        )
        useful = "yes" in response.choices[0].message.content.lower()
        results.append({"index": i, "useful": useful})

    num_useful = sum(1 for r in results if r["useful"])
    return {
        "details": results,
        "precision": num_useful / len(contexts) if contexts else 0
    }

Komplettes Evaluierungsframework

Diese drei Funktionen sind so lange nutzlos, wie sie manuell bleiben. Der entscheidende Schritt besteht darin, sie bei jeder Änderung der Pipeline auf einen festen Satz von Fragen auszuführen: Das verwandelt ein Bauchgefühl – „Ich denke, es ist besser, seit ich das Chunking geändert habe” – in einen vergleichbaren Zahlenwert. Die Klasse unten führt die Pipeline für jeden Eintrag im Datensatz aus, sammelt die Scores und gibt deren Mittelwerte zurück.

Schauen Sie sich den Beispiel-Datensatz an: Zwei HR-Fragen, jede mit einer erwarteten Antwort und der Kennzeichnung der Dokumente, die abgerufen werden sollten. Zwanzig bis fünfzig Einträge dieser Art, einmal und für alle Zeiten aus echten Benutzerfragen geschrieben, reichen aus, um Regressionen zu erkennen.

class RAGEvaluator:
    def __init__(self, rag_pipeline):
        self.pipeline = rag_pipeline
        self.client = OpenAI()

    def evaluate_dataset(self, dataset: list[dict]) -> dict:
        """Evaluates the pipeline on a question-answer dataset.

        Each item: {question, expected_answer, relevant_docs}
        """
        results = {
            "faithfulness": [],
            "relevance": [],
            "context_precision": [],
        }

        for item in dataset:
            # Execute the pipeline
            output = self.pipeline.respond(item["question"])
            answer = output["answer"]
            contexts = [s["excerpt"] for s in output["sources"]]

            # Evaluate
            relevance = evaluate_relevance(item["question"], answer)
            results["relevance"].append(relevance)

            ctx_eval = evaluate_context_precision(
                item["question"], contexts, answer
            )
            results["context_precision"].append(ctx_eval["precision"])

        # Averages
        import numpy as np
        return {
            metric: round(float(np.mean(scores)), 4)
            for metric, scores in results.items()
        }

# Usage
dataset = [
    {
        "question": "How many leave days do I have?",
        "expected_answer": "25 days of paid leave per year",
        "relevant_docs": ["leave-policy"]
    },
    {
        "question": "Can I work remotely on Mondays?",
        "expected_answer": "Yes, remote work is allowed except on Tuesdays and Thursdays",
        "relevant_docs": ["remote-work-policy"]
    },
]

evaluator = RAGEvaluator(rag_pipeline)
metrics = evaluator.evaluate_dataset(dataset)
print("Results:")
for k, v in metrics.items():
    print(f"  {k}: {v}")

Metriken-Dashboard

Hier sind die Ziele, die Sie in einem Produktionssystem anstreben sollten, sowie die Bedeutung jeder Zahl.

MetrikZielWas es misst
Recall@5> 0,90Sind die richtigen Dokumente in den Top 5?
Precision@5> 0,70Sind die zurückgegebenen Dokumente relevant?
Treue (Faithfulness)> 0,85Ist die Antwort den Quellen treu?
Relevanz der Antwort> 0,80Beantwortet die Antwort die Frage?
Kontextpräzision> 0,60Sind die bereitgestellten Kontexte nützlich?

Lesen Sie diese Tabelle von oben nach unten als Diagnosebaum. Ein Recall unter 0,90 macht jede Analyse der folgenden Zeilen sinnlos: Korrigieren Sie zuerst das Chunking oder den Index. Umgekehrt weist ein exzellenter Recall bei schlechter Treue auf den Generierungs-Prompt hin, nicht auf die Suche.

Zusammenfassung

  • Evaluieren Sie Abruf (Recall, Präzision) und Generierung (Treue, Relevanz) getrennt
  • Treue (Faithfulness) erkennt LLM-Halluzinationen
  • Relevanz überprüft, dass die Antwort nützlich ist
  • Erstellen Sie einen Testdatensatz und messen Sie vor jeder Änderung