RAG von Grund auf: Teile aufteilen, einbetten, speichern, abrufen, mit Kontext antworten
Ein schrittweiser Überblick über einen RAG-Pipeline mit LangChain und FAISS: Chunking, Embeddings, Retrieval, Prompt-Grundierung, häufige Fehler und zukünftige Verbesserungen.
Quellen: RAG, Explained the Way I Wish Someone Had Explained It to Me
Fragen Sie ein Chat-Modell, was die Rückerstattungspolitik eines bestimmten Unternehmens vorsieht, und es wird selten zugeben, nichts zu wissen. Häufig liefert es einen fließenden, glaubwürdigen Absatz, der nichts mit der tatsächlichen Politik zu tun hat. Der Grund dafür liegt nicht in einem Defekt. Ein Sprachmodell wird einmal trainiert und anschließend „gefroren“. Es hat niemals einen privaten PDF-Datei, eine interne Wiki oder ein Notion-Arbeitsumfeld gelesen und weiß nichts über Inhalte, die nach dem Ende seiner Trainingsphase entstanden sind.
Retrieval-augmented Generation, meist abgekürzt als RAG, ist die gängige Lösung, und die Idee dahinter ist erstaunlich einfach. Anstatt zu erwarten, dass das Modell sich an eine Antwort erinnert, sucht das System den relevanten Text und fügt ihn direkt in die Anfrage ein, bevor es das Modell bittet, diesen zu lesen und zu antworten. Alles Weitere sind technische Maßnahmen, um diese Suche zuverlässig zu gestalten. Das untenstehende Beispiel zeigt einen Support-Bot, der Fragen aus einer PDF mit Unternehmensrichtlinien beantwortet.
Zwei Studenten, eine Prüfung
Stellen Sie sich zwei Personen vor, die denselben Test machen. Die erste hat vor Monaten ein Lehrbuch auswendig gelernt und schreibt aus dem Gedächtnis. Wenn das Buch veraltet ist oder ein Kapitel halb vergessen wurde, sind die Antworten selbstbewusst, aber falsch. Die zweite Person darf ein offenes Buch verwenden. Bevor sie schreibt, blättert sie zur richtigen Seite, liest sie und verbindet deren Inhalt mit ihrem eigenen logischen Denken.
In der Regel gewinnt der zweite Schüler – und das nicht aufgrund besonderer Intelligenz. Informationen vor dem Sprechen nachzuschlagen, ist einfach eine bessere Methode, um Antworten zu geben. In einem RAG-System übernimmt das Sprachmodell die Rolle des zweiten Schülers, die Unternehmensdaten bilden das offene Buch und die Vektordatenbank dient als Index, der anzeigt, zu welcher Seite man blättern soll.
Zwei Pipelines, nicht einer
Jedes RAG-System, egal wie ausgefeilt, besteht aus zwei getrennten Pipelines, die in der Mitte miteinander verbunden sind.
- Die Indizierung erfolgt einmal offline und erneut jedes Mal, wenn sich die Dokumente ändern. Die Rohdateien werden zu einem suchbaren Index.
- Die Abfrage und Erstellung laufen bei jeder Anfrage. Der Index wird durchsucht, die besten Ergebnisse werden ausgewählt und dem Modell übergeben.
Anfänger versuchen oft, sich ein einziges, langes Verarbeitungssystem vorzustellen, was verwirrend ist. Es hilft, es in zwei Teile zu teilen, da die beiden Teile zu unterschiedlichen Zeitpunkten arbeiten und unterschiedliche Probleme lösen.
Erstellung des Indexes
Dokumente laden
Beginnen Sie damit, den Rohinhalt in reinen Text umzuwandeln. PDFs, Word-Dateien, Webseiten sowie exportierte Notizen benötigen alle einen Lader, der Text zurückgibt.
from langchain_community.document_loaders import PyPDFLoader
loader = PyPDFLoader("company_policy.pdf")
documents = loader.load()
print(f"Loaded {len(documents)} pages")
Text in Abschnitte aufteilen
Die Aufteilung von Text in Abschnitte verdient mehr Aufmerksamkeit, als sie normalerweise bekommt. Ein sehr häufiger erster Fehler besteht darin, ein ganzes Dokument als eine einzige riesige Einheit in den Vektor-Speicher zu laden. Ein Embedding-Modell komprimiert einen Textabschnitt in einen Vektor fester Größe. Wenn man ihm zwanzig Seiten zur Verarbeitung gibt, entsteht eine verschwommene Durchschnittswertung aller Inhalte, die zwar ausdrücken kann, dass das Dokument im Allgemeinen über Unternehmensrichtlinien handelt, aber keinen bestimmten Absatz über ein dreißigtägiges Rückerstattungsfenster wiedergeben kann.
Die Lösung besteht darin, Dokumente in kleine, überschneidende Teile zu zerlegen. Die Überschneidung ist wichtig, denn ein Satz, der genau an einer Grenze abgeschnitten wird, verliert seinen Kontext auf beiden Seiten; ein Puffer von etwa zehn bis zwanzig Prozent der Größe eines Abschnitts dient dabei als Sicherheitsnetz.
from langchain.text_splitter import RecursiveCharacterTextSplitter
splitter = RecursiveCharacterTextSplitter(
chunk_size=500, # characters per chunk
chunk_overlap=80, # overlap between consecutive chunks
separators=["\n\n", "\n", ". ", " "]
)
chunks = splitter.split_documents(documents)
print(f"Split into {len(chunks)} chunks")
Als Richtwert führen zu große Textabschnitte zu unklaren Ergebnissen, während zu kleine Abschnitte den Kontext entfernen. Ein Ausgangsbereich von etwa 300 bis 800 Zeichen ist sinnvoll; danach sollte man anhand echter Antworten anpassen.
Jeden Abschnitt einbetten
Ein Embedding-Modell wandelt Text in eine Liste von Zahlen um, die seine Bedeutung widerspiegeln und nicht nur seine Wortwahl. Zwei Sätze, die dasselbe aussagen, ergeben Vektoren, die sich in der Nähe zueinander befinden – selbst wenn sie keinerlei gemeinsame Wörter haben.
from langchain_openai import OpenAIEmbeddings
embedding_model = OpenAIEmbeddings(model="text-embedding-3-small")
# under the hood, this is what happens per chunk:
vector = embedding_model.embed_query("30-day refund window for annual plans")
print(len(vector)) # e.g. 1536 numbers representing this sentence's meaning
Eine Möglichkeit, sich das vorzustellen, ist eine flache Karte, auf der verwandte Ideen in Clustern zusammengefasst sind.
"refund policy" •
\
• "money-back guarantee"
"vacation days" •
\
• "paid time off"
Eine Formulierung wie „Rückerstattungspolitik“ wird neben „Geld-zurück-Garantie“ angezeigt, obwohl die Wörter unterschiedlich sind – und das ist das ganze Geheimnis hinter der semantischen Suche. Das erklärt, warum RAG die alte Methode des Schlüsselwortabgleichs per Ctrl+F übertrifft.
Die Vektoren speichern
Jeder Vektor wird zusammen mit dem ursprünglichen Text in eine Vektordatenbank eingefügt. Diese Datenbank ist der Index, den der „offene-Buch“-Lernende durchblättert.
from langchain_community.vectorstores import FAISS
vectorstore = FAISS.from_documents(chunks, embedding_model)
vectorstore.save_local("faiss_index")
FAISS ist ein guter Ausgangspunkt, da es kostenlos ist und auf einem lokalen Rechner läuft, und Chroma ist etwa genauso einfach. Verwaltete Dienste wie Pinecone oder Weaviate werden sinnvoll, wenn die Produktionsumgebung skaliert. Das Konzept bleibt bei allen Optionen gleich, nur Speicher und Infrastruktur unterscheiden sich.
Das Indexieren ist nun abgeschlossen. Das Ergebnis ist ein suchbarer Speicher, der aus den Dokumenten erstellt wird, und er muss nur dann neu erstellt werden, wenn sich diese Dokumente ändern, nicht bei jeder Anfrage eines Benutzers.
Antwort auf eine Frage
Dieser Teil wird jedes Mal ausgeführt, wenn jemand eine Abfrage eintippt.
Die Frage mit demselben Modell einbetten
query = "Can I get a refund on my annual subscription?"
Die Frage muss mit genau demselben Modell eingebettet werden, das bei der Indizierung verwendet wurde. Die Leute machen ständig diesen Fehler. Wenn der Index mit einem Embedding-Modell erstellt und mit einem anderen abgefragt wird, befinden sich die Vektoren in unterschiedlichen Bedeutungsräumen, wodurch die Ähnlichkeitssuche stillschweigend nicht mehr funktioniert. Es tritt kein Fehler auf, nur schlechte Ergebnisse – daher gilt die Regel, ausschließlich ein Modell zu verwenden.
Die nächsten Passagen abrufen
retriever = vectorstore.as_retriever(search_kwargs={"k": 4})
relevant_chunks = retriever.invoke(query)
for chunk in relevant_chunks:
print(chunk.page_content[:100], "...")
Intern führt FAISS eine Ähnlichkeitssuche durch. Es misst die Distanz – in der Regel die kosinusähnliche Distanz – zwischen dem Vektor der Frage und dem Vektor jedes gespeicherten Fragments und gibt anschließend die nächstgelegenen zurück. Es handelt sich dabei wieder um das Konzept der Clustern auf einer Karte, das umgekehrt angewandt wird, um den Cluster zu finden, zu dem die Frage gehört.
Fügen Sie die Fragmente zu einem Prompt hinzu
Dies ist der Schritt, der RAG funktionieren lässt: Der abgerufte Text wird buchstäblich in den Prompt eingefügt.
from langchain_core.prompts import ChatPromptTemplate
prompt = ChatPromptTemplate.from_template("""
Answer the question using ONLY the context below.
If the answer isn't in the context, say "I don't have that information."
Context:
{context}
Question: {question}
""")
Die Anweisung, nur mit dem bereitgestellten Kontext zu antworten, sorgt dafür, dass das Modell realitätsbezogen bleibt und nicht Dinge erfindet. Sie unterscheidet eine Prüfung mit offenen Unterlagen von einer mit geschlossenen Unterlagen, bei der Improvisation erlaubt ist.
In eine Kette einbinden
Nun werden die Komponenten zu der RAG-Kette zusammengesetzt, was die Leute meinen, wenn sie von der Erstellung einer solchen Kette sprechen. In LangChain wird dies in der Regel mit dem Pipe-Operator dargestellt, der die Ausgabe links an den nächsten Schritt rechts weiterleitet.
from langchain_openai import ChatOpenAI
from langchain_core.runnables import RunnablePassthrough
from langchain_core.output_parsers import StrOutputParser
llm = ChatOpenAI(model="gpt-4o-mini")
def format_docs(docs):
return "\n\n".join(doc.page_content for doc in docs)
rag_chain = (
{"context": retriever | format_docs, "question": RunnablePassthrough()}
| prompt
| llm
| StrOutputParser()
)
answer = rag_chain.invoke("Can I get a refund on my annual subscription?")
print(answer)
Man liest von links nach rechts – die Kette erzählt sich selbst. Die Frage wird eingegeben, Abschnitte werden abgerufen und als Kontext formatiert, während die Frage unverändert bleibt. Das Prompt-Muster wird ausgefüllt, das Modell wird aufgerufen, und der reine Text wird aus seiner Antwort extrahiert.
Wenn man es ausführt, erhält man anstelle einer erfundenen Schätzung etwas wie: Ja, Jahrespläne sind gemäß der Rückerstattungspolitik innerhalb von dreißig Tagen nach Kauf für eine vollständige Rückerstattung geeignet. Die Antwort ist fundiert und konkret und lässt sich auf einen echten Absatz im PDF zurückverfolgen.
Warum nicht alles einfügen?
Ein berechtigter Einwand ist, dass die Kontextfenster heutzutage sehr groß sind. Warum nicht das gesamte Dokument in jedes Prompt aufnehmen und die Vektorabfrage überspringen?
- Kosten und Latenz. Jeder gesendete Token kostet Geld und dauert Zeit. Bei einer 500-seitigen Wissensdatenbank ist es verschwenderisch, bei jeder Frage alles zu senden, wenn die Antwort in einem einzigen Absatz zu finden ist.
- Verminderte Erinnerungsfähigkeit. Selbst bei großen Informationsfenstern nutzen Modelle Informationen, die mitten im Prompt versteckt sind, deutlich schlechter als Informationen, die direkt und kurz übergeben werden.
- Skalierbarkeit. Das Einfügen aller Inhalte scheitert völlig, sobald die Wissensdatenbank ein bestimmtes Fenster überschreitet – wie beispielsweise bei Tausenden von PDFs, einer vollständigen Unternehmens-Wiki oder Jahren an Support-Tickets.
RAG ermöglicht es einem System, von einem Dokument zu einer ganzen Wissensdatenbank zu wachsen, während die Größe des Prompts unverändert bleibt. Nur der Abrufschritt greift auf den gesamten Datensatz zu, und der Prompt erhält nur einige relevante Abschnitte.
Fehler, die früh auftreten
- Eine schlechte Aufteilung in Blöcke verschlechtert die Ergebnisse unabhängig von der Qualität des Modells. Wenn eine Tabelle in der Mitte geteilt wird oder ein Satz mitten im Gedanken abbricht, kann nichts danach dies wieder in Ordnung bringen. Überprüfen Sie die Qualität der Ergebnisse, bevor Sie das Modell dafür verantwortlich machen.
- Nicht übereinstimmende Embedding-Modelle machen Similaritätswerte bedeutungslos. Ein Index, der mit text-embedding-3-small erstellt wurde, kann nicht mit einem anderen Modell abgefragt werden.
- Sowohl zu wenige als auch zu viele Blöcke zu extrahieren, schadet den Ergebnissen. Bei k=1 kann die eigentliche Antwort übersehen werden, und bei k=20 versinkt das Modell im Rauschen und den hohen Kosten. Beginnen Sie mit drei bis fünf und passen Sie die Anzahl anhand der Ergebnisse an.
- Ohne eine Alternative für den Fall, dass keine Information vorliegt, wird das Modell trotz vorhandenen relevanten Kontexts weiterhin eine Antwort erfinden. Legen Sie klar fest, was zu tun ist, wenn der Kontext keine Antwort enthält.
Was als Nächstes?
Sobald der grundlegende Ablauf funktioniert, sind drei Verbesserungen die logischen nächsten Schritte.
- Die hybride Suche kombiniert Vektorähnlichkeit mit Schlüsselwortabgleich, da bestimmte Abfragen, wie z. B. genaue Produktnummern oder -namen, besser mit Schlüsselwörtern bearbeitet werden können.
- Durch das Neuranking werden zunächst eine größere Anzahl von Kandidaten abgerufen, anschließend lässt ein zweites, präziseres Modell diese neu ordnen, bevor das Sprachmodell sie überhaupt betrachtet.
- Durch das Filtern von Metadaten werden Details wie Datum, Quelle oder Abteilung jedem Datenblock zugeordnet, sodass die Suche bereits vor dem eigentlichen Ähnlichkeitsabgleich eingegrenzt werden kann.
Nichts davon ist von Bedeutung, solange der grundlegende Ablauf aus Aufteilen in Blöcke, Einbetten, Speichern, Abrufen und Erstellen funktioniert. Der schnellste Weg, dies wirklich zu verstehen, besteht darin, ein wichtiges PDF zu laden, absichtlich Fehler zu erzeugen – beispielsweise durch inkompatible Einbettungsmodelle oder Blöcke ohne Überschneidung – und zu beobachten, wie sich die Ergebnisse verschlechtern. Bei RAG lehren die Fehlermöglichkeiten mehr als der erfolgreiche Ablauf.
Verwandte Literatur
- Retrieval-Augmented Generation erläutert: Wie man Wissenslücken bei LLMs behebt — Erfahren Sie, warum LLMs Halluzinationen erzeugen und veraltet werden, und sehen Sie anschließend Schritt für Schritt, wie RAG Anfragen abruft, in Blöcke aufteilt, verankert und erweitert, um dieses Problem zu lösen.
- RAG-Datenvergiftung: Wie Pipelines gehackt werden können, ohne das LLM anzufassen — Wie Angreifer Wissensbasen von RAG durch Dokumente, Metadaten und indirekte Prompt-Injektionen vergiften sowie die Datenlebenszyklus-Steuerungen, die tatsächlich dagegen schützen.