Wir haben Vector Stores verstanden – sie speichern semantische Fingerabdrücke. Wir haben Embeddings und Chunking durchdrungen – sie transformieren Text in durchsuchbare Geometrie. Doch ein entscheidendes Element fehlt noch. Wie kommt die gefundene Information zum Modell? Wie wird aus einer Suchanfrage eine nutzbare Antwort?
Das ist Retrieval.
Retrieval ist die Brücke zwischen Index und Intelligenz. Es ist der Prozess, der relevante Informationen aus verschiedenen Speichersystemen extrahiert und sie dort hinbringt, wo sie gebraucht werden – in den Kontext eines Language Models. Diese scheinbar simple Operation – “finde relevante Dokumente” – entfaltet sich in der Praxis zu einem komplexen System aus Query-Analyse, Such-Orchestrierung und Ergebnis-Aggregation.
LangChain abstrahiert diese Komplexität mit dem Konzept des Retrievers. Ein Retriever ist eine standardisierte Schnittstelle, die eine einfache Regel befolgt: String rein, Dokumente raus. Egal ob dahinter ein Vector Store, eine SQL-Datenbank oder eine Suchmaschine steht – das Interface bleibt konsistent. Diese Abstraktion ist mächtiger, als sie zunächst erscheint. Sie erlaubt es, Retrieval-Strategien auszutauschen, zu kombinieren und zu orchestrieren, ohne die darüberliegende Anwendungslogik anzufassen.
Ein Retriever in LangChain ist ein Runnable – Teil der
standardisierten Ausführungsschnittstelle, die durch das gesamte
Framework zieht. Das bedeutet: Er hat eine invoke-Methode.
Diese akzeptiert eine Query als String und gibt eine Liste von
Document-Objekten zurück.
from langchain_core.vectorstores import InMemoryVectorStore
from langchain_openai import OpenAIEmbeddings
# Vector Store als Retriever konfigurieren
embeddings = OpenAIEmbeddings(model="text-embedding-3-small")
vector_store = InMemoryVectorStore(embedding=embeddings)
# ... Dokumente hinzufügen (wie in vorherigen Kapiteln gezeigt)
# Vector Store wird zum Retriever
retriever = vector_store.as_retriever(
search_kwargs={"k": 4} # Anzahl der zurückzugebenden Dokumente
)
# Retrieval durchführen
query = "Wie funktioniert semantische Suche?"
docs = retriever.invoke(query)
# Resultat: Liste von Document-Objekten
for doc in docs:
print(f"Content: {doc.page_content[:100]}...")
print(f"Metadata: {doc.metadata}\n")Die Einfachheit täuscht. Hinter dieser Schnittstelle verbirgt sich die gesamte Komplexität des jeweiligen Retrieval-Systems. Ein Vector Store-Retriever embeddet die Query, führt eine Ähnlichkeitssuche durch, wendet möglicherweise Metadaten-Filter an und rankt die Ergebnisse. Ein SQL-Retriever könnte die natürliche Sprache in SQL übersetzen, die Datenbank abfragen und die Resultate in Document-Objekte verpacken. Die Implementierungsdetails sind verborgen. Das Interface bleibt stabil.
Diese Stabilität ist keine Einschränkung – sie ist eine Befreiung. Sie erlaubt es, Retrieval-Logik unabhängig von der restlichen Anwendung zu entwickeln und zu optimieren. Ein Retriever kann ausgetauscht werden, ohne dass Chains, Agents oder andere Komponenten davon wissen müssen.
Menschen formulieren Fragen in natürlicher Sprache. Suchsysteme erwarten präzise Queries. Diese Diskrepanz ist fundamental. Ein Nutzer fragt: “Was waren nochmal die wichtigsten Punkte aus dem letzten Quartalsbericht?” Ein Vector Store will einen Query-Vektor. Eine SQL-Datenbank will eine SQL-Anweisung. Ein Graph-Store erwartet Cypher.
Query Analysis ist die Übersetzungsschicht. Sie transformiert rohe Nutzereingaben in optimierte Such-Queries. Das kann von einfacher Keyword-Extraktion bis zu sophistizierter Query-Expansion reichen. LangChain unterscheidet zwei Hauptkategorien: Query Re-writing für unstrukturierte Daten und Query Construction für strukturierte Daten.
Eine einzelne Frage kann auf viele Arten gestellt werden. Query Re-writing nutzt Language Models, um aus einer Nutzerfrage mehrere Varianten zu generieren oder die Frage so umzuformulieren, dass bessere Retrieval-Ergebnisse entstehen. Vier Techniken haben sich etabliert.
Multi-Query generiert mehrere Umformulierungen derselben Frage. Die Intuition: Eine Frage kann auf unterschiedliche Weise beantwortet werden. Verschiedene Formulierungen könnten unterschiedliche relevante Dokumente matchen. Die Ergebnisse aller Queries werden zusammengeführt und dedupliziert.
from typing import List
from pydantic import BaseModel, Field
from langchain_openai import ChatOpenAI
from langchain_core.messages import SystemMessage, HumanMessage
class MultiQueries(BaseModel):
"""Mehrere alternative Formulierungen einer Frage."""
queries: List[str] = Field(
description="Alternative Formulierungen der ursprünglichen Frage"
)
# Strukturierte Ausgabe erzwingen
model = ChatOpenAI(model="gpt-4o", temperature=0)
structured_model = model.with_structured_output(MultiQueries)
system_prompt = """Du bist ein Assistent, der Fragen in mehrere alternative
Formulierungen umschreibt. Ziel ist es, durch verschiedene Phrasierungen
eine höhere Recall-Rate beim Retrieval zu erreichen. Generiere 3-5
semantisch ähnliche, aber unterschiedlich formulierte Varianten."""
original_query = "Welche Faktoren beeinflussen die Performance von Embeddings?"
multi_queries = structured_model.invoke([
SystemMessage(content=system_prompt),
HumanMessage(content=original_query)
])
print("Original:", original_query)
print("\nAlternative Formulierungen:")
for i, q in enumerate(multi_queries.queries, 1):
print(f"{i}. {q}")
# Jede Query einzeln gegen den Retriever laufen lassen
# und Ergebnisse zusammenführen
all_docs = []
for query in multi_queries.queries:
docs = retriever.invoke(query)
all_docs.extend(docs)
# Deduplizierung basierend auf Inhalt oder IDs
unique_docs = {doc.page_content: doc for doc in all_docs}.values()Decomposition zerlegt komplexe Fragen in einfachere Teilfragen. Eine Frage wie “Vergleiche die Performance von BERT und GPT-3 in Bezug auf Few-Shot Learning und erläutere die Architekturunterschiede” ist schwer auf einmal zu beantworten. Zerlegt man sie in drei Teilfragen – Performance von BERT, Performance von GPT-3, Architekturunterschiede – wird jede einzeln lösbar. Die Teilantworten werden am Ende konsolidiert.
class DecomposedQuestions(BaseModel):
"""Eine Frage zerlegt in beantwortbare Teilfragen."""
sub_questions: List[str] = Field(
description="Unabhängige Teilfragen, die zusammen die Hauptfrage beantworten"
)
structured_model = model.with_structured_output(DecomposedQuestions)
system_prompt = """Du zerlegst komplexe Fragen in einfachere Teilfragen.
Jede Teilfrage sollte unabhängig beantwortbar sein. Die Antworten auf alle
Teilfragen zusammen sollten die ursprüngliche Frage vollständig beantworten."""
complex_query = """Wie hat sich die Entwicklung von Transformer-Architekturen
auf NLP-Benchmarks ausgewirkt und welche Rolle spielten dabei Attention-Mechanismen?"""
decomposed = structured_model.invoke([
SystemMessage(content=system_prompt),
HumanMessage(content=complex_query)
])
print("Komplexe Frage:", complex_query)
print("\nTeilfragen:")
for i, q in enumerate(decomposed.sub_questions, 1):
print(f"{i}. {q}")Step-Back Prompting nimmt einen Schritt zurück und stellt zunächst eine allgemeinere, konzeptionelle Frage. Bevor man “Wie optimiere ich die Latenz bei Vektor-Similarity-Suchen in Produktionsumgebungen?” beantwortet, fragt das System: “Was sind die fundamentalen Prinzipien effizienter Similarity-Suche?” Die Antwort auf die allgemeine Frage liefert Kontext für die spezifische Frage. Das Modell wird mit Grundlagenwissen geerdet, bevor es ins Detail geht.
HyDE (Hypothetical Document Embeddings) ist radikaler. Statt die Frage direkt zu embedden, lässt man das LLM eine hypothetische Antwort generieren – ein Dokument, das die Frage beantworten würde. Dieses hypothetische Dokument wird eingebettet und für die Suche verwendet. Die Intuition: Dokument-zu-Dokument-Ähnlichkeit könnte relevanter sein als Query-zu-Dokument-Ähnlichkeit, weil Antworten strukturell ähnlicher zu anderen Antworten sind als Fragen zu Antworten.
Nicht alle Daten leben in Vector Stores. Relationale Datenbanken sprechen SQL. Graph-Datenbanken verstehen Cypher. Vector Stores mit komplexen Metadaten-Filtern brauchen strukturierte Filter-Queries. Query Construction übersetzt natürliche Sprache in diese spezialisierten Query-Sprachen.
Text-to-SQL ist der Klassiker. Ein Nutzer fragt: “Zeige mir alle Kunden aus Deutschland mit einem Bestellwert über 10.000 Euro im letzten Quartal.” Das LLM generiert:
SELECT * FROM customers
WHERE country = 'Germany'
AND total_orders > 10000
AND order_date >= DATE_SUB(NOW(), INTERVAL 3 MONTH);Diese SQL-Query wird gegen die Datenbank ausgeführt. Die Resultate werden in Document-Objekte verpackt und zurückgegeben. Der Nutzer merkt nichts von der SQL-Ebene.
Self-Query ist eine Variante für Vector Stores mit
Metadaten. Ein Nutzer fragt: “Finde Dokumente über Machine Learning aus
dem Jahr 2024.” Das LLM zerlegt dies in zwei Komponenten: eine
semantische Suche nach “Machine Learning” und einen Metadaten-Filter
{"year": 2024}. Beide werden kombiniert angewendet.
from langchain.retrievers.self_query.base import SelfQueryRetriever
from langchain.chains.query_constructor.base import AttributeInfo
from langchain_openai import ChatOpenAI
# Metadaten-Schema definieren
metadata_field_info = [
AttributeInfo(
name="year",
description="Das Jahr der Veröffentlichung",
type="integer"
),
AttributeInfo(
name="category",
description="Die Kategorie des Dokuments",
type="string"
),
AttributeInfo(
name="author",
description="Der Autor des Dokuments",
type="string"
)
]
document_content_description = "Technische Artikel über KI und ML"
# Self-Query Retriever erstellen
llm = ChatOpenAI(model="gpt-4o", temperature=0)
retriever = SelfQueryRetriever.from_llm(
llm=llm,
vectorstore=vector_store,
document_contents=document_content_description,
metadata_field_info=metadata_field_info
)
# Natürliche Sprache mit impliziten Metadaten-Filtern
query = "Artikel über Transformer-Architekturen von 2024"
docs = retriever.invoke(query)Das LLM erkennt automatisch, dass “von 2024” ein zeitlicher Filter ist, und konstruiert die entsprechende Metadaten-Query. Das ist mächtiger als manuelles Filter-Bauen – es erlaubt natürlichsprachliche Interfaces für strukturierte Filter-Logik.
Das Retriever-Interface ist universell, aber die darunterliegenden Systeme sind divers. Jedes hat eigene Stärken, eigene Anwendungsfälle, eigene Trade-offs.
Vector Stores haben wir ausführlich behandelt. Als Retriever sind sie
die Default-Wahl für unstrukturierte Textdaten. Sie verstehen Bedeutung,
nicht nur Keywords. Sie erlauben Metadaten-Filterung. Sie skalieren auf
Millionen von Dokumenten. Die as_retriever()-Methode ist
der Standard-Einstiegspunkt.
Semantische Suche ist mächtig, aber nicht allwissend. Manchmal sind exakte Keywords wichtig. Produktnamen. Codes. Akronyme. Ein Dokument über “HTTP/2” matcht semantisch vielleicht mit “Webprotokollen”, aber ein Nutzer, der explizit “HTTP/2” sucht, will exakt dieses Dokument.
Lexikalische Suchsysteme basieren auf Wort-Frequenzen. BM25 und TF-IDF sind die bekanntesten Algorithmen. Sie bauen einen invertierten Index auf – eine Datenstruktur, die für jedes Wort eine Liste von Dokumenten führt, in denen es vorkommt. Die Suche wird zur Schnittmengenbildung: Welche Dokumente enthalten die Query-Wörter? Und wie prominent?
from langchain_community.retrievers import BM25Retriever
from langchain_core.documents import Document
# Dokumente vorbereiten
docs = [
Document(page_content="HTTP/2 ist ein Netzwerkprotokoll für das Web."),
Document(page_content="RESTful APIs nutzen HTTP-Methoden für CRUD-Operationen."),
Document(page_content="WebSockets ermöglichen bidirektionale Kommunikation."),
Document(page_content="HTTP/2 unterstützt Multiplexing und Header-Kompression.")
]
# BM25-Retriever initialisieren
bm25_retriever = BM25Retriever.from_documents(docs)
bm25_retriever.k = 2 # Top-2 Dokumente zurückgeben
# Lexikalische Suche
results = bm25_retriever.invoke("HTTP/2")
for doc in results:
print(doc.page_content)BM25 findet beide HTTP/2-Dokumente mit hoher Präzision – die exakte Keyword-Übereinstimmung schlägt durch. Ein semantischer Search hätte möglicherweise auch das WebSockets-Dokument als relevant eingestuft.
Warum wählen, wenn man kombinieren kann? Ensemble-Retriever verschmelzen mehrere Retrieval-Strategien. Ein typisches Setup: BM25 für exakte Keywords plus Vector Store für semantische Ähnlichkeit. Die Ergebnisse werden gewichtet kombiniert.
from langchain.retrievers import EnsembleRetriever
from langchain_core.vectorstores import InMemoryVectorStore
from langchain_openai import OpenAIEmbeddings
from langchain_community.retrievers import BM25Retriever
# Zwei Retriever vorbereiten
vector_store = InMemoryVectorStore(embedding=OpenAIEmbeddings())
vector_retriever = vector_store.as_retriever(search_kwargs={"k": 4})
bm25_retriever = BM25Retriever.from_documents(docs)
bm25_retriever.k = 4
# Ensemble mit gewichteter Kombination
ensemble_retriever = EnsembleRetriever(
retrievers=[bm25_retriever, vector_retriever],
weights=[0.5, 0.5] # Gleiche Gewichtung
)
# Hybrid-Suche
results = ensemble_retriever.invoke("Netzwerkprotokolle für moderne Webanwendungen")Die Gewichte steuern den Einfluss. [0.3, 0.7] würde
semantische Suche bevorzugen. [0.7, 0.3] würde lexikalische
Präzision höher werten. Die optimale Gewichtung ist domänenspezifisch
und sollte auf einem Validierungsset ermittelt werden.
Doch wie kombiniert man Scores verschiedener Systeme? BM25 gibt einen Score von 0 bis ∞. Kosinus-Ähnlichkeit gibt Werte von 0 bis 1. Direktes Addieren funktioniert nicht. Hier kommt Reciprocal Rank Fusion (RRF) ins Spiel – ein Algorithmus, der Rankings statt Scores kombiniert. Jeder Retriever liefert eine geordnete Liste. RRF berechnet für jedes Dokument einen Score basierend auf seiner Position in den Listen. Ein Dokument, das in beiden Listen weit oben steht, gewinnt.
Retrieval ist selten eine einfache Ein-Schritt-Operation. In produktiven Systemen kommen raffinierte Patterns zum Einsatz, die Kontext bewahren oder Relevanz nachträglich optimieren.
Erinnern wir uns an das Chunking-Kapitel: Große Dokumente werden in kleine Chunks zerlegt. Jeder Chunk wird einzeln eingebettet und indexiert. Das erhöht die Granularität – wir finden den relevanten Abschnitt, nicht nur das gesamte Dokument.
Doch es gibt einen Trade-off. Ein kleiner Chunk isoliert betrachtet verliert Kontext. Ein Absatz aus der Mitte eines technischen Berichts ergibt möglicherweise wenig Sinn ohne die Einleitung. Ein Code-Snippet braucht die umgebende Funktionsdefinition. Wir wollen bei der Suche die Präzision kleiner Chunks, aber bei der Nutzung den Kontext großer Dokumente.
ParentDocumentRetriever löst dieses Dilemma. Bei der Indexierung werden Dokumente in kleine Chunks zerlegt und diese werden gesucht. Doch zurückgegeben wird nicht der Chunk, sondern das gesamte Parent-Dokument.
from langchain.retrievers import ParentDocumentRetriever
from langchain.storage import InMemoryStore
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_core.vectorstores import InMemoryVectorStore
from langchain_openai import OpenAIEmbeddings
# Document Store für Parent-Dokumente
docstore = InMemoryStore()
# Splitter für kleine Chunks (Indexierung)
child_splitter = RecursiveCharacterTextSplitter(chunk_size=200)
# Vector Store für Chunks
vectorstore = InMemoryVectorStore(embedding=OpenAIEmbeddings())
# ParentDocumentRetriever konfigurieren
retriever = ParentDocumentRetriever(
vectorstore=vectorstore,
docstore=docstore,
child_splitter=child_splitter,
)
# Dokumente hinzufügen - werden automatisch gesplittet
# Chunks werden indexiert, Parents im Docstore gespeichert
retriever.add_documents(documents)
# Suche findet Chunks, gibt aber Parent-Dokumente zurück
results = retriever.invoke("Wie funktioniert Attention?")
# Resultat: Vollständige Parent-Dokumente, nicht isolierte ChunksDer ParentDocumentRetriever hält eine zweistufige Architektur: Vector Store für die schnelle Chunk-Suche, Document Store für die vollständigen Parent-Dokumente. Bei einer Query werden zuerst die relevantesten Chunks gefunden. Dann werden die zugehörigen Parent-Dokumente aus dem Store geladen und zurückgegeben.
MultiVectorRetriever geht noch einen Schritt weiter. Er erlaubt beliebige Transformationen bei der Indexierung. Man könnte beispielsweise ein LLM nutzen, um Summaries von Dokumenten zu generieren und diese Summaries zu indexieren. Bei der Suche werden die Summaries gefunden, aber die Original-Dokumente zurückgegeben. Oder man extrahiert strukturierte Informationen – Tabellen als JSON – und indexiert diese, gibt aber das Original zurück.
Wir haben alle Komponenten beisammen. Vector Stores speichern semantische Fingerabdrücke. Embeddings transformieren Text in Vektoren. Chunking zerlegt Dokumente in durchsuchbare Einheiten. Retriever extrahieren relevante Information. Was fehlt? Die Verbindung zum Language Model.
Retrieval-Augmented Generation.
RAG ist die Technik, die externe Wissensquellen mit der Generierungsfähigkeit großer Language Models verbindet. Die Grundidee ist elegant: Bevor das Modell eine Frage beantwortet, suchen wir relevante Informationen aus einer Wissensbasis. Diese Informationen werden in den Prompt integriert. Das Modell generiert eine Antwort basierend auf diesem Kontext.
Warum ist das wichtig? Language Models sind auf fixen Trainingsdaten trainiert. Ihr Wissen ist statisch, eingefroren zum Zeitpunkt des letzten Trainings. Sie können nicht über Ereignisse nach ihrem Cutoff sprechen. Sie haben kein domänenspezifisches Wissen aus firmeninternen Dokumenten. Sie halluzinieren gelegentlich – erfinden plausibel klingende, aber falsche Fakten.
RAG adressiert all diese Probleme. Es gibt dem Modell Zugriff auf aktuelle Informationen. Es integriert domänenspezifisches Wissen ohne teures Fine-Tuning. Es reduziert Halluzinationen durch Grounding in abgerufenen Fakten.
Eine typische RAG-Pipeline durchläuft vier Schritte. Erstens: Eine Nutzeranfrage kommt herein. Zweitens: Das Retrieval-System sucht relevante Informationen basierend auf der Anfrage. Drittens: Die gefundenen Informationen werden in den Prompt des Language Models integriert. Viertens: Das Modell generiert eine Antwort unter Nutzung des Kontexts.
from langchain_openai import ChatOpenAI
from langchain_core.messages import SystemMessage, HumanMessage
from langchain_core.vectorstores import InMemoryVectorStore
from langchain_openai import OpenAIEmbeddings
# 1. Retriever vorbereiten (aus vorherigen Schritten)
vectorstore = InMemoryVectorStore(embedding=OpenAIEmbeddings())
# ... Dokumente wurden bereits hinzugefügt ...
retriever = vectorstore.as_retriever(search_kwargs={"k": 3})
# 2. System-Prompt definieren
system_prompt = """Du bist ein hilfreicher Assistent für technische Fragen.
Nutze die folgenden abgerufenen Informationen, um die Frage zu beantworten.
Wenn du die Antwort nicht weißt, sage das ehrlich. Erfinde keine Informationen.
Halte die Antwort präzise und auf maximal drei Sätze begrenzt.
Abgerufener Kontext:
{context}"""
# 3. Nutzerfrage
question = "Was sind die Hauptkomponenten eines LLM-basierten autonomen Agenten?"
# 4. Retrieval: Relevante Dokumente finden
docs = retriever.invoke(question)
# 5. Kontext aus Dokumenten zusammensetzen
context = "\n\n".join(doc.page_content for doc in docs)
# 6. Prompt mit Kontext befüllen
populated_system_prompt = system_prompt.format(context=context)
# 7. Language Model initialisieren
model = ChatOpenAI(model="gpt-4o", temperature=0)
# 8. Generierung mit Kontext
response = model.invoke([
SystemMessage(content=populated_system_prompt),
HumanMessage(content=question)
])
print("Frage:", question)
print("\nAntwort:", response.content)
print("\nQuellen:")
for i, doc in enumerate(docs, 1):
source = doc.metadata.get('source', 'unbekannt')
print(f"{i}. {source}")Diese Pipeline ist funktional, aber grundlegend. In produktiven Systemen kommen Verfeinerungen hinzu. Query Analysis optimiert die Retrieval-Query vor der Suche. Re-Ranking ordnet die gefundenen Dokumente nach Relevanz neu an. Zitationen werden automatisch extrahiert, um Quellenangaben zu ermöglichen. Konversationshistorie wird berücksichtigt, um Follow-up-Fragen im Kontext zu verstehen.
RAG ist keine Universallösung, aber für viele Szenarien überlegen gegenüber Alternativen. Vergleichen wir die Optionen.
Fine-Tuning passt ein Modell an eine spezifische Domäne an, indem es auf domänenspezifischen Daten weitertrainiert wird. Das ist teuer – es braucht Rechenressourcen, Zeit und Expertise. Es ist statisch – neues Wissen erfordert erneutes Training. Es ist risikobehaftet – das Modell könnte seine allgemeinen Fähigkeiten verlieren (Catastrophic Forgetting). Fine-Tuning eignet sich für Stil-Anpassungen oder sehr spezifische Aufgaben, weniger für Faktenwissen.
Continued Pre-Training trainiert ein Modell auf zusätzlichen Daten weiter. Es ist noch kostspieliger als Fine-Tuning und ebenfalls statisch. Es eignet sich für grundlegende Domänen-Anpassungen bei Foundation Models, nicht für häufig ändernde Informationen.
RAG hingegen ist dynamisch. Die Wissensbasis kann jederzeit aktualisiert werden – neue Dokumente hinzufügen, alte entfernen, Informationen ändern. Es ist kosteneffizient – keine GPU-Tage für Training. Es ist transparent – die Quellen sind nachvollziehbar. Es ist flexibel – verschiedene Wissensdatenbanken für verschiedene Anfragen.
Die Grenzen von RAG? Es ist nur so gut wie das Retrieval-System. Schlechte Chunk-Größen, unzureichende Embeddings oder fehlerhafte Metadaten führen zu schlechten Ergebnissen. Die Latenz ist höher als bei reiner Generierung – jede Anfrage triggert eine Suche. Die Kontextfenster-Größe begrenzt, wie viele Dokumente inkludiert werden können.
Bringen wir alles zusammen. Ein RAG-System für technische Dokumentation, das Query-Analyse, Hybrid-Retrieval und strukturierte Zitation kombiniert.
from langchain_openai import ChatOpenAI, OpenAIEmbeddings
from langchain_core.vectorstores import InMemoryVectorStore
from langchain_community.retrievers import BM25Retriever
from langchain.retrievers import EnsembleRetriever
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_core.documents import Document
from langchain_core.messages import SystemMessage, HumanMessage
from typing import List
# 1. Dokumente vorbereiten und splitten
documents = [
Document(
page_content="""
Transformer-Architekturen nutzen Self-Attention-Mechanismen, um
Abhängigkeiten zwischen Tokens zu modellieren. Im Gegensatz zu
rekurrenten Netzen können Transformer alle Positionen parallel
verarbeiten, was das Training beschleunigt.
""",
metadata={"source": "transformer-guide.pdf", "category": "architecture"}
),
Document(
page_content="""
BERT verwendet bidirektionale Attention, während GPT autoregressive
Attention nutzt. Diese Unterschiede führen zu verschiedenen
Anwendungsfällen: BERT für Verständnis-Aufgaben, GPT für Generation.
""",
metadata={"source": "bert-vs-gpt.pdf", "category": "models"}
),
Document(
page_content="""
Embedding-Modelle transformieren Text in hochdimensionale Vektoren.
Die Wahl der Dimensionalität beeinflusst sowohl die Qualität als auch
die Performance. Typische Dimensionen sind 384, 768 oder 1536.
""",
metadata={"source": "embeddings-intro.pdf", "category": "embeddings"}
)
]
splitter = RecursiveCharacterTextSplitter(chunk_size=200, chunk_overlap=30)
chunks = splitter.split_documents(documents)
# 2. Hybrid-Retriever aufsetzen
# Vector Store für semantische Suche
vectorstore = InMemoryVectorStore(embedding=OpenAIEmbeddings())
vectorstore.add_documents(chunks)
vector_retriever = vectorstore.as_retriever(search_kwargs={"k": 3})
# BM25 für lexikalische Suche
bm25_retriever = BM25Retriever.from_documents(chunks)
bm25_retriever.k = 3
# Ensemble kombiniert beide
ensemble_retriever = EnsembleRetriever(
retrievers=[bm25_retriever, vector_retriever],
weights=[0.4, 0.6] # Leichte Präferenz für semantische Suche
)
# 3. RAG-Chain aufbauen
def rag_pipeline(question: str) -> dict:
"""
Vollständige RAG-Pipeline mit Retrieval und Generierung.
Returns:
Dict mit Antwort, Quellen und verwendetem Kontext
"""
# Retrieval
docs = ensemble_retriever.invoke(question)
# Kontext aufbauen mit Quellenangaben
context_parts = []
for i, doc in enumerate(docs, 1):
source = doc.metadata.get('source', 'unbekannt')
context_parts.append(f"[Quelle {i}: {source}]\n{doc.page_content}")
context = "\n\n".join(context_parts)
# System-Prompt mit Kontext
system_prompt = f"""Du bist ein technischer Assistent. Beantworte die Frage
basierend auf dem bereitgestellten Kontext. Gib bei deiner Antwort an, auf
welche Quellen du dich beziehst (z.B. "Laut Quelle 1..."). Wenn die Antwort
nicht im Kontext steht, sage das ehrlich.
Kontext:
{context}"""
# Generierung
model = ChatOpenAI(model="gpt-4o", temperature=0)
response = model.invoke([
SystemMessage(content=system_prompt),
HumanMessage(content=question)
])
return {
"answer": response.content,
"sources": [doc.metadata for doc in docs],
"context": context
}
# 4. Pipeline nutzen
question = "Welche Rolle spielt Attention in Transformer-Modellen?"
result = rag_pipeline(question)
print("=" * 60)
print("FRAGE:", question)
print("=" * 60)
print("\nANTWORT:")
print(result["answer"])
print("\n" + "=" * 60)
print("VERWENDETE QUELLEN:")
for i, source_meta in enumerate(result["sources"], 1):
print(f"{i}. {source_meta['source']} (Kategorie: {source_meta['category']})")
print("=" * 60)Diese Pipeline demonstriert die Kernelemente produktionsnaher RAG-Systeme. Hybrid-Retrieval kombiniert semantische und lexikalische Suche. Strukturierte Metadaten ermöglichen Quellenangaben. Der Prompt instruiert das Modell explizit, Quellen zu zitieren. Das Resultat ist eine Antwort, die nicht nur korrekt, sondern auch nachvollziehbar ist.
Lassen wir die Reise Revue passieren. Wir begannen bei Vector Stores – spezialisierten Speichern für semantische Fingerabdrücke. Wir verstanden, dass sie nicht die Daten selbst speichern, sondern deren Vektorrepräsentationen. Metadaten verknüpfen diese Abstraktionen mit konkreten Informationen.
Wir tauchten ein in Embeddings und Chunking. Wir lernten, dass Embedding-Modelle Text in hochdimensionale Räume projizieren, wo Ähnlichkeit zu geometrischer Nähe wird. Wir erkannten, dass die Wahl des Modells, der Dimensionalität und der Splitting-Strategie entscheidend für die Qualität ist.
Wir erweiterten den Blick auf Retrieval. Wir sahen, dass Query-Analyse Nutzerfragen in optimierte Such-Queries transformiert. Wir verstanden, dass verschiedene Retrieval-Systeme – vector stores, lexikalische Suche, Datenbanken – hinter einem einheitlichen Interface koexistieren. Wir lernten fortgeschrittene Patterns kennen, die Kontext bewahren und Relevanz maximieren.
Und schließlich fügten wir alles zu RAG zusammen. Retrieval liefert Kontext. Generation erzeugt Antworten. Die Kombination ergibt Systeme, die aktuell, präzise und nachvollziehbar sind – Systeme, die nicht nur wiedergeben, was sie im Training gesehen haben, sondern auf spezifische, dynamische Wissensdatenbanken zugreifen können.
Das ist keine Magie. Es ist Architektur.
Jede Komponente hat ihre Rolle. Vector Stores organisieren den semantischen Raum. Embeddings kodieren Bedeutung. Chunking strukturiert Information. Retriever extrahieren Relevanz. Language Models synthetisieren Antworten. Gemeinsam bilden sie ein System, das größer ist als die Summe seiner Teile.
Was kommt als nächstes? Die Optimierung. In kommenden Kapiteln werden wir uns mit Evaluation beschäftigen – wie misst man die Qualität von Retrieval? Wie testet man RAG-Systeme systematisch? Wir werden tiefer in spezifische Retrieval-Techniken eintauchen – Re-Ranking-Algorithmen, Kontextfenster-Management, Multi-Hop-Reasoning. Wir werden produktionsnahe Aspekte behandeln – Skalierung, Caching, Monitoring.
Doch das Fundament steht. Wir verstehen jetzt, wie unstrukturierte Daten durchsuchbar werden. Wie Fragen zu Antworten werden. Wie externe Wissensquellen Language Models informieren. Wir haben die Werkzeuge, um intelligente Retrieval-Systeme zu bauen – Systeme, die nicht nur schnell sind, sondern auch verstehen, was Nutzer wirklich suchen.