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)
| Модель | Провайдер | 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 | 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».
-- 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
| Стратегия | 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 для полного контроля над данными.
Реализация
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
Модель после генерации оценивает собственный ответ:
- Нужен ли retrieval для этого вопроса? (retrieve / no-retrieve)
- Релевантны ли найденные документы? (relevant / irrelevant)
- Подтверждается ли ответ источниками? (supported / contradicted / no info)
- Полезен ли ответ? (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:
- Faithfulness — главная метрика. Галлюцинации в RAG недопустимы.
- Answer Relevancy — ответ должен соответствовать вопросу.
- Retrieval Precision@5 — качество поиска.
- Latency p95 — время от запроса до ответа.
- 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 токенов) через 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.
Минимальная рабочая конфигурация:
- Chunking: fixed-size, 512 токенов, overlap 64
- Embedding: text-embedding-3-small (для MVP) или voyage-4 (для production)
- Vector store: pgvector через Supabase
- Retrieval: hybrid search (BM25 + vector)
- Reranking: Cohere Rerank 4 (rerank-v4.0-pro / fast)
- Generation: GPT-5.4-Mini с промптом, требующим цитирования
- Evaluation: RAGAS (faithfulness + answer relevancy как минимум)
- Routing (production): Adaptive RAG — классификатор запросов для снижения latency и стоимости на простых запросах
Начинайте с простого: fixed-size chunking, один vector store, базовый vector search. Измеряйте качество через RAGAS. Итеративно добавляйте hybrid search, reranking, advanced-стратегии — каждый шаг с измерением impact. RAG-система, которую нельзя оценить — система, которую не контролируешь.