Туториалы

RAG система: как построить и когда нужна

LLM знает то, на чём обучена. Ваши внутренние документы, база знаний, тикеты в Jira, контракты — не знает. Fine-tuning дорогой и негибкий: каждое обновление данных требует переобучения. RAG (Retrieval-Augmented Generation) решает задачу иначе — подставляет релевантные фрагменты из внешнего хранилища прямо в контекст модели перед генерацией.

Схема работы: пользователь задаёт вопрос, система находит подходящие куски из базы знаний, передаёт их в промпт LLM вместе с вопросом, модель генерирует ответ на основе переданного контекста. Переобучать модель не нужно. Хранить все данные в контексте — тоже. Данные обновляются в реальном времени.

Статья о том, как спроектировать RAG-систему от нуля до production: архитектура, выбор компонентов, стратегии retrieval, advanced-паттерны и реальный расчёт стоимости.

Что такое RAG и зачем он нужен

RAG — архитектурный паттерн, предложенный Facebook AI Research в 2020 году. Идея: параметрическая память модели (то, что она «запомнила» при обучении) дополняется непараметрической — внешним хранилищем документов с поиском.

Три сценария, где RAG критически необходим:

Приватные данные. Модель не знает содержимого корпоративной Wiki, внутренних документов, базы знаний поддержки. Fine-tuning на приватных данных — дорого, медленно, и модель может «забыть» то, что знала раньше (catastrophic forgetting). RAG подключает любой корпус данных без переобучения.

Актуальность. Модель обучена на данных с cutoff-датой. RAG подставляет свежие данные из хранилища. Обновили документацию — RAG сразу использует новую версию.

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

Почему не хватает long context

Современные модели поддерживают окна до 1–2 миллионов токенов. Возникает вопрос: зачем RAG, если можно загрузить все документы в контекст?

Четыре причины, по которым long context не заменяет RAG:

Стоимость. Миллион токенов в контексте GPT-5.5 — это $5 за один запрос только на вход (GPT-5.5: $5/1M input). При 1000 запросов в день — $5000/день. RAG с embedding + retrieval обходится на 2–3 порядка дешевле.

Latency. Чем длиннее контекст, тем дольше генерация. 1 млн токенов на входе = секунды только на prefill. RAG возвращает 5–20 чанков — сотни, а не миллионы токенов.

Needle-in-a-haystack. Модели теряют точность при поиске конкретного факта в длинном контексте — особенно в середине документа. RAG делает precision-поиск и подаёт модели только релевантное.

Масштаб данных. Миллион токенов — примерно 750 000 слов или ~3000 страниц. Для корпоративной базы знаний из 50 000 документов context window физически не хватит.

Правило: long context подходит для задач, где весь корпус умещается в окно и стоимость приемлема (анализ одного документа, работа с кодовой базой). Для поиска по большим и обновляемым базам данных — RAG.

Архитектура RAG-системы

RAG состоит из двух пайплайнов: indexing (подготовка данных) и query (обработка запросов).

Indexing Pipeline

Подготовка документов для поиска. Выполняется один раз, плюс инкрементальные обновления при изменении данных.

Documents → [Load] → [Split/Chunk] → [Embed] → [Store in Vector DB]

Load. Извлечение текста из источников: PDF, HTML, Markdown, Notion, Confluence, Google Docs. Инструменты: unstructured, docling, langchain document loaders.

Split/Chunk. Разбиение документов на фрагменты (чанки). Размер чанка — критический параметр. Подробнее — в разделе про chunking.

Embed. Преобразование текстовых чанков в числовые векторы (embeddings) через embedding-модель. Вектор фиксированной размерности (768–3072) отражает семантический смысл текста.

Store. Сохранение векторов и метаданных в векторную базу данных с индексами для быстрого поиска по сходству.

Query Pipeline

Обработка пользовательского запроса. Выполняется на каждый вопрос.

Query → [Embed] → [Retrieve top-k] → [Rerank] → [Generate with LLM]

Embed query. Та же embedding-модель, что и при индексации, преобразует вопрос в вектор.

Retrieve. Поиск top-k ближайших векторов в базе. Типичные значения k: 5–20. Метрики расстояния: cosine similarity, dot product, L2.

Rerank. Переранжирование найденных чанков более точной моделью. Необязательный, но критически важный шаг для качества.

Generate. Найденные чанки подставляются в промпт LLM вместе с вопросом. Модель генерирует ответ, опираясь на переданный контекст.

# Минимальный query pipeline
async def rag_query(question: str, top_k: int = 5) -> str:
    # 1. Embed вопрос
    query_vector = await embed(question)

    # 2. Retrieve из vector store
    chunks = await vector_store.search(query_vector, limit=top_k)

    # 3. Собрать контекст
    context = "\n\n".join([c.text for c in chunks])

    # 4. Generate ответ
    response = await llm.chat(
        system="Отвечай на вопрос, используя только предоставленный контекст.",
        user=f"Контекст:\n{context}\n\nВопрос: {question}"
    )
    return response

Chunking: стратегии разбиения документов

Chunking — первый и один из самых влиятельных этапов RAG-пайплайна. Слишком маленькие чанки теряют контекст. Слишком большие — размывают релевантность и тратят токены.

Fixed-size chunking

Разбиение по фиксированному количеству символов или токенов с перекрытием (overlap).

from langchain.text_splitter import RecursiveCharacterTextSplitter

splitter = RecursiveCharacterTextSplitter(
    chunk_size=512,
    chunk_overlap=64,
    separators=["\n\n", "\n", ". ", " "]
)
chunks = splitter.split_text(document)

Размер чанка: 256–1024 токенов. Золотая середина для большинства задач — 512 токенов.

Overlap: 10–20% от chunk_size. Overlap предотвращает потерю контекста на границах чанков. Если важный факт попал на стык двух чанков, overlap гарантирует, что хотя бы один из них содержит его полностью.

Когда использовать: по умолчанию. Простой, предсказуемый, работает для 80% случаев.

Semantic chunking

Разбиение по смысловым границам. Алгоритм вычисляет embedding для каждого предложения, затем находит точки, где семантическое сходство между соседними предложениями резко падает — это границы чанков.

from langchain_experimental.text_splitter import SemanticChunker
from langchain_openai import OpenAIEmbeddings

chunker = SemanticChunker(
    OpenAIEmbeddings(model="text-embedding-3-small"),
    breakpoint_threshold_type="percentile",
    breakpoint_threshold_amount=90
)
chunks = chunker.split_text(document)

Когда использовать: документы с неоднородной структурой, где фиксированный размер разрезает логические блоки. Научные статьи, юридические документы, длинные инструкции.

Недостаток: дороже (требует embedding для каждого предложения), менее предсказуемый размер чанков.

Parent-child (hierarchical) chunking

Двухуровневая стратегия. Документ разбивается на крупные «родительские» чанки (1000–2000 токенов) и мелкие «дочерние» (200–400 токенов). Поиск идёт по дочерним чанкам (точнее релевантность), но в контекст LLM подставляется родительский чанк (больше контекста).

from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain.schema import Document
import hashlib

parent_splitter = RecursiveCharacterTextSplitter(chunk_size=2000)
child_splitter = RecursiveCharacterTextSplitter(chunk_size=400)

# create_documents возвращает список Document с .page_content и .metadata
parent_docs = parent_splitter.create_documents([document])
for parent_doc in parent_docs:
    parent_id = hashlib.md5(parent_doc.page_content.encode()).hexdigest()[:8]
    child_docs = child_splitter.create_documents([parent_doc.page_content])
    for child_doc in child_docs:
        child_doc.metadata["parent_id"] = parent_id
        # Индексировать child_doc в vector store
        # При retrieval — возвращать parent_doc по parent_id

Когда использовать: когда нужен баланс между точностью поиска и полнотой контекста. Техническая документация, FAQ, базы знаний.

Late chunking

Подход, предложенный Jina AI. Вместо «сначала чанкнуть, потом эмбеддить» — сначала прогнать весь документ через embedding-модель с длинным контекстом (8192+ токенов), получить контекстуализированные токен-эмбеддинги, а затем разбить на чанки и усреднить. Каждый чанк сохраняет контекст всего документа. Jina Embeddings v4 поддерживает 32K контекст и late interaction (ColBERT-style).

Когда использовать: документы, где контекст критически важен для понимания отдельных фрагментов. Требует embedding-моделей с поддержкой длинного контекста (Jina Embeddings v4, voyage-4, Qwen3-Embedding).

Сравнение стратегий

СтратегияСложностьТочностьКонтекстСтоимость
Fixed-sizeНизкаяСредняяСреднийНизкая
SemanticСредняяВысокаяСреднийСредняя
Parent-childСредняяВысокаяВысокийСредняя
Late chunkingВысокаяВысокаяВысокийВысокая

Рекомендация: начинайте с fixed-size (512 токенов, 64 overlap). Переходите на parent-child, если retrieval находит правильные чанки, но LLM не хватает контекста для ответа.

Embedding-модели: сравнение и выбор

Embedding-модель — ядро RAG-системы. Она определяет качество поиска: если модель плохо кодирует семантику документов, никакой reranking не спасёт.

Ключевые параметры

Размерность (dimensions). Длина вектора. Больше — точнее, но дороже хранение и поиск. Современные модели поддерживают Matryoshka embeddings: можно использовать укороченные векторы (256, 512) с минимальной потерей качества.

Контекстное окно. Максимальная длина входного текста. Для чанков в 512 токенов хватает любой модели. Для late chunking нужны модели с 8192+ токенов.

MTEB Score. Benchmark качества embeddings на задачах retrieval, classification, clustering. Не единственный критерий, но полезный ориентир.

Сравнение моделей (июнь 2026)

МодельПровайдерDimsMax tokensЦена / 1M tokMTEB Retrieval
text-embedding-3-largeOpenAI30728191$0.1364.6
text-embedding-3-smallOpenAI15368191$0.0262.3
Embed v4Cohere1536128000$0.1265.2
voyage-4Voyage AIдо 204832000$0.06
voyage-4-largeVoyage AIдо 204832000$0.1265.1+
jina-embeddings-v4Jina AI204832000$0.02
Gemini Embedding 001Google30728192$0.04
Qwen3-Embedding-8BAlibaba/BAAI409632000Бесплатно (self-hosted)70.6
BGE-M3BAAI10248192Бесплатно (self-hosted)63.5

Как выбирать

Бюджетный вариант: text-embedding-3-small ($0.02/1M tok) или jina-embeddings-v4 ($0.02/1M tok). Достаточно для MVP. text-embedding-3-small поддерживает Matryoshka — можно использовать 256 dims для экономии хранения. Jina v4 добавляет мультимодальность (текст + изображения + PDF) и 32K контекст.

Оптимальный баланс: voyage-4 ($0.06/1M tok). Длинный контекст (32K), flexible dims 256–2048, Matryoshka + квантизация. Принадлежит MongoDB — хорошая интеграция с Atlas Vector Search.

Максимальное качество (managed): voyage-4-large ($0.12/1M tok) или Cohere Embed v4 ($0.12/1M tok). voyage-4-large лидирует на RTEB retrieval benchmark. Cohere Embed v4 выигрывает там, где нужен мультимодальный RAG: текст, изображения, PDF — в одном embedding-пространстве.

Self-hosted, open-source: Qwen3-Embedding-8B. 32K контекст, 100+ языков, instruction-aware, flexible dims — один из лучших open-source вариантов на MTEB 2026 (70.6). Требует GPU с 16 GB VRAM. Для лёгкого варианта — BGE-M3 (dense + sparse + ColBERT, 4 GB VRAM).

Мультимодальный RAG: Cohere Embed v4 ($0.12/1M текстовых токенов, $0.47/1M image-токенов) или Gemini Embedding 001 ($0.04/1M) для задач, где данные уже в Google-экосистеме.

Vector stores: где хранить embeddings

Vector store — база данных, оптимизированная для поиска по сходству векторов. Два пути: расширение для существующей БД (pgvector) или специализированное решение (Pinecone, Qdrant).

Сравнение vector stores

StoreТипHostingБесплатный tierМакс. векторовАлгоритмыЦена (paid)
pgvector (Supabase)Расширение PostgreSQLManaged / Self-hosted500 MB (Free tier Supabase)Без ограниченийHNSW, IVFFlatот $25/мес
PineconeManaged SaaSCloud only2 GB (Starter)Без ограниченийProprietaryот $70/мес
QdrantNative vector DBCloud / Self-hosted1 GB (Cloud Free)Без ограниченийHNSWот $25/мес
WeaviateNative vector DBCloud / Self-hosted50 MB (Sandbox)Без ограниченийHNSWот $25/мес
ChromaDBEmbeddedSelf-hostedБесплатноRAM-limitedHNSW
MilvusNative vector DBCloud / Self-hosted5M векторов (Zilliz Free)Без ограниченийHNSW, IVFFlat, DiskANNот $65/мес
LanceDBEmbedded / CloudSelf-hosted / CloudБесплатно (self-hosted)Без ограниченийIVF, HNSW, IVFPQот $25/мес

Когда что выбирать

pgvector (Supabase). PostgreSQL уже в стеке, добавлять ещё одну базу не хочется, векторов до 5–10 млн. Единый бэкап, единые транзакции, SQL-фильтрация по метаданным. pgvector 0.8+ с HNSW-индексом держит 1–5M векторов на обычном Postgres-железе. Медленнее специализированных решений на больших объёмах и при selective-фильтрах.

Pinecone. Zero-ops: не нужно настраивать индексы, масштабирование, резервное копирование. Минимум DevOps. Serverless-tier удобен для старта, pod-based — быстрее, но дороже. Минус: vendor lock-in, нет self-hosted.

Qdrant. Наилучшая производительность при filtered search (Rust-реализация). Гибкие фильтры, sparse vectors, payload indexing. Есть managed cloud и Docker для self-hosted. Лучший выбор, если нужна максимальная скорость с фильтрацией.

ChromaDB. Прототипирование и небольшие проекты. Запускается in-process, нулевая конфигурация. Самый быстрый старт. В production с миллионами векторов не тянет.

LanceDB. Embedded/lakehouse-style vector DB. Хорош для мультимодальных данных (текст + изображения + видео), offline-сценариев, интеграции с Apache Arrow и DuckDB. Альтернатива ChromaDB для прототипов с более гибкой схемой.

Рекомендация: если вы на Supabase или PostgreSQL — начинайте с pgvector. Не добавляйте инфраструктуру, пока pgvector не упрётся в лимиты. Для highload-сценариев с 10M+ векторов или сложной фильтрацией — Qdrant или Pinecone.

Retrieval: стратегии поиска

Качество RAG определяется качеством retrieval. Если система не находит нужные чанки, LLM получает нерелевантный контекст и генерирует неправильный ответ. Garbage in — garbage out.

Vector search (базовый)

Cosine similarity между вектором запроса и векторами чанков. Быстрый, простой, но уязвим к vocabulary mismatch: запрос «как настроить аутентификацию» может не найти чанк со словами «конфигурация OAuth 2.0».

-- pgvector: cosine similarity search
SELECT id, content, 1 - (embedding <=> query_embedding) AS similarity
FROM documents
ORDER BY embedding <=> query_embedding
LIMIT 10;

Hybrid search (BM25 + vector)

Комбинация lexical search (BM25, точное совпадение ключевых слов) и semantic search (vector similarity). BM25 ловит точные термины, vector search — семантически близкие.

-- pgvector + pg_trgm для hybrid search в PostgreSQL
WITH vector_results AS (
    SELECT id, content, 1 - (embedding <=> query_embedding) AS vector_score
    FROM documents
    ORDER BY embedding <=> query_embedding
    LIMIT 20
),
keyword_results AS (
    SELECT id, content, ts_rank(to_tsvector('russian', content),
        plainto_tsquery('russian', 'аутентификация OAuth')) AS keyword_score
    FROM documents
    WHERE to_tsvector('russian', content) @@ plainto_tsquery('russian', 'аутентификация OAuth')
    LIMIT 20
)
SELECT COALESCE(v.id, k.id) AS id,
    COALESCE(v.content, k.content) AS content,
    COALESCE(v.vector_score, 0) * 0.7 + COALESCE(k.keyword_score, 0) * 0.3 AS combined_score
FROM vector_results v
FULL OUTER JOIN keyword_results k ON v.id = k.id
ORDER BY combined_score DESC
LIMIT 10;

Когда использовать: при любой возможности — hybrid search стабильно на 5–15% лучше чистого vector search по метрикам retrieval.

Reciprocal Rank Fusion (RRF)

Алгоритм объединения результатов из нескольких retrieval-стратегий. Не требует нормализации скоров — работает с рангами.

def reciprocal_rank_fusion(results_lists: list[list], k: int = 60) -> list:
    """
    results_lists: список из N списков ранжированных документов
    k: константа сглаживания (стандарт: 60)
    """
    scores = {}
    for results in results_lists:
        for rank, doc in enumerate(results):
            if doc.id not in scores:
                scores[doc.id] = 0
            scores[doc.id] += 1 / (k + rank + 1)

    return sorted(scores.items(), key=lambda x: x[1], reverse=True)

RRF — стандарт де-факто для fusion в hybrid search. Простой, эффективный, не требует обучения весов.

Multi-query retrieval

Один вопрос пользователя переформулируется в 3–5 вариаций, каждая вариация ищется отдельно, результаты объединяются через RRF. Решает проблему vocabulary mismatch на уровне запроса.

async def multi_query_retrieve(question: str, top_k: int = 10) -> list:
    # LLM генерирует вариации вопроса
    variations = await llm.chat(
        system="Сгенерируй 3 разных формулировки вопроса для поиска по базе знаний.",
        user=question
    )

    all_results = []
    for q in [question] + variations:
        results = await vector_store.search(await embed(q), limit=top_k)
        all_results.append(results)

    return reciprocal_rank_fusion(all_results)

Стоимость: 3–5 дополнительных embedding-вызовов + один LLM-вызов для генерации вариаций. При $0.06/1M tok (voyage-4) и $0.02/1M tok (embedding) — доли цента на запрос.

HyDE (Hypothetical Document Embeddings)

Вместо прямого embedding запроса — LLM генерирует гипотетический ответ на вопрос, и уже этот ответ эмбеддится для поиска. Идея: гипотетический ответ семантически ближе к реальному документу, чем короткий вопрос.

async def hyde_retrieve(question: str, top_k: int = 10) -> list:
    # LLM генерирует гипотетический ответ
    hypothetical = await llm.chat(
        system="Напиши подробный ответ на вопрос, как если бы ты был экспертом. Не упоминай, что это гипотетический ответ.",
        user=question
    )

    # Embed гипотетический ответ, а не вопрос
    hyde_vector = await embed(hypothetical)
    return await vector_store.search(hyde_vector, limit=top_k)

Когда использовать: для сложных, абстрактных вопросов, где прямой вектор запроса слишком далёк от документов. Не рекомендуется для factoid-вопросов — LLM может «нагаллюцинировать» гипотетический ответ в неправильном направлении.

Сравнение стратегий retrieval

СтратегияComplexityPrecisionRecallLatencyСтоимость
Vector searchНизкаяСредняяСредняя~50 мсМинимальная
Hybrid (BM25 + vector)СредняяВысокаяВысокая~100 мсМинимальная
Multi-queryСредняяВысокаяОчень высокая~300 мс+LLM вызов
HyDEСредняяВысокая*Средняя~500 мс+LLM вызов

*HyDE: высокая precision для conceptual queries, средняя для factoid.

Reranking: второй проход для точности

Retrieval возвращает top-k кандидатов. Bi-encoder (embedding-модель) — компромисс между скоростью и точностью: он кодирует запрос и документ независимо. Cross-encoder (reranker) обрабатывает пару «запрос + документ» совместно — точнее, но медленнее.

Типичная архитектура: bi-encoder находит top-50 кандидатов за миллисекунды, cross-encoder переранжирует их до top-5 за 100–300 мс.

Зачем нужен reranking

Без reranking результаты retrieval содержат шум. Vector search может вернуть чанки, семантически близкие к запросу по поверхностным признакам, но не содержащие ответа. Reranker оценивает релевантность глубже — на уровне конкретных фактов и утверждений.

Практический эффект: reranking поднимает precision@5 на 10–25% в зависимости от задачи.

Доступные rerankers

Cohere Rerank 4 (rerank-v4.0-pro и rerank-v4.0-fast) — managed API, $2.00 за 1000 searches (1 search = 1 query + до 100 документов). Мультиязычная, 32K контекст, лучшее качество среди managed-решений.

Voyage Rerank 2.5 (rerank-2.5 и rerank-2.5-lite) — интегрирован в Voyage AI, работает в связке с Voyage embeddings. 32K контекст, instruction-following, multilingual.

BGE Reranker v2-m3 — open-source cross-encoder от BAAI. Запускается локально на GPU. Бесплатно, но ограничен 8K токенами — не лучший выбор для long-document RAG.

Qwen3-Reranker (0.6B, 4B, 8B) — open-source от Alibaba. 32K контекст, 100+ языков, instruction-aware. Сильные результаты на MTEB-R (69.76) и CMTEB-R (75.94). Хорошая замена BGE для multilingual-сценариев.

Cohere Rerank 4 (self-hosted) — доступен через AWS Bedrock и Azure AI для полного контроля над данными.

Реализация

import cohere

co = cohere.Client()

async def retrieve_and_rerank(question: str, top_k: int = 5) -> list:
    # Шаг 1: Широкий retrieval (top-50)
    query_vector = await embed(question)
    candidates = await vector_store.search(query_vector, limit=50)

    # Шаг 2: Rerank до top-k
    reranked = co.rerank(
        model="rerank-v4.0-pro",
        query=question,
        documents=[c.text for c in candidates],
        top_n=top_k
    )

    return [candidates[r.index] for r in reranked.results]

Практика: retrieve 50–100 кандидатов → rerank → top 5–10 в LLM. Reranking стоит $2/1000 запросов, а precision@5 поднимает на 10–25%. Пропускать имеет смысл только при жёстком ограничении по latency (<100 мс total).

Advanced RAG: паттерны следующего уровня

Базовый RAG (retrieve → generate) покрывает 70% задач. Для остальных 30% — advanced-паттерны: система сама решает, как и что искать. В production наиболее ценны два: Adaptive RAG (экономия на простых запросах) и Agentic RAG (мощность для сложных).

Agentic RAG

Вместо фиксированного pipeline «embed → search → generate» — LLM-агент решает, какие действия предпринять. Агент может:

  • Переформулировать запрос, если первый retrieval не дал результатов
  • Запросить данные из нескольких источников (Wiki + CRM + Jira)
  • Выполнить multi-step reasoning: найти факт A, на его основе сформулировать запрос B, найти факт B
  • Решить, что retrieval не нужен — ответ есть в параметрической памяти модели
from openai import OpenAI

tools = [
    {
        "type": "function",
        "function": {
            "name": "search_knowledge_base",
            "description": "Поиск по базе знаний компании",
            "parameters": {
                "type": "object",
                "properties": {
                    "query": {"type": "string"},
                    "source": {"type": "string", "enum": ["docs", "jira", "confluence"]}
                }
            }
        }
    },
    {
        "type": "function",
        "function": {
            "name": "search_web",
            "description": "Поиск в интернете для актуальной информации",
            "parameters": {
                "type": "object",
                "properties": {"query": {"type": "string"}}
            }
        }
    }
]

response = client.chat.completions.create(
    model="gpt-5.5",
    messages=[{"role": "user", "content": question}],
    tools=tools,
    tool_choice="auto"  # Агент сам решает, какой инструмент вызвать
)

Когда использовать: сложные запросы, требующие рассуждений; мульти-источниковый поиск; задачи, где иногда retrieval не нужен.

Self-RAG

Модель после генерации оценивает собственный ответ:

  1. Нужен ли retrieval для этого вопроса? (retrieve / no-retrieve)
  2. Релевантны ли найденные документы? (relevant / irrelevant)
  3. Подтверждается ли ответ источниками? (supported / contradicted / no info)
  4. Полезен ли ответ? (useful score 1–5)

Если ответ не supported — система повторяет retrieval с другим запросом или признаёт, что не может ответить.

async def self_rag(question: str) -> str:
    # Шаг 1: Определить, нужен ли retrieval
    need_retrieval = await llm.chat(
        system="Определи, нужен ли поиск в базе знаний для ответа на вопрос. Ответь 'yes' или 'no'.",
        user=question
    )

    if need_retrieval.strip() == "no":
        return await llm.chat(user=question)

    # Шаг 2: Retrieve и generate
    chunks = await retrieve_and_rerank(question)
    context = "\n\n".join([c.text for c in chunks])
    answer = await llm.chat(
        system=f"Контекст:\n{context}",
        user=question
    )

    # Шаг 3: Проверить faithfulness
    check = await llm.chat(
        system="Оцени, подтверждается ли ответ предоставленным контекстом. Ответь 'supported', 'contradicted' или 'no_info'.",
        user=f"Контекст:\n{context}\n\nОтвет:\n{answer}"
    )

    if check.strip() != "supported":
        # Повторный retrieval с переформулированным запросом
        return await self_rag_retry(question, previous_answer=answer)

    return answer

Adaptive RAG

В production RAG-система часто тратит одинаковые ресурсы на простой вопрос («какой адрес офиса?») и сложный multi-hop запрос («найди все проекты, связанные с клиентом X и затронутые уязвимостью CVE-2025-1234»). Adaptive RAG решает это через routing.

Система классифицирует каждый запрос и выбирает стратегию:

async def adaptive_rag(question: str) -> str:
    # Классифицировать запрос
    route = await llm.chat(
        system="""Определи стратегию поиска для вопроса.
Ответь одним словом:
- simple: факт из одного документа
- hybrid: нужен keyword + semantic search
- multi_hop: несколько документов, reasoning цепочка
- no_retrieval: общий вопрос без приватных данных""",
        user=question
    )

    if route == "no_retrieval":
        return await llm.chat(user=question)
    elif route == "simple":
        chunks = await vector_store.search(await embed(question), limit=5)
    elif route == "hybrid":
        chunks = await hybrid_search(question, limit=10)
    elif route == "multi_hop":
        chunks = await multi_query_retrieve(question, top_k=15)

    chunks = await rerank(question, chunks, top_k=5)
    return await generate(question, chunks)

Практический эффект: 60–70% запросов в корпоративных RAG-системах — простые фактоидные вопросы. Adaptive RAG направляет их по дешёвому пути (vector search без multi-query и heavy reranking), снижая latency и cost без потери качества на сложных запросах.

Когда использовать: production RAG с разнородными запросами — mix фактоидных вопросов, аналитических запросов и multi-hop reasoning. Работает в связке с Agentic RAG: агент — это extreme-вариант adaptive routing.

Corrective RAG (CRAG)

Модификация Self-RAG с внешней проверкой. Если внутренняя база не даёт уверенного ответа — система обращается к внешним источникам (web search) для корректировки.

Три сценария:

  • Correct: retrieval нашёл релевантные документы → генерация по ним
  • Incorrect: документы нерелевантны → web search → генерация по web-результатам
  • Ambiguous: частичная релевантность → объединение internal + web results

GraphRAG

RAG на основе Knowledge Graph вместо (или в дополнение к) vector search. Документы преобразуются в граф сущностей и связей. Запрос — это graph traversal.

Microsoft GraphRAG поддерживает несколько режимов поиска:

  • Local Search — вопросы вокруг конкретных сущностей.
  • Global Search — вопросы «про весь корпус» (суммаризация, corpus-level patterns).
  • DRIFT Search — гибрид Local + Global: стартует от community summaries, затем уточняет через follow-up traversal. Баланс качества и стоимости за счёт гибкого обхода графа.

Альтернативы:

  • LightRAG — более лёгкий open-source GraphRAG-подход, проще для прототипов.
  • Neo4j GraphRAG — хорош, если уже есть Neo4j / Cypher.
  • LlamaIndex Property Graph — удобно для интеграции в LlamaIndex-стек.

Когда нужен GraphRAG:

  • Вопросы о связях между сущностями («кто из наших клиентов связан с компанией X?»)
  • Multi-hop reasoning («найди все проекты, которые зависят от библиотеки, разработанной командой Y»)
  • Суммаризация больших корпусов через иерархические community summaries

Сложность и стоимость: построение knowledge graph — дорогая операция. Entity extraction + community summaries требуют множества LLM-вызовов при индексации. Нужны NER, relation extraction, entity resolution + prompt tuning для качества графа.

Рекомендация: GraphRAG — дополнение к vector search, не замена. Начинайте с vector RAG, graph-слой добавляйте только для multi-hop и corpus-level задач, которые vector search не берёт.

Практический туториал: RAG с Supabase pgvector + Python

Полный рабочий пример RAG-системы с нуля. Стек: Supabase (PostgreSQL + pgvector), OpenAI embeddings, Python.

Шаг 1: Настройка Supabase

Включите расширение pgvector в Supabase Dashboard → Database → Extensions → ищите vector.

Создайте таблицу и функцию поиска:

-- Создание таблицы для документов
CREATE TABLE documents (
    id BIGSERIAL PRIMARY KEY,
    content TEXT NOT NULL,
    metadata JSONB DEFAULT '{}',
    embedding VECTOR(1536)
);

-- Индекс для быстрого поиска (HNSW — основной выбор до 1M векторов)
CREATE INDEX ON documents
USING hnsw (embedding vector_cosine_ops)
WITH (m = 16, ef_construction = 64);

-- Функция для similarity search
CREATE OR REPLACE FUNCTION match_documents(
    query_embedding VECTOR(1536),
    match_threshold FLOAT DEFAULT 0.7,
    match_count INT DEFAULT 5
)
RETURNS TABLE (
    id BIGINT,
    content TEXT,
    metadata JSONB,
    similarity FLOAT
)
LANGUAGE plpgsql
AS $$
BEGIN
    RETURN QUERY
    SELECT
        documents.id,
        documents.content,
        documents.metadata,
        1 - (documents.embedding <=> query_embedding) AS similarity
    FROM documents
    WHERE 1 - (documents.embedding <=> query_embedding) > match_threshold
    ORDER BY documents.embedding <=> query_embedding
    LIMIT match_count;
END;
$$;

Шаг 2: Python — индексация документов

import os
from openai import OpenAI
from supabase import create_client

# Инициализация клиентов
openai_client = OpenAI()
supabase = create_client(
    os.environ["SUPABASE_URL"],
    os.environ["SUPABASE_SERVICE_KEY"]
)

def get_embedding(text: str) -> list[float]:
    """Получить embedding через OpenAI API."""
    response = openai_client.embeddings.create(
        model="text-embedding-3-small",
        input=text
    )
    return response.data[0].embedding

def chunk_text(text: str, chunk_size: int = 512, overlap: int = 64) -> list[str]:
    """Разбить текст на чанки с перекрытием."""
    words = text.split()
    chunks = []
    start = 0
    while start < len(words):
        end = start + chunk_size
        chunk = " ".join(words[start:end])
        chunks.append(chunk)
        start += chunk_size - overlap
    return chunks

def index_document(text: str, metadata: dict = None) -> int:
    """Разбить документ на чанки и проиндексировать."""
    chunks = chunk_text(text)
    rows = []
    for i, chunk in enumerate(chunks):
        embedding = get_embedding(chunk)
        rows.append({
            "content": chunk,
            "metadata": {**(metadata or {}), "chunk_index": i},
            "embedding": embedding
        })

    result = supabase.table("documents").insert(rows).execute()
    return len(result.data)

Шаг 3: Python — поиск и генерация

def search(query: str, top_k: int = 5, threshold: float = 0.7) -> list[dict]:
    """Семантический поиск по базе документов."""
    query_embedding = get_embedding(query)

    result = supabase.rpc("match_documents", {
        "query_embedding": query_embedding,
        "match_threshold": threshold,
        "match_count": top_k
    }).execute()

    return result.data

def ask(question: str, top_k: int = 5) -> str:
    """RAG: поиск контекста + генерация ответа."""
    # Retrieve
    chunks = search(question, top_k=top_k)

    if not chunks:
        return "Не найдено релевантных документов для ответа на вопрос."

    # Build context
    context = "\n\n---\n\n".join([
        f"[Источник {i+1}] (similarity: {c['similarity']:.2f})\n{c['content']}"
        for i, c in enumerate(chunks)
    ])

    # Generate
    response = openai_client.chat.completions.create(
        model="gpt-5.4-mini",
        messages=[
            {
                "role": "system",
                "content": (
                    "Ты — ассистент, отвечающий на вопросы на основе предоставленного контекста. "
                    "Используй только информацию из контекста. "
                    "Если ответа нет в контексте — скажи об этом. "
                    "Цитируй источники [Источник N]."
                )
            },
            {
                "role": "user",
                "content": f"Контекст:\n{context}\n\nВопрос: {question}"
            }
        ],
        temperature=0.1
    )

    return response.choices[0].message.content

Шаг 4: Использование

# Индексация
with open("knowledge_base.md") as f:
    text = f.read()

count = index_document(text, metadata={"source": "knowledge_base", "version": "1.0"})
print(f"Проиндексировано {count} чанков")

# Вопрос
answer = ask("Как настроить аутентификацию в проекте?")
print(answer)

Весь код — 70 строк (без комментариев). Рабочая RAG-система с semantic search и цитированием источников. Metadata-фильтрацию можно добавить через match_documents — передайте условие в SQL-функцию через параметр фильтра.

Evaluation: как измерить качество RAG

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

Метрики retrieval

Precision@k — доля релевантных документов среди top-k результатов. Precision@5 = 0.6 означает: из 5 возвращённых чанков 3 релевантны.

Recall@k — доля найденных релевантных документов из всех релевантных в базе. Recall@10 = 0.8 означает: из 10 релевантных чанков в базе retrieval нашёл 8.

MRR (Mean Reciprocal Rank) — средняя обратная позиция первого релевантного результата. MRR = 1 означает, что первый результат всегда релевантен. MRR = 0.5 — релевантный результат в среднем на второй позиции.

NDCG@k (Normalized Discounted Cumulative Gain) — учитывает не только наличие релевантных документов, но и их позиции. Релевантный документ на 1-й позиции ценнее, чем на 5-й.

Метрики генерации

Faithfulness — соответствие ответа контексту. Ответ не содержит информации, которой нет в источниках (отсутствие галлюцинаций). Диапазон: 0–1.

Answer Relevancy — насколько ответ соответствует вопросу. Ответ может быть faithful (соответствует контексту), но irrelevant (не отвечает на вопрос).

Context Precision — насколько найденный контекст содержит информацию, нужную для ответа. Низкая precision = много мусора в контексте.

Context Recall — содержит ли найденный контекст всю информацию, нужную для полного ответа. Низкий recall = часть ответа отсутствует в контексте.

RAGAS Framework

RAGAS — open-source фреймворк для автоматической оценки RAG-систем. LLM выступает судьёй. Большинство метрик — reference-free (не требуют эталонных ответов): faithfulness, answer_relevancy, context_precision. Исключение — context_recall: эта метрика сравнивает контекст с ground_truth, поэтому эталонные ответы нужны.

from ragas import evaluate
from ragas.metrics import (
    faithfulness,
    answer_relevancy,
    context_precision,
    context_recall
)
from datasets import Dataset

# Подготовить данные для оценки
eval_data = {
    "question": ["Как настроить OAuth?", "Какие лимиты API?"],
    "answer": [rag_answer_1, rag_answer_2],
    "contexts": [[chunk1, chunk2], [chunk3, chunk4]],
    "ground_truth": ["OAuth настраивается через...", "Лимиты API: 1000 req/min..."]
}

dataset = Dataset.from_dict(eval_data)

results = evaluate(
    dataset,
    metrics=[faithfulness, answer_relevancy, context_precision, context_recall]
)

print(results)
# {'faithfulness': 0.92, 'answer_relevancy': 0.88,
#  'context_precision': 0.85, 'context_recall': 0.78}

Что измерять в production

Минимальный набор метрик для production RAG:

  1. Faithfulness — главная метрика. Галлюцинации в RAG недопустимы.
  2. Answer Relevancy — ответ должен соответствовать вопросу.
  3. Retrieval Precision@5 — качество поиска.
  4. Latency p95 — время от запроса до ответа.
  5. Cost per query — стоимость одного запроса (embedding + retrieval + generation).

Рекомендация: eval pipeline — до оптимизации. Без метрик изменения в chunking, retrieval, prompts — слепые эксперименты.

Когда RAG не нужен

RAG — не серебряная пуля. Есть сценарии, где он избыточен или проигрывает альтернативам.

Long context window

Если весь корпус данных помещается в контекстное окно и стоимость приемлема — RAG не нужен.

Пример: анализ одного документа на 50 страниц (~25K токенов). Дешевле загрузить целиком, чем строить RAG-пайплайн. Стоимость: ~$0.019 за запрос с GPT-5.4-Mini ($0.75/1M input).

Prompt caching

Провайдеры (Anthropic, OpenAI, Google) кэшируют повторяющийся prefix контекста. Если вы задаёте разные вопросы к одному и тому же документу — cached context стоит в 10 раз дешевле, чем полный (скидка 90%).

Пример: 100K токенов документации, 100 вопросов в день. Без кэширования (GPT-5.5, $5/1M): 100K × 100 × $5/1M = $50/день. С prompt caching ($0.50/1M cached): ~$5/день + доли центов за уникальную часть каждого запроса. Дороже RAG при масштабе, но проще в реализации.

Малый корпус

Корпус из 10–50 документов до 200K токенов. Загрузить всё в контекст через prompt caching проще, чем поддерживать RAG-инфраструктуру.

Fine-tuning

Если данные статичны и задача — не «ответить на вопрос по документу», а «вести себя определённым образом» (стиль, формат, domain expertise) — fine-tuning берёт это лучше RAG.

Decision framework

Корпус > 500K токенов?
  ├─ Да → RAG
  └─ Нет
      ├─ Данные обновляются часто? → RAG
      ├─ Нужны цитаты источников? → RAG
      ├─ Бюджет на inference ограничен? → RAG
      └─ Ничего из вышеперечисленного → Long context + caching

Стоимость RAG в production: расчёт

Рассчитаем стоимость RAG-системы для реального сценария: корпоративная база знаний, 10 000 документов, 1000 запросов в день.

Параметры

  • 10 000 документов, средний размер 2000 слов (~2700 токенов)
  • Chunking: 512 токенов, overlap 64 → ~6 чанков на документ → 60 000 чанков
  • Embedding-модель: text-embedding-3-small (1536 dims, $0.02/1M tok)
  • Vector store: Supabase Pro ($25/мес)
  • Reranker: Cohere Rerank 4 ($2/1000 searches)
  • LLM: GPT-5.4-Mini ($0.75/1M input, $4.50/1M output)
  • 1000 запросов/день

Единовременные затраты (индексация)

КомпонентРасчётСтоимость
Embedding документов60K чанков × 512 tok = 30.7M tok × $0.02/1M$0.61
Итого индексация$0.61

Ежемесячные затраты

КомпонентРасчётСтоимость/мес
Vector store (Supabase Pro)Фиксированно$25.00
Embedding запросов1000 req/день × 30 дн × ~20 tok/query × $0.02/1M$0.01
Reranking1000 req/день × 30 дн = 30K searches × $2/1K$60.00
LLM generation (input)1000 req × 30 дн × ~3000 tok context × $0.75/1M$67.50
LLM generation (output)1000 req × 30 дн × ~500 tok answer × $4.50/1M$67.50
Итого ежемесячно~$220/мес

Сравнение с long context

Тот же корпус (27M токенов) через long context + GPT-5.4-Mini ($0.75/1M input):

ПодходСтоимость/запрос1000 req/деньМесяц
Long context (full)27M tok × $0.75/1M = $20.25$20 250$607 500
Long context + caching~$20.25 первый + ~$2.03 cached~$2 050~$61 500
RAG~$0.007$7~$220

RAG на 3–4 порядка дешевле полного long context и в ~280 раз дешевле cached context при 1000 запросах в день. При росте нагрузки разница увеличивается.

Оптимизация стоимости

Embedding: переход на voyage-4 ($0.06/1M) даст лучшее качество, но разница в стоимости embedding запросов ничтожна при 1000 req/день. Основная экономия — при массовой переиндексации.

Reranking: $60/мес — основная статья расходов помимо LLM. Если бюджет ограничен, можно заменить на self-hosted BGE Reranker (бесплатно, но нужен GPU).

LLM: GPT-5.4-Mini ($0.75/1M input) — разумный выбор для большинства задач. Для сложных multi-hop запросов — GPT-5.5 ($5/1M input) или Gemini 3.5 Flash — выбор зависит от сложности запросов и бюджета.

Batch embedding: OpenAI Batch API даёт 50% скидку на embeddings. Используйте для массовой индексации.

Итого

RAG — не временный хак, пока модели не научатся запоминать всё. Это архитектурный паттерн, решающий фундаментальные задачи: приватность данных, актуальность, цитируемость, стоимость inference.

Минимальная рабочая конфигурация:

  1. Chunking: fixed-size, 512 токенов, overlap 64
  2. Embedding: text-embedding-3-small (для MVP) или voyage-4 (для production)
  3. Vector store: pgvector через Supabase
  4. Retrieval: hybrid search (BM25 + vector)
  5. Reranking: Cohere Rerank 4 (rerank-v4.0-pro / fast)
  6. Generation: GPT-5.4-Mini с промптом, требующим цитирования
  7. Evaluation: RAGAS (faithfulness + answer relevancy как минимум)
  8. Routing (production): Adaptive RAG — классификатор запросов для снижения latency и стоимости на простых запросах

Начинайте с простого: fixed-size chunking, один vector store, базовый vector search. Измеряйте качество через RAGAS. Итеративно добавляйте hybrid search, reranking, advanced-стратегии — каждый шаг с измерением impact. RAG-система, которую нельзя оценить — система, которую не контролируешь.