flowchart TD
A[Antwort ist schlecht] --> B{Steht der Fakt im Korpus?}
B -- nein --> D[daten]
B -- ja --> C{Steht er ganz in einem Chunk?}
C -- nein --> E[chunking]
C -- ja --> F{Ist dieser Chunk unter den Kandidaten?}
F -- nein --> G[suche]
F -- ja --> H{Ist er nach dem Reranking im Kontext?}
H -- nein --> I[reranking]
H -- ja --> J[generierung]
Belegbare Antworten und Fehleranalyse, M2-Abschluss
Track KI · M2 RAG, Baustein 05 und Abschluss-Check 06 · ca. 55 Min. plus Projektaufgabe
Worum es geht
Eine RAG-Antwort mit Quellenangabe wirkt vertrauenswürdig, aber die Angabe ist nur eine Bitte an das Modell. In dieser Lektion machst du Antworten belegbar: Kontext nummerieren, Quellen nennen und per Code prüfen, dass jede Aussage einen Beleg hat. Danach lernst du, schlechte Antworten einzusortieren: War es Retrieval (Daten, Chunking, Suche, Reranking) oder Generierung? Das ist der Abschluss-Check von M2. Am Ende steht die Projektaufgabe mit einem lokalen Skript.
Was du aus Teil a brauchst: Du kennst den RAG-Ablauf (Suche, Kontext, Antwort) und die zweistufige Suche mit billig_score und teuer_score, die Reranking-Stufe hast du dort in Übung 2 gebaut. Falls nicht: Teil a, Wann RAG und Reranking.
Alles läuft im Browser ohne echtes LLM. Das Modell wird von einer kleinen Funktion simuliert. Zeitplan: etwa 15 Minuten Lesen und 40 Minuten für die drei Übungen, zusammen rund 55 Minuten plus Projektaufgabe.
Von JS/TS her gedacht
| Idee | JS/TS | Python |
|---|---|---|
| Teilstring prüfen | text.includes(zitat) |
zitat in text |
| Nummerierte Zeile | `[${n}] ${text}` |
f"[{n}] {text}" |
| Zähler mit Index | arr.map((x, i) => ...) |
enumerate(arr, 1) (Start bei 1) |
Wert oder undefined aus Map |
map.get(key) |
dict.get(key) (liefert None, wirft nichts) |
Wichtig: "" in text ist in Python immer True (der leere String steckt in jedem Text). Das wird bei der Zitatprüfung zur Falle.
Konzept
Schritt 1: Belegbare Antworten
Eine Antwort ist belegbar, wenn jede Aussage auf einen Chunk zeigt, der sie trägt. Dafür brauchst du drei Dinge im Prompt-Bau:
- Jeder Chunk bekommt im Prompt eine Nummer (
[1],[2], …). - Eine Anweisung: nur aus dem Kontext antworten, nach jeder Aussage die Nummer nennen, sonst “weiß ich nicht”.
- Nach der Antwort ein Zitat-Check im Code. Die Anweisung bittet das Modell nur, die Nummern zu nennen. Ob es das auch ehrlich tut, prüfst du selbst.
Der Zitat-Check für eine einzelne Aussage ist einfach: Die Nummer muss im Kontext existieren, und das wörtliche Zitat der Aussage muss genau in diesem Chunk stehen.
Die erste Aussage ist belegt (True), die zweite zitiert etwas, das im angegebenen Chunk nicht steht (False). Eine ganze Antwort besteht aus mehreren Aussagen, und jede einzelne muss durch den Check. Das baust du in Übung 2.
Eine Grenze gleich vorweg: Der Check beweist nur, dass das Zitat dort steht. Ob es die Aussage auch stützt, sieht er nicht (ein Zitat kann stimmen und trotzdem zu etwas anderem gehören). Das erkennst du mit Stichproben von Hand oder, wie in M3 der Quelle, mit einem LLM als Richter.
Schritt 2: Schlechte Antworten einsortieren (Abschluss-Check)
Der Abschluss-Check von M2 verlangt: Die fünf schlechtesten Antworten eines kleinen Testfragensets sind angeschaut und du kannst erklären, warum sie schwach sind (Chunking? Suche? Reranking? Prompt?). Wichtig ist die Reihenfolge der Fragen. Sie geht dem Datenfluss entlang und hört beim ersten Fehler auf:
Die ersten vier Labels sind Retrieval-Fehler: Das Modell hat den richtigen Beleg nie gesehen, und selbst das beste Modell konnte nicht richtig antworten. Nur das letzte Label ist ein Generierungsfehler: Der richtige Chunk lag im Prompt und die Antwort ist trotzdem falsch (Prompt oder Modell). Das ist wichtig, weil du je nach Label an einer ganz anderen Stelle ansetzt. Ein anderes Modell hilft bei suche nichts. Dass es oft das Retrieval ist und nicht das Sprachmodell, ist eine verbreitete Erfahrung (Quelle, andere Stelle: “oft ist es Retrieval, nicht das Sprachmodell selbst”, bitte an eigenen Daten prüfen).
Falle
Falle 1: Quellenangabe ohne Prüfung. “Nenne die Quelle” im Prompt ist nur eine Bitte. Erfundene Nummern, falsche Zitate und Aussagen ohne Beleg erkennst du nur mit einem Check im Code, und selbst der sieht nur die Form, nicht die Bedeutung.
Falle 2: Dem Modell die Schuld geben. Bei einer schlechten Antwort zuerst fragen, ob der richtige Chunk überhaupt im Prompt lag. Wenn nicht, ist es kein Modellproblem.
Falle 3: Leeres Zitat. "" in text ist True. Ein Zitat-Check ohne Prüfung auf leere Zitate lässt Aussagen ohne Beleg durch.
Übungen
Übung 1: Prompt mit nummeriertem Kontext bauen (leicht, ca. 10 Min.)
Schreibe baue_prompt(frage, chunks). chunks ist eine Liste von Dicts mit id (Dokumentstelle, z. B. "pumpe-3.2") und text. Die Funktion gibt ein Tupel (prompt, quellen) zurück:
promptist genau so aufgebaut (Leerzeile zwischen den Teilen, Nummerierung ab 1 in Listenreihenfolge):
<ANWEISUNG>
Kontext:
[1] <Text von Chunk 1>
[2] <Text von Chunk 2>
Frage: <frage>
quellenordnet jede Nummer der Dokumentstelle zu, z. B.{1: "pumpe-3.2", 2: "pumpe-5.1"}. Damit kannst du später aus[2]in der Antwort die echte Quelle machen.
Die Konstante ANWEISUNG ist schon definiert: "Antworte nur mit Informationen aus dem Kontext. Nenne nach jeder Aussage die Quelle als [Nummer]. Steht die Antwort nicht im Kontext, antworte: Das weiß ich nicht."
enumerate(chunks, 1) liefert Nummer und Chunk zugleich. Baue erst die Kontextzeilen als Liste und füge sie mit "\n".join(...) zusammen. Die Quellen-Zuordnung kannst du in derselben Schleife oder als Dict-Comprehension bauen.
def baue_prompt(frage, chunks):
zeilen = [f"[{n}] {c['text']}" for n, c in enumerate(chunks, 1)]
prompt = ANWEISUNG + "\n\nKontext:\n" + "\n".join(zeilen) + "\n\nFrage: " + frage
quellen = {n: c["id"] for n, c in enumerate(chunks, 1)}
return prompt, quellen
baue_promptÜbung 2: Zitat-Check für eine ganze Antwort (mittel, ca. 15 Min.)
Das Modell hat geantwortet. Seine Antwort besteht aus Aussagen, jede ein Dict mit text, beleg (Nummer des Chunks oder None) und zitat (wörtlicher Ausschnitt oder None). Schreibe pruefe_belege(aussagen, kontext). kontext ist ein Dict Nummer zu Chunk-Text. Die Funktion gibt die Indizes (ab 0) aller unbelegten Aussagen als aufsteigende Liste zurück. Eine Aussage ist belegt, wenn alle drei Dinge zutreffen:
- Sie hat eine Beleg-Nummer, und diese Nummer gibt es im Kontext.
- Sie hat ein Zitat, und es ist nicht leer.
- Das Zitat steht genau so geschrieben im Chunk mit dieser Nummer (nicht in irgendeinem anderen).
Probiere mit den Beispieldaten in der Zelle, danach prüft der Check mit anderen Daten.
Gehe jede Aussage mit enumerate durch und sortiere sie einzeln aus. Überlege, in welcher Reihenfolge du die drei Bedingungen prüfst, damit keine Prüfung selbst auf einen Fehler läuft (denke an None und an die Falle aus der Einleitung). kontext.get(nummer) wirft keine Exception.
KONTEXT = {
1: "Urlaub wird mindestens zwei Wochen vor Beginn im Personalportal beantragt.",
2: "Resturlaub aus dem Vorjahr verfällt am 31. März.",
3: "Bei Krankheit im Urlaub zählen die Krankheitstage nicht als Urlaub, wenn ein ärztliches Attest vorliegt.",
}
AUSSAGEN = [
{"text": "Urlaub beantragst du im Portal.", "beleg": 1, "zitat": "im Personalportal beantragt"},
{"text": "Resturlaub verfällt Ende März.", "beleg": 2, "zitat": "verfällt am 31. März"},
{"text": "Der Teamleiter genehmigt Urlaub.", "beleg": None, "zitat": None},
{"text": "Krankheitstage werden gutgeschrieben.", "beleg": 3, "zitat": "werden gutgeschrieben"},
{"text": "Resturlaub verfällt am 31. März.", "beleg": 4, "zitat": "verfällt am 31. März"},
]
def pruefe_belege(aussagen, kontext):
schlecht = []
for i, a in enumerate(aussagen):
text = kontext.get(a["beleg"]) if a["beleg"] is not None else None
zitat = a["zitat"]
if text is None or not zitat or zitat not in text:
schlecht.append(i)
return schlecht
pruefe_belegeÜbung 3: Die fünf schlechtesten Antworten einsortieren (mittel, ca. 15 Min.)
Du hast fünf schlechte Antworten aus einem Testlauf einer Richtlinien-FAQ. Zu jedem Fall kennst du: die Frage, die Fakt-Teile (Textstücke, die zusammen den richtigen Fakt ergeben), die Kandidaten der ersten Suche, den Kontext nach dem Reranking (beides als Chunk-IDs) und die Antwort des Modells. Ordne jeden Fall einem Label zu: "daten", "chunking", "suche", "reranking" oder "generierung". Gehe dazu die Entscheidungsfolge aus Schritt 2 durch. Führe die Zelle einmal aus, dann siehst du die Fälle und den Korpus.
Suche für jeden Fakt-Teil im Korpus per Auge (oder mit in), in welchen Chunks er steht. Erst wenn du weißt, ob und in welchem Chunk der ganze Fakt steht, schaust du auf Kandidaten und Kontext. Ein Fakt in zwei Chunks ist etwas anderes als ein Fakt in keinem Chunk.
antwort = ["suche", "reranking", "generierung", "daten", "chunking"]
antwortProjektaufgabe: RAG-Pipeline mit Belegen und Fehleranalyse (M2-Abschluss)
Das ist der Abschluss-Check von M2 (Baustein 06). Er läuft lokal im Lernlabor, nicht im Browser. Das Skript braucht keine zusätzlichen Pakete. Einen API-Schlüssel liest das Skript nur aus der Umgebungsvariable ANTHROPIC_API_KEY, er steht nie in einer Datei. Ohne Schlüssel (oder ohne das Paket anthropic) läuft ein Trockenlauf mit dem Simulator aus dieser Lektion. Dann entstehen keine Kosten.
Aufgabe (Quelle, Abschluss-Check): Ein System steht, das auf dem eigenen Korpus (die Quelle schlägt CGMES/BDEW vor, weil dir das vertraut ist) für eine Frage die passenden Chunks findet, reranked, in den Prompt einbettet und eine Antwort mit erkennbarem Bezug zur Quelle liefert. Aus einem kleinen Testfragenset von etwa 10 Fragen sind die fünf schlechtesten Antworten angeschaut, und du kannst erklären, warum sie schwach sind (Chunking? Suche? Reranking? Prompt?).
Das Skript lernlabor/uebung/ki/ki_10_rag_pipeline.py bringt den Rahmen mit: einen kleinen erfundenen Korpus, zehn Testfragen, den Simulator, die erste Suchstufe (stufe1) und die Pipeline. Vier Teile sind Platzhalter (NotImplementedError), die du aus den Übungen kennst: rerank (Stufe 2 aus Teil a, Übung 2), baue_prompt, pruefe_belege und klassifiziere (Übungen 1 bis 3 dieses Teils). Aufruf:
cd lernlabor && uv run python uebung/ki/ki_10_rag_pipeline.pyDas Skript meldet pro Teil OK, FEHLT oder FEHLER. Sind alle Teile in Ordnung, läuft die Pipeline über die zehn Testfragen, gibt die fünf schlechtesten Antworten mit Diagnose aus und macht (nur mit Schlüssel, Paket und gesetzter ANTHROPIC_MODEL) einen einzigen echten Aufruf. Der kostet Geld (Preise beim Anbieter prüfen). Das Paket anthropic ist im Lernlabor nicht vorinstalliert. Für den echten Aufruf installierst du es bewusst selbst (uv add anthropic). Ohne das Paket läuft der Trockenlauf.
Aufgabenliste M2 (Quelle, ca. 28 bis 35 h gesamt):
- Testkorpus zusammenstellen (CGMES/BDEW-Dokumente) und Chunking-Strategie festlegen (4 bis 6 h)
- Embeddings für alle Chunks berechnen und in pgvector ablegen (4 bis 6 h)
- Hybrid Search (Vektor + BM25) mit Ranking-Kombination bauen (6 bis 9 h)
- Reranking-Schritt einbauen (3 bis 5 h, diese Lektion)
- Prompt-Konstruktion mit Kontext + Quellenangabe (3 bis 4 h, diese Lektion)
- Abschluss-Check: 10 Testfragen durchspielen, 5 schlechteste Antworten analysieren (3 bis 4 h)
Hinweis zur Vereinfachung: Das Skript nutzt einen erfundenen Mini-Korpus und die Wortzählung statt Embeddings, pgvector und Hybrid Search. Für dein echtes Projekt tauschst du stufe1 (die erste Suche) gegen deine Hybrid Search aus ki/09 und den teuer_score gegen einen echten Reranker. Die Pipeline, der Zitat-Check und die Diagnose bleiben gleich.
Stolperfalle (Quelle): Ein unsauberer, widersprüchlicher Korpus liefert unsaubere, widersprüchliche Antworten, egal wie gut Chunking, Suche und Reranking sind.
Fertig, wenn:
- Das Skript meldet im Trockenlauf für
rerank,baue_prompt,pruefe_belegeundklassifizierejeweilsOK. - Die Pipeline beantwortet alle zehn Testfragen, und für jede Antwort steht fest, ob sie belegt ist (Zitat-Check).
- Die fünf schlechtesten Antworten sind ausgegeben, jede mit einem Label (
daten,chunking,suche,reranking,generierung). - Du kannst zu jedem der fünf Fälle in einem Satz sagen, was du ändern würdest (anderer Chunking-Schnitt, größeres
N, bessere Daten, anderer Prompt) und warum diese Änderung am Label ansetzt. - Im Schlüssel-Fall: Der Schlüssel steht nur in der Umgebung, nicht in der Datei, nicht im Log.
Selbstcheck:
Merksatz
Eine Antwort zählt erst, wenn jede Aussage auf einen Chunk zeigt, den dein Code geprüft hat, und bei einer schlechten Antwort fragst du zuerst, ob der richtige Chunk überhaupt im Prompt lag (der Beleg-Check ist hier über die Quelle hinaus erweitert).
Prüfstein
Eine Antwort deines Systems ist falsch, obwohl sie eine Quelle nennt und der Zitat-Check grün ist. Nenne zwei Wege, wie das möglich ist, sag, welches Label aus Schritt 2 jeweils passt, und beschreibe, an welcher Stelle der Pipeline du für jeden Weg ansetzt.
Quelle: quellen/kursbuch-lerninhalte.md, Modul M2, Baustein “06 Abschluss-Check” mit der Aufgabenliste M2 (Zeilen 571 bis 610) und die Stolperfalle “RAG ist kein Ersatz für gute Daten” aus Baustein 01. Aus der Quelle stammen: “im besten Fall mit Quellenangabe”, der Abschluss-Check (fünf schlechteste Antworten analysieren, Chunking, Suche, Reranking, Prompt) mit Aufgabenliste und Stundenangaben. Über die Quelle hinaus (allgemeines Fachwissen, mit Python 3.13 ausgeführt): der Zitat-Check mit Nummer und wörtlichem Zitat, die Entscheidungsfolge mit den fünf Labels (die Quelle nennt nur die Fragen Chunking, Suche, Reranking, Prompt), "" in text. Der Hinweis, dass oft das Retrieval und nicht das Modell die Schwäche ist, und der Verweis auf einen LLM als Richter stehen an anderer Stelle der Quelle (Modul M3, Fehleranalyse und LLM-as-Judge). Korpora (Passwort-FAQ, Urlaub) sind erfunden. Bitte prüfen: ob die Entscheidungsfolge zu deinem System passt (z. B. mit Reranker oder ohne).