Wann RAG und Reranking
Track KI · M2 RAG, Bausteine 01 und 05 · ca. 50 Min.
Worum es geht
Ein LLM kennt nur, was im Training stand. Deine internen Dokumente kennt es nicht, und wenn es etwas nicht weiß, erfindet es gern etwas Plausibles. RAG (Retrieval-Augmented Generation) löst genau das: Passende Textausschnitte aus deinem Korpus kommen vor der Antwort in den Prompt, das Modell antwortet auf dieser Basis und nennt im besten Fall die Quelle (source).
In ki/08 hast du Chunks gebaut und per Kosinus-Ähnlichkeit gesucht. Wie du in einer Datenbank (pgvector) und mit Hybrid Search suchst, steht in ki/09. Diese Lektion (Teil a von M2-Abschluss) behandelt zwei Dinge:
- Wann RAG das richtige Werkzeug ist (und wann nicht).
- Reranking: Die erste Suche liefert viele Kandidaten billig, ein zweiter, teurerer Schritt sortiert nur diese wenigen neu. Und wie du herausfindest, wie viele Kandidaten die erste Stufe liefern muss.
Alles läuft im Browser ohne echtes LLM und ohne echtes Reranking-Modell. Beides wird von kleinen Funktionen simuliert, die ich dir vorher vorführe. Belegbare Antworten, Fehleranalyse und die Projektaufgabe stehen in Teil b.
Zur Orientierung: “(Quelle)” im Text meint das Kursbuch quellen/kursbuch-lerninhalte.md, aus dem diese Lektion stammt. Zeitplan: etwa 20 Minuten Lesen und 30 Minuten für die drei Übungen, zusammen rund 50 Minuten.
Von JS/TS her gedacht
| Idee | JS/TS | Python |
|---|---|---|
| Top-k nach Score | items.sort((a, b) => b.s - a.s).slice(0, k) |
sorted(items, key=lambda x: -x.s)[:k] |
| Wörter als Menge | new Set(text.toLowerCase().match(/[\p{L}\p{N}_]+/gu)) |
set(re.findall(r"\w+", text.lower())) |
| Schnittmenge | [...a].filter(x => b.has(x)) |
a & b |
| Duplikate entfernen, Reihenfolge behalten | [...new Set(arr)] |
list(dict.fromkeys(liste)) |
Beide Sortierungen sind stabil: Bei gleichem Score bleibt die ursprüngliche Reihenfolge (JS seit ES2019, Python immer, auch mit reverse=True). Darauf verlassen sich die Übungen. dict.fromkeys behält das erste Vorkommen jedes Eintrags, auch das nutzt du gleich.
Konzept
Schritt 1: Wann RAG das richtige Werkzeug ist (Baustein 01)
Der Ablauf in vier Schritten (Quelle): Frage kommt rein, passende Ausschnitte werden gesucht (Retrieval), Ausschnitte plus Frage gehen in den Prompt, das Modell antwortet nur auf dieser Basis, im besten Fall mit Quellenangabe.
Wann ist das besser als die Alternativen? Die Quelle sagt nur, dass RAG für eigene, oft interne Dokumente gedacht ist. Die Gegenüberstellung unten ist allgemeines Fachwissen, vereinfacht (bitte prüfen, bevor du sie in einer Entscheidung zitierst):
| Wenn dein Fall so aussieht | passt am besten | warum |
|---|---|---|
| Wissen ist klein (passt locker in den Prompt) und ändert sich selten | längerer Prompt | kein Index, kein Retrieval, nichts, was kaputtgehen kann |
| Wissen ist groß, ändert sich oft, Antworten müssen belegbar sein | RAG | nur die passenden Stellen kommen in den Prompt, Änderungen sind sofort drin (Index aktualisieren), jede Antwort kann auf einen Chunk zeigen |
| Das Modell soll anders schreiben oder sich verhalten (Format, Ton), nicht mehr Fakten wissen | Fine-Tuning | verändert das Verhalten des Modells, ist aber kein zuverlässiger Wissensspeicher und kann nicht auf eine Stelle zeigen |
Ein kleiner Simulator zeigt den Unterschied. fake_modell ist kein echtes Modell: Ohne Kontext “erfindet” es eine plausible Antwort. Mit Kontext antwortet es mit dem Chunk, der am besten zur Frage passt (gezählt nach gemeinsamen Wörtern), und nennt dessen Nummer. Ein Chunk-Vergleich per Wortzählung ist eine grobe Vereinfachung, dazu gleich mehr.
Ohne Kontext erfindet das Modell “5 Werktage” und nennt keine Quelle (beleg ist None). Mit zwei Chunks im Kontext kommt die Aussage aus Chunk 1, also HILFE[8] (“nach drei Werktagen”), samt Beleg-Nummer 1. Das ist die Übung der Quelle in klein: dieselbe Frage mit und ohne Textausschnitt.
Die Merkregel der Quelle gilt weiter: RAG ist kein Ersatz für gute Daten. Ein unsauberer oder widersprüchlicher Korpus liefert unsaubere Antworten, egal wie gut Chunking und Embeddings sind.
Schritt 2: Reranking, viele billig und wenige teuer (Baustein 05)
Die erste Suche (Vektor oder Hybrid, siehe ki/08 und ki/09) ist auf Tempo über den ganzen Korpus getrimmt, nicht auf höchste Präzision. Ein Reranker schaut danach nur auf die besten Kandidaten (die Quelle nennt Top 20) und bewertet sie viel genauer. In der Quelle sieht das so aus: erst hybrid_search(frage, k=20), dann reranker.rerank(query=frage, documents=..., top_n=5), und nur diese fünf gehen in den Prompt.
Ein Cross-Encoder liest Frage und Kandidat gemeinsam (beim Embedding wird jeder Text einzeln in einen Vektor verwandelt). Das ist genauer, aber zu teuer für den ganzen Korpus (Quelle).
Wir simulieren das mit zwei Bewertungsfunktionen:
billig_score(von oben): zählt gemeinsame Wörter. Reihenfolge und Zusammenhang sieht er nicht.teuer_score: zählt gemeinsame Wörter und gemeinsame Wortpaare (zwei Wörter hintereinander). Er “liest” also etwas vom Zusammenhang, wie ein Cross-Encoder, nur viel simpler. Er zählt seine Aufrufe mit, damit du die Kosten zählen kannst (Zeit messen ist im Browser unzuverlässig).
Die Frage zerfällt nach Entfernen der Füllwörter in storniere rechnung nach zahlung. Chunk 1 (“offene Rechnung … vor der Zahlung”) und Chunk 2 (“Nach der Zahlung … Gutschrift”) haben beide 3 gemeinsame Wörter, der billige Score kann sie nicht trennen. Der teure Score sieht in Chunk 2 zusätzlich das Wortpaar nach zahlung (3 + 2 = 5). Und genau Chunk 2 beantwortet die Frage: Nach der Zahlung geht es nur noch per Gutschrift.
Jetzt beide Stufen nacheinander. Stufe 1 bewertet alle 10 Chunks billig und behält die besten N. Stufe 2 bewertet nur diese N teuer:
Die billige Suche hatte Chunk 1 vorn. Nach dem Reranking steht Chunk 2 auf Platz 1, und der teure Score lief nur 3-mal statt 10-mal. Bei 10 Chunks ist das egal. Bei 10000 Chunks sind es 20 teure Aufrufe pro Frage statt 10000 (500-mal weniger, 10000 // 20). Genau deshalb ist die Reihenfolge nicht austauschbar: Ein Reranker auf dem ganzen Korpus wäre zu langsam (Quelle).
Und ein Haken: Der Reranker kann nur sortieren, was die erste Stufe ihm gibt. Mit N = 1 bleibt der falsche Chunk übrig:
Das Ergebnis ist [1], der falsche Chunk. Ist N zu klein, gibt es nichts mehr zu reparieren. Wie groß N sein muss, findest du an echten Fragen heraus: Auf welchem Platz der ersten Suche liegt der richtige Chunk typischerweise?
N am Testset bestimmen. Wie groß N sein muss, misst du. Für jede Testfrage notierst du den Platz, auf dem die erste Stufe den richtigen Chunk geliefert hat (1 ist ganz vorn, None heißt: gar nicht gefunden). Beispiel mit sechs Fragen: Plätze [1, 1, 2, 4, 1, 9]. Mit N = 2 liegen vier der sechs im Kandidatenkreis (die Plätze 1, 1, 2, 1), das sind 4/6 = 0.67. Mit N = 4 sind es fünf von sechs (0.83), erst mit N = 9 alle (1.0). Wer 80 Prozent der Fragen abdecken will, braucht hier N = 4. Je größer N, desto mehr teure Aufrufe pro Frage: Du wählst das kleinste N, das dein Ziel erreicht. Das übst du in Übung 3.
Falle
Falle 1: Reranker auf den ganzen Korpus. Genauer, aber nicht bezahlbar. Erst grob und billig eingrenzen, dann fein.
Falle 2: N zu klein. Was die erste Stufe verliert, holt kein Reranker zurück. Miss, auf welchem Platz der erste Suchlauf den richtigen Chunk liefert.
Falle 3: Schlechte Daten. Widersprüchliche Dokumente im Korpus (alte und neue Fassung) liefern widersprüchliche Antworten. Chunking, Suche und Reranking können das nicht reparieren. Dasselbe gilt für Dubletten: Derselbe Text zweimal im Korpus belegt zwei der wenigen Kandidatenplätze.
Übungen
Übung 1: RAG, Fine-Tuning oder längerer Prompt? (leicht, ca. 5 Min.)
Eine Spedition will Fragen von Disponenten zu ihren internen Arbeitsanweisungen beantworten lassen. Die Anweisungen umfassen mehrere hundert Seiten, jede Woche kommen Änderungen dazu, und jede Antwort muss auf den Abschnitt zeigen, aus dem sie stammt, damit die Disponenten sie nachprüfen können. Jede Anfrage soll nur so viel kosten, wie sie wirklich braucht. Welche Lösung passt?
- a) Das Modell mit den Texten per Fine-Tuning nachtrainieren und nach jeder Wochenänderung erneut trainieren lassen.
- b) Die Anweisungen in Chunks zerlegen, pro Frage die passenden Chunks suchen und nummeriert in den Prompt geben.
- c) Alle Anweisungen komplett in den System-Prompt kopieren und bei jeder einzelnen Anfrage unverändert mitsenden.
- d) Ein größeres, teureres Modell wählen, weil es die internen Anweisungen aus seinem Training schon kennen dürfte.
Trage den Buchstaben als String ein.
Gehe die Randbedingungen einzeln durch (Umfang, Änderungen jede Woche, Beleg pro Antwort, Kosten pro Anfrage) und frage bei jeder Option: Erfüllt sie das? Woher soll ein Modell Wissen haben, das nie öffentlich war?
antwort = "b"
antwortÜbung 2: Reranking-Pipeline bauen (mittel, ca. 15 Min.)
Schreibe suche(frage, korpus, n, k). Vorab entfernst du Dubletten: Kommt derselbe Text mehrfach im Korpus vor (zum Beispiel nach zwei Importen), bleibt nur das erste Vorkommen (in der ursprünglichen Reihenfolge). Danach bewertet Stufe 1 alle übrig gebliebenen Texte mit billig_score und behält die besten n. Stufe 2 bewertet nur diese n mit teuer_score und gibt die besten k Texte zurück (beste zuerst, als Liste).
Gleichstand: Bei gleichem Score gewinnt jeweils der Eintrag, der in der Liste davor stand (in Stufe 1 der frühere im bereinigten Korpus, in Stufe 2 der frühere in der Kandidatenliste). Beide Bewertungsfunktionen, tokens und der Aufrufzähler ZAEHLER sind schon definiert (wie in der Lektion). Der Check zählt, wie oft du teuer_score aufrufst: erlaubt sind genau so viele Aufrufe wie Kandidaten (n, oder weniger, wenn der bereinigte Korpus kleiner ist).
Beispiel zum Verständnis: Bei einem Korpus mit 12 verschiedenen Texten und n=2, k=1 bewertet Stufe 1 alle zwölf billig, Stufe 2 nur zwei teuer, und es kommt ein Text zurück. Steht einer der Texte zweimal im Korpus, sind es elf Texte in Stufe 1.
Arbeite mit Indizes oder Paaren (Score, Text), damit du nach dem Sortieren noch weißt, welcher Text es war. sorted ist stabil und kennt key=. Prüfe, ob Stufe 2 wirklich nur über die Kandidatenliste läuft und nicht über den ganzen Korpus. Welche eingebaute Struktur behält beim Entfernen von Dubletten die Reihenfolge?
def suche(frage, korpus, n, k):
korpus = list(dict.fromkeys(korpus))
grob = sorted(korpus, key=lambda t: -billig_score(frage, t))[:n]
fein = sorted(grob, key=lambda t: -teuer_score(frage, t))[:k]
return fein
sucheÜbung 3: Kleinstes N bestimmen (mittel, ca. 10 Min.)
Schreibe kleinstes_n(plaetze, ziel). plaetze ist die Liste der Plätze, auf denen die erste Stufe bei jeweils einer Testfrage den richtigen Chunk geliefert hat (ganze Zahlen ab 1, oder None, wenn der richtige Chunk gar nicht unter den Treffern war). ziel ist ein Anteil zwischen 0 (ausgeschlossen) und 1, zum Beispiel 0.8. Gesucht ist das kleinste ganzzahlige N ab 1, bei dem der Anteil der Fragen mit Platz <= N mindestens ziel erreicht. Der Anteil ist Anzahl / Gesamtzahl der Fragen (auch None-Fragen zählen im Nenner), verglichen wird mit anteil >= ziel. Ist das Ziel mit keinem N erreichbar, oder ist die Liste leer, gib None zurück.
Beispiel: kleinstes_n([1, 1, 2, 4, 1, 9], 0.8) gibt 4 zurück. kleinstes_n([1, None, 2], 0.8) gibt None zurück: Die None-Frage kommt nie in den Kandidatenkreis, höchstens zwei von drei sind erreichbar (0.67).
Probiere N = 1, 2, 3, ... der Reihe nach aus. Bis zu welchem N lohnt sich das Probieren überhaupt? Was passiert mit den None-Einträgen beim Vergleich p <= N?
def kleinstes_n(plaetze, ziel):
echte = [p for p in plaetze if p is not None]
if not plaetze or not echte:
return None
for n in range(1, max(echte) + 1):
if sum(1 for p in echte if p <= n) / len(plaetze) >= ziel:
return n
return None
kleinstes_nMerksatz
Retrieval sucht schnell aus vielen, Reranking sortiert präzise unter wenigen, und das Kandidaten-N bestimmst du an echten Testfragen: so klein wie möglich, so groß wie nötig (Quelle).
Prüfstein
Deine erste Suche liefert den richtigen Chunk bei 6 von 10 Testfragen auf Platz 1 bis 3, bei 3 weiteren auf Platz 8 bis 15 und bei einer Frage gar nicht. Welches N schlägst du vor, was kostet es dich, und was sagt dir die eine Frage ohne Treffer über die Stelle, an der du ansetzen musst?
Weiter geht es in Teil b: Belegbare Antworten und Fehleranalyse.
Quelle: quellen/kursbuch-lerninhalte.md, Modul M2, Baustein “01 Wann RAG das richtige Werkzeug ist” (Zeilen 482 bis 495) und Baustein “05 Reranking” (Zeilen 571 bis 610, soweit Reranking betroffen). Aus der Quelle stammen: RAG-Ablauf in vier Schritten, “das Modell antwortet nur auf dieser Basis, im besten Fall mit Quellenangabe”, der Merksatz “RAG ist kein Ersatz für gute Daten”, Reranking auf den Top 20 mit top_n=5, der Cross-Encoder liest Frage und Kandidat gemeinsam und ist zu teuer für den ganzen Korpus, die Übung (ohne und mit Textausschnitt). Über die Quelle hinaus (allgemeines Fachwissen, mit Python 3.13 ausgeführt): die Gegenüberstellung RAG, Fine-Tuning und längerer Prompt (vereinfacht, bitte prüfen), die Simulatoren fake_modell, billig_score und teuer_score (der teure Score ist kein Cross-Encoder, nur eine Wortpaar-Simulation), die Bestimmung von N über den Platz des richtigen Chunks, die stabile Sortierung bei Gleichstand und die Behandlung von Dubletten. Der Hilfecenter-Korpus und die Reiserichtlinie sind erfunden. Bitte prüfen: wie viele Kandidaten (N) und welcher Reranker für deinen echten Korpus passen, und ob die Gegenüberstellung RAG, Fine-Tuning und Prompt zu deinem Anbieter und Modell passt.