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

> Полное руководство по RAG: архитектура, chunking, embeddings, vector stores, retrieval, reranking. Практический туториал с pgvector и Python.
> Author: Roman Belov · Published: 2026-06-28 · Source: https://futurecraft.pro/ru/blog/rag-system-guide/

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 вместе с вопросом. Модель генерирует ответ, опираясь на переданный контекст.

```python
# Минимальный 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).

```python
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 для каждого предложения, затем находит точки, где семантическое сходство между соседними предложениями резко падает — это границы чанков.

```python
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 подставляется родительский чанк (больше контекста).

```python
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)

| Модель | Провайдер | Dims | Max tokens | Цена / 1M tok | MTEB Retrieval |
|--------|-----------|------|------------|----------------|----------------|
| text-embedding-3-large | OpenAI | 3072 | 8191 | $0.13 | 64.6 |
| text-embedding-3-small | OpenAI | 1536 | 8191 | $0.02 | 62.3 |
| Embed v4 | Cohere | 1536 | 128000 | $0.12 | 65.2 |
| voyage-4 | Voyage AI | до 2048 | 32000 | $0.06 | — |
| voyage-4-large | Voyage AI | до 2048 | 32000 | $0.12 | 65.1+ |
| jina-embeddings-v4 | Jina AI | 2048 | 32000 | $0.02 | — |
| Gemini Embedding 001 | Google | 3072 | 8192 | $0.04 | — |
| Qwen3-Embedding-8B | Alibaba/BAAI | 4096 | 32000 | Бесплатно (self-hosted) | 70.6 |
| BGE-M3 | BAAI | 1024 | 8192 | Бесплатно (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) | Расширение PostgreSQL | Managed / Self-hosted | 500 MB (Free tier Supabase) | Без ограничений | HNSW, IVFFlat | от $25/мес |
| Pinecone | Managed SaaS | Cloud only | 2 GB (Starter) | Без ограничений | Proprietary | от $70/мес |
| Qdrant | Native vector DB | Cloud / Self-hosted | 1 GB (Cloud Free) | Без ограничений | HNSW | от $25/мес |
| Weaviate | Native vector DB | Cloud / Self-hosted | 50 MB (Sandbox) | Без ограничений | HNSW | от $25/мес |
| ChromaDB | Embedded | Self-hosted | Бесплатно | RAM-limited | HNSW | — |
| Milvus | Native vector DB | Cloud / Self-hosted | 5M векторов (Zilliz Free) | Без ограничений | HNSW, IVFFlat, DiskANN | от $65/мес |
| LanceDB | Embedded / Cloud | Self-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».

```sql
-- 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 — семантически близкие.

```sql
-- 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-стратегий. Не требует нормализации скоров — работает с рангами.

```python
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 на уровне запроса.

```python
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 генерирует гипотетический ответ на вопрос, и уже этот ответ эмбеддится для поиска. Идея: гипотетический ответ семантически ближе к реальному документу, чем короткий вопрос.

```python
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

| Стратегия | Complexity | Precision | Recall | Latency | Стоимость |
|-----------|-----------|-----------|--------|---------|-----------|
| 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 для полного контроля над данными.

### Реализация

```python
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 не нужен — ответ есть в параметрической памяти модели

```python
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 с другим запросом или признаёт, что не может ответить.

```python
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.

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

```python
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)
```

**Практический эффект:** большинство запросов в корпоративных 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`.

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

```sql
-- Создание таблицы для документов
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 — индексация документов

```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 — поиск и генерация

```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: Использование

```python
# Индексация
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`, поэтому эталонные ответы нужны.

```python
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 |
| Reranking | 1000 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 токенов не помещается ни в одно контекстное окно (потолок моделей — 1–2M токенов), поэтому реалистичная long-context альтернатива — полный контекст в 1M токенов на каждый запрос: GPT-5.5, $5/1M input:

| Подход | Стоимость/запрос | 1000 req/день | Месяц |
|--------|-----------------|---------------|-------|
| Long context (1M, полный) | 1M tok × $5/1M = $5.00 | $5 000 | $150 000 |
| Long context + caching | ~$5.00 первый + ~$0.50 cached | ~$500 | ~$15 000 |
| **RAG** | **~$0.007** | **$7** | **~$220** |

RAG примерно в 680 раз дешевле отправки полного 1M-контекста на каждый запрос и примерно в 68 раз дешевле 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-система, которую нельзя оценить — система, которую не контролируешь.
