Пяць архітэктур RAG і калі кожная пасуе ў прадакшне
Практычны агляд наіўнага, гібрыднага, графавага, пашыранага і агентнага RAG на LlamaIndex і LangGraph: тыповыя збоі, кошт і як іх спалучаць.
Крыніцы: The 5 RAG Architectures and Exactly When to Use Each One in Production
Слова „RAG“ цяпер выражае цэлую групу даснажна разныях архітэктураў, кожная з яых створаная для рашэння адной з розных катэгорыяў задач на выкарыстоўванне інформацыі. Выбір неправільнай архітэктуры — гэта не проста пытанне карыстоўнасці. Гэта можа стаць прычыной таго, што сістэма будзе працаваць, але будзе даўаць корыстувачам правільныя, але некарэктныя адпаведзі на вялікіх масштабах.
Эта карта всей групы архітэктураў. Мета — магчымасць аналізаваць будзь-яю задачу на выкарыстоўванне інформацыі і вядомаць, якая архітэктура для яе патрэбна, а таксама як яе стварыць за дапамогою LangGraph для керавання і LlamaIndex для апрацоўкі дадзеных. Пяць архітэктураў ранжуецца ад простай да складнай: наўмовістная, гібрыдная, графавая, прыглыбленая і агентная. Яны не ўзаемныя канкурэнты. Гэта шары, якія дадаюцца поступова, калі задача стае болей складной, і большасць практычных сістэм аб’еднвае прынеймна два з іх.
Спакойная задача
У свою частку кожна структура RAG рашае адзін прыблэм: модэлю мовы патрэбны факты, якіх ён ніколі не бачыў пад час навучэння, якія павінны быць дастаўлены роўна тады, калі ён адпавядае, і якія павінны быць адформаваны так, каб ён мог усвядоміць іх.
Модэлі перастаюць навучацца у паказаны тэрмін. Яны ніколі не чыталі внутрашняе wiki компаніі, тэхнічныя характэрыстыкі чы ілі ўсё, што было опублікована пасля завершэння навучэння. Тонкай наладкі на такім кантэнце ёсць медленныя і дорогія, а наладжаны модэль працуе некорэктна, калі дакументы змінююцца.
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())
Дзе ўсё ламаецца
Naive RAG апоўзаецца на адной падазе, і гэтая падаза не выконваецца ў двух распашчытных сітуацыях.
Першая — гэта несувярэннасць у лексыканцы. Пользоватэль запытаеся пра умовы SLA, якія дыяюць для кліента 1-го рангу, але дасягненне обяцвае кліентам 1-го золатага рангу 99,9% часу працы. SLA, 1-й ранг і золаты ранг — гэта сувязаныя паняй, але не сувярэнныя, таму расчытанне схожасці можа прызначыць такі фрагмент занадта нізкім рангам, і адпаведна адказ застаецца незаўважаным.
Другая — гэта запытанні, якія выклікаюць неабходнасць выявлення сувязей. Ёнкія, якія продукты будуць пад вплывам, калі певны прадавець перастане працаваць, выклікаюць неабходнасць аналізу ланцоў сувязей у калькі дасягненняў. Жадны адзін фрагмент не мае адпаведнай адказы, і naive RAG вяртае фрагменты з разных дасягненняў без можлівасці ўз’яеднання іх.
Гэтыя два спосабы неяксамоў — гэта тое, што рашаюць наступныя два архітэктурныя падходы.
Архітэктура 2: гібрыдны RAG
Гібрыдны RAG адаптаваець тое, што спецыялісты вывучылі на практыцы: сэмантычная супараднасць і точна падборка ключоўых слоў ёсць взаімадопамагаючымі сігналамі рэлевантнасці, і ні адна з іх сама по сабе недастатня.
Жыццёздатны метод адналокавання, якім ёсць векторны пошук, добра справляецца з парафразамі: калі запытацца пра SLA, ён все равно можа знайсці абзац пра гарантаваны час працы на высокай прыгодзе. Однак ён неэфектыўны з прыводу власных імен, кодаў продуктам, клінічных тэрмінаў, прававых цітатаў і іншай спецыялізаванай тэрміналогіі, дзе саме значэнне несе точная фраза.
Метод розрэджанага адналокавання (BM25 або пошук за ключоўымі словамі) ёсць працоўным працэсам гібрыднага падходу. Ён выдаўна справляецца з точной падборкай тэрмінаў і завжды знайдзе „SKU-4829“ пад пошукам „SKU-4829“, але не мае уявлення пра сэмантычную эквівалентнасць, таму не знайдзе „гарантыя часу працы“ пад пошукам „SLA“.
Гібрыдны RAG адзейнае як пошукі паралельна, а потым выкарыстоўвае рэранкер, каб ператворыць з’едыненыя рэзультаты ў адны ранкаваны спіс. Рэранкер ўжо ўсё ў чыныю ключоўы элемент. Мадэлі эмбеддынга пораўнююць запіт і фрагмент незалежна, тады калі рэранкер на базе крос-кодэра чытае запіт і кожны кандыдат адночасна і безпосерэдна вырахоўвае бал за ўзаінтэрэсаванасць між ямі. Гэты падход медленней за пошук на адпоўненасці, але ён працюе толькі з ужэ скорачаным наборам кандыдатаў, таму час адпаведзення застаецца прыемлемым.
Код
Версія LlamaIndex спаўнае ў сабе ўжо ўсё: ўжо ўсё ў чыныю ўжо ўсё ў чыныю ўжо ў чыныю ўжо ў чыныю ўжо ў чыныю ўжо ў чыныю ўжо ў чыныю ўжо ў чыныю ўжо ў чыныю ўжо ў чыныю ўжо ў чыныю ўжо ў чыныю ўжо ў чыныю ўжо ў чыныю ўжо ў чыныю ўжо ў чыныю ўжо ў чыныю ўжо ў чыныю ўжо ў чыныю ўжо ў чыныю ўжо ў чыныю ўжо ў чыныю ўжо ў чыныю ўжо ў чыныю ўжо ў чыныю ўжо ў чыныю ўжо ў чыныю ўжо ў чыныю ўжо ў чыныю ўжо ў чыныю ўжо ў чыныю ўжо ў чыныю ўжо ў чыныю ўжо ў чыныю ўжо ў чыныю ўжо ў чыныю ўжо ў чыныю ўжо ў чыныю ўжо ў чыныю ўжо ў чыныю ўжо ў чыныю ўжо ў чыныю ўжо ў чыныю ўжо ў чыныю ўжо ў чыныю ўжо ў чыныю ўжо ў чыныю ўжо ў чыныю ўжо ў чыныю ўжо ў чыныю ўжо ў чыныю ўжо ў чыныю ўжо ў чыныю ўжо ў чыныю ўжо ў чыныю ўжо ў чыныю ўжо ў чыныю ўжо ў чыныю ўжо ў чыныю ўжо ў чыныю ўжо ў чыныю ўжо ў чыныю ўжо ў чыныю ўжо ў чыныю ўжо ў чыныю ўжо ў чыныю ўжо ў чыныю ўжо ў чыныю ўжо ў чыныю ўжо ў чыныю ўжо ў чыныю ўжо ў чыныю ўжо ў чыныю ўжо ў чыныю ўжо ў чыныю ўжо ў чыныю ўжо ў чыныю ўжо ў чыныю ўжо ў чыныю ўжо ў чыныю ўжо ў чыныю ўжо ў чыныю ўжо ў чыныю ўжо ў чыныю ўжо ў чыныю ўжо ў чыныю ўжо ў чыныю ўжо ў чыныю ўжо ў чыныю ўжо ў чыныю ўжо ў чыныю ўжо ў чыныю ўжо ў чыныю ўжо ў чыныю ўжо ў чыныю ўжо ў чыныю ўжо ў чыныю ўжо ў чыныю ўжо ў чыныю ўжо ў чыныю ўжо ў чыныю ўжо ў чыныю ўжо ў чыныю ўжо ў чыныю ўжо ў чыныю ўжо ў чыныю ўжо ў чыныю ўжо ў чыныю ўжо ў чыныю ўжо ў чыныю ўжо ў чыныю ўжо ў чыныю ўжо ў чыныю ўжо ў чыныю ўжо ў чыныю ўжо ў чыныю ўжо ў чыныю ўжо ў чыныю ўжо ў чыныю ўжо ў чыныю ўжо ў чыныю ўжо ў чыныю ўжо ў чыныю ўжо ў чыныю ўжо ў чыныю ўжо ў чыныю ўжо ў чыныю ўжо ў чыныю ўжо ў чыныю ўжо ў чыныю ўжо ў чыныю ўжо ў чыныю ўжо ў чыныю ўжо ў чыныю ўжо ў чыныю ўжо ў чыныю ўжо ў чыныю ўжо ў чыныю ўжо ў чыныю ўжо ў чыныю ўжо ў чыныю ўжо ў чыныю ўжо ў чыныю ўжо ў чыныю ўжо ў чыныю ўжо ў чыныю ўжо ў чыныю ўжо ў чыныю ўжо ў чыныю ўжо ў чыныю ўжо ў чыныю ўжо ў чыныю ўжо ў чыныю ўжо ў чыныю ўжо ў чыныю ўжо ў чыныю ўжо ў чыныю ўжо ў чыныю ўжо ў чыныю ўжо ў чыныю ўжо ў чыныю ўжо ў чыныю ўжо ў чыныю ўжо ў чыныю ўжо ў чыныю ўжо ў ч
# ============================================================
# 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 дакумент ёсць проста тэкст, а працэс аднаходжэння выбірае тыя фрагменты, якія найбольш супадаюць з запітам. У графічным 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.
Адварсая пра падакі
Працэс выкарыстоўвання элементаў адбываецца праз вызов LLM для кожнага фрагмента, таму большы набор дакументаў значыць тыячы вызоў пад час стварэння індэкса. Гэта дорога і медленная процедура: такі падход дазволяе атрымаць значна багатэйшую структуру дадзенаў за адну раз. Індэкс графа RAG ніколі не трэба перзбудаваць праз кожны запуск. Неабходна зберагчы граф і перзаванаць яго, так сама як і у простай версіі.
Калі выбіраць гэты падход
Выбірайце граф RAG, калі запіты выклікаюць патрабаванне да ланцаў асоціяцый. Тыповыя прыклады — аналіз супакоўнасці і рызыкаў (якія працэсы паўтараюць наэфект ад певных правілаў), аналіз ланцоў постачання (якія продукты залежнаць ад прадавца), організацыйныя знанні (хто ўладае чым і якімі ланцамі ўлады ўзаімаюцься) і картаванне залежнасцяў програмнага забезпечэння (што зламаецца, якшо будзе адключаны модуль).
Архітэктура 4: распраўнены RAG
Развітая тэхніка RAG — гэта не адна новая методыка. Гэта структураваны набор удосконаленняя, якія дадаюцца на базе яе існуючага механізма пошуку. Простая тэхніка RAG павераецца першым раундам пошуку, тады калі развітая тэхніка RAG ставіць запітанні, дапрацоўвае і пераканальваецца ў правільнасці рэзультатаў. Удосконаленняя дзеляцца на тры групы, якія прыкладываюцца да разных этапаў працэсу: да пошуку, пад час апрацоўкі запіту і пасля пошуку.
Да пошуку: перапісва запіту і HyDE
Тое, што пішаў корыстнік, рэдка калі ёсць найкрашчы запіт для пошуку. Проста запытанне пра правілы вярнення товара для корпоратыўных кліяўцоў ўсё ж здаецца цэлкам нормальным для чалавека, але даўа горшыя рэзультаты, чым фраза на кшталт "процедуры вярнення і вырачэння грошаў для корпоратыўных кліяўцоў". Перапісва запіту дазволяе ШЫ ператварыць запытанне на лепшы запіт для пошуку прычым яно не дасягне індэксу.
HyDE, як скраценна назва Hypothetical Document Embedding, выкорыстоўвае іншы падход. У замяне пошуку за дапамою запіту ён просіць LLM напісаць гіпотэтычны документ, які мог бы на яго адпаведаць, а пасля вбудоввае гэты документ для пошуку. Ідэя заключаецца у тым, што тэкст у фармаце адпаведзі выстоіць у вектарнам прасторэ бліжэй да іншага тэксту у фармаце адпаведзі, чым тэкст у фармаце запіту.
Разбіўка запіту
Багатоэтапныя запіты не падходзяць для простага падходу RAG, адколькі для ўсунення ўсіх працягоў неабходны кілька крокаў пошуку. Развіты падход RAG спачатку разбівае іх на частыні. Напрыклад, запіт пра порэванне політык вярнення грошаў для корпоратыўных і роздробных кліянтаў з паслягэтым узагальненням основных разлікаў на самай працоўцы складаецца з трохоў задач: запрашаецца даныя па політыцы для корпоратыўных кліянтаў, запрашаецца даныя па політыцы для роздробных кліянтаў, пасля чаго яны порэвнююцца. Разбіўка запіту дазволяе ператворыць яго на падзапіты, адбыць пошук для кожнага з іх і з’едыніць рэзультаты першым чым стварыць гэтальную адпаведзь.
Пасля пошуку: пераранжаванне і CRAG
Пераранжаванне, якое ужо рассматрывалася ў рамках гібрыднага пошуку, знову практыкуецца на этапе апрацоўкы рэзультатаў: крос-кодэр перацэнюе всі заўважаны фрагменты, нават калі пошук быў чыста інтэнсіўным.
CRAG (Corrective RAG) — гэта найболей складная тэхніка пасля адзысквання інфармацыі. Пасля таго, як інфармацыя заўважана, яна адбывае швыракі чыннік сумяжнасці для фрагментаў. Калі яны здаюцца недастатнімі, сістэма пераходзіць да іншага джэрага, напрыклад, да веб-пошуку або шырэйшагі індэксу, каб LLM не была змушана адпавядаць на пытанні на адносна слабкай базе даных.
Код
Адна з рэалізацыяў адразу застаўляе калькі з гэтых тэхнік:
# ============================================================
# 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 для чыстых, фактычных запытанняў і адпаведзей у дакументах, перайдзіце на гібрыдны падход, калі тэрмінологія ёсць спецыялізаванай або важлівыя точныя ідэнтыфікаторы; выберыце графічны 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 — на кераванні, гэтыя падходы падтрымаюць практычна все, што патрэбна продакшн-зялодзею для адміністрацыі.