Fünf RAG-Architekturen und die Situationen, in denen jede von ihnen in der Produktion geeignet ist.
Eine praktische Übersicht über naive, hybride, grafbasierte, fortgeschrittene sowie agierende RAG-Lösungen mit LlamaIndex und LangGraph, einschließlich Fehlermöglichkeiten, Kosten und Möglichkeiten ihrer Kombination.
Quellen: The 5 RAG Architectures and Exactly When to Use Each One in Production
Der Begriff „RAG“ umfasst heute eine Familie ganz unterschiedlicher Architekturen, von denen jede für eine andere Kategorie von Suchproblemen entwickelt wurde. Die falsche Wahl ist nicht nur eine Frage der Leistung – sie kann den Unterschied zwischen einem funktionierenden System und einem ausmachen, das den Nutzern in großem Maßstab zuverlässig falsche Antworten liefert.
Das ist eine Übersicht der gesamten Familie. Das Ziel ist es, jedes Suchproblem betrachten zu können und festzustellen, welche Architektur dafür erforderlich ist sowie wie man sie mit LangGraph für die Orchestrierung und LlamaIndex für die Datenschicht erstellt. Die fünf Architekturen reichen vom Einfachsten bis zum Komplexesten: naiv, hybrid, graphbasiert, fortgeschritten und agierend. Sie sind keine Konkurrenten – es handelt sich um Schichten, die schrittweise hinzugefügt werden, je nachdem, was das Problem erfordert, wobei die meisten Produktionsysteme mindestens zwei davon kombinieren.
Das gemeinsame Problem
Im Grunde befasst sich jede RAG-Lösung mit einem einzigen Problem: Ein Sprachmodell benötigt Fakten, die es während des Trainings nie gesehen hat, und zwar genau dann, wenn es antwortet, sowie in einer Form, die es ihm ermöglicht, über diese Fakten zu reasoningen.
Modelle hören beim Trainingsende auf zu lernen. Sie haben niemals die interne Wiki eines Unternehmens, eine Spezifikationsdatei oder irgendetwas, das nach Abschluss des Trainings veröffentlicht wurde, gelesen. Die Feinabstimmung an solchem Inhalt ist langsam und teuer, und selbst das angepasste Modell wird veraltet, sobald sich die Dokumente ändern.
RAG umgeht all das. Anstatt dem Modell neue Fakten beizubringen, holt das System zum Zeitpunkt der Abfrage relevante Kenntnisse ab und fügt sie als Kontext in die Anfrage ein. Das Modell muss nichts auswendig lernen, sondern muss die Informationen nur dann lesen, wenn es darauf ankommt. Die fünf unten aufgeführten Architekturen sind fünf Lösungsansätze dafür, wie man effektiv Informationen abrufen kann, wobei jede für ein anderes Problem geeignet ist.
Architektur 1: naive RAG
Naive RAG ist der Ausgangspunkt, und viele reale Arbeitslasten benötigen nie mehr davon: interne Richtlinien-Bots, FAQ-Assistenten, Dokumentensuche, Onboarding-Hilfen. Trotz des Namens handelt es sich um ein ausgereiftes Muster, das in großem Umfang eingesetzt werden kann. Man lädt Dokumente hoch, erstellt einen LlamaIndex VectorStoreIndex, stellt seinen Abfragemotor als @tool zur Verfügung und gibt dieses Tool an einen LangGraph-Agenten weiter.
Die fünf sequenziellen Schritte entsprechen direkt dem, was LlamaIndex standardmäßig anbietet. Die ersten vier finden beim Erstellen des Index statt, der fünfte bei jeder Frage.
Stages 1–4: Indexing time (run once)
Stage 5: Query time (run on every user question)
Der wichtigste Unterschied in allen RAG-Systemen besteht zwischen der Indizierungszeit und der Abfragedauer. Die Kosten für die Embedding-Verarbeitung werden einmalig im Voraus entrichtet. Bei der Abfrage zahlt das System nur für eine einzige Ähnlichkeitssuche sowie einen Aufruf des LLMs.
Warum es funktioniert
Die Frage eines Benutzers wird in denselben Vektorraum wie die Dokumentteile eingebettet, und die Teile, deren Vektoren geometrisch am nächsten am Fragevektor liegen, werden zurückgegeben. Die zugrunde liegende Annahme ist, dass Nähe im Vektorraum Nähe in der Bedeutung widerspiegelt. Für saubere, faktenbasierte Korpora hält sich diese Annahme überraschend gut.
Der Code
Im Folgenden finden Sie ein vollständiges Template, das Importe und Konfiguration, den Zustand, eine persistierte Wissensbasis, das Tool, Knoten, Routing sowie die Graphenbildung umfasst:
# ============================================================
# NAIVE RAG — COMPLETE TEMPLATE
# ============================================================
# ── MODULE 1: IMPORTS & CONFIGURATION ───────────────────────
import os
from typing import Literal
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage, SystemMessage
from langchain_core.tools import tool
from langgraph.graph import StateGraph, MessagesState, START, END
from langgraph.prebuilt import ToolNode
from langgraph.checkpoint.memory import MemorySaver
from llama_index.core import (
Settings, SimpleDirectoryReader, VectorStoreIndex,
StorageContext, load_index_from_storage
)
from llama_index.llms.openai import OpenAI as LlamaOpenAI
from llama_index.embeddings.openai import OpenAIEmbedding
# LangGraph's reasoning model
llm = ChatOpenAI(model="gpt-4o", temperature=0)
# LlamaIndex's internal model - separate from the above
# These two model configs do NOT share state or conversation history
Settings.llm = LlamaOpenAI(model="gpt-4o-mini", temperature=0.1)
Settings.embed_model = OpenAIEmbedding(model="text-embedding-3-small")
Settings.chunk_size = 512
Settings.chunk_overlap = 50
# ── MODULE 2: STATE ──────────────────────────────────────────
class State(MessagesState):
pass # messages list inherited; extend if you need more
# ── MODULE 3: KNOWLEDGE BASE (Build once, persist, reload) ───
PERSIST_DIR = "./storage/naive"
if os.path.exists(PERSIST_DIR):
storage_context = StorageContext.from_defaults(persist_dir=PERSIST_DIR)
index = load_index_from_storage(storage_context)
else:
documents = SimpleDirectoryReader("./data").load_data()
index = VectorStoreIndex.from_documents(documents, show_progress=True)
index.storage_context.persist(persist_dir=PERSIST_DIR)
query_engine = index.as_query_engine(similarity_top_k=3)
# ── MODULE 3 (cont.): TOOL ───────────────────────────────────
@tool
def search_knowledge_base(query: str) -> str:
"""Search internal company documents for policies, product specs,
and procedures. Use this for any question requiring domain-specific
knowledge rather than general world knowledge."""
response = query_engine.query(query)
return str(response)
tools = [search_knowledge_base]
llm_with_tools = llm.bind_tools(tools)
tool_node = ToolNode(tools)
# ── MODULE 4: NODES ──────────────────────────────────────────
def agent_node(state: State) -> dict:
system_prompt = SystemMessage(content=(
"You are a helpful assistant with access to an internal knowledge base. "
"Use search_knowledge_base for company-specific questions. "
"Answer general questions directly."
))
response = llm_with_tools.invoke([system_prompt] + state["messages"])
return {"messages": [response]}
# ── MODULE 5: ROUTING ────────────────────────────────────────
def should_continue(state: State) -> Literal["tools", "__end__"]:
last = state["messages"][-1]
if hasattr(last, "tool_calls") and last.tool_calls:
return "tools"
return "__end__"
# ── MODULE 6: GRAPH ASSEMBLY ─────────────────────────────────
builder = StateGraph(State)
builder.add_node("agent", agent_node)
builder.add_node("tools", tool_node)
builder.add_edge(START, "agent")
builder.add_conditional_edges("agent", should_continue,
{"tools": "tools", "__end__": END})
builder.add_edge("tools", "agent")
graph = builder.compile(checkpointer=MemorySaver())
Wo es versagt
Naives RAG basiert auf einer Annahme, und diese Annahme versagt in zwei gängigen Situationen.
Erstens kommt es zu einer Unstimmigkeit im Vokabular. Ein Benutzer fragt nach dem SLA, das für Kunden der Stufe 1 gilt, doch das Dokument verspricht Kunden der Gold-Stufe eine Verfügbarkeit von 99,9 %. SLA, Stufe 1 und Gold-Stufe bezeichnen zwar verwandte Konzepte, stimmen aber nicht überein; daher kann eine Ähnlichkeitsbewertung diesen Textabschnitt zu niedrig einstufen, wodurch die Antwort übersehen wird.
Zweitens treten relationale Fragen auf. Wenn gefragt wird, welche Produkte betroffen sind, falls ein bestimmter Lieferant ausfällt, muss eine Kette von Beziehungen in mehreren Dokumenten verfolgt werden. Kein einzelner Textabschnitt enthält die Antwort, und naives RAG liefert Teile aus verschiedenen Dokumenten ohne Möglichkeit, sie miteinander zu verknüpfen.
Genau diese beiden Versagensmuster werden von den nächsten beiden Architekturen adressiert.
Architektur 2: hybrides RAG
Hybrid RAG spiegelt wider, was Praktiker in der Praxis gelernt haben: semantische Ähnlichkeit und exakte Schlüsselwortabgleiche sind ergänzende Anzeichen für Relevanz, und keines von beiden allein reicht aus.
Dichte Suche, also Vektorabfrage, kommt mit Paraphrasen gut zurecht: Wenn man nach einem SLA fragt, kann sie den entsprechenden Abschnitt zur Verfügbarkeitsgarantie trotzdem finden. Bei Eigennamen, Produktnummern, klinischen Begriffen, rechtlichen Zitaten sowie anderen fachspezifischen Formulierungen, bei denen der genaue Textausdruck die Bedeutung trägt, versagt sie hingegen.
Sparse Suche (BM25 oder Schlüsselwortabfrage) ist das Gegenteil davon. Sie ist hervorragend darin, exakte Begriffe abzugleichen, und findet bei einer Suche nach „SKU-4829“ immer „SKU-4829“, hat aber kein Verständnis für semantische Äquivalenz, weshalb sie für „SLA“ nicht „Verfügbarkeitsgarantie“ finden wird.
Hybrid-RAG führt beide Suchvorgänge parallel durch und verwendet anschließend einen Reranker, um die zusammengeführten Ergebnisse in eine einzige gerankte Liste umzuwandeln. Der Reranker ist der entscheidende Bestandteil. Embedding-Modelle vergleichen eine Abfrage und einen Textabschnitt unabhängig voneinander, während ein cross-encoder Reranker die Abfrage sowie jeden Kandidaten gemeinsam liest und direkt eine Bewertung für ihre Beziehung erzeugt. Dieser Ansatz ist langsamer als die Ähnlichkeitssuche, berührt jedoch nur bereits reduzierte Kandidatensätze, sodass die Latenz in Grenzen bleibt.
Der Code
Die LlamaIndex-Version kombiniert einen dichten Retriever, einen BM25-Retriever sowie die rekursive Rangfusionsmethode:
# ============================================================
# HYBRID RAG — LLAMAINDEX IMPLEMENTATION
# pip install llama-index-retrievers-bm25 rank-bm25
# ============================================================
from llama_index.core import SimpleDirectoryReader, VectorStoreIndex
from llama_index.core.retrievers import VectorIndexRetriever
from llama_index.retrievers.bm25 import BM25Retriever
from llama_index.core.retrievers import QueryFusionRetriever
from llama_index.core.query_engine import RetrieverQueryEngine
from langchain_core.tools import tool
# Build document store and index as before
documents = SimpleDirectoryReader("./data").load_data()
index = VectorStoreIndex.from_documents(documents)
# ── DENSE RETRIEVER (vector similarity) ──────────────────────
vector_retriever = VectorIndexRetriever(
index=index,
similarity_top_k=5, # Fetch more candidates pre-rerank
)
# ── SPARSE RETRIEVER (BM25 keyword match) ────────────────────
# BM25Retriever builds directly from the index's nodes
bm25_retriever = BM25Retriever.from_defaults(
index=index,
similarity_top_k=5,
)
# ── FUSION: Merge both retrievers with Reciprocal Rank Fusion ─
# QueryFusionRetriever is LlamaIndex's built-in hybrid combiner
# mode="reciprocal_rerank" implements the RRF algorithm:
# it combines ranked lists without needing score calibration
hybrid_retriever = QueryFusionRetriever(
retrievers=[vector_retriever, bm25_retriever],
similarity_top_k=3, # Final top-k after fusion
num_queries=1, # No query expansion in this mode
mode="reciprocal_rerank", # The fusion algorithm
use_async=True, # Run both retrievers in parallel
)
# Build a QueryEngine on top of the hybrid retriever
hybrid_query_engine = RetrieverQueryEngine.from_args(
retriever=hybrid_retriever,
)
# ── LANGGRAPH TOOL ────────────────────────────────────────────
@tool
def search_hybrid(query: str) -> str:
"""Search internal knowledge base using both semantic similarity
and keyword matching. More precise than pure vector search,
especially for technical terms, product codes, and exact names."""
response = hybrid_query_engine.query(query)
return str(response)
# The LangGraph graph structure is identical to Naive RAG.
# You are only swapping the tool. Everything from Module 4 onward
# stays exactly the same.
Größe der Blöcke bei hybrider Suche
Bei der hybriden Suche werden in der Regel kleinere Blöcke indiziert als bei einer reinen Vektorsuche. BM25 erhält ein stärkeres Signal von einem wiederholten Begriff in einem kurzen Block als in einem langen. Etwa 256 Token mit 30 Token Überschneidung bilden eine sinnvolle Ersteingestellung, sobald BM25 in den Prozess integriert wird.
Wann ist die hybride Suche geeignet
Verwenden Sie sie, wenn Dokumente freie Prosa mit strukturierter Terminologie kombinieren – was nahezu jeden ernsthaften Unternehmensanwendungsfall abdeckt. Beispiele sind rechtliche Prüfungen, bei denen Zitierformen genau übereinstimmen müssen, medizinische Unterlagen mit präzisen Arzneimittelnamen, Dosierungscode sowie ICD-Code, finanzielle Analysen mit Börsenkürzeln und Klauselidentifikatoren sowie technische Dokumentation mit Fehlercodes, API-Methennamen und Versionszahlen.
Architektur 3: graphbasiertes RAG
Graph RAG definiert die Einheit des Wissens neu. Bei naiven und hybriden RAG-Systemen ist ein Dokument lediglich Text, und die Suche wählt die Abschnitte aus, die dem Abfragedtext am ähnlichsten sind. Hier werden Dokumente zu Entitäten, die durch Beziehungen miteinander verbunden sind, und die Suche folgt den Pfaden in diesem Netzwerk.
Betrachten Sie die Frage, welche Unternehmenskunden betroffen wären, wenn das veraltete Authentifizierungsmodul deaktiviert würde. Ein naives System würde nach Abschnitten suchen, die sowohl „Unternehmenskunden“ als auch „Authentifizierungsmodul“ zusammen erwähnen, und könnte einige finden. Tatsächlich ist jedoch eine Kette erforderlich: vom Modul zu den Produkten, die davon abhängen, von diesen Produkten zu den Kunden, die sie nutzen. Kein einzelner Abschnitt enthält diese Antwort, die sich aus der Struktur des Wissensgraphen ergibt. Graph RAG zielt genau auf diese Situation ab – egal ob es darum geht, über mehrere Verbindungen zu springen, eine Beziehung nachzuvollziehen oder eine Frage zu beantworten, die Fakten aus weit voneinander entfernten Teilen des Korpus erfordert.
Wie der Index erstellt wird
Anstatt Dokumente in Blöcke aufzuteilen und diese zu embedden, beginnt das graphbasierte RAG mit einem Schritt zur Extraktion von Entitäten aus jedem Dokument. Dabei werden benannte Entitäten (Produkte, Personen, Organisationen, Konzepte) sowie die Beziehungen zwischen ihnen identifiziert, wie zum Beispiel „hängt von ab“, „ist Kunde von“, „wurde von verfasst“ oder „wurde durch ersetzt“. Diese bilden die Knoten und Kanten eines Wissensgraphen. Anschließend gruppieren Algorithmen zur Graphkluft eng miteinander verbundene Entitäten zu Gemeinschaften, und ein LLM erstellt eine Zusammenfassung für jede Gemeinschaft. Bei einer Abfrage durchsucht das System die Zusammenfassungen der Gemeinschaften und durchquert den Graphen, um auf relevante Entitäten zuzugreifen.
Der Code
Der Property-Graph-Index von LlamaIndex kümmert sich um den Extraktionsprozess:
# ============================================================
# GRAPH RAG — LLAMAINDEX PROPERTY GRAPH INDEX
# pip install llama-index-core
# ============================================================
from llama_index.core import SimpleDirectoryReader, PropertyGraphIndex
from llama_index.core.indices.property_graph import (
ImplicitPathExtractor,
SimpleLLMPathExtractor,
)
from langchain_core.tools import tool
# Load documents
documents = SimpleDirectoryReader("./data").load_data()
# ── BUILD THE PROPERTY GRAPH INDEX ───────────────────────────
# LlamaIndex's PropertyGraphIndex handles the full extraction pipeline.
# SimpleLLMPathExtractor uses an LLM to extract (subject, relation, object)
# triples from each chunk - these become graph edges.
# ImplicitPathExtractor uses fast heuristics (cheaper, less precise).
index = PropertyGraphIndex.from_documents(
documents,
kg_extractors=[
# LLM-based extraction: higher quality, higher cost
SimpleLLMPathExtractor(
llm=Settings.llm,
max_paths_per_chunk=10,
),
# Heuristic extraction: fast fallback
ImplicitPathExtractor(),
],
show_progress=True,
)
# ── CREATE A GRAPH-AWARE RETRIEVER ────────────────────────────
# This retriever traverses the graph rather than searching flat vectors.
kg_retriever = index.as_retriever(
include_text=True, # Include surrounding text with each entity
retriever_mode="hybrid", # Combines keyword + embedding on the graph
similarity_top_k=3,
)
from llama_index.core.query_engine import RetrieverQueryEngine
graph_query_engine = RetrieverQueryEngine.from_args(retriever=kg_retriever)
# ── LANGGRAPH TOOL ────────────────────────────────────────────
@tool
def search_knowledge_graph(query: str) -> str:
"""Search the knowledge graph for questions involving relationships
between entities - dependencies, organizational hierarchies, impact
analysis, and multi-hop reasoning across connected information.
Use this when the answer requires tracing a chain of relationships
rather than finding a single relevant document."""
response = graph_query_engine.query(query)
return str(response)
# ── COMBINING WITH NAIVE RAG: DUAL-TOOL AGENT ─────────────────
# In practice, Graph RAG and Naive RAG are often combined.
# The agent's LLM decides which tool fits the query.
@tool
def search_knowledge_base(query: str) -> str:
"""Search for factual information in company documents.
Best for direct questions with answers in a single document."""
response = query_engine.query(query) # The naive query engine
return str(response)
# Give the LangGraph agent BOTH tools.
# It will route to the right one based on the question type.
tools = [search_knowledge_base, search_knowledge_graph]
llm_with_tools = llm.bind_tools(tools)
tool_node = ToolNode(tools)
# Graph assembly remains identical - only the tool list changes.
Warnung bezüglich der Kosten
Die Entitätenextraktion führt für jeden Teilabschnitt einen Aufruf eines LLM durch, wodurch bei großen Dokumentensätzen zur Indexierung Tausende von Aufrufen entstehen. Das ist aus Designgründen teuer und langsam: Der Preis deckt lediglich einmal eine viel umfangreichere Datenstruktur ab. Ein Graph-RAG-Index muss bei jedem Neustart nicht neu erstellt werden. Der Graph sollte persistent gespeichert und wie bei der einfachen Version neu geladen werden.
Wann man ihn wählen sollte
Wählen Sie Graph-RAG, wenn Fragen die Verfolgung von Beziehungsketten erfordern. Typische Anwendungsfälle sind Compliance- und Risikoanalyse (welche Prozesse von einer bestimmten Vorschrift betroffen sind), Lieferkettenanalyse (welche Produkte von einem Lieferanten abhängen), organisatorisches Wissen (wer was besitzt und wie diese Besitzverhältnisse miteinander verbunden sind) sowie die Kartenbildung von Software-Abhängigkeiten (was kaputtgeht, wenn ein Modul entfernt wird).
Architektur 4: fortgeschrittener RAG
Advanced RAG ist keine einzelne neue Technik. Es handelt sich um eine strukturierte Reihe von Verbesserungen, die auf der bereits vorhandenen Grundretrieval-Methode aufgebaut werden. Naive RAG verlässt sich auf die erste Recherche, während advanced RAG diese hinterfragt, verfeinert und validiert. Die Verbesserungen lassen sich in drei Gruppen einteilen, die an unterschiedlichen Stellen des Prozesses ansetzen: vor der Recherche, während der Abfrageverarbeitung und nach der Recherche.
Vor der Recherche: Abfragen umformulieren und HyDE
Was ein Benutzer eintippt, ist selten die beste Suchanfrage. Eine beiläufige Frage zur Rückgabepolitik für Unternehmenskunden ist für einen Menschen völlig normal, liefert aber schlechtere Ergebnisse als eine Formulierung wie „Rückgabepolitik und Rückerstattungsverfahren für Unternehmenskunden“. Durch das Umformulieren der Abfrage lässt das LLM die Frage in eine bessere Suchanfrage umwandeln, bevor sie den Index erreicht.
HyDE, abgekürzt für Hypothetical Document Embedding, verfolgt einen anderen Ansatz. Anstatt mit der Frage zu suchen, bittet es den LLM, ein hypothetisches Dokument zu schreiben, das diese Frage beantworten würde, und embeddet anschließend dieses Dokument für die Suche. Die Idee dahinter ist, dass textliche Inhalte in Form von Antworten im Vektorraum näher an anderen antwortenähnlichen Texten liegen als an fragenähnlichen Texten.
Abbildung der Abfrage
Mehrschrittige Fragen überfordern naive RAG-Methoden, da mehrere Abfragen erforderlich sind. Fortgeschrittene RAG-Methoden zerlegen sie zunächst in kleinere Teilaufgaben. Wenn beispielsweise die Rückerstattungspolitik für Unternehmens- und Einzelkunden verglichen sowie die wichtigsten Unterschiede zusammengefasst werden sollen, handelt es sich tatsächlich um drei Aufgaben: Die Politik für Unternehmen abrufen, die Politik für Einzelkunden abrufen und diese anschließend vergleichen. Durch die Aufteilung werden die Aufgaben in Unterabfragen umgewandelt, jeweils abgerufen und die Ergebnisse vor der Synthese zusammengeführt.
Nach der Abfrage: Neubewertung und CRAG
Reranking, das bereits im Rahmen der hybriden Suche behandelt wurde, passt in dieser Phase erneut dazu: Ein Cross-Encoder bewertet erneut die heruntergeladenen Abschnitte, selbst wenn die Abfrage rein dicht war.
CRAG (Corrective RAG) ist die ausgefeilteste Technik nach der Informationsbeschaffung. Nach dem Herunterladen führt es eine schnelle Relevanzprüfung der Abschnitte durch. Falls diese unzureichend erscheinen, wendet es sich einer anderen Quelle zu, beispielsweise einer Websuche oder einem umfassenderen Index, damit das LLM nicht gezwungen wird, aus schwachem Kontext zu antworten.
Der Code
Eine Implementierung vereint mehrere dieser Techniken:
# ============================================================
# ADVANCED RAG — MULTI-TECHNIQUE IMPLEMENTATION
# ============================================================
from llama_index.core import SimpleDirectoryReader, VectorStoreIndex
from llama_index.core.query_engine import (
SubQuestionQueryEngine, # Handles query decomposition
RetrieverQueryEngine,
)
from llama_index.core.tools import QueryEngineTool, ToolMetadata
from llama_index.core.postprocessor import SentenceTransformerRerank
from langchain_core.tools import tool
documents = SimpleDirectoryReader("./data").load_data()
index = VectorStoreIndex.from_documents(documents)
# ── RERANKER: Post-retrieval cross-encoder scoring ────────────
# Fetch 8 candidates, rerank down to 3
# This is the most impactful single improvement in Advanced RAG
reranker = SentenceTransformerRerank(
model="cross-encoder/ms-marco-MiniLM-L-2-v2",
top_n=3,
)
# ── RETRIEVER WITH RERANKING ──────────────────────────────────
base_retriever = index.as_retriever(similarity_top_k=8)
reranked_engine = RetrieverQueryEngine.from_args(
retriever=base_retriever,
node_postprocessors=[reranker], # Applied after retrieval
)
# ── QUERY DECOMPOSITION with SubQuestionQueryEngine ───────────
# Wrap the base engine as a "tool" that the decomposer can call
engine_tools = [
QueryEngineTool(
query_engine=reranked_engine,
metadata=ToolMetadata(
name="company_knowledge_base",
description=(
"Searches company documents for policies, procedures, "
"and product information."
),
),
)
]
# SubQuestionQueryEngine decomposes complex questions into
# sub-questions, runs each against the available tools,
# and synthesizes a final answer from all sub-answers
decomposed_engine = SubQuestionQueryEngine.from_defaults(
query_engine_tools=engine_tools,
use_async=True, # Sub-queries run in parallel when possible
)
# ── HYDE: Hypothetical Document Embedding ─────────────────────
from llama_index.core.indices.query.query_transform.base import (
HyDEQueryTransform,
)
from llama_index.core.query_engine import TransformQueryEngine
hyde_transform = HyDEQueryTransform(include_original=True)
hyde_engine = TransformQueryEngine(
query_engine=reranked_engine,
query_transform=hyde_transform,
)
# ── LANGGRAPH TOOLS: Different engines for different patterns ──
@tool
def search_with_decomposition(query: str) -> str:
"""Search for answers to complex questions that may require
combining information from multiple documents. Automatically
breaks the question into sub-questions and merges the results.
Best for comparison questions, multi-part questions, and anything
requiring synthesis across different topics."""
response = decomposed_engine.query(query)
return str(response)
@tool
def search_with_hyde(query: str) -> str:
"""Search the knowledge base using hypothetical document embedding.
More effective than standard search for abstract or exploratory
questions where the exact terminology in the answer differs from
the terminology in the question."""
response = hyde_engine.query(query)
return str(response)
# The LangGraph agent now has specialized retrieval tools
# and its LLM decides which retrieval strategy each question needs.
tools = [search_with_decomposition, search_with_hyde]
Die zu befolgende Regel
Vermeiden Sie es, alles zusammenzufügen. Wählen Sie die Änderung, die am ehesten das tatsächlich festgestellte Problem behebt. Eine Neubewertung ist in der Regel der beste erste Schritt, danach folgt die Aufschlüsselung, und HyDE ist eine geeignete dritte Option für abstrakte oder konzeptionelle Sammlungen. Arbeiten Sie schrittweise voran und überprüfen Sie nach jedem Schritt das Erinnerungsvermögen.
Architektur 5: agiler RAG
Agiler RAG geht über eine verbesserte Informationsabrufung hinaus: Er überträgt die Entscheidungen zur Informationsabrufung auf eine andere Instanz. Die ersten vier Arten sind Pipelines, feste Sequenzen, die bei jedem Ausführungsvorgang identisch funktionieren. Hier wird die Informationsabrufung zu einem Zyklus, in dem ein LLM-Agent entscheidet, wonach gesucht werden soll, die Ergebnisse bewertet, entscheidet, ob er erneut suchen soll, und aufhört, wenn genügend Informationen vorliegen, oder feststellt, dass die Frage keine Antwort hat.
Genau dafür wurde LangGraph entwickelt. Der Agentenknoten, der Tool-Knoten sowie die bedingten Kanten bilden bereits eine Grundstruktur für ein agierendes RAG-System. Was sich ändert, ist die Vielfalt des Tool-Setups sowie die Komplexität der darum aufgebauten Routierungslogik.
Mehrere Suchwerkzeuge gleichzeitig
Die wahre Stärke des agierenden RAG in LangGraph besteht darin, dass dem Agenten alle oben beschriebenen Suchstrategien gleichzeitig zur Verfügung gestellt werden können, und sein LLM wählt für jede Teilfrage das passende Tool aus. Diese Grundstruktur macht alle fünf Architekturen für einen einzigen Agenten verfügbar:
# ============================================================
# AGENTIC RAG — COMPLETE MULTI-TOOL TEMPLATE
# This is the full production scaffold: all five architectures
# available to a single agent, which selects dynamically.
# ============================================================
# ── MODULE 1: IMPORTS & CONFIGURATION ───────────────────────
import os
from typing import Literal, Annotated
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage, SystemMessage, BaseMessage
from langchain_core.tools import tool
from langgraph.graph import StateGraph, MessagesState, START, END
from langgraph.prebuilt import ToolNode
from langgraph.checkpoint.memory import MemorySaver
from llama_index.core import (
Settings, SimpleDirectoryReader, VectorStoreIndex,
StorageContext, load_index_from_storage, PropertyGraphIndex
)
from llama_index.core.indices.property_graph import SimpleLLMPathExtractor
from llama_index.core.postprocessor import SentenceTransformerRerank
from llama_index.core.query_engine import RetrieverQueryEngine, SubQuestionQueryEngine
from llama_index.core.tools import QueryEngineTool, ToolMetadata
from llama_index.llms.openai import OpenAI as LlamaOpenAI
from llama_index.embeddings.openai import OpenAIEmbedding
from llama_index.retrievers.bm25 import BM25Retriever
from llama_index.core.retrievers import QueryFusionRetriever, VectorIndexRetriever
# The agent's main reasoning model (LangGraph)
llm = ChatOpenAI(model="gpt-4o", temperature=0)
# LlamaIndex internal configuration
Settings.llm = LlamaOpenAI(model="gpt-4o-mini", temperature=0.1)
Settings.embed_model = OpenAIEmbedding(model="text-embedding-3-small")
Settings.chunk_size = 512
Settings.chunk_overlap = 50
# ── MODULE 2: STATE ──────────────────────────────────────────
class AgentState(MessagesState):
# Extend with retrieval metadata if you need observability
retrieval_count: int = 0 # Track how many retrieval calls were made
# ── MODULE 3: KNOWLEDGE BASES (all built at startup) ──────────
VECTOR_DIR = "./storage/vector"
GRAPH_DIR = "./storage/graph"
documents = SimpleDirectoryReader("./data").load_data()
# Vector index (for Naive, Hybrid, Advanced)
if os.path.exists(VECTOR_DIR):
ctx = StorageContext.from_defaults(persist_dir=VECTOR_DIR)
vector_index = load_index_from_storage(ctx)
else:
vector_index = VectorStoreIndex.from_documents(documents, show_progress=True)
vector_index.storage_context.persist(persist_dir=VECTOR_DIR)
# Property Graph index (for Graph RAG)
if os.path.exists(GRAPH_DIR):
ctx = StorageContext.from_defaults(persist_dir=GRAPH_DIR)
graph_index = load_index_from_storage(ctx)
else:
graph_index = PropertyGraphIndex.from_documents(
documents,
kg_extractors=[SimpleLLMPathExtractor(llm=Settings.llm)],
show_progress=True,
)
graph_index.storage_context.persist(persist_dir=GRAPH_DIR)
# ── Set up individual engines ─────────────────────────────────
reranker = SentenceTransformerRerank(
model="cross-encoder/ms-marco-MiniLM-L-2-v2", top_n=3
)
# Tool 1: Naive / vector search
vector_engine = vector_index.as_query_engine(
similarity_top_k=3,
node_postprocessors=[reranker],
)
# Tool 2: Hybrid (vector + BM25)
hybrid_retriever = QueryFusionRetriever(
retrievers=[
VectorIndexRetriever(index=vector_index, similarity_top_k=5),
BM25Retriever.from_defaults(index=vector_index, similarity_top_k=5),
],
similarity_top_k=3,
mode="reciprocal_rerank",
use_async=True,
)
hybrid_engine = RetrieverQueryEngine.from_args(
retriever=hybrid_retriever,
node_postprocessors=[reranker],
)
# Tool 3: Graph traversal
graph_engine = graph_index.as_query_engine(
include_text=True,
retriever_mode="hybrid",
similarity_top_k=3,
)
# Tool 4: Decomposed (for complex multi-part questions)
decomposed_engine = SubQuestionQueryEngine.from_defaults(
query_engine_tools=[
QueryEngineTool(
query_engine=vector_engine,
metadata=ToolMetadata(
name="docs",
description="Company documents and policies."
)
)
],
use_async=True,
)
# ── MODULE 3 (cont.): ALL TOOLS ───────────────────────────────
@tool
def search_documents(query: str) -> str:
"""Search company documents by semantic meaning. Best for
conceptual questions where the exact wording in the answer
may differ from the question."""
return str(vector_engine.query(query))
@tool
def search_exact_terms(query: str) -> str:
"""Search using both keyword and semantic matching. Best when
the query contains specific terminology, product codes, names,
or exact phrases that must appear in the result."""
return str(hybrid_engine.query(query))
@tool
def search_relationships(query: str) -> str:
"""Search the knowledge graph for questions about how things
connect: dependencies, impact chains, organizational links,
and multi-hop reasoning. Use when the answer requires tracing
a relationship across multiple entities."""
return str(graph_engine.query(query))
@tool
def search_complex_question(query: str) -> str:
"""For multi-part questions requiring synthesis across several
topics. Automatically decomposes the question into sub-queries,
retrieves each independently, and combines the results."""
return str(decomposed_engine.query(query))
tools = [
search_documents,
search_exact_terms,
search_relationships,
search_complex_question,
]
llm_with_tools = llm.bind_tools(tools)
tool_node = ToolNode(tools)
# ── MODULE 4: NODES ──────────────────────────────────────────
def agent_node(state: AgentState) -> dict:
system_prompt = SystemMessage(content=(
"You are a precise research assistant with access to four retrieval tools:\n\n"
"1. search_documents - semantic search over company documents\n"
"2. search_exact_terms - hybrid semantic + keyword search\n"
"3. search_relationships - graph traversal for relationship questions\n"
"4. search_complex_question - decomposed retrieval for multi-part questions\n\n"
"Think step by step. Use the tool that best fits the question type. "
"You may call multiple tools if a question has multiple parts. "
"Only answer when you have retrieved sufficient evidence."
))
response = llm_with_tools.invoke([system_prompt] + state["messages"])
return {
"messages": [response],
"retrieval_count": state.get("retrieval_count", 0),
}
# ── MODULE 5: ROUTING ────────────────────────────────────────
def should_continue(state: AgentState) -> Literal["tools", "__end__"]:
last = state["messages"][-1]
if hasattr(last, "tool_calls") and last.tool_calls:
return "tools"
return "__end__"
# ── MODULE 6: GRAPH ASSEMBLY ─────────────────────────────────
builder = StateGraph(AgentState)
builder.add_node("agent", agent_node)
builder.add_node("tools", tool_node)
builder.add_edge(START, "agent")
builder.add_conditional_edges("agent", should_continue,
{"tools": "tools", "__end__": END})
builder.add_edge("tools", "agent")
graph = builder.compile(checkpointer=MemorySaver())
# ── MODULE 7: ENTRYPOINT ──────────────────────────────────────
if __name__ == "__main__":
config = {"configurable": {"thread_id": "agentic-session-001"}}
print("Agentic RAG ready. Ask anything.\n")
while True:
user_text = input("You: ").strip()
if not user_text or user_text.lower() in ("exit", "quit"):
break
response = graph.invoke(
{"messages": [HumanMessage(content=user_text)]},
config=config,
)
print(f"\nAgent: {response['messages'][-1].content}\n")
Was Pipelines nicht können
Was dieses Konzept von anderen unterscheidet, ist die Selbstkorrektur. Ein Pipeline hat keine Möglichkeit, festzustellen, dass es falsches Material abgerufen hat; ein Agent hingegen schon. Wenn die ersten Ergebnisse unzureichend sind, überarbeitet der Agent in seinem nächsten Denkschritt die Abfrage mit neuen Begriffen. Wenn eine Frage eine Abhängigkeit verbirgt, erkennt der Agent dies bereits nach kurzer Zeit und holt zusätzliche Informationen. Wenn die Frage vage ist, kann er vor der Suche den Benutzer um Klarstellung bitten.
Wählen Sie diese Architektur, wenn falsche Antworten kostspielig sind – beispielsweise in Bereichen wie Compliance, Recht, Finanzen oder Medizin –, denn ÜberprüfungsSchritte, Zuverlässigkeitsschwellenwerte sowie Kontrollpunkte mit menschlicher Beteiligung können direkt in das Agentenmodell integriert werden.
Der Preis in Form von Latenz
Die Verlangsamung lässt sich nicht verbergen. Pipelines mit einem Tool benötigen in der Regel 200 bis 500 Millisekunden; ein Agent, der zunächst drei Abfragen durchführt, kann 8 bis 12 Sekunden brauchen, was für den Hauptweg vieler Echtzeit-Interfaces zu langsam ist. Zwei Ansätze können dies abmildern: Die Zwischenschritte werden als Stream angezeigt, sodass die Nutzer den Fortschritt verfolgen können, anstatt schweigend zu warten, und die agentebasierte Abfrage läuft im Hintergrund, um mögliche weitere Schritte im Voraus abzurufen.
Wahl und Kombination
Jedes Abfragedesign hat unter diesen fünf Ansätzen die richtige Lösung. Als grober Entscheidungsweg: Beginnen Sie mit dem naiven RAG für saubere, faktenbasierte Dokumenten-FAQs, wechseln Sie zu einem hybriden Ansatz, wenn spezielle Fachbegriffe oder genaue Identifikatoren wichtig sind; wenden Sie Graph-RAG an, wenn die Antworten von Beziehungen zwischen Dokumenten abhängen; integrieren Sie fortgeschrittene Techniken, wenn die Qualität der Abfragen verbessert werden muss; und nutzen Sie einen Agenten, wenn die Aufgabe offener, risikoreicher ist oder Selbstkorrektur erfordert.
Die Architekturen sind außerdem komponierbar, sodass es nicht notwendig ist, eine auszuwählen und den Rest wegzulassen. Das gängigste Produktionsmuster ist ein Stack. In LangGraph wird dieser Stack direkt in die Liste der Tools übersetzt. Ein Agent mit einem naiven Tool, einem hybriden Tool, einem Graphen-Tool und einem dekompilierten Abfragen-Tool stellt genau den fünf-Architekturen-Stack dar, wobei der Agent als oberste Schicht bei jedem Schritt aus den anderen Tools auswählt und sie koordiniert.
Referenzkarte zu Schlüsselbegriffen
Diese Karte listet die wichtigsten Klassen und Einstellungen für jede Schicht auf:
NAIVE RAG
SimpleDirectoryReader Load files into Document objects
VectorStoreIndex.from_documents Build the embed-and-store index
index.as_query_engine() Full retrieve-and-answer pipeline
index.as_retriever() Retrieve only (no answer generation)
Settings.chunk_size Token size per Node
Settings.chunk_overlap Token overlap between adjacent Nodes
HYBRID RAG
BM25Retriever Keyword-based sparse retriever
VectorIndexRetriever Dense embedding retriever
QueryFusionRetriever Merges multiple retrievers (RRF algorithm)
SentenceTransformerRerank Cross-encoder reranker for post-retrieval
GRAPH RAG
PropertyGraphIndex Builds a knowledge graph from documents
SimpleLLMPathExtractor LLM-based entity and relation extraction
ImplicitPathExtractor Heuristic-based entity extraction (fast)
ADVANCED RAG
SubQuestionQueryEngine Decomposes complex queries into sub-queries
HyDEQueryTransform Hypothetical Document Embedding transform
TransformQueryEngine Wraps any engine with a query transform
node_postprocessors Where rerankers and filters attach
AGENTIC RAG (LANGGRAPH LAYER)
@tool The bridge - every LlamaIndex engine becomes
a LangGraph tool through this decorator
ToolNode Executes whatever tool the agent selects
bind_tools() Gives the agent LLM its tool registry
MemorySaver / SqliteSaver Thread-level memory across turns (Part 2)
interrupt() Human approval checkpoint before retrieval (Part 3)
Auswertung als Infrastruktur
Es handelt sich bei diesen fünf Ansätzen nicht um fünf verschiedene Wege, um dasselbe zu erreichen. Es sind Lösungen für fünf unterschiedliche Auswertungsprobleme, die in einer klaren Abfolge von einfach bis komplex angeordnet sind.
- Naiver RAG: schnell, kostengünstig und ideal für die meisten einfachen Dokumenten-Frage-Antwort-Szenarien.
- Hybrider RAG: die sinnvolle Standardlösung, wenn das Vokabular spezialisiert ist.
- Graph-RAG: die Wahl, wenn Verbindungen zwischen Fakten wichtiger sind als einzelne Dokumente.
- Fortgeschrittener RAG: das Muster zur Steigerung der Genauigkeit, wenn das Schwachpunkt die Qualität der Auswertung ist.
- Agenter RAG: für offene, hochriskante Aufgaben, bei denen das System selbstständig reasoning leisten muss.
Setzen Sie LlamaIndex für die Datenebene und LangGraph für die Orchestrierung ein – dann decken diese Ansätze nahezu alle Anforderungen einer produktiven Auswertungsanwendung ab.