# ГЛАВА 12. RAG: КОГДА МОДЕЛИ НЕ ХВАТАЕТ СОБСТВЕННЫХ ЗНАНИЙ

---

У каждого эксперта есть предел. Он может знать всё о гражданском праве — но не помнить конкретный пункт из приложения к договору, подписанному вчера. Он может блестяще рассуждать об архитектуре ПО — но не знать, что ваша команда три дня назад перешла на новый API. Знания, вшитые в голову (или в веса модели), — это **параметрическая память**. Она мощна, но у неё есть дата отсечения (knowledge cutoff) и физический предел — нельзя запомнить всё.

Параметрическая память LLM — это то, что модель «выучила» за время тренировки. После деплоя она статична. Новый регламент компании, свежий баг-репорт, обновлённый прайс-лист — модель о них не знает, пока вы не сообщите ей явно.

Наивное решение — засунуть всё в context window. Как мы разобрали в [Главе 5](05_long_context.md), это путь с квадратичной стоимостью, деградацией качества (Lost in the Middle) и растущей латенси. Запихнуть 500 документов в контекст — это дорого, медленно и ненадёжно.

**RAG (Retrieval-Augmented Generation)** — альтернативная стратегия: вместо того чтобы давать модели *всё*, дать ей *только то, что нужно*. Библиотекарь не читает все книги в библиотеке перед каждым ответом — он знает, где искать, находит нужный том, открывает нужную страницу и цитирует. RAG превращает LLM из эрудита, ограниченного собственной памятью, в исследователя с доступом к актуальной базе знаний.

Lewis et al. (2020) формализовали эту идею в рамках NeurIPS: модель, которая *сначала* извлекает релевантные документы, *а потом* генерирует ответ с опорой на них, — превосходит чисто параметрические модели на knowledge-intensive задачах. С тех пор RAG стал одним из базовых паттернов production-систем на LLM.

---

## 12.1. Почему context stuffing не масштабируется

Казалось бы, зачем возиться с retrieval, если у Claude Opus 4.6 контекст 1M токенов? Три причины.

**Стоимость.** Даже с оптимизациями (Flash Attention (Dao et al., 2022), KV-кэш, PagedAttention (Kwon et al., 2023)), inference стоимость растёт с длиной контекста. Отправить 200K токенов в каждый вызов API — это десятки центов за запрос. При 1000 запросов в день расходы становятся значительными.

**Качество.** Lost in the Middle (Liu et al., 2023): релевантная информация, помещённая в середину длинного контекста, обрабатывается хуже, чем та же информация в начале или в конце. Модель «видит» длинный контекст, но attention-веса распределяются неравномерно. Подробный разбор — в [Главе 5, раздел 5.3](05_long_context.md).

**Латенси.** Time-to-first-token растёт с длиной входа. Для интерактивных приложений (чат, поиск, автокомплит) задержка в 5–10 секунд на prefill — неприемлема.

RAG решает все три проблемы: вместо того чтобы скармливать модели всю базу знаний, мы извлекаем 5–20 релевантных фрагментов и подаём только их. Контекст остаётся коротким, стоимость — низкой, качество — высоким.

| Стратегия | Стоимость за запрос | Латенси | Качество на 500 документах |
|-----------|--------------------|---------|-----------------------------|
| Context stuffing (все документы) | $$$ | Высокая (секунды на prefill) | Деградирует (Lost in the Middle) |
| RAG (top-10 чанков) | $ | Низкая (~200 мс retrieval + стандартный inference) | Стабильное (фокус на релевантном) |
| Fine-tuning | Амортизировано | Быстрая (нет retrieval) | Хорошее для узкой области, не обновляется |

---

## 12.2. Анатомия RAG-пайплайна

Стандартный RAG-пайплайн — это конвейер из шести этапов. Каждый можно настраивать и заменять независимо:

```
┌─────────────┐    ┌─────────────┐    ┌─────────────┐
│  Документы  │ ─→ │  Чанкинг    │ ─→ │  Embedding  │
│  (корпус)   │    │  (нарезка)  │    │  (векторы)  │
└─────────────┘    └─────────────┘    └──────┬──────┘
                                             │
                                             ▼
┌─────────────┐    ┌─────────────┐    ┌─────────────┐
│  Генерация  │ ←─ │  Reranking  │ ←─ │  Retrieval  │
│  (LLM)      │    │  (пересорт) │    │  (поиск)    │
└─────────────┘    └─────────────┘    └─────────────┘
```

**1. Чанкинг** — нарезка документов на фрагменты фиксированного или переменного размера.
**2. Embedding** — превращение каждого фрагмента в вектор в семантическом пространстве (см. [Главу 1](01_tokens_vectors_and_semantic_space.md)).
**3. Индексация** — сохранение векторов в хранилище с поддержкой similarity search.
**4. Retrieval** — поиск топ-k фрагментов, ближайших к запросу пользователя.
**5. Reranking** — пересортировка результатов более точной моделью.
**6. Генерация** — LLM генерирует ответ, опираясь на извлечённые фрагменты.

Этапы 1–3 выполняются **один раз** при индексации корпуса (offline). Этапы 4–6 — **при каждом запросе** (online). Это ключевое архитектурное свойство: RAG разделяет подготовку данных и inference.

---

## 12.3. Чанкинг: как нарезать документы

Чанкинг — первый этап, и ошибки здесь каскадируются на все последующие. Слишком маленькие чанки — точный retrieval, но потерянный контекст. Слишком большие — контекст сохранён, но релевантность размывается.

### Стратегии чанкинга

| Стратегия | Размер | Плюсы | Минусы | Когда использовать |
|-----------|--------|-------|--------|-------------------|
| Fixed-size (по символам/токенам) | 256–1024 токенов | Просто, предсказуемо | Режет предложения и абзацы | Однородные тексты, логи |
| Sentence-level | 1–5 предложений | Сохраняет грамматическую целостность | Разброс размеров | Справочные статьи, FAQ |
| Recursive (LangChain-стиль) | Адаптивный | Сначала делит по `\n\n`, потом по `\n`, потом по предложениям | Сложнее контролировать | Markdown, документация |
| Semantic | Динамический | Группирует по смысловой близости | Требует embedding на этапе чанкинга | Длинные нарративные тексты |
| Document-aware | По структуре документа | Учитывает заголовки, разделы, таблицы | Нужен парсер формата | PDF, HTML, код |

### Overlap

Overlap (перекрытие) между чанками — 10–20% от размера чанка — решает проблему «разрезанного абзаца». Если критическое предложение попало на границу, перекрытие гарантирует, что оно целиком окажется хотя бы в одном чанке.

> **Промпт для генерации кода.** *«Напиши recursive text splitter для документов: chunk_size 512 токенов, overlap 64 токена (~12%), разделители по приоритету — двойной перенос строки, одинарный перенос, точка с пробелом, пробел. Используй LangChain RecursiveCharacterTextSplitter или аналог текущей версии фреймворка. Покажи пример нарезки одного документа.»*

### Практические рекомендации

- **Документация, база знаний**: recursive splitting, 400–600 токенов, overlap 50–80.
- **Код**: document-aware splitting по функциям/классам (tree-sitter для AST).
- **Юридические документы**: sentence-level с сохранением номеров пунктов в метаданных.
- **Таблицы**: не режьте — сериализуйте строки с заголовками в каждом чанке.

---

## 12.4. Embedding и retrieval

### Dense embeddings

Embedding-модели превращают текст в вектор фиксированной размерности. Семантически близкие тексты оказываются рядом в пространстве (подробнее — [Глава 1](01_tokens_vectors_and_semantic_space.md)). Для RAG это означает: запрос «как настроить авторизацию» окажется близко к чанку про OAuth, даже если в чанке нет слова «авторизация».

Популярные модели (2025–2026):

| Модель | Размерность | Контекст | Особенности |
|--------|------------|----------|-------------|
| OpenAI `text-embedding-3-large` | 3072 (сжимаемо) | 8K токенов | Matryoshka: можно усечь до 256–1536 dim |
| Cohere `embed-v4` | 1536 (по умолчанию; сжимаемо до 256) | 128K токенов | Мультимодальные (текст + изображения), search/classification |
| `bge-large-en-v1.5` (BAAI) | 1024 | 512 токенов | Open-source, English |
| `bge-m3` (BAAI) | 1024 | 8K токенов | Open-source, мультиязычный, multi-granularity |
| Voyage `voyage-4-large` | 1024 (Matryoshka: 256/512/2048) | 32K токенов | Серия voyage-4 (large/standard/lite) |

**Matryoshka embeddings** (Kusupati et al., 2022) — подход, где первые координаты полного вектора уже сохраняют большую часть полезного сигнала. Это позволяет хранить укороченные векторы (256 dim вместо 3072) с минимальной потерей качества retrieval — и экономить на хранении и скорости поиска.

### Sparse retrieval: BM25

Dense embeddings ловят *семантическую* близость — но иногда нужна *лексическая*. Если пользователь ищет «ошибка ERR-4021», dense embedding может не найти точный номер ошибки, зато BM25 найдёт моментально — по точному совпадению терминов.

BM25 — вероятностная модель ранжирования, основанная на TF-IDF с нормализацией по длине документа. Несмотря на возраст (Robertson et al., 1994), BM25 остаётся конкурентным baseline и в 2026 году.

### Hybrid search

Лучшая практика — **гибридный поиск**: объединение dense и sparse retrieval с последующим слиянием результатов.

```python
# Псевдокод: hybrid search с Reciprocal Rank Fusion (RRF)
def hybrid_search(query: str, k: int = 10) -> list[Chunk]:
    dense_results = vector_db.search(embed(query), top_k=k * 2)
    sparse_results = bm25_index.search(query, top_k=k * 2)

    # Reciprocal Rank Fusion (Cormack et al., 2009)
    scores: dict[str, float] = {}
    for rank, chunk in enumerate(dense_results):
        scores[chunk.id] = scores.get(chunk.id, 0) + 1 / (60 + rank)
    for rank, chunk in enumerate(sparse_results):
        scores[chunk.id] = scores.get(chunk.id, 0) + 1 / (60 + rank)

    return sorted(scores, key=scores.get, reverse=True)[:k]
```

RRF (Reciprocal Rank Fusion) — простой и эффективный способ объединения: чем выше документ стоит в каждом списке, тем больше вклад он получает в итоговый score; вклад убывает с номером позиции, после чего результаты суммируются. Это устойчивее, чем нормализация скоров, потому что шкалы dense и sparse метрик — разные.

### Векторные базы данных

| База | Тип | Особенности |
|------|-----|-------------|
| Qdrant | Dedicated | Фильтрация по payload, distributed, Rust |
| ChromaDB | Embedded | Простой API, для прототипов |
| Pinecone | Managed | Serverless, автомасштабирование |
| Weaviate | Dedicated | Hybrid search из коробки, GraphQL |
| pgvector | Extension | PostgreSQL-расширение, HNSW-индекс |

Для прототипа — ChromaDB или pgvector. Для production с > 10M векторов — Qdrant или Pinecone. Критерий выбора: не «какая база модная», а *как она интегрируется с вашим стеком*, поддерживает ли фильтрацию по метаданным и какой latency на вашем объёме.

### Квантизация

Хранение 10M векторов по 3072 dim в float32 — это ~115 ГБ. Квантизация (int8, binary) снижает объём в 4–32 раз с потерей ~1–3% retrieval quality. На масштабе — это компромисс, который почти всегда оправдан.

---

## 12.5. Reranking и фильтрация

### Зачем нужен reranking

Embedding-based retrieval — быстрый, но грубый. Bi-encoder (embedding-модель) обрабатывает запрос и документ *независимо*, а потом сравнивает их векторы. Cross-encoder (reranker) смотрит на пару (запрос, документ) *совместно* — и точнее определяет релевантность.

Аналогия: bi-encoder — это фильтр по резюме (быстро, но много false positives). Cross-encoder — это техническое собеседование (дороже, но отсеивает точно).

```
Retrieval (top-50, быстро) → Reranking (top-50 → top-5, точно) → LLM (top-5)
```

### Инструменты

| Reranker | Тип | Особенности |
|----------|-----|-------------|
| Cohere Rerank | API | Коммерческий, высокое качество |
| `bge-reranker-v2-m3` | Open-source | Мультиязычный, можно запустить локально |
| `cross-encoder/ms-marco-MiniLM-L-6-v2` | Open-source | Лёгкий, быстрый |
| Voyage Rerank | API | Code-aware |

### Metadata filtering

Reranking работает на уровне семантики. Но часто нужна фильтрация по метаданным *до* или *вместе* с semantic search. Паттерн: при поисковом запросе передайте вектор запроса, top-k и словарь фильтров (`source`, `updated_after`, `department`, `language`). Все major vector-базы (Qdrant, Weaviate, Pinecone, pgvector) поддерживают metadata-фильтры при search.

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

### The retrieval-generation gap

Важный нюанс: высокое качество retrieval не гарантирует высокое качество генерации. Модель может получить правильные чанки — и всё равно сгаллюцинировать, если:
- Чанк содержит нужный факт, но в неочевидной формулировке.
- Несколько чанков противоречат друг другу.
- Модель «предпочитает» свои параметрические знания извлечённому контексту.

Этот зазор между retrieval quality и generation faithfulness — одна из центральных проблем RAG. Решение — в правильном prompt engineering (раздел 12.6) и в eval-фреймворках (раздел 12.8).

---

## 12.6. Генерация с контекстом: prompt engineering для RAG

### Инъекция контекста

RAG-промпт — это промпт с явно выделенной секцией для извлечённого контекста. XML-разметка (см. [Главу 7](07_markup_tags_and_prompt_architecture.md)) — идеальный формат:

```xml
<system>
Ты — ассистент технической поддержки. Отвечай ТОЛЬКО
на основе предоставленных документов. Если ответа нет
в документах — скажи "Информация не найдена в базе знаний".
</system>

<retrieved_documents>
  <document id="1" source="docs/auth.md" updated="2025-11-15">
    OAuth 2.0 используется для авторизации внешних приложений.
    Токен обновления действует 30 дней. Для внутренних сервисов
    используется mTLS.
  </document>
  <document id="2" source="docs/api-limits.md" updated="2026-01-03">
    Rate limit для API v3: 1000 req/min для плана Enterprise,
    100 req/min для Free.
  </document>
</retrieved_documents>

<user_query>
Какой rate limit для Enterprise-клиентов?
</user_query>

<instructions>
Ответь на вопрос пользователя, цитируя конкретные документы.
Формат: [doc_id] после каждого утверждения.
</instructions>
```

### Citation grounding

Требование цитировать источники — один из самых эффективных способов снизить галлюцинации в RAG. Когда модель должна указать `[doc_id]` после каждого утверждения, это:
1. **Фиксирует attention** на извлечённых документах, а не на параметрической памяти.
2. **Создаёт проверяемый артефакт** — можно программно верифицировать, что cited document действительно содержит claim.
3. **Повышает калибровку** — модель реже выдумывает, если знает, что нужно привязать ответ к конкретному источнику.

### «Отвечай только по документам» — работает ли?

Инструкция «Answer ONLY based on provided context» снижает extrinsic hallucinations, но не устраняет их полностью. Модели с сильными параметрическими знаниями (GPT-5.4, Claude Opus 4.6) иногда «дополняют» ответ собственными знаниями, даже если промпт запрещает это.

Повышает надёжность:
- Explicit «If the answer is not in the documents, say so» — модели лучше реагируют на явный fallback.
- Structured output с полем `"source_doc_id"` — если поле обязательное, модель вынуждена привязаться к документу или вернуть null.
- Верификация постфактум — второй вызов или regex-проверка наличия citation (см. [Главу 13](13_anti_hallucination_loop.md)).

### Handling «Я не знаю»

RAG-система, которая не умеет отказываться отвечать, — бомба замедленного действия. Если в извлечённых документах нет ответа, модель должна сказать об этом. Это проектируется явно через системный промпт:

```
You are a support assistant.
Rules:
1. Answer ONLY from <retrieved_documents>.
2. Cite [doc_id] for every claim.
3. If no document answers the question, respond:
   {"answer": null, "reason": "Not found in knowledge base"}
4. NEVER fabricate information.
```

---

## 12.7. Продвинутые паттерны

Базовый RAG (single query → retrieval → generation) — отправная точка. Production-системы часто требуют более сложных архитектур.

### Multi-hop RAG

Некоторые вопросы невозможно ответить одним retrieval. «Какой бюджет был у проекта, руководитель которого ушёл в Q3?» — требует сначала найти руководителя, потом найти проект, потом найти бюджет.

Паттерн: **query decomposition → sequential retrieval → synthesis**.

```
Вопрос: "Какой бюджет у проекта, руководитель которого ушёл в Q3?"

→ Подзапрос 1: "Кто из руководителей проектов ушёл в Q3?"
  → Retrieval → "Иванов, проект Alpha"

→ Подзапрос 2: "Какой бюджет проекта Alpha?"
  → Retrieval → "12.5M руб."

→ Синтез: "Бюджет проекта Alpha (руководитель Иванов, ушёл в Q3) — 12.5M руб."
```

Это пересекается с chain-of-thought декомпозицией из [Главы 9](09_multistep_reasoning.md), но на уровне retrieval, а не рассуждения.

### GraphRAG

Edge et al. (2024) предложили строить **граф знаний** поверх чанков: извлекать сущности и отношения, строить граф, затем использовать community summaries для ответов на сложные вопросы, требующие «глобального» понимания корпуса.

```
Документы → Чанки → LLM извлекает (сущность, отношение, сущность)
  → Граф → Community detection → Summaries → Retrieval по summaries
```

GraphRAG силён на вопросах вида «каковы основные темы в этом корпусе» или «как связаны X и Y через цепочку отношений» — то, что плоский vector search не покрывает.

### RAPTOR

Sarthi et al. (ICLR 2024): **рекурсивная абстрактивная обработка**. Чанки кластеризуются, для каждого кластера генерируется summary, summaries снова кластеризуются — и так до корневого уровня. Получается дерево абстракций, по которому retrieval может идти на разных уровнях детализации.

Полезно, когда вопросы варьируются от «что конкретно написано в параграфе 3.2?» до «о чём вообще эта документация?».

### Self-RAG

Asai et al. (ICLR 2024): модель сама решает, **когда нужен retrieval**, а когда — нет. На каждом шаге генерации модель выдаёт специальные токены: `[Retrieve]` (нужен поиск), `[No Retrieve]` (знаю сам), `[Relevant]`/`[Irrelevant]` (оценка полученного контекста), `[Supported]`/`[Not Supported]` (самопроверка faithfulness).

Это элегантнее, чем «всегда ищи» или «никогда не ищи» — модель адаптивно включает retrieval только там, где параметрической памяти недостаточно.

### HyDE

Gao et al. (ACL 2023): **Hypothetical Document Embeddings**. Вместо того чтобы искать по запросу пользователя напрямую, модель сначала генерирует *гипотетический документ*, который мог бы содержать ответ, а затем ищет по embedding этого документа.

```
Запрос: "Как настроить mTLS между сервисами?"
→ LLM генерирует гипотетический ответ (может быть неточным)
→ Embedding гипотетического ответа → Vector search
→ Находит реальные документы, семантически близкие к ответу
```

HyDE помогает при vocabulary mismatch: запрос пользователя и документ корпуса могут описывать одно и то же разными словами. Гипотетический документ «переводит» запрос на язык корпуса.

### Corrective RAG (CRAG)

Базовый RAG предполагает, что retrieval уже принёс «достаточно хорошие» документы. CRAG (Yan et al., 2024) делает следующий шаг: **сначала оценивает качество retrieval, а потом решает, что делать дальше**.

Паттерн такой:
- retriever возвращает top-k документов;
- лёгкий retrieval evaluator выставляет confidence score;
- при высоком confidence система сразу идёт в генерацию;
- при низком confidence запускается коррекция: web search, альтернативный retriever, очистка или декомпозиция документов.

Это особенно полезно для шумных корпоративных корпусов, где часть документов устарела, дублируется или противоречит друг другу. CRAG переводит RAG из одношаговой схемы «нашли → сгенерировали» в более надёжную схему **«нашли → оценили → при необходимости исправили → только потом сгенерировали»**.

---

## 12.8. Оценка качества RAG-системы

Без метрик RAG — это чёрный ящик поверх чёрного ящика. Нужно измерять три независимых измерения.

### Три измерения качества

```
                  ┌─────────────────────┐
                  │  Retrieval Quality   │
                  │  (нашли ли нужное?)  │
                  └──────────┬──────────┘
                             │
              ┌──────────────┼──────────────┐
              ▼                              ▼
┌──────────────────────┐    ┌──────────────────────┐
│  Faithfulness        │    │  Answer Relevancy    │
│  (ответ верен        │    │  (ответ полезен      │
│   контексту?)        │    │   пользователю?)     │
└──────────────────────┘    └──────────────────────┘
```

1. **Retrieval quality** — нашла ли система нужные документы? Измеряется через context precision (какая доля извлечённых чанков релевантна) и context recall (какая доля нужной информации извлечена).

2. **Faithfulness** — генерирует ли модель ответ, *верный* извлечённым документам? Или добавляет факты, которых в контексте нет? Это ключевая метрика для RAG: галлюцинация *при наличии правильных документов* — самый коварный режим отказа (см. [Главу 3](03_hallucinations.md)).

3. **Answer relevancy** — полезен ли ответ пользователю? Можно извлечь правильные документы и сгенерировать faithful ответ, который при этом не отвечает на вопрос.

Отдельная современная тонкость: retrieval и параметрическая память модели находятся в режиме **tug-of-war**. Даже если вы положили в контекст документ, модель не обязана ему подчиниться; и наоборот, неверный retrieved факт может «переписать» корректный prior модели. Поэтому в eval-наборе полезно иметь не только обычные вопросы, но и **conflict cases**, где retrieved context специально противоречит prior модели. Это быстро показывает, умеет ли система различать «контекст прав» и «контекст шумит».

### RAGAS framework

Es et al. (EACL 2024) предложили RAGAS — автоматический eval-framework для RAG, который оценивает все три измерения с помощью LLM-as-a-judge:

| Метрика | Что измеряет | Как считается |
|---------|-------------|---------------|
| Context Precision | Доля релевантных чанков в top-k | LLM оценивает каждый чанк |
| Context Recall | Покрытие ground-truth ответа контекстом | LLM сопоставляет claims с чанками |
| Faithfulness | Все ли claims в ответе подтверждены контекстом | LLM разбивает ответ на claims, проверяет каждый |
| Answer Relevancy | Отвечает ли ответ на вопрос | LLM генерирует вопросы по ответу, сравнивает с оригиналом |

> **Промпт для генерации кода.** *«Настрой eval-пайплайн с RAGAS (ragas.io): подготовь датасет из вопросов, ground-truth-ответов, retrieved contexts и сгенерированных ответов. Оцени метрики faithfulness, context_precision, answer_relevancy. Покажи пример запуска evaluate() и интерпретацию результатов. Целевые значения: faithfulness ≥ 0.85, context_precision ≥ 0.70.»*

### RAGChecker и более тонкая диагностика

RAGAS хорошо подходит для быстрой итерации, но часто остаётся слишком грубым инструментом: видно, что «RAG плохой», но не видно, **какой модуль сломался**. Для этого появились более детальные фреймворки, например RAGChecker (Ru et al., 2024), который разносит диагностику по retrieval- и generation-модулю и даёт более точную картину причин деградации.

Практическое правило:
- **RAGAS** — когда нужна быстрая обратная связь в цикле разработки;
- **RAGChecker или аналогичная fine-grained диагностика** — когда нужно понять, почему система просела после изменения retriever, reranker, чанкинга или промпта.

### Ручная оценка

LLM-as-a-judge — удобно для итеративной разработки, но не заменяет human evaluation. Минимальный протокол:

1. Соберите 50–100 вопросов, покрывающих типичные сценарии.
2. Для каждого зафиксируйте ground-truth ответ и релевантные документы.
3. Прогоните RAG-пайплайн, соберите ответы.
4. Оцените по шкале 1–5: faithfulness, completeness, relevancy.
5. Посчитайте процент critical failures (полностью неверные ответы).

### Когда RAG проваливается

RAG не панацея. Типичные режимы отказа:

| Режим отказа | Причина | Как обнаружить |
|-------------|---------|----------------|
| Retrieval miss | Нужного документа нет в корпусе | Context recall → 0 |
| Relevant but buried | Нужный чанк на позиции 50+, не попал в top-k | Увеличить k, добавить reranking |
| Unfaithful generation | Модель игнорирует контекст | Faithfulness metric, citation check |
| Contradictory chunks | Два чанка противоречат друг другу | Ручная проверка, metadata filtering по дате |
| Bad chunking | Критическая информация разрезана | Проверить чанки вручную, увеличить overlap |
| Wrong context dominates prior | Retriever принёс ошибочный факт, и модель послушно встроила его в ответ | Conflict-set eval, retrieval confidence, contradiction check |

---

## 12.9. Data layer: жизненный цикл знаний

В предыдущих секциях мы разобрали, как строить retrieval pipeline: чанкинг, embedding, reranking, генерация. Но в production качество RAG-системы чаще ломается не на этапе retrieval, а на этапе данных: документы устаревают, индекс загрязняется дубликатами, удалённые файлы продолжают отдаваться, а access control игнорируется. Эта секция — про инженерию данных в RAG.

### Ingestion pipeline: от источника до индекса

Ingestion — это не одноразовая загрузка, а постоянный pipeline. Источники разнообразны: файлы, базы данных, API, веб-сайты, корпоративные мессенджеры, email.

Этапы ingestion-конвейера:

```
Источник → Extraction (парсинг формата) → Cleaning (удаление мусора, нормализация)
  → Chunking → Enrichment (metadata: source, date, author, ACL)
    → Embedding → Indexing
```

Критический этап — **metadata enrichment**. Метаданные, добавленные при ingestion (источник, дата, автор, ACL-тег, версия документа), определяют качество фильтрации на этапе retrieval. Без metadata filtering релевантность падает: система отдаёт устаревшие документы наравне с актуальными, а чанки из разных контекстов смешиваются.

### Freshness: актуальность индекса

Документы устаревают. Регламент обновился, API поменялся, сотрудник уволился — а в индексе лежат старые чанки. Два паттерна обновления:

- **Incremental ingestion** — отслеживание изменений в источнике (webhooks, polling, Change Data Capture). Переиндексируются только изменённые документы. Эффективно, но требует, чтобы источник поддерживал change tracking.
- **Full re-index** — периодическая полная переиндексация. Проще в реализации, но дороже по compute. Подходит для источников без change tracking или для небольших корпусов.

**Provenance tracking**: каждый чанк должен хранить ссылку на исходный документ, дату индексации и версию. Без provenance невозможно отличить свежий чанк от устаревшего — и невозможно корректно удалить данные.

**Deletion semantics**: при удалении документа из источника все его чанки должны удаляться из индекса. Типичный анти-паттерн — «призраки»: удалённые документы продолжают влиять на ответы, потому что их чанки остались в vector store.

### ACL-aware индексация и retrieval

В enterprise-среде не все документы доступны всем пользователям. RAG без access control — это потенциальная утечка данных.

Два подхода:

| Подход | Механизм | Плюсы | Минусы |
|--------|----------|-------|--------|
| Metadata-фильтрация | Запрос + ACL-фильтр при retrieval | Один индекс, проще поддерживать | Embedding «видит» все документы; фильтрация — на финальном этапе |
| Tenant-изолированные индексы | Отдельный индекс на tenant/роль | Strict isolation, нет риска утечки | Дороже в поддержке, дублирование общих документов |

Metadata-фильтрация при retrieval — минимальное требование для production RAG в организациях. Для систем со strict compliance (финансы, медицина) tenant-изоляция надёжнее.

### Contextual retrieval и enrich-before-embed

Стандартный embedding чанка теряет контекст документа: чанк из середины регламента превращается в безликий фрагмент. Anthropic Contextual Retrieval (2024): перед embedding'ом каждый чанк обогащается кратким описанием контекста документа, из которого он извлечён (prepended context). LLM генерирует 1–2 предложения: «Этот фрагмент из регламента безопасности, раздел про доступ к production-среде».

Это улучшает retrieval accuracy без изменения retrieval-алгоритма — за счёт того, что embedding теперь кодирует не только содержание чанка, но и его место в документе. Стоимость: один LLM-вызов на чанк на этапе ingestion. Оправдано для корпусов с высокой ценностью (внутренняя документация, юридические базы); избыточно для ephemeral данных (логи, временные заметки).

### Re-embed и re-index: миграция embedding-модели

При смене embedding-модели (новая версия, другой провайдер) все чанки нужно перевекторизовать — старые и новые embeddings несовместимы.

Практическое правило: **хранить исходный текст чанка**, а не только embedding. Без исходного текста миграция невозможна — придётся заново проходить весь ingestion pipeline от источников.

**Blue-green indexing**: создаём новый индекс параллельно старому, прогоняем eval-сет на обоих, переключаем трафик после верификации. Это тот же паттерн, что blue-green deployment в backend — только для vector store.

### Hosted retrieval: File Search и managed RAG

OpenAI File Search, Google Vertex AI Search, Azure AI Search — managed-решения, где провайдер берёт на себя ingestion, chunking, embedding и retrieval.

Преимущество: zero-ops для retrieval pipeline — загрузил файлы, получил API для поиска. Недостаток: меньше контроля над chunking-стратегией, выбором embedding-модели и reranking.

Для быстрого старта и средних нагрузок hosted retrieval часто достаточен. Для тонкой настройки, strict data residency или корпусов с нестандартной структурой (код, таблицы, мультимодальные документы) — self-hosted pipeline даёт больше контроля.

Качество RAG-системы определяется не только retrieval-алгоритмом, но и дисциплиной управления данными. Ingestion pipeline, freshness, ACL-aware indexing и provenance — это infrastructure, без которой даже идеальный retriever будет отдавать мусор.

---

## 12.10. RAG failure diagnosis playbook

Когда RAG-система даёт неверный ответ, недостаточно сказать «RAG сломался». Нужно определить, *какой именно компонент* отказал. Этот playbook — пошаговая диагностика для каждого из восьми режимов отказа (§12.8).

### Диагностическая процедура

Для каждого инцидента фиксируйте в диагностической карточке:

1. **Вопрос пользователя** — точный текст.
2. **Ответ системы** — что вернул RAG.
3. **Эталонный ответ** — что должно было быть (из golden dataset или ручная разметка).
4. **Извлечённые чанки** — top-k результатов retrieval (до reranking и после).
5. **Промпт, отправленный в LLM** — с injected контекстом.
6. **Метрики** — faithfulness, context_precision, answer_relevancy (из RAGAS или ручные).

Теперь — восемь режимов отказа и что делать в каждом.

---

### Режим 1: Retrieval miss — нужного документа нет в топе

**Симптомы:** RAGAS context_precision < 0.5. В извлечённых чанках нет информации, которая есть в корпусе.

**Диагностика:**
1. Вручную проверьте корпус — есть ли в нём ответ на вопрос? Если нет → это не retrieval miss, это missing content (Режим 7).
2. Найдите правильный чанк вручную. На какой он позиции в retrieval-результате?
3. Проверьте embedding запроса: совпадает ли «язык» запроса с языком корпуса? (разные термины для одного понятия)

**Исправление:**
- Позиция 11-20 → увеличьте k (retrieve top-30 вместо top-10), добавьте reranking.
- Позиция 21+ → попробуйте HyDE (§12.7): сгенерируйте гипотетический ответ, ищите по нему.
- Систематически → пересмотрите chunking (слишком мелкие чанки теряют контекст), попробуйте contextual retrieval (§12.9).
- Разный «язык» → добавьте синонимы в запрос (query expansion), используйте hybrid search (dense + BM25).

---

### Режим 2: Relevant but buried — чанк найден, но не в топе

**Симптомы:** Ручная проверка показывает, что нужный чанк есть на позиции 8-15, но в генерацию попадают только top-5.

**Диагностика:** Проверьте позицию правильного чанка в retrieval results. Если она стабильно 6-15 → проблема в ранжировании.

**Исправление:**
- Добавьте reranking (cross-encoder) — это самый эффективный способ поднять релевантные чанки.
- Увеличьте k для reranking: retrieval top-50 → rerank → top-5.
- Проверьте metadata filtering: возможно, правильный чанк имеет устаревшую дату и исключается фильтром.

---

### Режим 3: Unfaithful generation — модель игнорирует контекст

**Симптомы:** RAGAS faithfulness < 0.7. Правильные чанки в контексте есть, но модель генерирует ответ «мимо» них или дополняет своими знаниями.

**Диагностика:**
1. Сравните claims в ответе с содержанием извлечённых чанков (вручную или через RAGAS).
2. Есть ли в ответе факты, которых нет ни в одном чанке? → модель использует параметрическую память.
3. Есть ли в ответе утверждения, противоречащие чанкам? → модель «переспорила» контекст.

**Исправление:**
- Усилите промпт: `"Answer ONLY using the provided documents. If the answer is not in the documents, say 'Not found in knowledge base'."`
- Добавьте citation grounding: требуйте `[doc_id]` после каждого утверждения (§12.6).
- Используйте structured output с обязательным полем `source_doc_id` — модель вынуждена привязаться к документу.
- Добавьте Verifier (Глава 13): второй LLM-вызов проверяет faithfulness ответа.
- Проверьте conflict cases: возможно, retrieved context противоречит сильным prior модели — тогда модель «выбирает» prior.

---

### Режим 4: Contradictory chunks — чанки противоречат друг другу

**Симптомы:** Ответ модели непоследователен. В retrieved chunks есть взаимоисключающая информация (например, «API v1 активен» и «API v1 отключён с марта»).

**Диагностика:** Просмотрите retrieved chunks на предмет противоречий. Обратите внимание на даты — часто противоречие вызвано устаревшим документом.

**Исправление:**
- Metadata filtering по дате: `updated_after: 2025-01-01` или `version: latest`.
- Provenance tracking (§12.9): каждый чанк хранит дату индексации и версию исходного документа.
- В промпт добавьте инструкцию: `"If documents contradict each other, prefer the most recent one (by date). If ambiguity remains, state both positions with sources."`
- Deletion semantics: убедитесь, что удалённые документы действительно удалены из индекса.

---

### Режим 5: Bad chunking — информация разрезана

**Симптомы:** Извлечённый чанк содержит часть нужной информации, но ответ модели неполный или неверный. При ручной проверке видно, что соседний чанк содержит недостающую часть, но она не попала в контекст.

**Диагностика:** Просмотрите retrieved chunks. Есть ли «оборванные» предложения на границах? Числа, отделённые от единиц измерения? Таблицы, разрезанные пополам?

**Исправление:**
- Увеличьте overlap до 20-25% от размера чанка.
- Перейдите на семантический или document-aware chunking (§12.3).
- Для таблиц: не нарезайте их — сериализуйте с повторением заголовков в каждом чанке.
- Проверьте recursive splitting: правильный ли порядок разделителей? Добавьте domain-specific разделители.

---

### Режим 6: Missing content — ответа нет в корпусе

**Симптомы:** RAGAS context_recall = 0. Ручная проверка корпуса подтверждает: ответа на вопрос действительно нет в проиндексированных документах. RAG не сломан — просто знания отсутствуют.

**Диагностика:** Проверьте корпус вручную. Может ли человек найти ответ? Если нет → missing content.

**Исправление:**
- Расширьте корпус: добавьте недостающие документы.
- Настройте fallback: если retrieval confidence ниже порога → web search или «я не знаю».
- Используйте Corrective RAG (CRAG, §12.7): при низком confidence retrieval → автоматический web search.
- В промпте — явный fallback: `"If no document answers the question, respond: {'answer': null, 'reason': 'Not found in knowledge base'}"`.

---

### Режим 7: Wrong context dominates prior — неверный чанк «переубедил» модель

**Симптомы:** Retrieval принёс ошибочный или устаревший факт. Модель использовала его для ответа, проигнорировав свои (верные) параметрические знания. Ответ неверен, но faithful по отношению к неверному контексту.

**Диагностика:** Faithfulness высокий (> 0.85), но ответ фактически неверен. Проверьте: если убрать retrieved context и задать вопрос напрямую — модель отвечает правильно? Если да → проблема в контексте, не в модели.

**Исправление:**
- Reranking: качественный reranker должен опустить неверный чанк.
- Добавьте conflict cases в eval-набор: пары (правильный факт, контекст с неправильным фактом) — проверяйте, что модель выбирает контекст только когда он достовернее prior.
- Retrieval confidence: если все retrieved chunks имеют низкий similarity score → fallback к «я не знаю».
- Metadata filtering: исключите устаревшие документы по дате или версии.

---

### Режим 8: Query formulation mismatch — запрос и корпус на «разных языках»

**Симптомы:** Пользователь использует разговорные термины («как ускорить сайт»), а корпус — технические («оптимизация TTFB и LCP»). Embedding близости нет, retrieval пустой.

**Диагностика:** Сравните формулировку запроса с формулировками в корпусе. Есть ли семантическая близость? Проверьте cosine similarity запрос-чанк.

**Исправление:**
- HyDE (§12.7): сгенерируйте гипотетический ответ «техническим языком», ищите по нему.
- Query expansion: добавьте синонимы и технические термины к запросу.
- Hybrid search: BM25 поймает точные совпадения терминов, dense — семантическую близость.
- Multi-query retrieval: сгенерируйте 3-5 переформулировок запроса, объедините результаты.

---

### Диагностическая карточка инцидента

Используйте этот шаблон для каждого production-инцидента RAG:

```text
## RAG Incident #NNN
- Дата: [YYYY-MM-DD HH:MM]
- Вопрос: [точный текст]
- Ответ системы: [текст]
- Эталонный ответ: [текст или null]
- Режим отказа: [1-8]
- Root cause: [конкретный компонент/настройка]
- Исправление: [что изменено]
- Верификация исправления: [повторный прогон на этом и похожих кейсах]
- Добавлен в eval-набор: [да/нет, ID тест-кейса]
```

> **Промпт для ИИ:** «Напиши Python-скрипт для автоматической диагностики RAG-инцидента. Принимает: запрос, ответ системы, retrieved chunks (до и после reranking), ground-truth ответ (опционально). Автоматически определяет вероятный режим отказа (1-8) по правилам: (1) нет релевантных чанков → режим 1/6/8; (2) чанки есть, но faithfulness низкий → режим 3; (3) в чанках противоречия → режим 4; (4) ответ faithful, но неверен → режим 7. Генерирует diagnostic report в Markdown. Используй RAGAS для расчёта faithfulness и context_precision.»

---

## Практический вывод

### Когда RAG, когда long context, когда fine-tuning

| Критерий | RAG | Long context | Fine-tuning |
|----------|-----|-------------|-------------|
| Данные обновляются часто | Индексируй заново | Перезагрузи весь контекст | Переобучи модель |
| Нужна точная цитата | Citation grounding | Возможно, но сложнее | Модель не цитирует |
| Объём данных > 1M токенов | Масштабируется | Не влезет в контекст | Ограничено |
| Нужен специфический «тон» | RAG не меняет поведение | Аналогично | Обучает стилю |
| Латенси критична | Retrieval добавляет ~100–300 мс | Prefill долгий | Нет overhead |
| Простота реализации | Пайплайн из 6 компонент | Просто — запихни в контекст | Нужны данные и GPU |

**Правило:** RAG — для актуальных фактов из большого корпуса. Long context — для коротких документов (< 100K токенов), когда retrieval overhead не оправдан. Fine-tuning — для изменения поведения модели, а не для вливания фактов (факты забываются после fine-tuning — catastrophic forgetting).

### Чеклист: минимальный production RAG

1. **Чанкинг**: recursive splitting, 400–600 токенов, overlap 50–80. Проверьте вручную 20 чанков — читаемы ли они?
2. **Embedding**: начните с `text-embedding-3-large` или `bge-m3`. Matryoshka-редукция до 512–1024 dim для экономии.
3. **Vector store**: pgvector для MVP, Qdrant для production.
4. **Hybrid search**: dense + BM25 + RRF. Это baseline, который обычно лучше чистого dense.
5. **Reranking**: всегда. Даже лёгкий `bge-reranker` добавляет 3–8% к retrieval precision.
6. **Prompt**: XML-разметка, citation grounding, explicit «I don't know» fallback.
7. **Eval**: RAGAS + 50 ручных вопросов. Отслеживайте faithfulness ≥ 0.85.
8. **Мониторинг**: логируйте retrieval results, latency, user feedback.

### Типичные ошибки

- **Нет reranking**. Top-k от embedding-поиска содержит 30–50% нерелевантного шума. Без reranking модель получает плохой контекст.
- **Нет eval**. «Вроде работает» — это не метрика. Без faithfulness/relevancy вы не знаете, что сломалось.
- **Слепое доверие retrieval**. Retrieval нашёл — значит, правда? Нет. Устаревший документ, чанк с противоречием, неполная информация — всё это попадает в контекст.
- **Плохой чанкинг**. Разрезали таблицу пополам, потеряли заголовок, оторвали число от его описания — и модель галлюцинирует, потому что контекст бессмыслен.
- **Игнорирование метаданных**. RAG без фильтрации по дате, источнику, версии — это поиск без фильтров: найдёт, но не обязательно то, что нужно *сейчас*.
- **Нет систематической диагностики отказов**. Когда RAG ошибается, недостаточно править промпт наугад. Используйте **RAG failure diagnosis playbook** (§12.10): восемь режимов отказа с пошаговой процедурой диагностики и исправления для каждого.

### Задания

1. **Постройте минимальный RAG-пайплайн.** Возьмите 20–50 документов из вашего проекта (документация, FAQ, регламенты). Нарежьте recursive splitter'ом (400–600 токенов, overlap 50–80), проиндексируйте в pgvector или ChromaDB, подключите reranker. Составьте 10 вопросов с эталонными ответами. Измерьте faithfulness и context_precision через RAGAS. **Ожидаемый результат:** работающий пайплайн с базовыми метриками; понимание, где теряется качество — на этапе retrieval или генерации.

2. **A/B-тест dense vs. hybrid retrieval.** На том же корпусе сравните два режима: (a) только dense embedding search, (b) hybrid (dense + BM25 + RRF). Прогоните одинаковый набор из 10+ вопросов, сравните context_precision и answer_relevancy. **Ожидаемый результат:** количественная оценка прироста от гибридного поиска на ваших данных (типичный выигрыш — 5–15% precision).

3. **Проверьте чанкинг вручную.** Выберите 20 случайных чанков из индекса и оцените: читаемы ли они без окружающего контекста? Сохранён ли смысл? Не разрезана ли таблица или список? **Ожидаемый результат:** список проблемных чанков и скорректированные параметры splitter'а.

4. **Проведите диагностику RAG-инцидента.** Возьмите 3 реальных кейса, где RAG-система дала неверный ответ. Для каждого заполните диагностическую карточку, определите режим отказа, найдите root cause, предложите и примените исправление. Измерьте метрики до и после. **Ожидаемый результат:** 3 заполненные карточки + улучшение faithfulness/context_precision минимум на 10 п.п. для исправленных кейсов.

---

## Источники

- Lewis, P., et al. (2020). "Retrieval-Augmented Generation for Knowledge-Intensive NLP Tasks." NeurIPS 2020.
- Gao, Y., et al. (2024). "Retrieval-Augmented Generation for Large Language Models: A Survey." arXiv:2312.10997.
- Edge, D., et al. (2024). "From Local to Global: A Graph RAG Approach to Query-Focused Summarization." Microsoft Research.
- Sarthi, P., et al. (2024). "RAPTOR: Recursive Abstractive Processing for Tree-Organized Retrieval." ICLR 2024.
- Asai, A., et al. (2024). "Self-RAG: Learning to Retrieve, Generate, and Critique through Self-Reflection." ICLR 2024.
- Yan, S., et al. (2024). "Corrective Retrieval Augmented Generation." arXiv:2401.15884.
- Es, S., et al. (2024). "RAGAS: Automated Evaluation of Retrieval Augmented Generation." EACL 2024.
- Ru, D., et al. (2024). "RAGChecker: A Fine-grained Framework for Diagnosing Retrieval-Augmented Generation." arXiv:2408.08067.
- Wu, K., Wu, E., Zou, J. (2025). "ClashEval: Quantifying the tug-of-war between an LLM's internal prior and external evidence." arXiv:2404.10198.
- Gao, L., et al. (2023). "Precise Zero-Shot Dense Retrieval without Relevance Labels." (HyDE) ACL 2023.
- Liu, N., et al. (2023). "Lost in the Middle: How Language Models Use Long Contexts." TACL.
- Kwon, W., et al. (2023). "Efficient Memory Management for Large Language Model Serving with PagedAttention." SOSP 2023.
- Kusupati, A., et al. (2022). "Matryoshka Representation Learning." NeurIPS 2022.
- Robertson, S., et al. (1994). "Okapi at TREC-3." NIST Special Publication.
- Anthropic. (2024). "Introducing Contextual Retrieval." Anthropic Blog.

---

**Навигация:**
- Назад: [Глава 11. Не заставляй модель считать — дай ей инструмент](11_tools.md)
- Далее: [Глава 13. Антигаллюцинационный контур и защита от деградации](13_anti_hallucination_loop.md)
