/ Дневник курса / Урок 3 /

RAG-системы: качество ответа определяет поиск, а не модель

Ответ модели хорош ровно настолько, насколько хорош поиск по вашим данным. Как собрать такой поиск, что делать со сканами и таблицами и почему на сложных вопросах слабым звеном становится не поиск, а рассуждение модели.

  • rag
  • hybrid-search
  • reranking
  • vector-databases
  • retrieval

LLM знает мир, но не знает вашу компанию. RAG (Retrieval-Augmented Generation — генерация с дополнением через поиск) подмешивает релевантные куски ваших данных в промпт перед генерацией: запрос → поиск в БД → подмешивание (inject) найденного в контекст → генерация. Дальше — как устроен этот конвейер, чем его чинят на грязных документах и почему «улучшать retrieval» перестаёт помогать ровно там, где задача становится многошаговой.

1. RAG — это конвейер, а не модель

Базовое правило: качество retrieval (поиска) критичнее качества LLM — «мусор на входе = мусор на выходе». Можно взять флагманскую модель, но если поиск вернул не те чанки, ответ будет уверенно неверным. Поэтому RAG разбирают не как «умную модель», а как конвейер из двух слоёв:

 INDEXING  →  docs → chunking → embeddings → vector store
              offline, тяжёлый, один раз
     │
     ▼
  QUERY    →  запрос пользователя (online, на каждый запрос)
     │
     ▼
  SEARCH   →  hybrid search — recall (полнота): top-50…100 кандидатов
     │
     ▼
  RERANK   →  reranking — precision (точность): top-5 чанков
     │
     ▼
 GENERATE  →  inject в контекст → генерация с цитатами
Рисунок — конвейер RAG: офлайн-индексация (docs → chunking → embeddings → vector store) и онлайн-retrieval (hybrid search, top-50…100 кандидатов → reranking, top-5 чанков → инъекция в контекст и генерация с цитатами).

Индексация — дорогая разовая операция: даже короткий документ нужно прогнать через нарезку и embedding-модель (модель векторного представления), и это заметно тяжелее одного поискового запроса. Поиск — лёгкий, его гоняют на каждый запрос. Это разделение определяет всю эксплуатацию: индекс строят один раз на мощном узле, сохраняют на диск и переиспользуют, а инференс крутят хоть на CPU.

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

2. Не только векторный поиск

Чистого векторного поиска в 2026 уже недостаточно. У каждого метода своя слепая зона: вектор «размазывает» точные ID, коды и имена собственные («ошибка GO-124», «договор №123»), а лексический BM25 пропускает синонимы и смысл. Hybrid search (гибридный поиск) ловит обе категории — берёт топ от каждого метода и сливает ранги через RRF (Reciprocal Rank Fusion — слияние по обратному рангу):

RRF(d) = 1/(k + rank_bm25(d)) + 1/(k + rank_vector(d)),   k ≈ 60

Документ, высоко стоящий в обоих списках, получает максимальный итоговый ранг. Константа k (магическое число 60) сглаживает вклад низких позиций. Схема recall-этапа: BM25 топ-50 + Vector топ-50 → RRF топ-20.

Нюанс хранилища: PostgreSQL full-text search (tsvector/tsquery) — это лексический поиск, но он не равноценен полноценному вероятностному BM25 из dedicated-систем. Для честного production-hybrid чаще берут отдельный слой (Qdrant с нативным sparse/BM25, OpenSearch).

Смежные приёмы, когда запрос короткий или неоднозначный:

  • Query expansion — запрос расширяют синонимами.
  • HyDE (Hypothetical Document Embeddings) — LLM генерирует «гипотетический ответ», и поиск идёт уже по нему, а не по голому вопросу. Близкий вариант — Query2doc.
  • Parent-document retrieval (small-to-large) — ищем по мелким чанкам (точность retrieval), а в LLM отдаём родительский крупный документ (полнота контекста). Прибавка Context Precision порядка 15–25%.
  • Multi-hop (многошаговый поиск по цепочке документов) — связать факты из разных документов: туда обычный топ-K плохо дотягивается, это уже территория Agentic RAG и GraphRAG.

Связка Hybrid Search + Reranking покрывает примерно 80% use cases — это и есть минимальный продакшен-стандарт 2026.

3. Чанкинг и грязные документы

Чанкинг — первая точка, где ломается RAG. Граница чанка определяет, окажется ли нужный факт целиком в одном фрагменте или расколется пополам. Классический провал — анафоры: чанк «Его цена 500 руб» не знает, что «его» = товар X из соседнего фрагмента.

Наивный fixed-чанкинг 512/128 — не универсальный дефолт: он рвёт предложения и отделяет вопрос от ответа. Диагностика oversized-чанкинга проста: возьмите 15 запросов из production-логов и измерьте плотность релевантных предложений в топовом чанке — если ниже 25%, нарезка слишком широкая. Рабочие ориентиры размеров (не закон, а старт):

Тип контента Размер чанка Overlap
Проза 500–800 10–15%
Код 200–400 structure-aware (AST)
Регуляторика 800–1200 + parent-heading
Таблицы row-by-row + инъекция заголовков колонок

Отдельная боль — грязные документы: сканы, фото, многоколоночные PDF, таблицы с объединёнными ячейками. Голый текстовый экстрактор разрушает структуру таблицы, и чанк теряет смысл. Здесь нужен layout-aware препроцессинг: Docling (локально, парсит таблицы и формулы, держит таблицу в изолированном чанке), LlamaParse (облако, чистый иерархический Markdown), Reducto (вложенные заголовки, merged cells, multi-page таблицы), DocStrange (OCR по низкокачественным сканам).

Чтобы чинить анафоры на уровне эмбеддинга, есть три пути — и третий снимает компромисс первых двух:

  • Late Chunking (Jina, 2024) — весь документ сначала прогоняется через embedding-модель (self-attention видит всё), потом режется на чанки со span pooling. Каждый токен уже «знает» глобальный контекст, без доп. LLM-вызовов. Ограничение — контекстное окно модели.
  • Contextual Retrieval (Anthropic, 2024) — LLM дописывает к каждому чанку краткий контекст документа перед эмбеддингом. Решает анафоры лучше всех, но дорого: LLM-вызов на каждый чанк. «Для VIP-документов».
  • voyage-context-3 — нативные contextualized-эмбеддинги в один проход, без ручной аугментации и без лимита окна. На chunk-level retrieval обходит late chunking на 23.66% и contextual retrieval на 6.76%; на document-level — на 20.54% и 2.40%. Это снимает trade-off «дорогой Contextual против ограниченного окна Late Chunking».

4. Reranking: воронка точности

Векторный поиск (bi-encoder) кодирует вопрос и документ независимо и сравнивает косинусом — быстро, но грубо. Cross-encoder подаёт пару «вопрос + документ» в модель вместе, поэтому ранжирует точнее, но медленнее, и его нельзя гонять по всей базе: простой запрос на cross-encoder по всему корпусу может занять около двух минут.

Поэтому его применяют как сужающуюся воронку — только к уже отфильтрованному топу:

hybrid search   ──▶  over-retrieve: 12–20 кандидатов
cross-encoder   ──▶  rerank down: 6–10
финал           ──▶  в промпт LLM
Рисунок — воронка точности: hybrid search over-retrieve 12–20 кандидатов → cross-encoder rerank до 6–10 → финал в промпт LLM.

Это прямой контраст с дефолтным top-3: сначала набрать с запасом, потом точно отсеять. Reranking даёт стабильный +5–8% accuracy при контролируемой latency и при этом самый дешёвый по усилиям — поэтому входит в Production Baseline вместе с hybrid search. Целевые ориентиры: P95 < 3 c, TTFT (время до первого токена) < 500 мс.

from sentence_transformers import CrossEncoder
reranker = CrossEncoder("cross-encoder/ms-marco-MiniLM-L-6-v2")

def rerank(question, candidates, k=8):
    pairs  = [(question, c.page_content) for c in candidates]  # пара вместе
    scores = reranker.predict(pairs)                           # точный балл
    ranked = sorted(zip(scores, candidates), key=lambda x: x[0], reverse=True)
    return [c for _, c in ranked][:k]

Из мультиязычных reranker'ов текущий ориентир — jina-reranker-v3 (0.6B, listwise, BEIR 61.94 nDCG@10, покрывает русский). Но конкретные SOTA-модели устаревают быстро — на эту цифру не молиться.

5. Предобработка: метаданные, версии, дедупликация

Retrieval-конвейер легко недооценить со стороны «гигиены корпуса». Несколько вещей, без которых прод тихо деградирует:

  • Метаданные на каждом чанке: doc_id, заголовок секции, url/путь, timestamp, версия embedding-модели (детект дрейфа) и content hash.
  • Дедупликация в два слоя: идентичные файлы отсекаются SHA-256 по каноническому содержимому; near-duplicate ловят через MinHash + Jaccard и LSH-джойны (Locality-Sensitive Hashing) и выпиливают.
  • Жизненный цикл (lifecycle) / каскадные удаления: векторные БД не делают нативный update. Когда документ изменён или удалён, нужен Document Registry (doc_id → chunk_ids), чтобы найти и зачистить устаревшие чанки и эмбеддинги, иначе в выдаче всплывают мёртвые версии.
  • Embedding drift: устаревшие векторы тихо теряют 10–20 пунктов качества retrieval за год. Лечение — отслеживать версию модели в метаданных и переиндексировать.
  • Faithfulness floor: целевая точность привязки ответа к источникам около 90%, абсолютный минимум безопасности — 70%; ниже него алерт должен остановить тихую деградацию качества.

Смена модели эмбеддингов — это, по сути, миграция схемы БД: старые векторы геометрически несовместимы с новыми запросами, нужна полная переиндексация. Делают её zero-downtime через alias: строят теневой индекс новой моделью, валидируют на бенчмарк-наборе, атомарно переключают alias, старый держат для отката.

6. Где хранить векторы

После чанкинга и эмбеддинга векторы надо где-то держать и быстро искать ближайших соседей (ANN). Выбор — по масштабу, скорости и потребности в фильтрации:

БД Масштаб Сильная сторона
FAISS / Chroma in-memory Прототип, минимум настройки, persist на диск
pgvector (Postgres) ≤~1M уверенно, до ~10M с тюнингом Развёрнут везде; embeddings рядом с relational data
Qdrant >100M, единицы мс* Лучшая фильтрация по метаданным, нативный BM25
Weaviate Популярный универсал
Milvus >100M, K8s Лучшее масштабирование, RRF из коробки
Redis Низкая latency, vector-поиск поверх кэша

Про потолок pgvector важно без догматизма: он уверенно тянет малые и средние корпуса, особенно когда векторы — атрибут relational-строки и не нужна селективная фильтрация или hybrid. Комфортный ориентир — до ~1M векторов, до ~10M с тюнингом и компромиссами по index build / памяти / recall. Порог 50M не брать как дефолт без своего бенчмарка — строгие числа (включая «единицы мс» латентности Qdrant, отмеченные *) идут от вендора и в независимых замерах на 10M ближе к 20+ мс. Когда нужна filterable HNSW, sparse/BM25, multivector — переходят на dedicated store. Отдельная ловушка: ColBERT эмбеддит каждый токен, и 100k документов превращаются в 10M+ векторов — это уже за пределами pgvector.

7. Agentic RAG и GraphRAG: когда они оправданы

Линейный RAG — один проход. Две архитектуры из Enterprise-фазы надстраиваются над ним, но обе дорогие, и включать их «по умолчанию» — ошибка.

Agentic RAG ставит над поиском агентный цикл: query planner декомпозирует запрос, retrieval agent выбирает источник (vector / SQL / API), reflection через LLM-as-Judge проверяет полноту, и при нехватке данных запрос переписывается и поиск повторяется. Это снимает single-shot-ограничение на multi-step и cross-system задачах. Но у автономии есть цена: на верификации фактов (FEVER) у Agentic RAG recall падает до ~49% — агент рано решает, что данных достаточно, и останавливает поиск, тогда как детерминированный Enhanced RAG держит ~84%. Поэтому его не берут под жёсткий SLA, ограниченный бюджет или требование воспроизводимости (финансы, медицина).

GraphRAG вместо изолированных чанков строит граф знаний: LLM извлекает сущности и связи, и ответ собирается обходом по рёбрам. Незаменим для global / multi-hop вопросов («какие основные темы во всём архиве?», «как связаны X и Y из разных документов?»), где топ-K похожих чанков бессилен. Comprehensiveness на global-вопросах растёт на 72–83%. Расплата — отдельная база поверх векторной: стоимость памяти и индексации примерно ×2. Семейство расширяется (NodeRAG, RAPTOR, TreeRAG), доступно коробочно в RAGFlow и Dify.

Для advanced-сборки этих пайплайнов удобен LlamaIndex: QueryFusionRetriever оркестрирует семантико-лексический hybrid (dense + sparse BM25) из коробки, а поверх ложатся reranking и агентные flow.

8. Узкое место смещается в reasoning

Тезис «качество упирается в retrieval» верен, пока запрос простой. На multi-hop он перестаёт быть полным. Свежие замеры на Graph-RAG показывают разрыв: покрытие контекста (gold-ответ где-то в найденном тексте) достигает 77–91%, а итоговая точность ответа держится всего между 35% и 78%. То есть нужное уже найдено, но модель не может его связать.

retrieval coverage   █████████████████░░░  77–91%  (нашли)
response accuracy    ███████████░░░░░░░░░  35–78%  (ответили)
                     └─ разрыв = reasoning failure
Рисунок — разрыв coverage/accuracy на multi-hop: нужное найдено в 77–91% случаев, а верный ответ дан лишь в 35–78%; разница и есть reasoning failure.

Разбор ошибок: 73–84% из них — это reasoning failures, а не retrieval failures. Сильный поиск не гарантирует сильный ответ. Лечение здесь — не «ещё лучше искать», а структурировать рассуждение: structured prompting (например, SPARQL chain-of-thought на графовых пайплайнах) даёт +7.6 п.п. на 2WikiMultiHopQA. Свежий приём ConRAG согласует relational / entity / textual сигналы в едином ранжировании и прибавляет до 10.2 пункта Accuracy на MuSiQue — самом сложном из multi-hop бенчмарков (2–4 шага рассуждения, похожие дистракторы).

Практический вывод: прежде чем усложнять retrieval, стоит померить, где именно ломается цепочка — в поиске или в рассуждении. Для этого появились process-level инструменты: RAGCap-Bench оценивает планирование, извлечение свидетельств, grounded inference (вывод с опорой на найденные источники) и устойчивость к шуму по отдельности; AgenticRAGTracer делает hop-aware трассировку и показывает, на каком конкретно хопе агент «схлопнулся» или «переусердствовал». Не доверяй одной цифре итоговой точности — она прячет, на каком этапе утекло качество.

9. Метрики: чем мерить качество

Финал §8 — частный случай общего правила: одна итоговая цифра прячет место утечки. Поэтому качество RAG меряют не одним числом, а дашбордом из ~12 показателей, разбитых по группам — и каждая группа отвечает на свой вопрос: нашли ли (Retrieval), хорошо ли ответили (Generation), как сработал конвейер целиком (End-to-End). Если итоговая точность просела, именно разбивка по группам показывает, где — в поиске, в рассуждении или в склейке контекста.

Главное правило старта: начни с Hit Rate. Если документы не находятся, остальные метрики бессмысленны — улучшать генерацию поверх пустого контекста нечего. Вторая по приоритету — Faithfulness: галлюцинации остаются главной проблемой RAG в проде, и ловит их именно она.

Метрика Группа Цель Что измеряет
Hit Rate@K Retrieval ≥ 0.85 recall: попал ли релевантный документ в топ-K
MRR Retrieval ≥ 0.75 позиция первого релевантного
NDCG@K Retrieval ≥ 0.70 качество ранжирования с учётом позиции
Faithfulness Generation ≥ 0.85 доля утверждений ответа, подтверждённых контекстом (галлюцинации)
Answer Relevancy Generation ≥ 0.80 соответствие ответа вопросу
Answer Correctness Generation ≥ 0.75 F1 + семантическое сходство с эталоном
Toxicity Generation ≤ 0.05 безопасность ответа
Context Precision End-to-End ≥ 0.70 доля релевантных чанков в контексте
Context Recall End-to-End ≥ 0.80 покрытие всех нужных документов
Context F1 End-to-End ≥ 0.75 баланс Precision и Recall
Noise Robustness End-to-End ≥ 0.90 устойчивость к нерелевантным чанкам в контексте
Latency P95 / TTFT Операц. < 3 c / < 500 мс время ответа end-to-end и до первого токена

Пороги — ориентир из материалов, не закон: для русскоязычного домена реалистичные планки стоит откалибровать на своём наборе. Faithfulness здесь даётся как планка RAGAS (≥0.85); это согласуется с прод-floor из §5 — целевая привязка к источникам около 90%, а 0.85 — рабочий порог алерта, ниже которого систему уже нельзя выпускать.

Считать всё это руками не нужно. RAGAS — open-source стандарт де-факто: LLM-as-Judge оценивает Faithfulness, Answer Relevancy, Context Precision/Recall без ground truth, плюс автогенерирует синтетический eval-датасет из ваших документов (from ragas import evaluate). Рядом — DeepEval (pytest-стиль для CI/CD), RAGChecker (Amazon, claim-level диагностика), TruLens (RAG Triad). Цена offline-прогона на GPT-4o-судье — порядка $0.05–0.20 за запрос, можно сбить локальной моделью.

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

 ЛОГИРОВАНИЕ →  query, retrieved_chunks, answer, latency, feedback
      │
      ▼
   ОЦЕНКА    →  online (без GT): cosine, Faithfulness
                offline: тест-датасет 50–200 вопросов
      │
      ▼
 МОНИТОРИНГ  →  дашборд + алерты при падении ниже порога
      │
      ▼
  УЛУЧШЕНИЕ  →  анализ провалов, A/B на одном параметре, реиндекс/промпт
      │
      └──▶  повторный замер → цикл с начала
Рисунок — замкнутый цикл качества: логирование (query, retrieved_chunks, answer, latency) → оценка (online без ground truth + offline на тест-датасете 50–200 вопросов) → мониторинг с алертами при падении ниже порога → улучшение и повторный замер.
  • Логирование — на каждый запрос сохраняют query, retrieved_chunks, answer, latency и feedback (лайк/дизлайк, копирование ответа как implicit-сигнал).
  • Оценка — online без ground truth (cosine similarity, Faithfulness через LLM-as-Judge, Toxicity-классификатор на каждый ответ) и offline на тест-датасете 50–200 вопросов с эталонами, прогоняемом при каждой смене модели, промпта или индекса.
  • Мониторинг — дашборд со скользящими средними и алерты: Faithfulness < 0.80 → critical, Hit Rate < 0.75 → warning, Latency P95 > 5 c → warning.
  • Улучшение — классифицировать провал (retrieval / ranking / generation / missing knowledge), приоритизировать по частоте и бизнес-импакту, прогнать A/B, поменяв ровно один параметр, и сравнить все метрики до/после.

Без этого цикла RAG невозможно отличить от демо: цифры показывают не только текущее качество, но и где именно оно утекает между фазами конвейера.

Итог

  • Качество ответа упирается в retrieval — но только пока запрос простой: мусор на входе = мусор на выходе, и флагманская модель этого не спасёт.
  • Минимальный продакшен-стандарт 2026 — Hybrid Search + Reranking: он закрывает ~80% use cases и самый дешёвый по усилиям. Всё остальное (Agentic RAG, GraphRAG, contextualized-эмбеддинги) — надстройки под конкретную боль, не дефолт.
  • На multi-hop узкое место смещается из поиска в reasoning: нужное уже найдено (coverage 77–91%), но модель не связывает факты (accuracy 35–78%). Лечат не «ещё лучше искать», а структурированием рассуждения.
  • Мерить надо не одной цифрой, а дашбордом по группам (Retrieval / Generation / End-to-End), начиная с Hit Rate и Faithfulness, и встраивать замер в замкнутый цикл логирование → оценка → мониторинг → улучшение.

FAQ

Чем hybrid search лучше чисто векторного?

У каждого метода своя слепая зона: вектор «размазывает» точные ID, коды и имена собственные («ошибка GO-124», «договор №123»), а лексический BM25 пропускает синонимы и смысл. Hybrid берёт топ от обоих и сливает ранги через RRF (Reciprocal Rank Fusion), так что документ, высоко стоящий в обоих списках, получает максимальный итоговый ранг. Это и есть минимальный продакшен-baseline вместе с reranking.

Когда нужен GraphRAG, а когда он лишний?

GraphRAG оправдан на global и multi-hop вопросах — «какие основные темы во всём архиве?», «как связаны X и Y из разных документов?» — где топ-K похожих чанков бессилен; comprehensiveness там растёт на 72–83%. Расплата — отдельная база знаний поверх векторной, стоимость памяти и индексации примерно ×2. Для линейного «найди факт в документе» это избыточно: включать его по умолчанию — ошибка.

Какие метрики мерить у RAG?

Не одну итоговую цифру, а дашборд по трём группам: Retrieval (Hit Rate@K, MRR, NDCG), Generation (Faithfulness, Answer Relevancy, Toxicity) и End-to-End (Context Precision/Recall, Noise Robustness) плюс операционные Latency P95 / TTFT. Начинать с Hit Rate (если документы не находятся, остальное бессмысленно), вторая по приоритету — Faithfulness, она ловит галлюцинации. Считать удобно через RAGAS без ground truth.

pgvector или Qdrant?

По масштабу и потребности в фильтрации. pgvector хорош, когда векторы — атрибут relational-строки и не нужны селективная фильтрация или hybrid: комфортный ориентир до ~1M векторов, до ~10M с тюнингом и компромиссами по памяти и recall. Когда нужны filterable HNSW, нативный sparse/BM25 или multivector, либо корпус крупнее — переходят на dedicated store вроде Qdrant. Порог 50M не стоит брать как дефолт без своего бенчмарка.

Что делать с таблицами и сканами?

Голый текстовый экстрактор разрушает структуру таблицы, и чанк теряет смысл, поэтому нужен layout-aware препроцессинг: Docling (локально, парсит таблицы и формулы, держит таблицу в изолированном чанке), LlamaParse (облако, иерархический Markdown), Reducto (merged cells, multi-page таблицы), DocStrange (OCR по низкокачественным сканам). Таблицы режут row-by-row с инъекцией заголовков колонок, а структурные заголовки секций добавляют в текст чанка до эмбеддинга.

Когда брать Agentic RAG?

Когда задача multi-step или cross-system и нужно декомпозировать запрос, выбирать источник и при нехватке данных переписывать запрос и искать снова. Но у автономии есть цена: на верификации фактов (FEVER) recall у Agentic RAG падает до ~49% против ~84% у детерминированного pipeline — агент рано решает, что данных достаточно. Поэтому его не берут под жёсткий SLA, ограниченный бюджет или требование воспроизводимости (финансы, медицина).

Источники

Числовые ориентиры из текста — пороги метрик, бенчмарки векторных БД и проценты приростов — зависят от корпуса и профиля нагрузки; на своих данных и своей нагрузке они будут другими, калибруйте на собственном наборе.