Главная / Статьи

Пять архитектур 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 заключается в разнице между временем индексации и временем выполнения запроса. Расходы на генерацию векторных представлений оплачиваются один раз, заранее. Во время выполнения запроса система оплачивает лишь один поиск по сходству и один вызов большой языковой модели.

Почему это работает

Вопрос пользователя преобразуется в векторное представление и помещается в тот же векторный пространство, что и фрагменты документов; возвращаются те фрагменты, чьи векторы геометрически наиболее близки к вектору вопроса. Основная предпосылка заключается в том, что близость в векторном пространстве отражает близость по смыслу. Для чистых, фактологически точных корпусов данных эта предпосылка удивительно хорошо справляется со своей задачей.

Код

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

# ============================================================
# 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, он всё равно сможет найти информацию о гарантии времени работы на уровне Gold. Однако он терпит неудачу с собственными именами, кодами продуктов, медицинской терминологией, юридическими цитатами и другим специализированным языком, где именно точная последовательность символов несёт смысл.

Редкий поиск (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 начинает с этапа извлечения сущностей из каждого документа. Он выявляет именованные сущности (товары, люди, организации, понятия) и связи между ними, такие как «зависит от», «является клиентом», «написан автором» или «заменен». Эти элементы становятся узлами и рёбрами графа знаний. Затем алгоритмы кластеризации графа объединяют тесно связанные сущности в группы, а большая языковая модель пишет краткое описание для каждой группы. При поступлении запроса система ищет в этих описаниях групп и прослеживает пути в графе, чтобы найти соответствующие сущности.

Код

Индекс свойственных графов 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.

Предупреждение о затратах

При извлечении сущностей для каждого фрагмента текста выполняется вызов большой языковой модели, поэтому большой набор документов означает тысячи таких вызовов во время индексации. Это дорого и медленно по своей сути: такая цена позволяет получить гораздо более сложную структуру данных один раз. Индекс типа 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 выходит за рамки простого улучшения процесса поиска: он передаёт принятие решений о поиске кому-то другому. Предыдущие четыре подхода представляют собой фиксированные последовательности действий, которые ведут себя одинаково при каждом запуске. В агентном подходе поиск превращается в цикл, в котором агент на основе больших языковых моделей решает, что искать, оценивает результаты, решает, стоит ли продолжать поиск, и останавливается, когда получает достаточно информации, или приходит к выводу, что на вопрос нет ответа.

Именно для этого и был создан LangGraph. Узел агента, узел инструмента и условная рёбра уже образуют основу для агентного RAG. Изменяется лишь количество инструментов и сложность логики маршрутизации, построенной на их основе.

Несколько инструментов поиска одновременно

Настоящая сила агентного RAG в LangGraph заключается в том, что агенту можно одновременно предоставить все описанные выше стратегии поиска, и его большая языковая модель выбирает подходящий инструмент для каждого подвопроса. Благодаря этой структуре все пять архитектур становятся доступными одному агенту:

# ============================================================
# 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 для организации процессов, эти подходы покроют практически все потребности производственного приложения для поиска.