RAG
Embeddings
Vector DB
Production

RAG в production: Best practices и типичные ошибки

Практическое руководство по созданию production-ready RAG систем: от выбора embeddings до оптимизации retrieval

28 января 2026 г.
Команда QZX Studio
9 min read

RAG в production: Best practices и типичные ошибки

Retrieval-Augmented Generation (RAG) — это сейчас основа большинства enterprise AI-приложений. Но между «работает в demo» и «работает в production» — огромная пропасть.

За последний год мы запустили 12 RAG-систем для российских компаний. В этой статье — всё, что мы узнали методом проб и ошибок.

Что такое RAG (если вдруг не знаете)

RAG = поиск релевантной информации + генерация ответа

Вопрос пользователя
  ↓
Преобразование в embedding
  ↓
Поиск похожих документов в векторной БД
  ↓
Добавление найденных документов в промпт
  ↓
LLM генерирует ответ на основе документов
  ↓
Ответ пользователю

Зачем нужен RAG?

  • LLM не знает ваших внутренних данных
  • Fine-tuning слишком дорогой и медленный
  • Нужны свежие данные (LLM обучены на старых данных)
  • Важна прозрачность (можно показать источники)

Архитектура: как мы делаем RAG

Простая версия (MVP)

from langchain.vectorstores import Chroma
from langchain.embeddings import OpenAIEmbeddings
from langchain.llms import OpenAI
from langchain.chains import RetrievalQA

# 1. Загружаем документы
documents = load_documents("./docs")

# 2. Создаём embeddings
embeddings = OpenAIEmbeddings()
vectorstore = Chroma.from_documents(documents, embeddings)

# 3. Создаём retriever
retriever = vectorstore.as_retriever(search_kwargs={"k": 3})

# 4. Создаём QA chain
qa = RetrievalQA.from_chain_type(
    llm=OpenAI(),
    retriever=retriever
)

# 5. Спрашиваем
answer = qa.run("Как оформить возврат?")

Проблема: это работает только на игрушечных датасетах.

Production-версия

В реальной жизни нужно:

  1. Умное chunking (не просто split по 500 символов)
  2. Hybrid search (векторный + keyword поиск)
  3. Reranking (переранжирование результатов)
  4. Query expansion (расширение запроса)
  5. Caching (кэширование популярных запросов)
  6. Monitoring (отслеживание качества)

Разберём каждый пункт.

1. Chunking: разбивка документов

❌ Плохо: наивный split

# Делим текст каждые 500 символов
chunks = [text[i:i+500] for i in range(0, len(text), 500)]

Проблемы:

  • Разрывает предложения и абзацы
  • Теряется контекст
  • Невозможно найти связные блоки информации

✅ Хорошо: семантическое chunking

from langchain.text_splitter import RecursiveCharacterTextSplitter

splitter = RecursiveCharacterTextSplitter(
    chunk_size=1000,
    chunk_overlap=200,  # Перекрытие между чанками
    separators=["\n\n", "\n", ". ", " ", ""],
    length_function=len,
)

chunks = splitter.split_text(text)

Почему лучше:

  • Разбивка по логическим границам (абзацы, предложения)
  • Перекрытие сохраняет контекст
  • Размер чанка оптимизирован под embedding модель

⭐ Идеально: multi-level chunking

# Создаём чанки разных размеров
small_chunks = split_text(text, size=300)   # Для точного поиска
medium_chunks = split_text(text, size=1000) # Основные
large_chunks = split_text(text, size=3000)  # Для контекста

# При поиске:
# 1. Ищем в small chunks
# 2. Возвращаем соответствующие medium chunks с контекстом

Реальный кейс: для технической документации мы используем 3 уровня:

  • Заголовки разделов (small) — для навигации
  • Параграфы (medium) — для точных ответов
  • Целые разделы (large) — для глубокого понимания

2. Embeddings: выбор модели

Сравнение популярных моделей

МодельРазмерностьКачество (MTEB)СкоростьСтоимость
OpenAI text-embedding-3-large307264.6%Средняя$0.13/1M tokens
OpenAI text-embedding-3-small153662.3%Быстрая$0.02/1M tokens
multilingual-e5-large102461.5%БыстраяБесплатно (self-hosted)
sentence-transformers/paraphrase-multilingual76858.2%Очень быстраяБесплатно

Наши рекомендации:

  • Русский + английский: intfloat/multilingual-e5-large — лучшее качество на русском среди open-source
  • Только английский: OpenAI text-embedding-3-large
  • Бюджет ограничен: sentence-transformers — бесплатно и достаточно хорошо
  • Специфичная domain: fine-tuned e5 на ваших данных

Fine-tuning embeddings

Для узкоспециализированных задач (медицина, юриспруденция, финтех) стандартные модели работают плохо.

Решение: fine-tune embedding модели на ваших данных.

from sentence_transformers import SentenceTransformer, InputExample, losses
from torch.utils.data import DataLoader

# Подготовка пар (вопрос, релевантный документ)
train_examples = [
    InputExample(texts=['Как оформить возврат?', 'Процедура возврата...'], label=1.0),
    InputExample(texts=['Сроки доставки', 'Доставляем в течение...'], label=1.0),
    # ...
]

# Загрузка базовой модели
model = SentenceTransformer('intfloat/multilingual-e5-large')

# Fine-tuning
train_dataloader = DataLoader(train_examples, shuffle=True, batch_size=16)
train_loss = losses.CosineSimilarityLoss(model)

model.fit(
    train_objectives=[(train_dataloader, train_loss)],
    epochs=3,
    warmup_steps=100
)

Результат: для одного финтех-клиента мы улучшили точность поиска с 67% до 89% после fine-tuning.

3. Vector Database: выбор и настройка

Основные варианты

Pinecone (управляемый сервис)

  • ✅ Просто начать
  • ✅ Автоматический scaling
  • ❌ Дорого на больших объёмах
  • ❌ Данные на зарубежных серверах

Qdrant (self-hosted или облако)

  • ✅ Отличная производительность
  • ✅ Rich filtering
  • ✅ Можно развернуть локально
  • ❌ Требует DevOps для self-hosted

Weaviate (self-hosted или облако)

  • ✅ Hybrid search из коробки
  • ✅ GraphQL API
  • ❌ Сложнее в настройке
  • ❌ Потребляет много памяти

PostgreSQL + pgvector (расширение Postgres)

  • ✅ Не нужна дополнительная БД
  • ✅ Трансакции и ACID
  • ❌ Медленнее специализированных решений
  • ❌ Плохо масштабируется >1M векторов

Наш выбор для production: Qdrant self-hosted на российских серверах.

Оптимизация Qdrant

from qdrant_client import QdrantClient
from qdrant_client.models import Distance, VectorParams

client = QdrantClient(host="localhost", port=6333)

# Создаём коллекцию
client.create_collection(
    collection_name="documents",
    vectors_config=VectorParams(
        size=1024,  # Размерность embeddings
        distance=Distance.COSINE,
        on_disk=False,  # В RAM для скорости
    ),
    # Индексация
    hnsw_config={
        "m": 16,  # Количество связей
        "ef_construct": 100,
        "full_scan_threshold": 10000,
    },
    # Квантизация для экономии памяти
    quantization_config={
        "scalar": {
            "type": "int8",
            "quantile": 0.99,
            "always_ram": True,
        }
    }
)

Результаты оптимизации:

  • Использование RAM: -40%
  • Скорость поиска: примерно та же
  • Точность: -1% (незаметно на практике)

4. Hybrid Search: векторы + ключевые слова

Чистый векторный поиск плохо работает для:

  • Точных названий (SKU, номера документов)
  • Имён собственных
  • Дат и чисел
  • Специфичной терминологии

Решение: комбинируем векторный поиск с keyword search (BM25).

from langchain.retrievers import BM25Retriever, EnsembleRetriever

# Векторный retriever
vector_retriever = vectorstore.as_retriever(search_kwargs={"k": 10})

# Keyword retriever
bm25_retriever = BM25Retriever.from_documents(documents)
bm25_retriever.k = 10

# Гибридный retriever
ensemble_retriever = EnsembleRetriever(
    retrievers=[vector_retriever, bm25_retriever],
    weights=[0.7, 0.3]  # 70% вес векторному поиску
)

Улучшение точности: на наших бенчмарках hybrid search даёт +15-20% к accuracy.

5. Reranking: улучшаем качество результатов

После retrieval получаем 10-20 потенциально релевантных документов. Но не все они одинаково полезны.

Reranker — это модель, которая переранжирует результаты по релевантности к конкретному запросу.

from sentence_transformers import CrossEncoder

# Получаем топ-20 документов
initial_docs = retriever.get_relevant_documents(query, k=20)

# Reranking модель
reranker = CrossEncoder('cross-encoder/ms-marco-MiniLM-L-6-v2')

# Переоценка релевантности
pairs = [[query, doc.page_content] for doc in initial_docs]
scores = reranker.predict(pairs)

# Сортируем по новым скорам
reranked_docs = [doc for _, doc in sorted(
    zip(scores, initial_docs),
    key=lambda x: x[0],
    reverse=True
)][:5]  # Берём топ-5 после reranking

Результат: precision@5 увеличивается с 68% до 84%.

Важно: reranking медленнее embedding search, поэтому делаем в 2 этапа:

  1. Быстрый retrieval → топ-20
  2. Медленный reranking → топ-5

6. Query Expansion: расширяем запрос

Пользователи формулируют вопросы по-разному. Query expansion помогает найти релевантные документы даже при нестандартных формулировках.

HyDE (Hypothetical Document Embeddings)

# Генерируем гипотетический ответ
hypothetical_answer = llm.generate(
    f"Напиши подробный ответ на вопрос: {query}"
)

# Ищем по embedding гипотетического ответа
results = vectorstore.similarity_search(hypothetical_answer)

Идея: embedding хорошего ответа ближе к релевантным документам, чем embedding короткого вопроса.

Multi-query

# Генерируем вариации вопроса
variations = llm.generate(
    f"Сгенерируй 3 вариации вопроса: {query}"
).split("\n")

# Ищем по всем вариациям
all_results = []
for variation in variations:
    results = vectorstore.similarity_search(variation, k=3)
    all_results.extend(results)

# Дедупликация и ранжирование
unique_results = deduplicate(all_results)

Применение: когда пользователь задаёт вопрос неточно или слишком кратко.

7. Кэширование

RAG-запрос — это дорого:

  • Embedding: $0.02/1M tokens
  • Vector search: 50-100ms
  • LLM generation: $10-75/1M tokens
  • Reranking: 200-300ms

Кэшируем популярные запросы:

from langchain.cache import RedisSemanticCache

# Semantic cache: кэширует семантически похожие запросы
cache = RedisSemanticCache(
    redis_url="redis://localhost:6379",
    embeddings=embeddings,
    similarity_threshold=0.95
)

# При запросе сначала проверяем кэш
cached_answer = cache.lookup(query)
if cached_answer:
    return cached_answer

# Если нет в кэше — делаем полный RAG
answer = rag_pipeline(query)
cache.update(query, answer)

Результаты:

  • 40% запросов обрабатываются из кэша
  • Latency для cached: 20ms vs 2000ms
  • Экономия на LLM API: $800/месяц

8. Мониторинг и метрики

Ключевые метрики RAG

Retrieval metrics:

  • Recall@k: сколько релевантных документов найдено в топ-k
  • Precision@k: какой процент из топ-k действительно релевантен
  • MRR (Mean Reciprocal Rank): на какой позиции первый релевантный результат

Generation metrics:

  • Faithfulness: насколько ответ соответствует источникам
  • Answer relevance: насколько ответ отвечает на вопрос
  • Context relevance: насколько извлечённый контекст релевантен

User metrics:

  • Thumbs up/down rate
  • Abandon rate: пользователь переформулировал вопрос
  • Time to answer

Автоматическая оценка качества

from ragas import evaluate
from ragas.metrics import faithfulness, answer_relevancy

# Готовим тестовый датасет
test_questions = [
    {
        "question": "Как оформить возврат?",
        "ground_truth": "Для оформления возврата нужно...",
        "contexts": retrieved_contexts,
        "answer": generated_answer
    },
    # ...
]

# Оцениваем
results = evaluate(
    test_questions,
    metrics=[faithfulness, answer_relevancy]
)

print(f"Faithfulness: {results['faithfulness']}")
print(f"Answer Relevancy: {results['answer_relevancy']}")

Мы запускаем эту оценку каждую ночь на тестовом датасете из 100 вопросов. Если метрики падают — алерт в Slack.

Типичные ошибки (и как их избежать)

❌ Ошибка 1: Слишком маленький chunk size

Проблема: chunks по 200 символов теряют контекст
Решение: минимум 500-1000 символов, с перекрытием 10-20%

❌ Ошибка 2: Игнорирование метаданных

Проблема: все документы в одной куче, сложно фильтровать
Решение: добавляйте метаданные (дата, категория, источник)

vectorstore.add_texts(
    texts=chunks,
    metadatas=[
        {"source": "user_manual", "date": "2026-01-15", "category": "setup"},
        {"source": "faq", "date": "2026-02-01", "category": "returns"},
    ]
)

# При поиске фильтруем
results = vectorstore.similarity_search(
    query,
    filter={"category": "returns"}
)

❌ Ошибка 3: Не тестировать на edge cases

Проблема: RAG хорошо работает на типичных запросах, но ломается на необычных
Решение: создайте датасет из 100+ реальных вопросов, включая сложные

❌ Ошибка 4: Передавать в LLM слишком много контекста

Проблема: топ-20 документов = 20K tokens = медленно + дорого + "lost in the middle"
Решение: топ-3-5 после reranking достаточно

❌ Ошибка 5: Не обновлять индекс

Проблема: данные в векторной БД устаревают
Решение: автоматическая переиндексация (daily/weekly) или инкрементальные обновления

Production checklist

Перед выкаткой RAG в production проверьте:

  • Chunking оптимизирован (семантическая разбивка, перекрытие)
  • Embedding модель выбрана и протестирована
  • Vector DB настроена и оптимизирована
  • Hybrid search включен (векторы + keywords)
  • Reranking работает
  • Кэширование настроено
  • Мониторинг метрик настроен
  • Тестовый датасет создан (100+ вопросов)
  • Latency p95 < 3 секунд
  • Есть graceful degradation (fallback если RAG не работает)

Выводы

RAG — это не «просто подключить LangChain». Для production нужны:

✅ Умный chunking
✅ Правильный выбор embeddings
✅ Hybrid search
✅ Reranking
✅ Query expansion
✅ Кэширование
✅ Мониторинг

Хорошая новость: один раз настроенный RAG pipeline можно переиспользовать для разных проектов.


Нужна помощь с RAG? Мы в QZX Studio запустили 12 production RAG систем. Обсудим ваш проект.

Поделиться статьёй:

Нужна помощь с AI-внедрением?

Наша команда AI-экспертов готова помочь вашему бизнесу внедрить передовые AI-решения. Обсудим ваш проект в Telegram.