← все статьи
8 мин

RAG в продакшне: что реально ломается

Retrieval-Augmented Generation выглядит понятно на демо: берём документы, разбиваем на чанки, кладём в векторное хранилище, при запросе достаём релевантные кусочки и передаём в LLM. На практике – в каждом шаге есть нюансы, которые не очевидны заранее.

Этот текст – выводы из реального проекта: медиамониторинговый сервис, несколько сотен тысяч документов, требование к latency <2 секунды, несколько пользователей одновременно. Не туториал – список конкретных граблей.

flowchart LR Q([Query]) --> E[Embed Model] E --> VS[(Vector Store)] VS --> RR[Reranker] RR --> CTX[Context Builder] DOCS[(Documents)] --> CH[Chunking] CH --> E2[Embed Model] E2 --> VS CTX --> LLM[LLM] LLM --> A([Answer]) style VS fill:#0e110e,stroke:#3d7a3d style LLM fill:#0e110e,stroke:#3d7a3d style RR fill:#0e110e,stroke:#6abf6a 

Chunking: главная переменная

Самое влиятельное решение во всём пайплайне – как именно вы режете документы. Фиксированный размер по токенам – самое простое, но редко оптимальное. Проблема: смысловые границы не совпадают с токенными. Предложение разрезается посередине, контекст теряется.

// что сработало

Recursive character text splitting с chunk_size=512 и chunk_overlap=64 – базовый вариант. Но лучше работал semantic chunking: разбиение по смысловым блокам через перплексию следующего предложения. Latency индексирования выше, но качество retrieval заметно лучше на длинных документах.

# Пример: semantic chunking через embeddings similarity def semantic_chunk(text, threshold=0.85): sentences = sent_tokenize(text) embeddings = embed_model.encode(sentences) chunks, current = [], [sentences[0]] for i in range(1, len(sentences)): sim = cosine_similarity(embeddings[i-1], embeddings[i]) if sim < threshold: chunks.append(" ".join(current)) current = [] current.append(sentences[i]) if current: chunks.append(" ".join(current)) return chunks

Качество эмбеддингов

OpenAI text-embedding-3-small – хороший выбор для русскоязычных документов. Но если у вас специфический домен (юридический, медицинский, технический) – стоит смотреть на fine-tuned модели или multilingual-e5-large.

Важный момент: эмбеддинг-модель для индексирования и для запросов должна быть одна и та же версия. Звучит очевидно, но при обновлении модели нужно переиндексировать весь корпус – иначе пространства не совпадают, и качество поиска деградирует незаметно.

Деградация качества при несовпадении версий эмбеддинга – тихая. Recall падает на 15-20%, но никаких явных ошибок нет. Метрики нужно считать регулярно.

Latency: где на самом деле тормозит

Стандартный профиль запроса: embed запроса → similarity search → fetch документов → LLM completion. В большинстве случаев узкое место – не LLM, а retrieval.

  • Векторный поиск в pgvector без HNSW-индекса – линейный по размеру корпуса
  • HNSW даёт ~10x speedup, но требует памяти: ~4KB на вектор
  • Кэшировать эмбеддинги запросов – почти бесплатный выигрыш для повторных
  • Асинхронный fetch документов после retrieval сокращает время на 30-40%

Hallucination под нагрузкой

LLM галлюцинирует, когда retrieval возвращает нерелевантный контекст. Это случается чаще при высокой нагрузке – не потому что модель "устаёт", а потому что кэши retrieval переполняются, и качество поиска снижается.

// mitigation

Добавили relevance_score_threshold: если ни один чанк не набирает >0.75 – не передаём в LLM вообще, возвращаем стандартный ответ "недостаточно данных". Пользователи воспринимают это лучше, чем уверенную ложь.

Второе – cross-encoder reranking поверх первичного retrieval. Дороже по latency (~200ms), но заметно снижает процент плохого контекста. В нашем случае оправдано: стоимость ошибки высокая.

Что в итоге

RAG – это не одно решение, это пайплайн из независимых компонентов, каждый из которых нужно тюнить под конкретный домен и нагрузку. Метрики нужны на каждом уровне: не только финальное качество ответа, но и recall@k, MRR, latency p95 по каждому этапу.

Если строите RAG систему – закладывайте время на эксперименты с chunking и эмбеддингами. Это не инфраструктурная задача, это исследовательская.


← все статьи Следующая →Kubernetes для небольшой команды: когда он оправдан