LlamaIndex Plus LangGraph: Чистий патерн для агентів RAG
Як створити базу знань за допомогою LlamaIndex та зробити її доступною для агента LangGraph як інструменту, з поясненням функцій зберігання даних, використання Chroma та режиму лише зворотного пошуку.
Джерела: RAG Without the Guesswork: A Standardized LangGraph + LlamaIndex Pattern.
Сліпа зона та розподіл роботи
Агент LangGraph знає лише те, що його LLM опанував під час навчання. На запитання про внутрішню політику ціноутворення, фінансовий звіт минулого кварталу чи посібник, завершений минулого тижня, він відповідає або визнає свою некомпетентність, або, що гірше, вигадує відповіді з упевненістю.
Retrieval-Augmented Generation (RAG) закриває цю прогалину. Під час отримання запиту він знаходить відповідні уривки з ваших власних документів та передає їх LLM як контекст перед тим, як той дасть відповідь, тож модель читає інформацію замість того, щоб здогадуватися. RAG — це функція, додана до окремого агента. Це не новий патерн оркестрації, тому керівники, підграфи та команда Send не беруть участі в цьому процесі. Все, що потрібно, — це певне розуміння структури агента LangGraph та способу надання йому інструментів.
Можливо створити потік обробки даних вручну всередині LangGraph: написати власний інструмент для розділення даних на частини, функції генерації ембеддингів, код для індексування векторів та їх пошуку. Багато команд так і роблять, хоча це значний обсяг роботи для проблеми, яка вже має рішення. LlamaIndex створений для того, щоб взяти на себе цю роботу. Це, безумовно, найбільш розвинена та популярна опція для обробки даних у AI-застосунках — вона охоплює завантаження, розділення на частини, генерацію ембеддингів, індексування та пошук. Частина, пов’язана з оркестрацією, тобто керування станом, логікою виконання та багатокроковим міркуванням, належить LangGraph.
Саме через це розділення існує схема, яку використовують більшість продакшн-систем RAG: LlamaIndex створює та надає базу знань. LangGraph розглядає її як інструмент та вирішує, коли її використовувати.
Ментальна модель LlamaIndex
Уявіть LlamaIndex як бібліотекаря для вашого LLM. LLM схожий на талановитого дослідницького асистента, який прочитав багато матеріалів з Інтернету, але ніколи не відкривав файлів вашої компанії, і ніхто не може попросити його запам’ятати зошит на 10 000 сторінок. Тож втручається бібліотекар: він читає кожен документ, розділяє його на невеликі картки для пошуку (фрагменти), зберігає ці картки там, де пошук за темою відбувається швидко (у векторному індексі), і витягує лише 3–5 найбільш релевантних карток кожного разу, коли асистент ставить запитання. Асистент ніколи нічого не запам’ятовує. Надходять кілька уривків, і міркування відбуваються на їхній основі.
П’ятиетапний процес
Кожна система RAG, незалежно від фреймворку, проходить через ті самі п’ять етапів, і LlamaIndex пропонує чітку абстракцію для кожного з них.
1. LOAD → Read raw files (PDF, Word, web pages, databases) into Documents
2. CHUNK → Split Documents into small, retrievable Nodes
3. EMBED → Convert each Node's text into a vector (a list of numbers
representing meaning)
4. STORE → Save those vectors in a Vector Index for fast lookup
5. RETRIEVE → At query time, embed the user's question, find the most
similar Nodes, and return them as context
Перші чотири етапи виконуються один раз під час створення бази знань, що називається індексуванням. П’ятий етап виконується щоразу, коли користувач ставить запит, що називається запитуванням. Співвідношення часу індексування та часу запитування, ймовірно, є найважливішою концепцією в RAG, і варто її запам’ятати.
Словниковий запас
Документ— це необроблений фрагмент вихідного контенту, такий як один PDF, одна веб-сторінка або один рядок бази даних разом із його метаданими. Це непроцесована, повноформатна одиниця.Вузол— це фрагмент, вирізаний зДокумента. Моделі LLM та моделі ембеддингів працюють краще з концентрованим, коротким текстом, ніж з цілими документами, тому LlamaIndex розділяє коженДокументна багато об’єктівВузла. Вузли передають метадані своєму батьківському елементу, тож будь-який фрагмент можна простежити до його джерела.SimpleDirectoryReader— це звичайний завантажувач: достатньо вказати йому папку, і він буде читати PDF-файли, текстові файли, файли Word та інші формати, створюючи по одному або кількох об’єктівDocumentна кожен файл.VectorStoreIndex— це основний клас для створення індексу. Маючи об’єктиDocumentабоNode, він вбудовує їх та формує індекс векторів, який можна шукати, або в пам’яті, або в зовнішній базі даних векторів у разі її налаштування.Settings— це об’єкт глобальної конфігурації. На ньому один раз налаштовуються модель LLM та модель ембеддингів, і кожен компонент бере їх звідти, тому їх не потрібно передавати при кожному виклику.QueryEngine— це те, до чого ви насправді ставите запитання. Він отримується через.as_query_engine()у індексі, і один виклик включає запит, виконує пошук за схожістю та передає отриманий контекст у LLM для остаточної відповіді.Retrieverотримується через.as_retriever()у індексі та обробляє лише крок пошуку. Він повертає необроблені об’єктиNode, що відповідають запиту, та не генерує відповіді, що підходить у випадках, коли пошук та генерація мають бути окремими, контрольованими кроками. Саме така ситуація виникає, коли використовується LangGraph.
Спочатку створюється сама конвеєрна схема, без LangGraph.
Робоча конвеєрна схема приблизно з десяти рядків
Спочатку відбувається встановлення.
# Core package + OpenAI LLM and embedding integrations (the common starting setup)
pip install llama-index-core llama-index-llms-openai llama-index-embeddings-openai
# Readers for common file types (PDF, Word, etc.)
pip install llama-index-readers-file pypdf
LlamaIndex навмисно постачається у вигляді багатьох невеликих пакетів. llama-index-core містить логіку фреймворку, тоді як вибір LLM, моделі ембеддингів, векторної бази даних та читача файлів знаходиться у окремих пакетах, які встановлюються лише за потреби. Це дозволяє зробити процес встановлення легким.
Невеликий файл конфігурації встановлює загальні значення за замовчуванням.
# ── llamaindex_config.py ─────────────────────────────────────
import os
from llama_index.core import Settings
from llama_index.llms.openai import OpenAI
from llama_index.embeddings.openai import OpenAIEmbedding
# Settings is global - configure once, used everywhere in LlamaIndex
Settings.llm = OpenAI(
model="gpt-4o-mini", # Used for generating final answers from retrieved context
temperature=0.1, # Low temperature: factual, not creative
)
Settings.embed_model = OpenAIEmbedding(
model="text-embedding-3-small", # Used to convert text into vectors
)
# Controls how documents are split into Nodes (chunks)
Settings.chunk_size = 512 # Max tokens per chunk
Settings.chunk_overlap = 50 # Overlap between consecutive chunks, to preserve context across boundaries
Щодо chunk_size: занадто великі частини містять настільки багато тексту, що отримання інформації стає нечітким, адже завантажена частина може бути лише на 10% релевантною. Занадто малі частини втрачають навколишній контекст, необхідний ШІ для розуміння факту. Зазвичай прийнятним є початкове значення 512 токенів із перекриттям у 50 токенів, після чого вносяться корективи залежно від документів.
А тепер про саму базу знань:
# ── build_knowledge_base.py ──────────────────────────────────
from llama_index.core import SimpleDirectoryReader, VectorStoreIndex
# ── LOAD: Read all files in a folder into Document objects ──
documents = SimpleDirectoryReader("./data").load_data()
print(f"Loaded {len(documents)} documents")
# ── CHUNK + EMBED + STORE: all three happen inside this one call ──
# VectorStoreIndex automatically:
# 1. Splits each Document into Nodes (using Settings.chunk_size)
# 2. Embeds each Node (using Settings.embed_model)
# 3. Stores the vectors in an in-memory index
index = VectorStoreIndex.from_documents(documents, show_progress=True)
# ── RETRIEVE + GENERATE: ask a question ──────────────────────
query_engine = index.as_query_engine(
similarity_top_k=3, # Retrieve the 3 most relevant chunks for each query
)
response = query_engine.query("What is our refund policy for enterprise customers?")
print(response)
Приблизно десять рядків дозволяють створити функціональну систему RAG, яка є своєрідним „hello world“ цієї платформи та переконливим прикладом її необхідності. Виконання цього вручну вимагатиме створення власного розділювача, циклу для виклику моделі ембеддингу, пошуку найближчого сусіда та шаблону запиту.
Зберігання індексу
Під час створення індексу кожен фрагмент проходить через модель ембеддингу, що коштує грошей та часу, тому його перестроювання з кожним запуском додатку є марнотратним. Зберігання індексу на диску та його завантаження пізніше дозволяє уникнути цього.
# ── Save the index after building it ─────────────────────────
index.storage_context.persist(persist_dir="./storage")
# ── Load it back later without re-embedding anything ──────────
from llama_index.core import StorageContext, load_index_from_storage
storage_context = StorageContext.from_defaults(persist_dir="./storage")
index = load_index_from_storage(storage_context)
storage_context.persist() зберігає векторні дані, метадані вузлів та базу документів у локальній папці, а load_index_from_storage() створює ідентичний індекс з цих файлів без повторного вбудовування даних та без викликів API. Цей підхід ґрунтується на тій самій ідеї одноразової створення, зберігання та недорогого завантаження даних, що й у checkpointer LangGraph.
Справжня векторна база даних
Індекс у оперативній пам’яті підходить для прототипів. У продакшені зазвичай потрібна спеціалізована векторна база даних, розроблена для обробки мільйонів векторів, одночасних читань та фільтрації метаданих у великих масштабах. Прикладом є Chroma.
# ── Using Chroma as a persistent, production-grade vector store ──
# pip install llama-index-vector-stores-chroma chromadb
import chromadb
from llama_index.vector_stores.chroma import ChromaVectorStore
from llama_index.core import StorageContext, VectorStoreIndex
chroma_client = chromadb.PersistentClient(path="./chroma_db")
chroma_collection = chroma_client.get_or_create_collection("my_knowledge_base")
vector_store = ChromaVectorStore(chroma_collection=chroma_collection)
storage_context = StorageContext.from_defaults(vector_store=vector_store)
# Build the index directly into Chroma
index = VectorStoreIndex.from_documents(
documents,
storage_context=storage_context
)
# Later, in a different process, reconnect without re-indexing:
index = VectorStoreIndex.from_vector_store(vector_store=vector_store)
Цей механізм обміну схожий на системи контрольних точок. Замінюється лише один об’єкт (буфер пам’яті для ChromaVectorStore, так само, як MemorySaver поступається місцем SqliteSaver), а двигун запитів та логіка пошуку залишаються незмінними. Pinecone, Qdrant, Weaviate та більшість інших векторних баз даних, які підтримує LlamaIndex, працюють аналогічним чином.
З’єднання двох фреймворків
Ось у чому суть. Фреймворки використовують різні мови: QueryEngine від LlamaIndex не є інструментом LangGraph, оскільки агент LangGraph очікує інструменти, створені за допомогою декоратора @tool від LangChain.
Міст є простим. Визначте звичайну функцію на Python, додайте до неї анотацію @tool, і нехай її тіло викликає query_engine.query(...). Це і є вся інтеграція. LangGraph не повинен нічого знати про внутрішню структуру LlamaIndex, адже він бачить інструмент, який приймає рядок та повертає рядок, як і будь-який інший інструмент.
# ── MODULE 3: TOOLS (LlamaIndex-backed) ─────────────────────
from langchain_core.tools import tool
# The query_engine built in Part B - created once, at startup
# (In a real app, you'd load this from persisted storage, not rebuild it every time)
@tool
def search_knowledge_base(query: str) -> str:
"""Search the internal knowledge base for company policies, product
documentation, and internal procedures. Use this whenever the user asks
a question that might be answered by internal company documents rather
than general knowledge.
Args:
query: A natural-language question to search for.
Returns:
A synthesized answer based on the most relevant retrieved documents.
"""
response = query_engine.query(query)
return str(response)
Документація має таке ж значення, як і для будь-якого інструменту. Штучний інтелект агента читає її, щоб вирішити, коли викликати цей інструмент, замість того, щоб відповідати на основі власних знань чи викликати інший інструмент.
Повний агент
Ось повний шаблон агента з інструментом RAG, підтримуваним LlamaIndex, вставленим у модуль інструментів. Більше нічого не змінюється.
# ============================================================
# LANGGRAPH + LLAMAINDEX RAG AGENT — COMPLETE TEMPLATE
# Extends: Part 1 (core structure)
# ============================================================
# ── MODULE 1: IMPORTS & CONFIGURATION ───────────────────────
import os
from typing import Literal
# LangChain / LangGraph imports (the orchestration layer)
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
# LlamaIndex imports (the retrieval / data layer)
from llama_index.core import (1
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 chat model - used by the agent's reasoning
llm = ChatOpenAI(model="gpt-4o", temperature=0)
# LlamaIndex's model config - used internally by the query engine
# Note: these are SEPARATE from the LangGraph llm above. Each framework
# manages its own model instances; they don't share state.
Settings.llm = LlamaOpenAI(model="gpt-4o-mini", temperature=0.1)
Settings.embed_model = OpenAIEmbedding(model="text-embedding-3-small")
Settings.chunk_size = 512
# ── MODULE 2: STATE ──────────────────────────────────────────
class State(MessagesState):
pass # messages field inherited; extend if your agent needs more
# ── MODULE 3: TOOLS (RAG-backed) ─────────────────────────────
# Build or load the LlamaIndex knowledge base ONCE, at startup
PERSIST_DIR = "./storage"
if os.path.exists(PERSIST_DIR):
# Reload existing index - no re-embedding, fast startup
storage_context = StorageContext.from_defaults(persist_dir=PERSIST_DIR)
index = load_index_from_storage(storage_context)
else:
# First run - build the index and persist it
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)
@tool
def search_knowledge_base(query: str) -> str:
"""Search internal company documents for policies, product specs,
procedures, and other domain-specific information. Use this for any
question that requires knowledge specific to this organization 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:
"""The reasoning node. Decides whether to answer directly or
search the knowledge base first."""
system_prompt = SystemMessage(content=(
"You are a helpful assistant with access to an internal knowledge base. "
"Use the search_knowledge_base tool when the user asks about company-specific "
"information. For general questions, answer directly."
))
messages = [system_prompt] + state["messages"]
response = llm_with_tools.invoke(messages)
return {"messages": [response]}
# ── MODULE 5: ROUTING ────────────────────────────────────────
def should_continue(state: State) -> Literal["tools", "__end__"]:
last_message = state["messages"][-1]
if hasattr(last_message, "tool_calls") and last_message.tool_calls:
return "tools"
return "__end__"
# ── MODULE 6: GRAPH ASSEMBLY ─────────────────────────────────
graph_builder = StateGraph(State)
graph_builder.add_node("agent", agent_node)
graph_builder.add_node("tools", tool_node)
graph_builder.add_edge(START, "agent")
graph_builder.add_conditional_edges(
"agent", should_continue,
{"tools": "tools", "__end__": END}
)
graph_builder.add_edge("tools", "agent")
graph = graph_builder.compile(checkpointer=MemorySaver())
# ── MODULE 7: ENTRYPOINT ──────────────────────────────────────
if __name__ == "__main__":
config = {"configurable": {"thread_id": "session-001"}}
print("RAG agent ready. Ask about your documents, or anything else.\n")
while True:
user_text = input("You: ").strip()
if not user_text or user_text.lower() == "exit":
break
response = graph.invoke(
{"messages": [HumanMessage(content=user_text)]},
config=config
)
print(f"Agent: {response['messages'][-1].content}\n")
Що відбувається під час одного запиту
Відстеження окремого запиту дозволяє чітко побачити розподіл обов’язків.
User: "What's our policy on remote work?"
↓
[agent_node] — LangGraph's LLM reads the message, recognizes this needs
internal info, decides to call search_knowledge_base
↓
[tools] — ToolNode executes search_knowledge_base("What's our policy on remote work?")
↓
Inside the tool: query_engine.query(...) runs —
this is 100% LlamaIndex, invisible to LangGraph:
1. Embeds the query
2. Searches the vector index for the 3 closest chunks
3. Feeds those chunks + the question to Settings.llm
4. Returns a synthesized answer string
↓
[agent_node] — LangGraph's LLM receives the tool's string result,
and crafts the final response shown to the user
↓
Response to user
Межа функціонування є чіткою. LangGraph ніколи не стикається з Node, ембеддингом чи вектором — лише з інструментом, який повертає текст, що не відрізняється від search_web чи calculate. LlamaIndex також не має справи з messages, state чи thread_id — тут є лише вхідний рядок запиту та вихідна відповідь. Кожна платформа використовує свої сильні сторони, а з’єднання між ними здійснюється через один виклик функції.
Режим лише збирача даних для більшого контролю
Схема query_engine.query() є найпростішою для інтеграції та підходить у більшості випадків. Іноді потрібен більший контроль, наприклад, коли власний LLM агента LangGraph має написати кінцеву відповідь, використовуючи наявні системні інструкції та контекст розмови, а не внутрішні налаштування Settings.llm LlamaIndex, які генерують окрему, не пов’язану між собою відповідь.
У такому випадку використовуйте Retriever замість QueryEngine. Він виконує лише крок пошуку та повертає необроблені фрагменти даних, а подальші дії залежать від агента.
# ── Retriever-only tool: returns raw chunks, not a synthesized answer ──
retriever = index.as_retriever(similarity_top_k=3)
@tool
def retrieve_documents(query: str) -> str:
"""Retrieve relevant document excerpts from the internal knowledge base.
Returns raw excerpts for you to read and reason over yourself -
use this when you need to cite specific sources or combine information
from multiple documents."""
nodes = retriever.retrieve(query)
# Format each retrieved chunk with its source for transparency
formatted_chunks = []
for i, node in enumerate(nodes):
source = node.metadata.get("file_name", "unknown source")
formatted_chunks.append(f"[Excerpt {i+1} from {source}]\n{node.text}")
return "\n\n---\n\n".join(formatted_chunks)
За такої конфігурації власний виклик LLM у agent_node, який використовує llm_with_tools разом із повною історією розмови та системним запитом, читає необроблені уривки та формує відповідь. Це приносить реальні переваги: агент може вказати, з якого документа походить факт, може поєднувати отримані уривки з попередніми розмовами, а користувачі чують єдиний голос, а не два LLM у різних стилях.
Який варіант обрати
Оберіть QueryEngine для найпростішої інтеграції, коли достатньо, щоб LLM від LlamaIndex генерував відповідь на основі пошуку у вигляді самодостатнього рядка. Це підходить для більшості чат-ботів та асистентів.
Виберіть Retriever, коли потрібні посилання на джерела, коли основний LLM агента має зберігати стабільний тон та стиль міркувань як у отриманих, так і у невідновлених відповідях, або коли отримані фрагменти потрібно поєднати з результатами інших інструментів перед остаточною відповіддю.
Посилання на ключові слова
Завантаження та індексування. Document — це завантажений джерело, один файл або одна сторінка. Node — це його частина, одиниця, яку можна отримати, з метаданими, що вказують на її походження. SimpleDirectoryReader("./path").load_data() завантажує кожен підтримуваний файл у папці як об’єкти Document. Settings — це глобальний синглтон конфігурації, а Settings.llm, Settings.embed_model та Settings.chunk_size налаштовуються один раз для всіх компонентів. VectorStoreIndex.from_documents(documents) розділяє на частини, вбудовує та зберігає дані за один виклик.
Запитування. index.as_query_engine(similarity_top_k=3) створює об’єкт QueryEngine, який отримує топ-k фрагментів та також формує синтезовану відповідь. index.as_retriever(similarity_top_k=3) створює об’єкт Retriever, який отримує лише топ-k фрагментів, що підходить для сценаріїв, де синтез здійснює ваш власний LLM. query_engine.query("...") виконує повний процес отримання даних та їх генерації та повертає об’єкт Response, а str(response) витягує текст. retriever.retrieve("...") виконує лише процес отримання даних та повертає об’єкти Node, які містять поля .text та .metadata.
Стійкість. Функція index.storage_context.persist(persist_dir="./storage") зберігає індекс, тож після перезапуску не потрібно його повторно імпортувати. Функція StorageContext.from_defaults(persist_dir="./storage") у поєднанні з load_index_from_storage(...) завантажує його знову. ChromaVectorStore, PineconeVectorStore та QdrantVectorStore — це зовнішні бекенди, які замінюють стандартний внутрішній механізм зберігання, за тим самим принципом, що й механізми створення контрольних точок у LangGraph.
Міст. Функція @tool, тіло якої викликає query_engine.query(...), і є всією інтеграцією: LangGraph бачить звичайний інструмент, а LlamaIndex залишається інкапсульованим. Існують також дві окремі конфігурації LLM. Settings.llm — це внутрішня модель LlamaIndex, яка використовується лише всередині двигуна запитів, тоді як llm у LangGraph агента відповідає за основні міркування. Вони працюють незалежно та не діляться історією чи станом.
Чи потрібно вам це?
- Якщо агенту потрібні лише загальні знання, математика чи пошук у мережі в реальному часі, LlamaIndex не потрібен, і достатньо базової шаблонної структури зі стандартними інструментами.
- Якщо він має відповідати на запити на основі фіксованого набору документів, таких як політики, посібники чи звіти, які рідко змінюються, то обгортання
QueryEngineу вигляді інструменту є саме тим рішенням.
Retriever.interrupt() від LangGraph та розмістіть виклик пошуку в вузлі, який чекає на схвалення, як це робиться з будь-яким конфіденційним інструментом.Два фреймворки та один з’єднувальний елемент
Основний урок полягає у тому, що надання агенту знань з реального світу — це не проблема LangGraph. Це проблема даних, і для її вирішення підходить фреймворк для роботи з даними. Складна багатоагентна система насправді є сукупністю кількох одноагентних графів, і доступ до знань також ґрунтується на принципах модульності.
LlamaIndex відповідає за індексування та пошук, а LangGraph — за міркування та координацію. Усе це об’єднано в одну функцію, позначену @tool. Це не є обмеженням, а оптимальним рівнем взаємозв’язку, адже кожен фреймворк залишається у своїй сфері, і кожну зі сторін можна оновлювати незалежно. Векторну базу даних можна замінити, не торкаючись графу, а паттерн керування можна додати, не впливаючи на логіку пошуку.
Пов’язана література
- MCP для агентів LangGraph, крок за кроком: сервери, механізми передачі даних та містки адаптерів — Дізнайтеся про протокол Model Context Protocol з точки зору ролей хоста, клієнта та сервера, про сервер FastMCP, відмінності між stdio та HTTP, адаптери langchain-mcp та поширені проблеми під час роботи в LangGraph.
- Створіть ShopBot: агента підтримки клієнтів у LangGraph із функціями пам’яті, інструментів та схвалення повернення грошей — Створіть агента підтримки клієнтів у LangGraph поетапно: визначте стани, інструменти, вузли, механізми маршрутизації, функції підсумовування інформації та механізм переривання, який чекає на схвалення повернення грошей.