Ваша модель отвечает уверенно — но врёт. Она не знает о вашей внутренней документации, о приказах за прошлый квартал, о базе клиентов. Это не баг GPT-4o или Claude — это архитектурная проблема. Решение называется RAG: Retrieval-Augmented Generation.

За один рабочий день вы можете собрать систему, которая ищет нужные фрагменты в ваших документах и передаёт их модели как контекст. Без дообучения, без дорогих GPU, без магии. Только Python, несколько библиотек и здравый смысл.

Что такое RAG и почему это не просто “поиск”

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


graph LR
    A[Вопрос пользователя] --> B[Embedding-модель]
    B --> C[Векторный поиск]
    D[База документов] --> E[Chunking + Indexing]
    E --> C
    C --> F[Топ-K фрагментов]
    F --> G[LLM с контекстом]
    A --> G
    G --> H[Ответ]

В отличие от простого keyword-поиска, векторный поиск понимает семантику. Запрос «как оформить отпуск» найдёт документ «порядок предоставления ежегодного оплачиваемого отдыха» — потому что их смысловые векторы близки в многомерном пространстве.

Языковая модель — это рассуждатель, а не хранилище. RAG даёт ей факты, а она умеет с ними работать.

Классический RAG состоит из двух фаз:

  1. Indexing (офлайн) — загружаем документы, нарезаем на чанки, превращаем в векторы, сохраняем в векторную БД.
  2. Retrieval + Generation (онлайн) — принимаем вопрос, ищем похожие чанки, формируем промпт, получаем ответ от LLM.

Шаг 1: Окружение и выбор инструментов

Для старта вам нужны три компонента: оркестратор, embedding-модель и векторная база.

Оркестратор

Два главных варианта в 2026 году:

ФреймворкПлюсыКогда выбирать
LangChainГибкость, огромная экосистема, лучший для агентовСложные пайплайны, много интеграций
LlamaIndexПроще для документов, нативный RAGDocument-heavy задачи, быстрый старт
Без фреймворкаПолный контроль, меньше магииПонимание основ, кастомные решения

Для этого туториала берём LlamaIndex — он оптимизирован именно под RAG и позволяет собрать рабочий прототип за 20 строк кода.

Векторная база данных

БазаSelf-hostedManagedЛучший сценарий
ChromaБесплатноБесплатно до 1M векторовПрототипы, разработка
Qdrant~$30-50/мес (VPS)от $100/месПродакшн, высокая нагрузка
Weaviate~$50-100/месот $150/месГибридный поиск, мультимодальность
Pineconeот $70/месEnterprise, managed без забот
pgvectorВ рамках PostgreSQLУже есть Postgres

Для первого дня — Chroma. Работает локально, не требует инфраструктуры, хранит данные на диске.

Установка

pip install llama-index llama-index-vector-stores-chroma \
    llama-index-embeddings-openai chromadb openai python-dotenv

Создайте .env:

OPENAI_API_KEY=sk-...
💡 Альтернатива без OpenAI
Если хотите работать полностью локально — замените OpenAI на Ollama. Установите Ollama, скачайте nomic-embed-text для эмбеддингов и llama3.2 для генерации. LlamaIndex поддерживает оба варианта через OllamaEmbedding и Ollama LLM.

Шаг 2: Indexing — загружаем и нарезаем документы

Качество RAG-системы на 80% определяется качеством индексирования. Плохие чанки — плохие ответы, независимо от того, насколько умна ваша LLM.

Загрузка документов

from llama_index.core import SimpleDirectoryReader, Settings
from llama_index.embeddings.openai import OpenAIEmbedding
from llama_index.llms.openai import OpenAI
from dotenv import load_dotenv

load_dotenv()

# Настраиваем модели глобально
Settings.embed_model = OpenAIEmbedding(model="text-embedding-3-small")
Settings.llm = OpenAI(model="gpt-4o-mini", temperature=0.1)

# Загружаем документы из папки
documents = SimpleDirectoryReader("./docs").load_data()
print(f"Загружено документов: {len(documents)}")

LlamaIndex умеет читать PDF, DOCX, TXT, HTML, Markdown и ещё дюжину форматов — всё через SimpleDirectoryReader.

Нарезка на чанки (chunking)

Это критический шаг. Чанк — это фрагмент текста, который попадёт в промпт как один контекстный блок.

from llama_index.core.node_parser import SentenceSplitter

# Оптимальные параметры для большинства задач
splitter = SentenceSplitter(
    chunk_size=512,      # токены на чанк
    chunk_overlap=50     # перекрытие для сохранения контекста
)

nodes = splitter.get_nodes_from_documents(documents)
print(f"Создано чанков: {len(nodes)}")
⚠ Частая ошибка с chunk_size
Не делайте чанки слишком большими (>1024 токенов) — модель получит слишком много шума и точность упадёт. Слишком маленькие (<128 токенов) — потеряете контекст. Для большинства задач оптимум: 256–512 токенов с overlap 10–15%.

Создание индекса в Chroma

import chromadb
from llama_index.vector_stores.chroma import ChromaVectorStore
from llama_index.core import VectorStoreIndex, StorageContext

# Инициализируем Chroma с сохранением на диск
chroma_client = chromadb.PersistentClient(path="./chroma_db")
chroma_collection = chroma_client.get_or_create_collection("my_docs")

# Создаём vector store и индекс
vector_store = ChromaVectorStore(chroma_collection=chroma_collection)
storage_context = StorageContext.from_defaults(vector_store=vector_store)

index = VectorStoreIndex(
    nodes,
    storage_context=storage_context,
)

print("Индексирование завершено. База сохранена в ./chroma_db")

Первый запуск отправит все чанки в OpenAI Embeddings API и сохранит векторы локально. При следующем запуске достаточно загрузить уже существующий индекс.

Шаг 3: Retrieval + Generation — задаём вопросы

Индекс готов. Теперь самая приятная часть — спрашиваем.

from llama_index.core import VectorStoreIndex
from llama_index.vector_stores.chroma import ChromaVectorStore
from llama_index.core import StorageContext
import chromadb

# Загружаем существующий индекс (не нужно индексировать заново)
chroma_client = chromadb.PersistentClient(path="./chroma_db")
chroma_collection = chroma_client.get_or_create_collection("my_docs")
vector_store = ChromaVectorStore(chroma_collection=chroma_collection)
storage_context = StorageContext.from_defaults(vector_store=vector_store)
index = VectorStoreIndex.from_vector_store(vector_store, storage_context=storage_context)

# Создаём query engine
query_engine = index.as_query_engine(
    similarity_top_k=5,       # сколько чанков извлекать
    response_mode="compact",  # compact | refine | tree_summarize
)

# Задаём вопрос
response = query_engine.query(
    "Какой порядок согласования командировок?"
)

print(response)

# Смотрим источники
for node in response.source_nodes:
    print(f"\n--- Источник (score: {node.score:.3f}) ---")
    print(node.text[:200])
ℹ Режимы ответа
compact — объединяет чанки в один промпт, быстро и дёшево. refine — итеративно уточняет ответ по каждому чанку, точнее но дороже. tree_summarize — строит иерархическое резюме, лучший вариант для длинных документов.

Шаг 4: Оцениваем качество и улучшаем

Рабочий прототип готов. Но прежде чем отдавать его пользователям — нужно понять, насколько он хорош.

Метрики RAG

Есть три ключевых вопроса:

  1. Retrieval Relevance — нашли ли мы правильные чанки?
  2. Faithfulness — ответ основан на найденных чанках или LLM придумала?
  3. Answer Relevance — ответ действительно отвечает на вопрос?

Самый простой способ оценить — добавить логирование source_nodes и вручную проверить 20–30 запросов. Для автоматической оценки используйте RAGAS (pip install ragas) — он считает все три метрики через ещё один вызов LLM.

Типичные проблемы и решения

ПроблемаСимптомРешение
Плохое извлечениеОтвет не по темеУменьшить chunk_size, попробовать hybrid search
ГаллюцинацииФакты не из документовСнизить temperature, добавить system prompt «отвечай только по контексту»
Медленный ответ>5 сек на запросУменьшить similarity_top_k, кэшировать эмбеддинги
Потеря контекстаОбрезанный ответУвеличить chunk_overlap, использовать refine mode
📝 Быстрая проверка качества retrieval
Добавьте в промпт инструкцию: «В конце ответа укажи, какие именно документы ты использовал». Если модель называет нерелевантные источники — проблема в retrieval. Если называет правильные, но врёт — проблема в generation (снижайте temperature, ужесточайте системный промпт).

Простейший улучшенный промпт

from llama_index.core import PromptTemplate

qa_prompt = PromptTemplate(
    """Ты — помощник, который отвечает на вопросы строго на основе предоставленного контекста.

Контекст:
{context_str}

Вопрос: {query_str}

Правила:
- Отвечай только на основе контекста выше
- Если информации нет в контексте — честно скажи об этом
- Давай конкретные ответы без лишней воды
- Если можешь — укажи, из какого раздела взята информация

Ответ:"""
)

query_engine = index.as_query_engine(
    similarity_top_k=5,
    text_qa_template=qa_prompt,
)

Шаг 5: Путь к продакшну

За один день вы получили работающий прототип. Что дальше, если система должна работать в проде?

Переход от Chroma к production-ready базе

Для продакшна рекомендуется Qdrant или Weaviate. Миграция занимает 15 минут — меняете только vector store, остальной код остаётся.

# Было (Chroma)
from llama_index.vector_stores.chroma import ChromaVectorStore

# Стало (Qdrant)
from llama_index.vector_stores.qdrant import QdrantVectorStore
from qdrant_client import QdrantClient

client = QdrantClient(url="http://localhost:6333")
vector_store = QdrantVectorStore(client=client, collection_name="my_docs")

Что улучшить в следующие итерации

Hybrid search — комбинирует векторный поиск с BM25 (keyword). Работает лучше для терминологии, аббревиатур, имён собственных. Поддерживается в Weaviate и Qdrant из коробки.

Reranking — после retrieval добавляете второй проход: cross-encoder модель (например, cross-encoder/ms-marco-MiniLM-L-6-v2) перепроверяет релевантность каждого чанка. Точность растёт на 10–20%.

Metadata filtering — к каждому чанку добавляете метаданные (дата, отдел, тип документа) и фильтруете при поиске. «Найди только в документах HR-отдела за 2025 год».

Кэширование — один и тот же вопрос не должен каждый раз идти в OpenAI. Кэшируйте эмбеддинги запросов через Redis или локальный dict.

Лучшая RAG-система — это не та, что использует самые сложные алгоритмы, а та, у которой хорошо структурированы документы и правильно подобраны размеры чанков.

Заключение

RAG — это не rocket science. За один день реально собрать систему, которая:

  • Индексирует PDF, DOCX, TXT из вашей папки
  • Отвечает на вопросы строго по вашим документам
  • Показывает источники для каждого ответа
  • Работает на локальной машине без серверной инфраструктуры

Стек для старта: LlamaIndex + Chroma + text-embedding-3-small + gpt-4o-mini. Стоимость индексирования 1000 страниц A4 — меньше $1. Стоимость ответа на вопрос — меньше цента.

Ключевые решения, которые определяют качество:

  1. Chunk size (512 токенов — хороший старт)
  2. similarity_top_k (5 — баланс точности и скорости)
  3. Системный промпт (явный запрет галлюцинаций)
  4. Качество исходных документов (мусор на входе — мусор на выходе)

Начните с простого прототипа, измерьте качество вручную, потом улучшайте. Это быстрее и дешевле, чем сразу проектировать сложную систему с гибридным поиском и reranking.