Pięć architektur RAG i sytuacje, w których każda z nich nadaje się do użycia w produkcji.
Praktyczny przegląd RAG prostego, hybrydowego, grafowego, zaawansowanego oraz opartego na agentach z wykorzystaniem LlamaIndex i LangGraph, obejmujący możliwe błędy, koszty oraz sposoby ich łączenia.
Źródła: The 5 RAG Architectures and Exactly When to Use Each One in Production
Słowo „RAG” obejmuje obecnie grupę zupełnie różnych architektur, z których każda jest przeznaczona do rozwiązywania innej kategorii zadań związanych z wyszukiwaniem informacji. Wybór niewłaściwej architektury to nie tylko kwestia wydajności – może to decydować o tym, czy system będzie funkcjonować prawidłowo, czy też będzie w dużych skali dostarczał użytkownikom pewnych, lecz błędnych odpowiedzi.
To mapa całej tej grupy architektur. Celem jest umożliwienie szybkiego określenia, jaka architektura jest potrzebna do rozwiązania danego problemu związanego z wyszukiwaniem, oraz sposobu jej budowy przy użyciu LangGraph do zarządzania procesami i LlamaIndex do obsługi warstwy danych. Pięć architektur jest uporządkowanych od najprostszej do najbardziej złożonej: prosta, hybrydowa, grafowa, zaawansowana i agentowa. Nie są one konkurentami – stanowią warstwy dodawane stopniowo w miarę rosnących wymagań, a większość systemów produkcyjnych łączy co najmniej dwie z nich.
Wspólny problem
Poniżej każda implementacja RAG rozwiązuje jeden konkretny problem: model językowy potrzebuje faktów, których nigdy nie widział podczas treningu, dostarczanych dokładnie w momencie udzielania odpowiedzi, oraz w takiej formie, aby mógł na ich podstawie rozumować.
Modele przestają się uczyć w określonej dacie. Nigdy nie czytały wewnętrznego wiki firmy, specyfikacji ani żadnych materiałów opublikowanych po zakończeniu treningu. Dostosowywanie modeli do takich treści jest powolne i kosztowne, a mimo to model pozostaje przestarzały, gdy zmieniają się dokumenty.
RAG omija te problemy. Zamiast uczyć model nowych faktów, system pobiera istotną wiedzę w momencie wysyłania zapytania i umieszcza ją w prompt jako kontekst. Model nie musi nic zapamiętywać – wystarczy, że przeczyta to w momencie, gdy jest to konieczne. Poniższe pięć architektur to pięć rozwiązań na pytanie, jak skutecznie pobierać informacje, a każda z nich nadaje się do rozwiązywania innego problemu.
Architektura 1: prosty RAG
Naive RAG stanowi punkt wyjścia, a wiele rzeczywistych zadań nie wymaga niczego więcej: boty do zarządzania politykami wewnętrznymi, asystenci do odpowiadania na pytania często zadawane, wyszukiwanie w dokumentacji, narzędzia pomocnicze przy integracji. Pomimo nazwy jest to dojrzały wzorzec, który może być stosowany na dużą skalę. Załadasz dokumenty, utworzysz indeks LlamaIndex VectorStoreIndex, udostępnisz jego silnik zapytań jako @tool i przekażesz to narzędzie agentowi LangGraph.
Jego pięć kolejnych kroków odpowiada bezpośrednio temu, co LlamaIndex oferuje od razu po instalacji. Pierwsze cztery kroki mają miejsce podczas tworzenia indeksu, a piąty – przy każdym zadaniu pytania:
Stages 1–4: Indexing time (run once)
Stage 5: Query time (run on every user question)
Najważniejszą różnicą we wszystkich rozwiązaniach typu RAG jest czas indeksowania w porównaniu z czasem wyszukiwania. Koszt tworzenia wektorów embedding jest płatny raz, z góry. Podczas wyszukiwania system płaci jedynie za jedno poszukiwanie podobieństwa oraz jedną próbę wywołania modelu LLM.
Dlaczego to działa
Pytanie użytkownika jest umieszczane w tym samym przestrzeni wektorowej co fragmenty dokumentów, a zwracane są te fragmenty, których wektory leżą geometrycznie najbliżej wektora pytania. Podstawowym założeniem jest to, że bliskość w przestrzeni wektorowej odzwierciedla bliskość znaczeniową. W przypadku czystych, faktograficznych korpusów to założenie sprawdza się zaskakująco dobrze.
Kod
Poniżej znajduje się kompletny szablon, obejmujący importy i konfigurację, stan, trwałą bazę wiedzy, narzędzie, węzły, routowanie oraz budowę grafu:
# ============================================================
# 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())
Gdzie występują problemy
Proste podejście RAG opiera się na jednym założeniu, które zawodzi w dwóch typowych sytuacjach.
Pierwszą z nich jest niezgodność słownictwa. Użytkownik pyta o warunki SLA obowiązujące dla klientów pierwszego poziomu, ale dokument obiecuje klientom poziomu Gold 99,9% czasu dostępności. SLA, poziom pierwszy i poziom Gold odnoszą się do powiązanych koncepcji, ale nie są ze sobą spójne, więc algorytm oceny podobieństwa może przypisać temu fragmencie zbyt niską wartość, przez co odpowiedź umyka uwadze.
Drugą sytuacją są pytania relacyjne. Aby dowiedzieć się, które produkty zostaną dotknięte w przypadku wyłączenia się konkretnego dostawcy, konieczne jest prześledzenie szeregu powiązań w kilku dokumentach. Żaden pojedynczy fragment nie zawiera odpowiedzi, a proste podejście RAG zwraca fragmenty z różnych dokumentów bez możliwości ich połączenia.
Te dwa sposoby awarii są przedmiotem rozważań w następnych dwóch architekturach.
Architektura 2: hybrydowe RAG
Hybrid RAG odzwierciedla to, czego nauczyli się praktycy w warunkach rzeczywistych: podobieństwo semantyczne oraz dokładne dopasowanie słów kluczowych to uzupełniające się wskaźniki istotności, a żaden z nich sam w sobie nie wystarcza.
Gęste wyszukiwanie, czyli wyszukiwanie wektorowe, dobrze radzi sobie z parafrazami: jeśli zadasz pytanie dotyczące SLA, nadal może znaleźć fragment o gwarancji dostępności na poziomie Gold. Ma jednak trudności z nazwami własnymi, kodami produktów, terminami medycznymi, cytatami prawnymi oraz innym specjalistycznym językiem, w którym dokładny ciąg znaków ma kluczowe znaczenie.
Rozproszone wyszukiwanie (BM25 lub wyszukiwanie słów kluczowych) jest jego przeciwieństwem. Doskonale radzi sobie z dokładnym dopasowywaniem terminów i zawsze znajdzie „SKU-4829” przy wyszukiwaniu tego samego kodu, ale nie rozumie równoważności semantycznej, więc nie znajdzie „gwarancji dostępności” dla „SLA”.
Hybrid RAG przeprowadza obie wyszukiwania równolegle, a następnie używa ponownego rankera, aby przekształcić połączone wyniki w jedną uporządkowaną listę. Ponowny ranker jest kluczowym elementem. Modele embeddingowe porównują zapytanie i fragmenty niezależnie, natomiast ponowny ranker typu cross-encoder czyta zapytanie wraz z każdym kandydatem i bezpośrednio oblicza ocenę ich wzajemnego powiązania. Jest wolniejszy od wyszukiwania podobieństwa, ale obejmuje jedynie już zredukowany zbiór kandydatów, dzięki czemu opóźnienie pozostaje akceptowalne.
Kod
Wersja LlamaIndex łączy gęstego retrievera, retrievera BM25 oraz fuzję rankingu wzajemnego:
# ============================================================
# 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.
Rozmiar fragmentów w hybrydowym wyszukiwaniu
W hybrydowym wyszukiwaniu zazwyczaj tworzy się indeksy z mniejszych fragmentów niż w przypadku zwykłego wyszukiwania wektorowego. BM25 reaguje silniej na powtarzający się termin w krótkim fragmencie niż w długim. Rozsądnym początkowym ustawieniem, gdy BM25 zostanie włączone do procesu, jest około 256 tokenów z 30 tokenami nakładania się.
Kiedy hybrydowe rozwiązanie jest odpowiednie
Należy go używać wtedy, gdy dokumenty łączą swobodny tekst z ustrukturyzowaną terminologią, co dotyczy niemal każdego poważnego przypadku zastosowania w przedsiębiorstwach. Przykładami są przeglądy prawne, gdzie formy cytowania muszą być dokładnie identyczne, karty medyczne zawierające precyzyjne nazwy leków, kody dawek i kody ICD, analiza finansowa z symbolami giełdowymi i identyfikatorami klauzul, a także dokumentacja techniczna z kodami błędów, nazwami metod API i numerami wersji.
Architektura 3: graph RAG
Graph RAG na nowo definiuje jednostkę wiedzy. W przypadku prostych i hybrydowych rozwiązań RAG dokument jest po prostu tekstem, a mechanizm wyszukiwania wybiera fragmenty najbardziej podobne do zapytania. Tutaj dokumenty stają się entytetami połączonymi relacjami, a mechanizm wyszukiwania przemieszcza się wzdłuż ścieżek w tej sieci.
Rozważmy pytanie, które klienci korporacyjni ucierpią, jeśli stary moduł autoryzacji zostanie wycofany. Prosty system szukałby fragmentów tekstowych zawierających jednocześnie wyrażenia „klienci korporacyjni” i „moduł autoryzacji” i mógłby znaleźć kilka takich przykładów. W rzeczywistości potrzebna jest łańcuchowa analiza: od modułu do produktów, od których zależą, a następnie od klientów, którzy je używają. Żaden pojedynczy fragment nie zawiera tej odpowiedzi – wynika ona z struktury grafu wiedzy. Graph RAG jest przeznaczony właśnie do radzenia sobie z takimi sytuacjami, czy to poprzez przechodzenie między kilkoma powiązaniami, śledzenie relacji, czy odpowiadanie na pytania wymagające faktów z odległych części korpusu.
Jak budowany jest indeks
Zamiast dzielić dokumenty na fragmenty i wstawiać te fragmenty, Graph RAG zaczyna od etapu wyodrębniania entytetów we wszystkich dokumentach. Identyfikuje on nazwane entytety (produkty, osoby, organizacje, koncepcje) oraz relacje między nimi, takie jak „zależy od”, „jest klientem”, „jest autorstwa” lub „został zastąpiony”. Te elementy stają się węzłami i krawędziami grafu wiedzy. Algorytmy klastrujące grafy następnie grupują ściśle powiązane entytety w społeczności, a model językowy generuje streszczenie dla każdej społeczności. Podczas wyszukiwania system przeszukuje te streszczenia społeczności i przemierza graf, aby dotrzeć do odpowiednich entytetów.
Kod
Indeks grafu właściwości LlamaIndex obsługuje proces wyodrębniania entytetów:
# ============================================================
# 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.
Ostrzeżenie dotyczące kosztów
Proces wyodrębniania entytetów polega na wykonywaniu zapytań do modeli językowych dla każdego fragmentu tekstu, więc duży zbiór dokumentów oznacza tysiące takich zapytań podczas indeksowania. Jest to drogie i wolne ze względu na specyfikę rozwiązania: opłacona kwota pozwala na uzyskanie znacznie bogatszej struktury danych tylko raz. Indeks typu graph RAG nie powinien być budowany na nowo przy każdym uruchomieniu. Należy zachować graf i ponownie go załadować, dokładnie tak jak w wersji prostszej.
Kiedy go wybrać
Wybierz rozwiązanie graph RAG, gdy pytania wymagają śledzenia łańcuchów powiązań. Typowymi przypadkami są audyty zgodności i analiza ryzyka (które procesy są objęte danymi regulacjami), inteligencja łańcucha dostaw (które produkty zależą od konkretnego dostawcy), wiedza organizacyjna (kto jest właścicielem czego i w jaki sposób łączą się te łańcuchy własności) oraz mapowanie zależności oprogramowania (co się zepsuje, jeśli usunięty zostanie dany moduł).
Architektura 4: zaawansowane RAG
Zaawansowany RAG to nie pojedyncza nowa technika. Jest to ustrukturyzowana seria ulepszeń dodawanych na wierzch istniejącej już bazy wyszukiwania. Prosty RAG polega na zaufaniu do pierwszej próby wyszukiwania, natomiast zaawansowany RAG kwestionuje ją, udoskonala i weryfikuje. Ulepszenia te dzielą się na trzy grupy, które wpływają w różnych etapach procesu: przed wyszukiwaniem, podczas obsługi zapytania oraz po nim.
Przed wyszukiwaniem: przepisywanie zapytań i HyDE
To, co wpisuje użytkownik, rzadko stanowi najlepsze zapytanie wyszukiwawcze. Zwykłe pytanie dotyczące polityki zwrotów dla klientów korporacyjnych jest zupełnie naturalne dla człowieka, ale przynosi gorsze wyniki niż fraza taka jak „procedury polityki zwrotów i zwrotów pieniędzy dla klientów korporacyjnych”. Przepisywanie zapytań polega na tym, że model językowy przekształca pytanie w lepsze zapytanie wyszukiwawcze, zanim dotrze ono do indeksu.
HyDE, skrót od Hypothetical Document Embedding, obiera inną drogę. Zamiast wyszukiwać na podstawie pytania, prosi model językowy o napisanie hipotetycznego dokumentu, który mógłby na nie odpowiedzieć, a następnie wstawia ten dokument do wyszukiwania. Chodzi o to, że tekst w formie odpowiedzi znajduje się w przestrzeni wektorowej bliżej innych tekstów w formie odpowiedzi niż tekst w formie pytania.
Rozbicie zapytania
Zapytania wieloetapowe pokonują prosty model RAG, ponieważ wymagają kilku operacji wyszukiwania. Zaawansowany model RAG najpierw je rozbija. Prośba o porównanie polityki zwrotów dla klientów korporacyjnych i detalicznych oraz podsumowanie głównych różnic to w rzeczywistości trzy zadania: pobrać politykę dla klientów korporacyjnych, pobrać politykę dla klientów detalicznych i je porównać. Rozbicie to dzieli zapytanie na podzapytania, przeprowadza wyszukiwanie dla każdego z nich i łączy wyniki przed syntezą.
Po wyszukiwaniu: ponowna klasyfikacja i CRAG
Przegrupowanie wyników, już omówione w kontekście wyszukiwania hybrydowego, ponownie ma zastosowanie na tym etapie: kodownik krzyżowy ponownie ocenia wszystkie pobrane fragmenty, nawet jeśli proces wyszukiwania był czysto gęsty.
CRAG (Corrective RAG) to najbardziej zaawansowana technika stosowana po pobraniu danych. Po ich pobraniu przeprowadza szybką weryfikację ich trafności. Jeśli wydają się niewystarczające, odwołuje się do innej bazy danych, na przykład do wyszukiwarki internetowej lub szerszego indeksu, aby model językowy nie musiał odpowiadać na podstawie słabego kontekstu.
Kod
Jedna z implementacji łączy w sobie kilka tych technik:
# ============================================================
# 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]
Zasada, której należy przestrzegać
Unikaj łączenia wszystkiego razem. Wybierz tę jedną zmianę, która najprawdopodobniej rozwiąże problem, który faktycznie widzisz. Najlepszym pierwszym krokiem jest zazwyczaj ponowne uporządkowanie, następnie dekompozycja, a HyDE stanowi godny trzeci wybór dla zbiorów abstrakcyjnych lub koncepcyjnych. Buduj stopniowo i sprawdzaj efekty po każdym kroku.
Architektura 5: agentic RAG
Agentic RAG wykracza poza prostą poprawę procesu wyszukiwania – przekazuje decyzje dotyczące wyszukiwania komuś innemu. Pierwsze cztery architektury to szyny przetwarzania, stałe sekwencje, które zachowują się identycznie przy każdym uruchomieniu. Tutaj wyszukiwanie staje się pętlą, w której agent oparty na LLM decyduje, co wyszukać, ocenia wyniki, decyduje, czy szukać ponownie, i zatrzymuje się, gdy ma już wystarczająco dużo informacji, albo stwierdza, że na pytanie nie ma odpowiedzi.
To właśnie po to został stworzony LangGraph. Węzeł agenta, węzeł narzędzia oraz krawędź warunkowa tworzą już podstawę dla systemu agentic RAG. To, co się zmienia, to bogactwo zestawu narzędzi oraz zaawansowanie logiki routingu opartej na nich.
Kilka narzędzi wyszukiwania jednocześnie
Prawdziwą zaletą systemu agentic RAG w LangGraph jest to, że agent może korzystać z każdej z opisanych powyżej strategii wyszukiwania jednocześnie, a jego model językowy wybiera odpowiednie narzędzie do każdego podpytania. Ta struktura umożliwia pojedynczemu agentowi korzystanie ze wszystkich pięciu architektur:
# ============================================================
# 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")
Czego nie potrafią pipeline’y
To, co odróżnia je od innych, to samokorekcja. Pipeline nie ma sposobu, by zauważyć, że pobrał niewłaściwy materiał; agent ma taką możliwość. Gdy wstępne wyniki są skąpe, jego kolejny krok rozumowania przepisuje zapytanie, dodając nowe terminy. Gdy pytanie kryje w sobie zależność, agent dostrzega to w połowie procesu i pobiera dodatkowe informacje. Gdy pytanie jest niejasne, może zapytać użytkownika przed przeprowadzeniem wyszukiwania.
Wybierz tę architekturę, gdy błędna odpowiedź ma poważne konsekwencje, np. w środowiskach związanych z przestrzeganiem przepisów, prawem, finansami lub medycyną, ponieważ kroki weryfikacji, progi pewności oraz punkty kontrolne z udziałem człowieka można bezpośrednio dodać do grafu agenta.
Cena w postaci opóźnienia
Nie da się ukryć spowolnienia. Pipeline oparte na jednym narzędziu odpowiadają w ciągu około 200 do 500 milisekund; agent wykonujący najpierw trzy wywołania do pobierania danych może potrzebować od 8 do 12 sekund, co jest zbyt wolne dla głównej ścieżki wielu interfejsów w czasie rzeczywistym. Dwa rozwiązania łagodzą ten problem: transmitowanie kroków pośrednich, dzięki czemu użytkownicy mogą obserwować postępy zamiast czekać w ciszy, oraz uruchamianie agenta do pobierania danych w tle w celu wcześniejszego pozyskania prawdopodobnych dalszych informacji.
Wybór i łączenie
Każdy problem związany z pobieraniem danych ma odpowiednie rozwiązanie spośród tych pięciu. Jako ogólny przewodnik decyzyjny: zacznij od prostego podejścia RAG do czystych, faktograficznych pytań i odpowiedzi w dokumentach, przejdź do rozwiązania hybrydowego, gdy terminologia jest specjalistyczna lub istotne są dokładne identyfikatory; wybierz podejście graph RAG, gdy odpowiedzi zależą od relacji pomiędzy dokumentami; zastosuj zaawansowane techniki, gdy konieczna jest poprawa jakości pobierania danych; i użyj agenta, gdy zadanie jest otwarte, wymaga wysokiego poziomu precyzji lub potrzebuje samokorekty.
Architektury te są również komponowalne, więc nie ma potrzeby wybrać jednej i odrzucić pozostałych. Najczęstszym wzorcem w środowiskach produkcyjnych jest stos. W LangGraph ten stos jest bezpośrednio przekładany na listę narzędzi. Agent wyposażony w proste narzędzie, hybrydowe narzędzie, narzędzie grafowe oraz narzędzie do dekompozycji zapytań stanowi dokładnie taki stos pięciu architektur, przy czym agent pełni rolę warstwy najwyższej, która na każdym kroku wybiera i koordynuje działanie pozostałych elementów.
Karta referencyjna słów kluczowych
Na tej karcie wymienione są główne klasy oraz ustawienia dla każdej warstwy:
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)
Pobieranie danych jako infrastruktura
Te pięć podejść to nie różne sposoby robienia tej samej rzeczy. Są to rozwiązania pięciu różnych problemów związanych z pobieraniem danych, ułożone w logiczną sekwencję od prostych do bardziej zaawansowanych.
- Naive RAG: szybki, tani i idealny do większości prostych zadań typu pytanie-odpowiedź w dokumentach.
- Hybrid RAG: rozsądny standard, gdy słownictwo jest specjalistyczne.
- Graph RAG: wybór, gdy powiązania między faktami są ważniejsze niż pojedynczy dokument.
- Advanced RAG: rozwiązanie służące do poprawy dokładności, gdy problemem jest jakość pobierania danych.
- Agentic RAG: przeznaczony do zadań otwartych o wysokim stopniu ryzyka, w których system musi samodzielnie rozumować.
Używając LlamaIndex do obsługi warstwy danych oraz LangGraph do zarządzania procesem, te podejścia pokrywają niemal wszystkie potrzeby aplikacji do pobierania danych w środowisku produkcyjnym.
Podobne materiały
- Wykorzystanie narzędzi produkcyjnych dla agentów: pochodzenie danych, próby ponowne i możliwości obserwacji — Agent banku niewłaściwie użył płatnej API i wymyślił numer telefonu. Dowiedz się o minimalnych zestawach narzędzi, pochodzeniu parametrów, trójkrotnych próbach ponownych oraz metrykach dla poszczególnych narzędzi.
- Mierzenie efektywności pipeline RAG: wskaźnik trafień, MRR i wyszukiwanie hybrydowe — Jak stworzyć zbiór danych referencyjnych, ocenić skuteczność wyszukiwania za pomocą wskaźnika trafień i MRR oraz porównać wyszukiwanie tekstowe, wektorowe i hybrydowe w 360 pytaniach.