RAG bewerten: Wird der Abschnitt gefunden, und ist die Antwort belegt
Kurz gesagt: Die Bewertung von RAG prüft zwei Dinge: ob die Suche den richtigen Abschnitt findet (recall@k) und ob sich die Antwort nur auf das Gefundene stützt.
Problem
Billiger, und es „funktioniert“
Der Assistent aus der vorigen Lektion legt bis zu 3 Abschnitte in den Prompt. Ein Kollege bemerkt, dass die Abschnitte den größten Teil der Tokens ausmachen. Er senkt k auf 1 und probiert zwei Fragen aus:
for (const question of ["was kostet die lieferung an eine filiale", "die bank hat meine zahlung abgelehnt"]) console.log(question + "\n → " + answer(question, 1));was kostet die lieferung an eine filiale → Die Lieferung an eine Filiale des Kuriers kostet 3 €. [lieferung-1]die bank hat meine zahlung abgelehnt → Versuche die Zahlung mit einer anderen Karte. [zahlung-2]Beide Antworten sind richtig, mit Quelle. Der Kollege liefert die Änderung aus.
Am nächsten Tag sagt der Assistent einem Kunden, das Geld komme innerhalb von 14 Tagen zurück. In den Dokumenten stehen fünf. Die zwei von Hand probierten Fragen konnten das nicht fangen: Bei beiden steht der richtige Abschnitt an erster Stelle.
Die Idee
Zwei getrennte Fragen
RAG kann an zwei Stellen schiefgehen, und sie werden getrennt gemessen:
- Wird er gefunden? Die Suche muss den richtigen Abschnitt unter den ersten k bringen. Sein Platz unter den gefundenen Abschnitten ist der Rang. Der Anteil der Fragen, bei denen der Rang zwischen 1 und k liegt, ist recall@k. Das ist dieselbe Trefferquote (Recall) wie in der Lektion über die Auswertung eines Modells: Von allem, was du finden solltest, wie viel hast du gefunden.
- Ist die Antwort belegt? Die Antwort muss aus den Abschnitten kommen, die das Modell gesehen hat: Jede Quelle muss aus dem Prompt stammen, und jede Zahl muss im zitierten Abschnitt stehen.
Die dritte Prüfung, „ist sie richtig“, ist die Prüfung „muss enthalten“ aus der Lektion über die Evaluation. Alle drei zusammen sagen dir, wo der Fehler liegt: in der Suche, im Modell oder in den Dokumenten.
Dafür brauchst du ein Testset: Kundenfragen und zu jeder der Abschnitt, der sie beantwortet. Das Set schreibt einmal von Hand jemand, der die Dokumente kennt.
Sieh es dir an
Das Testset und der Rang
// das Testsetconst CASES = [ { topic: "lieferpreis", question: "was kostet die lieferung an eine filiale", expect: "lieferung-1", has: "3 €" }, { topic: "lieferzeit", question: "wann ist meine ware da", expect: "lieferung-2", has: "zwei Tage" }, { topic: "rückgabe", question: "ich will die ware nicht", expect: "rückgabe-1", has: "14 Tage" }, { topic: "geld zurück", question: "wann kommt das geld zurück", expect: "rückgabe-2", has: "fünf Tage" }, { topic: "nachnahme", question: "kann ich per nachnahme bezahlen", expect: "zahlung-1", has: "Nachnahme" }, { topic: "karte abgelehnt", question: "die bank hat meine zahlung abgelehnt", expect: "zahlung-2", has: "anderen Karte" }, { topic: "garantie", question: "wie lange gilt die garantie auf die ware", expect: "garantie-1", has: "2 Jahre" }, { topic: "sonntag", question: "habt ihr sonntags geöffnet", expect: "", has: "Ich weiß es nicht" },];
// eine ehrliche Absage beginnt soconst DECLINE = "ich weiß es nicht";Die letzte Frage hat keinen Abschnitt: Der Shop hat nichts über Sonntag geschrieben. Die richtige Antwort ist „Ich weiß es nicht“.
Der Rang ist der Platz des richtigen Abschnitts unter den gefundenen, und recall@k zählt die Fragen, bei denen er unter den ersten k ist:
// Platz des richtigen Abschnitts (ab 1), 0 wenn er fehltfunction rank(c) { const hits = search(c.question, PIECES.length); const i = hits.findIndex((h) => h.piece.id === c.expect); return i + 1;}
// recall@k: bei wie vielen Fragen er unter den ersten k istfunction recall(k) { let found = 0; for (const c of CASES) { const r = rank(c); if (r >= 1 && r <= k) found++; } return found;}console.log("Thema".padEnd(18) + "Rang".padEnd(6) + "k=1".padEnd(5) + "k=3");let total = 0;for (const c of CASES) { if (c.expect === "") continue; total++; const r = rank(c); console.log( c.topic.padEnd(18) + String(r).padEnd(6) + mark(r >= 1 && r <= 1).padEnd(5) + mark(r >= 1 && r <= 3), );}for (const k of [1, 3]) console.log(`recall@${k} = ${recall(k)}/${total}`);Thema Rang k=1 k=3lieferpreis 1 ✓ ✓lieferzeit 3 ✗ ✓rückgabe 4 ✗ ✗geld zurück 2 ✗ ✓nachnahme 2 ✗ ✓karte abgelehnt 1 ✓ ✓garantie 1 ✓ ✓recall@1 = 3/7recall@3 = 6/7// Platz des richtigen Abschnitts (ab 1), 0 wenn er fehltfunction rank(c) { const hits = search(c.question, PIECES.length); const i = hits.findIndex((h) => h.piece.id === c.expect); return i + 1;}
// recall@k: bei wie vielen Fragen er unter den ersten k istfunction recall(k) { let found = 0; for (const c of CASES) { const r = rank(c); if (r >= 1 && r <= k) found++; } return found;}// stützt sich die Antwort auf die Abschnitte? "" oder Grundfunction unsupported(answer, hits) { if (answer.toLowerCase().startsWith(DECLINE)) return ""; const ids = sources(answer); if (ids.length === 0) return "ohne Quelle"; const words = new Set(); for (const source of ids) { const hit = hits.find((h) => h.piece.id === source); if (!hit) return "Quelle nicht im Prompt: " + source; hit.piece.text.split(" ").forEach((x) => words.add(x)); } for (const word of answer.split(" ")) { if (word.startsWith("[")) continue; const n = word.replace(/[^0-9]/g, ""); if (n !== "" && !words.has(n)) return "Zahl " + n + " fehlt in der Quelle"; } return "";}Schritt für Schritt, in Worten
- Das Testset: 8 Kundenfragen, und zu jeder der passende Abschnitt. Die letzte hat keinen Abschnitt in den Dokumenten.
- Rang: An welcher Stelle der richtige Abschnitt unter den gefundenen steht. Bei „rückgabe“ ist es erst Platz 4: Das kleine Vektormodell irrt sich.
- Bei k = 1 kommt nur der erste Abschnitt in den Prompt. Der richtige ist bei 3/7 Fragen dabei: recall@1 = 3/7.
- Bei k = 3 ist der richtige Abschnitt bei 6/7 dabei: recall@3 = 6/7. Nur „rückgabe“ bleibt draußen.
- Stützt sich die Antwort auf die Abschnitte? Jede Quelle muss im Prompt stehen und jede Zahl in der Quelle: 7/8.
- Zwei verschiedene Fehler: Die Suche hat den Abschnitt nicht gefunden (recall), und das Modell hat eine Zahl geschrieben, die nicht in seiner Quelle steht (belegt).
Bei k = 1 ist der richtige Abschnitt nur bei 3 von 7 Fragen im Prompt. Bei k = 3 ist er es bei 6. „rückgabe“ hat Rang 4: Das kleine Vektormodell findet „ich will die ware nicht“ der Garantie ähnlicher (Kosinus 1,00) als dem richtigen Abschnitt (0,60), denn die Frage enthält nur das Wort „ware“, und genau darum geht es auch in der Garantie.
Sieh es dir an
Ist die Antwort belegt
Die Prüfung nimmt die Antwort und die Abschnitte, die das Modell gesehen hat. Eine ehrliche Absage besteht sie. Jede Quelle muss unter den Abschnitten sein, und jede Zahl in der Antwort muss ein Wort in den zitierten Abschnitten sein:
// stützt sich die Antwort auf die Abschnitte? "" oder Grundfunction unsupported(answer, hits) { if (answer.toLowerCase().startsWith(DECLINE)) return ""; const ids = sources(answer); if (ids.length === 0) return "ohne Quelle"; const words = new Set(); for (const source of ids) { const hit = hits.find((h) => h.piece.id === source); if (!hit) return "Quelle nicht im Prompt: " + source; hit.piece.text.split(" ").forEach((x) => words.add(x)); } for (const word of answer.split(" ")) { if (word.startsWith("[")) continue; const n = word.replace(/[^0-9]/g, ""); if (n !== "" && !words.has(n)) return "Zahl " + n + " fehlt in der Quelle"; } return "";}Sie gibt einen leeren String zurück, wenn die Antwort belegt ist, sonst den Grund. Der Grund ist nützlicher als ✗: Er sagt dir, wo du suchen musst.
Probier es aus
Sag die Ausgabe voraus
Bei k = 1 kommt für „wann kommt das geld zurück“ nur „rückgabe-1“ in den Prompt: „du kannst die ware 14 tage lang in der originalverpackung zurückgeben“. Was gibt die Prüfung für diese Antwort aus?
const claim = "Das Geld kommt innerhalb von 14 Tagen zurück. [rückgabe-1]";const why = unsupported(claim, search("wann kommt das geld zurück", 1));console.log(why || "belegt");„belegt“. Die Quelle ist im Prompt, und 14 ist ein Wort im Abschnitt. Die Antwort stützt sich auf das Gefundene, aber sie ist falsch: Das Geld kommt innerhalb von fünf Tagen zurück, und das steht in „rückgabe-2“, der nicht gefunden wurde. Darum sind „belegt“ und „richtig“ zwei Prüfungen.
Sieh es dir an
Das ganze Set
report schickt jede Frage durch RAG und gibt die drei Zeichen aus: gefunden, belegt, richtig. Mit k = 3:
// das ganze Set mit k Abschnitten: gefunden, belegt, richtigfunction report(k) { let found = 0, total = 0, stands = 0, right = 0; console.log(`k = ${k}`); console.log("Thema gefunden belegt richtig"); for (const c of CASES) { const hits = search(c.question, k); const text = answer(c.question, k); const why = unsupported(text, hits); let f = "—"; if (c.expect !== "") { total++; const inTop = hits.some((h) => h.piece.id === c.expect); if (inTop) found++; f = mark(inTop); } if (why === "") stands++; if (text.includes(c.has)) right++; const row = c.topic.padEnd(18) + f.padEnd(10) + mark(why === "").padEnd(8) + mark(text.includes(c.has)); console.log(why === "" ? row : row + " " + why); } const n = CASES.length; console.log(`recall@${k} ${found}/${total}, belegt ${stands}/${n}, richtig ${right}/${n}`);}report(3);k = 3Thema gefunden belegt richtiglieferpreis ✓ ✓ ✓lieferzeit ✓ ✓ ✓rückgabe ✗ ✗ ✗ Zahl 30 fehlt in der Quellegeld zurück ✓ ✓ ✓nachnahme ✓ ✓ ✓karte abgelehnt ✓ ✓ ✓garantie ✓ ✓ ✓sonntag — ✓ ✓recall@3 6/7, belegt 7/8, richtig 7/8Der einzige Fehlschlag ist „rückgabe“: Der richtige Abschnitt wurde nicht gefunden, das Modell hat den Abschnitt über das Geld zitiert und 30 Tage geschrieben, obwohl dort fünf stehen. Die Prüfung fängt die Zahl.
Jetzt dasselbe mit k = 1, der Änderung des Kollegen:
report(1);k = 1Thema gefunden belegt richtiglieferpreis ✓ ✓ ✓lieferzeit ✗ ✗ ✓ Quelle nicht im Prompt: lieferung-2rückgabe ✗ ✗ ✗ Quelle nicht im Prompt: rückgabe-2geld zurück ✗ ✓ ✗nachnahme ✗ ✓ ✗karte abgelehnt ✓ ✓ ✓garantie ✓ ✓ ✓sonntag — ✓ ✓recall@1 3/7, belegt 6/8, richtig 5/8Alle drei Zahlen fallen. Die Gründe sind verschieden: Bei „lieferzeit“ zitiert das Modell einen Abschnitt, den es nicht gesehen hat (und hat zufällig recht), bei „rückgabe“ ebenso, nur mit der falschen Zahl, bei „geld zurück“ stützt es sich auf den falschen Abschnitt, und bei „nachnahme“ sagt es ehrlich „Ich weiß es nicht.“, weil der Abschnitt nicht gefunden wurde. Die zwei von Hand probierten Fragen gehörten zu den drei, die alles bestehen.
Probier es aus
Ergänze
Unter welchen Abschnitten wird jede Quelle der Antwort gesucht? Was kommt anstelle von ___?
// stützt sich die Antwort auf die Abschnitte? "" oder Grund
function unsupported(answer, hits) {
if (answer.toLowerCase().startsWith(DECLINE)) return "";
const ids = sources(answer);
if (ids.length === 0) return "ohne Quelle";
const words = new Set();
for (const source of ids) {
const hit = .find((h) => h.piece.id === source);
if (!hit) return "Quelle nicht im Prompt: " + source;
hit.piece.text.split(" ").forEach((x) => words.add(x));
}
for (const word of answer.split(" ")) {
if (word.startsWith("[")) continue;
const n = word.replace(/[^0-9]/g, "");
if (n !== "" && !words.has(n))
return "Zahl " + n + " fehlt in der Quelle";
}
return "";
}hits: die Abschnitte, die in den Prompt gekommen sind. Nicht alle Dokumente: Eine Quelle, die existiert, die das Modell aber nicht gesehen hat, bedeutet eine Antwort aus dem Gedächtnis.
Probier es aus
Finde den Fehler
Ein Kollege schreibt recall um. Jetzt ist recall@1 0/7 und recall@3 5/7. In welcher Zeile steckt der Fehler?
Wähle die Zeile mit dem Fehler.
r < k lässt den Abschnitt genau auf Platz k aus. Bei k = 1 besteht nichts, weil der Rang mindestens 1 ist. Die richtige Bedingung ist 1 ≤ r ≤ k.
Die Idee
Wann es wehtut
- Das Set ist klein. Hier ist eine Frage 14 % von recall. Ein echtes Set hat Hunderte Fragen aus echten Gesprächen und wächst mit jedem Fehler, den ein Kunde findet.
- Die Labels veralten. Der richtige Abschnitt ist über seine Quelle notiert. Werden die Dokumente neu zerlegt, ändern sich die Quellen, und das Set muss mit ihnen angepasst werden.
- Belegt heißt nicht richtig. Die Antwort „14 Tage“ stützt sich auf den falschen Abschnitt. Darum gehören die drei Prüfungen zusammen: gefunden, belegt, richtig.
- Die Prüfung ist grob. Sie schaut nur auf Ziffern und nur darauf, ob das Wort vorkommt. „fünf Tage“ in Worten wird nicht geprüft, und die „5“ aus „5 €“ besteht, auch wenn die Antwort 5 Tage sagt. Ein zweites Modell prüft genauer (siehe „Mehr“), macht aber auch Fehler.
- Es kann mehr als einen richtigen Abschnitt geben. Die Antwort kann in zwei Abschnitten stehen. Dann hält das Set alle fest, und „gefunden“ heißt mindestens einer, oder alle, wenn beide gebraucht werden.
- Das Set läuft bei jeder Änderung. Ein neues k, eine neue Schwelle, ein neues Vektormodell, neues Chunking, ein neuer Prompt: Jedes ändert die Zahlen. Darum ist das Set Code und läuft wie die Tests, mit einer Latte, unter der die Änderung nicht ausgeliefert wird, wie in der Lektion über die Evaluation.
Genauer, für Neugierige
Ein zweites Modell als Richter. Statt nach Ziffern zu suchen, kannst du ein zweites Modell fragen: „Folgt diese Antwort aus diesen Abschnitten? Ja oder nein.“ Es fängt mehr als die grobe Prüfung, kostet aber einen Aufruf mehr und muss selbst an einem Set mit bekannten Noten geprüft werden.
Andere Maße für die Suche. recall@k sagt nur, ob der Abschnitt drin ist. MRR (der Mittelwert von 1/Rang) sagt auch, wie weit oben er steht: Rang 1 ergibt 1, Rang 3 ergibt 0,33. precision@k sagt, wie viele der k Abschnitte zur Frage passen, also wie viele Tokens umsonst ausgegeben werden.
Das Set in CI. Wie das Set bei jeder Änderung automatisch läuft und eine schlechte Version stoppt, bevor sie die Kunden erreicht, ist Aufgabe von ZeroToOps.
Set hält die Wörter der zitierten Abschnitte. find gibt den ersten Treffer oder undefined zurück, und replace(/[^0-9]/g, "") behält nur die Ziffern.Übung
Wie groß soll k sein
Löse es in deinem Editor.
Schreibe eine Funktion, die das ganze Set für ein gegebenes k durchläuft und recall, belegt, richtig und die Zahl der Tokens ausgibt, die in die Prompts gegangen sind (die Tokens von Prompt und Frage, nur wenn das Modell aufgerufen wird). Führe sie für k von 1 bis 4 aus:
k = 1: recall 3/7, belegt 6/8, richtig 5/8, 732 Tokensk = 2: recall 5/7, belegt 6/8, richtig 7/8, 868 Tokensk = 3: recall 6/7, belegt 7/8, richtig 7/8, 952 Tokensk = 4: recall 7/7, belegt 8/8, richtig 8/8, 1024 TokensWelches k würdest du wählen, und warum? k = 4 behebt „rückgabe“ und kostet etwa 8 % mehr Tokens als k = 3. Denke auch daran, wie viel von diesen Zahlen nur an diesem kleinen Set liegt.
Weitere Aufgaben
- Schwelle. Lass das Set mit der Schwelle 0,6 und 0,7 bei k = 3 laufen. Was passiert mit recall und mit „sonntag“?
- MRR. Berechne den Mittelwert von 1/Rang für die sieben Fragen mit Abschnitt (Rang 0 ergibt 0).
Test
Teste dich
Verwandte Lektionen
Abschnitt „Verwandte Lektionen“- Baut auf: Kosten und Latenz: Budget, Cache, ein kleineres Modell · Semantische Suche: die k nächsten · Dokumente in Abschnitte teilen (Chunking) · RAG: Antworten aus den Dokumenten
- Geht weiter in: Der Support-Assistent
- Aufgaben zum Thema: Semantische Suche und RAG