Головна / Статті

П’ять архітектур RAG та ситуації, коли кожна з них підходить для використання в продакшені.

Практичний огляд наївних, гібридних, графових, розширених та агентних архітектур RAG із використанням LlamaIndex та LangGraph, що охоплює способи виникнення помилок, витрати та методи їх поєднання.

Видавець: REACTAPP.TOP19 хв читання4457 слів

Джерела: The 5 RAG Architectures and Exactly When to Use Each One in Production

Термін „RAG“ тепер охоплює сім’ю досить різних архітектур, кожна з яких створена для різних типів задач пошуку інформації. Вибір неправильної архітектури — це не лише питання продуктивності. Це може стати причиною того, що система буде працювати, або ж буде безперешкодно надавати користувачам правдоподібні, але хибні відповіді у великих масштабах.

Це карта всієї цієї сім’ї архітектур. Мета полягає у тому, щоб мати змогу розглянути будь-яку задачу пошуку та визначити, яка архітектура їй потрібна, а також як її побудувати за допомогою LangGraph для оркестрації та LlamaIndex для обробки даних. П’ять архітектур розташовані від найпростішої до найскладнішої: наївна, гібридна, графова, розширена та агентська. Вони не є конкурентами. Це шари, які додаються поступово у міру зростання складності задачі, і більшість продакшн-систем поєднують принаймні два з них.

Спільна проблема

По суті, кожна реалізація RAG вирішує одну проблему: моделі мови потребують фактів, яких вони ніколи не бачили під час навчання, які мають надходити саме тоді, коли вона відповідає, та бути оформленими так, щоб модель могла з ними працювати.

Моделі припиняють навчання у певну дату. Вони ніколи не читали внутрішньої вікі компанії, технічних характеристик чи будь-чого, що було опубліковано після завершення навчання. Доопрацювання моделі за допомогою такого контенту є повільним та дорогим, а модель все одно стає застарілою, як тільки документи змінюються.

RAG уникає всього цього. Замість того, щоб навчати модель новим фактам, система отримує необхідні знання під час запиту та включає їх у запит як контекст. Моделі ніколи не потрібно нічого запам’ятовувати — їм достатньо прочитати цей контекст у потрібний момент. П’ять описаних нижче архітектур є п’ятьма способами ефективного отримання інформації, причому кожна з них підходить для різних проблем.

Архітектура 1: примітивний RAG

Naive RAG є відправною точкою, і багато реальних завдань ніколи не потребують чогось більшого: внутрішні боти для дотримання правил, асистенти з часто ставленими запитаннями, пошук у документації, допоміжні інструменти для інтеграції. Незважаючи на назву, це зріла модель, яка працює у великих масштабах. Ви завантажуєте документи, створюєте LlamaIndex VectorStoreIndex, робите його двигун пошуку доступним як @tool та передаєте цей інструмент агенту LangGraph.

Його п’ять послідовних кроків безпосередньо відповідають тому, що пропонує LlamaIndex з готовими налаштуваннями. Перші чотири кроки виконуються під час створення індексу, а п’ятий — для кожного запитання:

  Stages 1–4: Indexing time (run once)
  Stage 5:    Query time (run on every user question)

Найважливіша відмінність у всіх системах типу RAG — це час індексації та час запиту. Витрати на створення ембеддингів сплачуються один раз, заздалегідь. Під час запиту система оплачує лише один пошук схожості та один виклик моделі LLM.

Чому це працює

Запит користувача ембеддується у той самий векторний простір, що й фрагменти документів, і повертаються ті фрагменти, чиї вектори геометрично знаходяться найближче до вектора запиту. Основна припущення полягає у тому, що близькість у векторному просторі відображає близькість у значенні. Для чистих, фактологічних корпусів це припущення дивовижно добре справджується.

Код

Нижче наведений повний шаблон, який включає імпорти та налаштування, стан, постійну базу знань, інструменти, вузли, маршрутизацію та формування графа:

# ============================================================
# 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())

Де виникають проблеми

Наївний RAG ґрунтується на одному припущенні, і це припущення не спрацьовує у двох поширених ситуаціях.

Перша — це неузгодженість словникового запасу. Користувач запитує про умови SLA, які поширюються на клієнтів першого рівня, але документ обіцяє клієнтам золотого рівня 99,9% часу роботи. SLA, перший та золотий рівні означають пов’язані речі, але не є ідентичними, тому оцінка схожості може поставити цей фрагмент занадто низько, і відповідь залишиться непоміченою.

Друга — це запитання, що вимагають аналізу взаємозв’язків. Щоб дізнатися, які продукти постраждають у разі відключення певного постачальника, потрібно простежити ланцюг взаємозв’язків у кількох документах. Жоден окремий фрагмент не містить відповіді, тож наївний RAG повертає фрагменти з різних документів без можливості їх поєднання.

Саме ці два способи збою вирішують наступні дві архітектури.

Архітектура 2: гібридний RAG

Гібридний підхід RAG відображає те, чого навчилися фахівці на практиці: семантична схожість та точне зіставлення ключових слів є взаємодоповнюючими ознаками релевантності, і жодна з них окремо недостатня.

Інтенсивний пошук, який є векторним пошуком, добре справляється з парафразуванням: якщо запитати про SLA, він все одно зможе знайти відповідний уривок про гарантований час роботи. Однак він має проблеми з власними іменами, кодами продуктів, клінічними термінами, юридичними цитатами та іншою спеціалізованою термінологією, де саме точний текст несе значення.

Розріджений пошук (BM25 або пошук за ключовими словами) є протилежністю цьому підходу. Він чудово справляється з точним зіставленням термінів та завжди знайде „SKU-4829“ при пошуку саме цього коду, але не розуміє семантичної еквівалентності, тому не знайде виразу „гарантія часу роботи“ замість „SLA“.

Гібридний RAG виконує обидві пошуки паралельно, а потім використовує реранкер для перетворення об’єднаних результатів у один рангований список. Реранкер є ключовим компонентом. Моделі ембеддингу порівнюють запит та фрагмент незалежно, тоді як реранкер типу крос-енкодер читає запит та кожен кандидат разом та безпосередньо обчислює оцінку їхньої взаємозв’язку. Цей підхід повільніший за пошук за схожістю, але він працює лише з уже скороченим набором кандидатів, тому затримка залишається прийнятною.

Код

Версія LlamaIndex поєднує щільного ретрівера, ретрівера BM25 та метод фузії рангів:

# ============================================================
# 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.

Розмір фрагментів у гібридному пошуку

При гібридному пошуку зазвичай створюється індекс менших фрагментів, ніж при звичайному векторному пошуку. BM25 отримує більш чіткий сигнал від повторюваного терміна у короткому фрагменті, ніж у довгому. Приблизно 256 токенів із 30 токенами перекриття є розумним початковим налаштуванням після включення BM25 у процес пошуку.

Коли гібридний підхід є правильним вибором

Використовуйте його тоді, коли документи поєднують вільний текст із структурованою термінологією, що стосується майже кожного серйозного корпоративного випадку використання. Прикладами є юридичний аналіз, де формати цитувань мають збігатися точно, медичні записи з точними назвами ліків, кодами дозування та кодами ICD, фінансовий аналіз із символами акцій та ідентифікаторами клозу, а також технічна документація з кодами помилок, назвами методів API та номерами версій.

Архітектура 3: графовий RAG

Графічний RAG переосмислює одиницю знань. У примітивному та гібридному RAG документ є просто текстом, а процес пошуку обирає уривки, які найбільше схожі на запит. Тут документи стають ентитетами, пов’язаними взаємозв’язками, а процес пошуку просувається по шляхах у цій мережі.

Розгляньмо питання про те, які корпоративні клієнти постраждають, якщо модуль автентифікації старої версії буде відмінений. Примітивна система шукала б фрагменти, у яких разом згадуються „корпоративні клієнти“ та „модуль автентифікації“, і могла б знайти кілька таких фрагментів. Насправді потрібна ланцюгова структура: від модуля до продуктів, які від нього залежать, від цих продуктів до клієнтів, які їх використовують. Жоден окремий фрагмент не містить цієї відповіді — вона випливає зі структури графа знань. Graph RAG спрямований саме на таку ситуацію, чи то для переходу через кілька посилань, відстеження зв’язку, чи для відповіді на запитання, які потребують фактів з далеко розташованих частин корпусу даних.

Як створюється індекс

Замість розділення документів на частини та вбудовування цих частин, графічний RAG починається з етапу вилучення ентитетів з кожного документа. Він ідентифікує іменовані ентитети (продукти, люди, організації, концепції) та взаємозв’язки між ними, такі як „залежить від“, „є клієнтом“, „створений автором“ чи „був замінений“. Це стає вузлами та ребрами графа знань. Алгоритми кластеризації графа потім групують тісно пов’язані ентитети в спільноти, а модель LLM пише короткий огляд для кожної спільноти. Під час запиту система шукає в оглядах спільнот та прокладає шлях по графу, щоб знайти відповідні ентитети.

Код

Індекс графа властивостей LlamaIndex обробляє процес вилучення даних:

# ============================================================
# 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.

Попередження щодо витрат

Процес вилучення ентитетів здійснює виклик LLM для кожного фрагмента, тому велика кількість документів означає тисячі викликів під час індексації. Це дорого та повільно за своєю суттю: така ціна дозволяє отримати набагато багатшу структуру даних лише один раз. Індекс типу graph RAG ніколи не слід перебудовувати при кожному запуску. Зберігайте граф та завантажуйте його знову, точно так само, як у примітивній версії.

Коли обирати його

Обирайте graph RAG, коли для відповідей на запитання необхідно простежувати ланцюги взаємозв’язків. Типовими прикладами є контроль дотримання правил та аналіз ризиків (які процеси впливають певні нормативи), аналіз ланцюгів постачання (які продукти залежать від постачальника), організаційні знання (хто є власником чого та як пов’язані ці ланцюги власності) та визначення залежностей програмного забезпечення (що зламається, якщо видалити певний модуль).

Архітектура 4: розширений RAG

Розширений RAG — це не окрема нова техніка. Це структурований набір покращень, які додаються до будь-якої наявної бази даних для пошуку. Простий RAG покладається на перший етап пошуку, тоді як розширений RAG ставить під сумнів результати, вдосконалює їх та перевіряє. Ці покращення поділяються на три групи, які застосовуються на різних етапах процесу: перед пошуком, під час обробки запиту та після пошуку.

Перед пошуком: переписування запиту та HyDE

Те, що вводить користувач, рідко є найкращим запитом для пошуку. Просте запитання щодо політики повернення товарів для корпоративних клієнтів є цілком природним для людини, але призводить до гірших результатів, ніж фраза на кшталт „процедури повернення та відшкодування коштів для корпоративних клієнтів“. Переписування запиту полягає у тому, що ШІ перетворює запитання на кращий запит для пошуку ще до того, як він потрапить до індексу.

HyDE, скорочення від Hypothetical Document Embedding, обирає інший підхід. Замість пошуку за запитом він просить ШІ написати гіпотетичний документ, який міг би на нього відповісти, а потім вбудовує цей документ для пошуку. Ідея полягає у тому, що текст у формі відповіді знаходиться у векторному просторі ближче до іншого тексту у формі відповіді, ніж до тексту у формі запиту.

Розкладання запиту

Багатокрокові запити ускладнюють простий підхід RAG, оскільки вимагають кількох операцій пошуку. Розширений підхід RAG спочатку розбиває їх на частини. Запит про порівняння політики повернення коштів для корпоративних та роздрібних клієнтів та узагальнення основних відмінностей насправді є трьома завданнями: отримати політику для корпоративних клієнтів, отримати політику для роздрібних клієнтів та порівняти їх. Розкладання запиту дозволяє розділити його на підзапити, виконати пошук для кожного з них та об’єднати результати перед синтезом.

Після пошуку: переранжування та CRAG

Поновна класифікація, яка вже розглядалася в рамках гібридного пошуку, знову стає актуальною на цьому етапі: крос-кодер переоцінює всі отримані фрагменти, навіть якщо пошук був суто інтенсивним.

CRAG (Corrective RAG) — це найскладніша техніка після отримання даних. Після їх завантаження вона проводить швидку перевірку релевантності фрагментів. Якщо вони здаються недостатніми, система звертається до іншого джерела, наприклад до пошуку в Інтернеті чи більш розгорнутого індексу, щоб ШІ не змушений був відповідати на основі слабкого контексту.

Код

Одна з реалізацій поєднує кілька цих технік:

# ============================================================
# 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]

Правило, якого слід дотримуватися

Утримуйтеся від об’єднання всього разом. Виберіть ту зміну, яка з найбільшою ймовірністю вирішить проблему, яку ви фактично бачите. Переупорядкування зазвичай є найкращим першим кроком, потім йде розкладання, а HyDE є прийнятним третім варіантом для абстрактних чи концептуальних колекцій. Продовжуйте роботу поступово та перевіряйте результати після кожного кроку.

Архітектура 5: агентний RAG

Агентний RAG виходить за межі простого покращення процесу пошуку: він передає рішення щодо пошуку комусь іншому. Попередні чотири типи є конвеєрними схемами — фіксованими послідовностями, які діють однаково під час кожного запуску. Тут пошук стає циклом, у якому агент на основі LLM вирішує, що шукати, оцінює результати, вирішує, чи потрібно шукати знову, та зупиняється, коли має достатньо інформації, або визначає, що на запит немає відповіді.

Саме для цього і був створений LangGraph. Вузол агента, вузол інструменту та умовний ребро вже утворюють основу для агентного RAG. Змінюється лише різноманітність набору інструментів та складність логіки маршрутизації, побудованої навколо них.

Кілька інструментів пошуку одночасно

Справжня перевага агентного RAG у LangGraph полягає у тому, що агент може отримувати всі вищеописані стратегії пошуку одночасно, а його LLM обирає інструмент для кожного підпитання. Ця основа дозволяє одному агенту використовувати всі п’ять архітектур:

# ============================================================
# 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")

Що не можуть робити пайплайни

Те, що відрізняє його від інших, — це самокорекція. У пайплайну немає способу помітити, що він отримав неправильний матеріал; у агента є. Коли початкові результати є обмеженими, наступний крок міркувань агента переписує запит із новими термінами. Коли у запитанні є прихована залежність, агент помічає це на півдорозі та отримує додаткову інформацію. Коли запитання є нечітким, він може запитати користувача перед пошуком.

Використовуйте цю архітектуру, коли неправильна відповідь має великі наслідки, наприклад у сферах комплаєнсу, права, фінансів чи медицини, оскільки кроки перевірки, пороги впевненості та контрольні точки з участю людини можна безпосередньо додати до графа агента.

Ціна у затримці

Неможливо приховати уповільнення. Пайплайни з одним інструментом відповідають приблизно за 200–500 мілісекунд; агент, який спочатку робить три запити на отримання даних, може знадобити від 8 до 12 секунд, що занадто повільно для основного шляху багатьох реальночасних інтерфейсів. Є два способи це полегшити: транслювати проміжні кроки, щоб користувачі бачили прогрес замість того, щоб мовчки чекати, та виконувати запити агента на тлі для попереднього отримання ймовірних наступних даних.

Вибір та поєднання

Кожна проблема отримання даних має правильну відповідь серед цих п’яти підходів. Ось приблизний алгоритм прийняття рішень: почніть з простого підходу RAG для чистих, фактологічних запитань та відповідей у документах; перейдіть до гібридного підходу, коли термінологія є спеціалізованою або важливими є точні ідентифікатори; використовуйте підхід graph RAG, коли відповіді залежать від зв’язків між документами; застосовуйте складніші техніки, коли потрібно покращити якість отримання даних; та використовуйте агента, коли завдання є відкритим, має високу значимість або потребує самокорекції.

Архітектури також є комбінованими, тож немає потреби обирати одну та відкидати решту. Найпоширенішим паттерном у продакшені є стек. У LangGraph цей стек безпосередньо перетворюється на список інструментів. Агент із примітивним інструментом, гібридним інструментом, графовим інструментом та інструментом для розкладення запитів — це саме стек з п’яти архітектур, де агент виступає верхнім шаром, який на кожному кроці вибирає та координує роботу інших інструментів.

Карта довідки з ключовими термінами

У цій картці перелічені основні класи та налаштування для кожного шару:

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)

Отримання даних як інфраструктура

Ці п’ять підходів — це не п’ять способів виконати одну й ту саму роботу. Це рішення п’яти різних проблем отримання даних у послідовному розвитку від простих до складних.

  • Naive RAG: швидкий, недорогий та ідеальний для більшості простих запитань-відповідей до документів.
  • Hybrid RAG: розумний стандартний варіант, коли словниковий запас є спеціалізованим.
  • Graph RAG: вибір, коли зв’язки між фактами мають більше значення, ніж будь-який окремий документ.
  • Advanced RAG — це підхід для підвищення точності, коли слабкою стороною є якість отримання даних.
  • Agentic RAG: для відкритих завдань високого рівня ризику, де система мусить міркувати самостійно.

Якщо використати LlamaIndex для роботи з даними та LangGraph для організації процесів, ці підходи задовольнять майже всі потреби продакшн-застосунків для отримання даних.