Ваш RAG работает только в демо? Добро пожаловать в клуб
Я пересмотрел десятки RAG-пайплайнов. Девять из десяти умирают при переходе из ноутбука в продакшн. Не потому что модели плохие. А потому что архитектура собрана на коленке: чанки без смысла, эмбеддинги без контекста, поиск без гибрида. Знакомо?
2026-й на дворе. Векторные базы стали быстрее, модели эмбеддингов — дешевле, а реранкеры — доступнее. Но количество граблей не уменьшилось. Просто они стали другими. В этой шпаргалке я разложу по полочкам каждый этап production RAG: от нарезки текста до метрик, которые покажут реальное качество. И главное — 8 ошибок, которые превращают ваш RAG в дорогой поиск по ключевым словам.
Внутренности буду показывать кодом. Актуальным на июнь 2026. Никакого вчерашнего дня.
Чанкинг — это не про нарезку колбасы
Самый частый вопрос в чатах: "Какой размер чанка выбрать? 512 токенов? 1024?". А потом удивляются, что ответы LLM — каша. Чанкинг — это семантическая граница, а не механический split.
Ошибка: Простой RecursiveCharacterTextSplitter со статическим размером. Теряется смысл абзацев, таблиц, списков. Результат — контекстная грязь.
В 2026 году стандарт — семантический чанкинг. Разбиваем текст по границам предложений, абзацев, секций, используя эмбеддинги для поиска точек разрыва. Вот пример с библиотекой LlamaIndex (версия 0.12+):
from llama_index.core.node_parser import SentenceSplitter
from llama_index.core import Document
# Плохо: статический размер
text = "...длинный документ..."
splitter_old = SentenceSplitter(chunk_size=1024, chunk_overlap=200)
nodes_old = splitter_old.get_nodes_from_documents([Document(text=text)])
# Результат: разрыв посреди таблицы
# Хорошо: семантический чанкинг с запасом
from llama_index.core.node_parser import SemanticSplitterNodeParser
from llama_index.embeddings.huggingface import HuggingFaceEmbedding
embed_model = HuggingFaceEmbedding(model_name="BAAI/bge-small-en-v1.5")
splitter = SemanticSplitterNodeParser(
buffer_size=1,
embed_model=embed_model,
breakpoint_percentile_threshold=95
)
nodes = splitter.get_nodes_from_documents([Document(text=text)])
# Разрывы там, где меняется тема
Семантический чанкинг берет скользящее окно, вычисляет сходство между соседними предложениями, и режет в точках резкого падения косинусной близости. Работает неидеально на коротких текстах, но для документации, статей, код-базы — must have.
Эмбеддинги и хранилище: три кита, на которых всё держится
Выбор модели эмбеддингов в 2026 — это компромисс между латентностью и качеством. Мои фавориты: Voyage-3-large (лучшее качество, 300$/млн токенов), Cohere Embed v3 (многоязычный, 100$/млн), BAAI/bge-m3 (бесплатно, неплохо).
Векторное хранилище. Забудьте про FAISS в продакшене — он не умеет гибридный поиск из коробки. Qdrant (люблю за простоту), Milvus (для масштаба 100M+ векторов), Chroma (прототипирование).
Вот пример подключения Qdrant с гибридным индексом (sparse + dense):
from qdrant_client import QdrantClient
from qdrant_client.models import VectorParams, SparseVectorParams, Distance
client = QdrantClient(host="localhost", port=6333)
client.create_collection(
collection_name="docs",
vectors_config={
"dense": VectorParams(size=1024, distance=Distance.COSINE),
},
sparse_vectors_config={
"sparse": SparseVectorParams(index=None),
}
)
Гибридный поиск — это когда вы ищете и семантически, и по ключевым словам. Я использую fastembed от Qdrant, который генерирует и dense, и sparse эмбеддинги на лету. Дальше — weighted sum или RRF.
from qdrant_client import QdrantClient
from qdrant_client.models import (
HybridFusion, FusionQuery, PrefetchQuery,
)
prefetch = [
PrefetchQuery(
query=vector_dense,
using="dense",
limit=20
),
PrefetchQuery(
query=vector_sparse,
using="sparse",
limit=20
)
]
query = FusionQuery(
prefetch=prefetch,
fusion=HybridFusion(alpha=0.7) # вес dense vs sparse
)
results = client.query_points(
collection_name="docs",
query=query,
limit=10
)
Совет. Если у вас много специфической терминологии (юридические, медицинские тексты) — обязательно добавьте sparse. Иначе запросы вроде "исключительная лицензия" превратятся в поиск по "лицензия" и потеряют нюанс.
Реранкинг — второй проход, который превращает мусор в золото
Векторный поиск возвращает 20-30 кандидатов. Среди них всегда есть шум. Реранкер (cross-encoder) проходит по парам (запрос, чанк) и выставляет точную оценку релевантности. Он медленнее, но гораздо точнее.
Лучшие на июнь 2026: Cohere Rerank v3 (дата-центричный, отлично работает с русским), BAAI/bge-reranker-v2-m3 (бесплатно, но требует GPU).
from cohere import Client
co = Client(api_key="YOUR_API_KEY")
query = "Как настроить KPI для отдела продаж?"
candidates = ["текст чанка1", "текст чанка2", ...]
response = co.rerank(
model="rerank-v3.5",
query=query,
documents=candidates,
top_n=3
)
for hit in response.results:
print(hit.index, hit.relevance_score)
Реранкер обязателен, если вы отдаете пользователю топ-3 чанка. Без него — 30% шанс, что второй или третий чанк нерелевантен. С ним — единицы процентов.
Метрики Ragas: как не врать самому себе
Большинство RAG-систем тестируют "на глаз". Задали три вопроса, посмотрели ответы — "вроде норм". Это самообман. Нужны объективные метрики.
Библиотека Ragas на 2026 год — стандарт. Четыре ключевые метрики:
- Faithfulness (верность) — не выдумывает ли LLM факты, которых нет в чанках. 0.9+ это хорошо.
- Answer Relevancy (релевантность ответа) — ответ соответствует вопросу, а не уходит в дебри.
- Context Precision (точность контекста) — насколько первые чанки содержат ответ. Если ответ во втором чанке — метрика падает.
- Context Recall (полнота контекста) — все ли нужные факты извлечены.
from ragas import evaluate
from ragas.metrics import faithfulness, answer_relevancy, context_precision, context_recall
from datasets import Dataset
questions = ["Что такое RAG?"]
answer = "RAG это метод дополнения LLM внешними знаниями."
contexts = [["RAG (Retrieval-Augmented Generation) использует поиск по базе знаний для обогащения промпта."]]
data = Dataset.from_dict({
"question": questions,
"answer": [answer],
"contexts": contexts
})
result = evaluate(
dataset=data,
metrics=[faithfulness, answer_relevancy, context_precision, context_recall]
)
print(result)
Грабли: Ragas использует LLM для оценки (по умолчанию GPT-4o-mini). Если ваша модель-судья — та же, что и в RAG, метрики будут завышены. Берите другую модель для оценки, например, Claude 4 Haiku.
8 граблей продакшена (и как не наступить)
1 Грабли: Одинаковые чанки для всех типов запросов
Факт: для фактоида ("Когда родился Пушкин?") нужны мелкие чанки. Для аналитики ("Сравните подходы к RAG") — крупные. Решение: мульти-чанкинг. Храните чанки разного размера, на этапе поиска подбирайте под тип запроса.
2 Грабли: Игнорирование перекрытия (overlap)
Без overlap вы теряете контекст на стыках. 15-20% — минимум. Лучше сливать чанки в раннере с помощью sliding window.
3 Грабли: Хранение метаданных только для отладки
Метаданные (источник, дата, автор) должны активно использоваться в фильтрации запроса. Пользователь спросил "новые правила?". Значит, отфильтровать по дате > 2025. Без фильтрации поиск найдет устаревшие документы.
4 Грабли: Использование одной модели эмбеддингов для всего
Код, текст, таблицы — разные домены. Лучше натренировать адаптеры или использовать мультимодальные эмбеддинги (например, гибридный RAG с AST и графами знаний из Ragex).
5 Грабли: Забыть про кеширование запросов
Одна и та же фраза от 10 пользователей — 10 дорогих поисков. Поставьте Redis-кеш на уровне эмбеддингов или результатов реранкинга. latency упадет в 2-3 раза.
6 Грабли: Подавать в LLM слишком много контекста
Окно 128k токенов — не значит, что надо пихать 127к. Это убивает точность. Эксперименты показывают: оптимально 3-5 чанков (около 4-6k токенов). Реранкинг должен оставлять топ-3. Больше — шум.
7 Грабли: Не учитывать latency поиска
Добавили гибридный поиск, реранкинг, а суммарно — 5 секунд. Пользователь уйдет. Решение: кэшировать частые запросы, стримить ответ LLM параллельно с поиском (предсказание следующего чанка), использовать быстрые модели эмбеддингов на CPU (ONNX).
8 Грабли: Не мониторить качество в продакшене
Метрики Ragas считаются разово — и забываются. В 2026 стандарт — онлайн-мониторинг. Собирайте логи запросов, ответов, релевантность от пользователей (лайк/дизлайк) и пересчитывайте faithfulness раз в час. Если метрика упала — алерт в PagerDuty.
Собираем production-пайплайн: код
Склеиваю всё вместе на базе LangChain 0.3+. Акцент на надежность и скорость.
import asyncio
from langchain_core.documents import Document
from langchain_qdrant import QdrantVectorStore
from langchain.retrievers import ContextualCompressionRetriever
from langchain.retrievers.document_compressors import CohereRerank
from langchain.chains import RetrievalQA
from langchain_openai import ChatOpenAI
# Сначала загружаем документы с семантическим чанкингом
from langchain_text_splitters import SemanticChunker
from langchain_community.embeddings import HuggingFaceEmbeddings
chunker = SemanticChunker(
HuggingFaceEmbeddings(model_name="BAAI/bge-small-en-v1.5"),
breakpoint_threshold_type="percentile",
breakpoint_threshold_amount=95
)
docs = chunker.split_documents(raw_documents)
# Сохраняем в Qdrant с dense + sparse
from langchain_qdrant import FastEmbedSparse
store = QdrantVectorStore.from_documents(
docs,
embedding=HuggingFaceEmbeddings(model_name="BAAI/bge-m3"),
sparse_embedding=FastEmbedSparse(model_name="Qdrant/bm25"),
location=":memory:", # или url
collection_name="production_rag",
)
# Гибридный поиск
retriever = store.as_retriever(search_type="hybrid", search_kwargs={"k": 20})
# Реранкинг
rerank = CohereRerank(model="rerank-v3.5", top_n=3)
compressor = ContextualCompressionRetriever(base_compressor=rerank, base_retriever=retriever)
# QA цепочка
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)
qa = RetrievalQA.from_chain_type(
llm=llm,
retriever=compressor,
return_source_documents=True
)
response = qa({"query": "Что такое Retrival-Augmented Generation?"})
print(response["result"])
Запустили — работает? Нет. Теперь добавьте метрики.
from ragas.integrations.langchain import evaluate_langchain
from ragas.metrics import faithfulness, context_recall
result = evaluate_langchain(
qa_chain=qa,
testset=test_questions,
metrics=[faithfulness, context_recall]
)
print(result)
# Если faithfulness < 0.8 — меняйте промпт или чанкинг
RAG умрет. Да здравствует Retrieval.
Не ждал? А зря. В 2026 году тренд четкий: LLM с большими окнами (1M+ токенов) убивают необходимость в RAG для коротких контекстов. Но retrieval остается — как слой, который подгружает актуальные данные. Сама генерация уходит в агентов, которые сами решают, когда искать, а когда ответить из памяти. Мой совет: не зацикливайтесь на RAG как на пайплайне. Стройте систему, которая умеет выбирать между поиском и генерацией, переключаться между разными источниками, переспрашивать пользователя.
А грабли? Их все равно будет 8. Или 12. Потому что продакшн — это сплошные грабли. Хотите меньше? Тестируйте метрики до деплоя, мониторьте после, и не бойтесь переписывать чанкинг с нуля. Удачи.