Как устроена RAG-система: путь от вопроса к ответу
В предыдущем уроке мы увидели общую схему. Теперь разберём её по компонентам: что именно делает каждый блок, какие у него есть варианты реализации, и какие инженерные решения влияют на качество и стоимость.
RAG-система состоит из двух pipeline:
- Indexing pipeline (offline, разово или по расписанию), превращает корпус документов в поисковый индекс.
- Query pipeline (online, на каждый запрос), превращает вопрос пользователя в ответ с цитатами.
Pipeline 1: Indexing (подготовка базы)
[документы]
│
▼
[1. Loaders / parsers] ── PDF, DOCX, HTML, Confluence, S3, Slack…
│
▼ чистый текст + метаданные
[2. Chunker] ── recursive / semantic / parent-document
│
▼ чанки 200–800 токенов
[3. Embedder] ── OpenAI v3 / BGE-M3 / Voyage-3
│
▼ векторы 768–3072 dim
[4. Vector DB writer] ── pgvector / Qdrant / Weaviate
│
▼
[5. Sparse index writer] ── Elasticsearch / OpenSearch BM25
Шаг 1. Loaders и parsers
Документы приходят в десятках форматов: PDF, DOCX, XLSX, PPTX, HTML, Markdown, JSON, Confluence/Notion exports, Google Drive, Slack threads, GitHub issues, Salesforce notes.
Реальные инструменты:
- Unstructured.io (open-source + paid), универсальный парсер с layout-aware PDF, OCR, таблицами.
- LlamaParse (от LlamaIndex), лучший парсер сложных PDF с таблицами и формулами; платный.
- PyMuPDF / pdfplumber, легковесные PDF-парсеры на Python.
- Apache Tika, JVM-based, поддерживает 1000+ форматов.
- Docling (IBM, 2024), open-source парсер с MLM для понимания layout.
Подводный камень: PDF, самый враждебный формат. В одном PDF могут быть сканы (нужен OCR, Tesseract, AWS Textract, Google Document AI), таблицы (нужен layout-anaware парсер), мультиколоночность, footnotes. У плохого парсера качество RAG падает на 20–40% сразу на этом шаге.
Шаг 2. Chunking (нарезка)
Документ нельзя засунуть в индекс целиком, нужны мелкие куски, которые поместятся в контекст LLM на момент генерации. Стратегии:
| Стратегия | Описание | Когда использовать |
|---|---|---|
| Fixed-size | Каждые N символов / токенов с overlap 10–20% | Быстро, простой baseline |
| Recursive character | Режет по \n\n, \n, . , в порядке приоритета |
Default в LangChain; универсальный |
| Sentence-window | Чанк = 1 предложение, в индексе хранится с window ±3 предложения | Точный поиск, широкий контекст для генерации |
| Semantic chunking | Режет на границах смысловых блоков (по cosine distance соседних предложений) | Гетерогенные документы |
| Parent-document | Индексируем мелкие чанки, в LLM передаём родительский документ | Точный retrieval + богатый контекст |
| Markdown / code-aware | Учитывает заголовки, code blocks, AST | Документация, кодовые базы |
| Agentic chunking | LLM сам делит документ на тематические блоки | Дорого, но качественно |
| Late chunking (Jina, 2024) | Эмбеддим весь документ, потом нарезаем эмбеддинги | Для длинных контекст-aware embedders |
Типичный размер чанка: 256–512 токенов для precision-задач, 800–1200 для exploration. Overlap 10–20% помогает не терять контекст на границах.
Шаг 3. Embedding
Эмбеддер, нейросеть, которая отображает текст в вектор фиксированной размерности (768, 1024, 1536, 3072 dim). Близкие по смыслу тексты получают близкие векторы (cosine similarity).
Топовые модели 2026:
| Модель | Dim | MTEB score | Цена / 1M tok | Особенности |
|---|---|---|---|---|
OpenAI text-embedding-3-large |
3072 (truncatable) | ~64 | $0.13 | Default outside-EU |
OpenAI text-embedding-3-small |
1536 | ~62 | $0.02 | Cheap baseline |
Voyage AI voyage-3-large |
1024 | ~67 | $0.18 | Лучший public benchmark |
Cohere embed-v3 |
1024 | ~64 | $0.10 | Multilingual, sparse mode |
| BGE-M3 (BAAI) | 1024 | ~63 | self-hosted | Open-source, dense+sparse+multi-vector в одной |
Jina jina-embeddings-v3 |
1024 | ~63 | self-hosted/API | 8K context, мультилингв |
Nomic nomic-embed-text-v1.5 |
768 | ~62 | open-source | Полностью open weights + dataset |
multilingual-e5-large-instruct |
1024 | ~63 | self-hosted | Топ для русского |
Для русского языка часто лучше работают BGE-M3, multilingual-e5-large, paraphrase-multilingual-mpnet, у OpenAI v3 русский слабее, чем английский.
Важный приём, Matryoshka embeddings (OpenAI v3, Nomic): можно обрезать вектор до 256–512 dim с минимальной потерей качества. Это сильно экономит хранилище и ускоряет поиск.
Шаг 4. Vector DB
Хранит векторы и делает ANN (Approximate Nearest Neighbor) поиск по cosine similarity. Топовые движки:
| DB | Тип | Сильная сторона |
|---|---|---|
| pgvector | Расширение PostgreSQL | Уже есть Postgres → не плодить инфраструктуру; HNSW и IVFFlat |
| Qdrant | Standalone (Rust) | Лучшие фильтры по метаданным, payload, hybrid search out-of-box |
| Weaviate | Standalone (Go) | Modules для embedders, GraphQL, multi-tenancy |
| Milvus / Zilliz | Standalone (C++/Go) | Масштаб 1B+ векторов, GPU-индексы |
| Pinecone | Managed-only | Серверлесс, простой API |
| LanceDB | Embedded (Rust) | Файловое хранение, S3-native, для AI-агентов |
| Chroma | Embedded / standalone | Простая для прототипов |
| Elasticsearch / OpenSearch | Search engine | Dense + BM25 в одном движке |
| Vespa | Standalone (Yahoo) | Tensor-search, ColBERT first-class |
Под капотом, индексы HNSW (Hierarchical Navigable Small World) или IVF (Inverted File). HNSW, стандарт для high-recall, IVF, для очень больших баз.
Шаг 5. Sparse index (BM25)
Параллельно векторам почти всегда строится классический полнотекстовый индекс на BM25. Зачем, увидим в query pipeline. Реализации: Elasticsearch, OpenSearch, Tantivy, или даже просто Postgres tsvector.
Pipeline 2: Query (на каждый запрос)
[вопрос пользователя]
│
▼
[1. Query understanding] ── классификация intent, decomposition, rewriting
│
▼
[2. Retrieval] ── dense (vectors) + sparse (BM25), параллельно
│ topK=50 candidates
▼
[3. Reranking] ── cross-encoder, top-50 → top-5
│
▼
[4. Context assembly] ── шаблон промпта, source IDs, инструкции
│
▼
[5. Generation] ── LLM пишет ответ
│
▼
[6. Post-processing] ── валидация цитат, фильтр PII, guardrails
│
▼
[ответ + citations]
Шаг 1. Query understanding
Опциональный, но мощный шаг. LLM-call, который превращает «сырой» запрос в форму, удобную для retrieval:
- Decomposition, сложный вопрос делится на под-вопросы. «Сравни нашу политику отпусков с прошлогодней» → [«политика отпусков 2026», «политика отпусков 2025»], два независимых retrieval.
- Rewriting, переформулировка под язык документов. «Как взять отпуск?» → «оформление ежегодного оплачиваемого отпуска».
- HyDE (Hypothetical Document Embeddings), LLM сначала пишет «гипотетический ответ», и поиск ведётся по нему, а не по короткому вопросу. Качество поиска +5–15%.
- Intent classification, определить, нужен ли вообще RAG. «Привет» → ответ без поиска.
- Routing, выбрать, в какой источник идти (FAQ, регламенты, CRM). Снижает шум.
Шаг 2. Retrieval
Гибридный поиск, почти всегда лучше чистого vector-поиска:
# псевдокод hybrid search
dense_results = vector_db.search(embed(query), top_k=50)
sparse_results = bm25_index.search(query, top_k=50)
fused = reciprocal_rank_fusion(dense_results, sparse_results, k=60)
candidates = fused[:50]
Reciprocal Rank Fusion (RRF), стандартный способ слить два ranking'а:score(doc) = Σ 1 / (k + rank_i(doc)) для каждого источника.
Гибридный поиск устойчиво даёт +10–25% к recall@10 по сравнению с чистым dense (Anthropic, 2024). Особенно помогает на запросах с конкретными терминами / артикулами / названиями.
Шаг 3. Reranking
Vector search быстрый, но грубоватый: считает «общую похожесть» вопроса и документа отдельно, а потом сравнивает векторы (это bi-encoder).
Reranker (cross-encoder) работает иначе: берёт пару (вопрос, документ) вместе, прогоняет через трансформер, и возвращает score 0..1, насколько документ отвечает на вопрос. Это медленнее (нельзя предвычислить), но точнее на 10–30%.
# псевдокод rerank
reranked = cohere.rerank(
query=query,
documents=[c.text for c in candidates],
top_n=5,
model="rerank-v3.5"
)
top_chunks = [candidates[r.index] for r in reranked.results]
Топовые rerankers 2026:
- Cohere
rerank-v3.5, managed API, $1 / 1K rerank calls, поддерживает 100+ языков. - BGE
reranker-v2-m3, open-source, ~600M params, можно self-hosted на GPU. - mxbai-rerank-large-v1, open-source, легче BGE.
- ColBERT-v2 / ColBERTv2.0, late-interaction подход, очень точный, требует своего индекса.
- Jina
reranker-v2-base-multilingual, 278M params, быстрый.
Reranker сокращает шум перед LLM: вместо 50 чанков (≈25K токенов) подаём 5 (≈2.5K). Это критически влияет на качество финального ответа из-за lost-in-middle.
Шаг 4. Context assembly
Шаблон промпта собирается так:
<system>
Ты ассистент по корпоративной базе знаний.
Отвечай ТОЛЬКО на основе предоставленных документов.
Каждое утверждение сопровождай ссылкой [^N], где N, id источника.
Если в документах нет ответа, скажи прямо: «Информации недостаточно».
</system>
<documents>
[^1] (Регламент об отпусках 2026, раздел 3.2): ...текст чанка...
[^2] (HR-FAQ, "Как оформить отпуск"): ...текст чанка...
[^3] (Положение о труде, ст. 14): ...текст чанка...
</documents>
<user>
{вопрос}
</user>
Тонкости, которые сильно влияют:
- Порядок чанков: самые релевантные, в начало или конец, не в середину (lost-in-middle).
- Источник перед текстом, облегчает цитирование и снижает галлюцинации.
- Жёсткая инструкция «не знаешь, скажи», снижает выдумки на 30–60%.
- Few-shot examples в system prompt с примерами правильного цитирования, дополнительные +5–10%.
Шаг 5. Generation
Тот же LLM-вызов, что в обычном чат-боте, но с собранным контекстом. Параметры:
temperature: 0..0.3, детерминизм важнее креативности.max_tokens, ограничить длину; обычно 500–2000.- Streaming, обязательно для UX (пользователь видит ответ за 0.5 сек, не за 3).
Выбор модели, компромисс цена/качество:
- Claude Sonnet 4.6, best для длинного контекста, цитирования, reasoning.
- GPT-5, sтрого следует инструкциям, JSON mode, function calling.
- Gemini 2.5 Pro, 2M контекст, мультимодальность.
- Llama 3.3 70B / Qwen 2.5 72B (self-hosted), when on-prem обязателен.
- Claude Haiku 4.5 / GPT-5-mini, для быстрых дешёвых ответов на простые вопросы.
Шаг 6. Post-processing
Что делают после ответа:
- Citation validation, проверить, что каждая
[^N]есть в переданных документах; если LLM выдумал ссылку, переcпросить. - PII filter, Microsoft Presidio, AWS Comprehend; маскировать персональные данные.
- Guardrails, NVIDIA NeMo Guardrails, Llama Guard 3, фильтр опасных тем.
- Hallucination check, отдельный LLM-call: «Содержит ли ответ утверждения, которых нет в документах?»
Что может пойти не так на каждом шаге
| Шаг | Типичная ошибка | Симптом |
|---|---|---|
| Loader | Парсер ломает таблицы | Цифры в ответах неверные |
| Chunker | Граница режет таблицу/список | Ответ обрывочный |
| Embedder | Слабая модель для русского | Нерелевантные чанки в топ-10 |
| Vector DB | HNSW параметры по умолчанию | Recall ниже теоретического |
| Retrieval | Только dense, без BM25 | Не находит точные термины/артикулы |
| Reranker | Не используется | LLM получает шум, ответы «о том же, но не о том» |
| Prompt | Нет инструкции «не знаешь, скажи» | Галлюцинации |
| Generation | Слишком много чанков в контексте | Lost-in-middle, медленно, дорого |
Главное
RAG, не одна технология, а конвейер из 8–10 связанных подсистем. Качество финального ответа = произведение качеств каждой ступени. Слабое звено в любом месте обрушивает всю систему.
Хорошая новость: каждый компонент, сменный. Можно начать с pgvector + OpenAI embeddings + GPT-5 без reranker'а, замерить, и точечно улучшать там, где видны проблемы.
В следующих уроках разберём конкретные техники для повышения качества каждого этапа: что такое HyDE, как работает Contextual Retrieval от Anthropic, когда нужен GraphRAG, и как считать метрики качества всего этого через RAGAS.