Как выглядит идеальное демо RAG
Вы пилите RAG-систему пару недель. Берёте 10 страниц документации из Confluence, нарезаете чанками по 500 токенов, индексируете через text-embedding-3-large и цепляете GPT-4o. Задаёте вопрос — ответ летит чёткий, цитаты сошлись, менеджер в восторге. Демо — огонь.
Через месяц выкатываете в прод. В базе уже 5000 документов, пользователи задают вопросы вроде «а что там с лицензией на продукт X в договоре Y от 2023 года?», и RAG начинает нести ахинею. Вместо ответа — галлюцинации с кусками из соседних страниц, а то и откровенный бред. Знакомо?
Золотое правило RAG: демо работает на идеальных данных, прод — на реальных. Разница — в деталях индексации и поиска.
Ниже — пять типичных ловушек, которые превращают блестящий прототип в бесполезную игрушку. Каждую разберу с примером из своей практики и покажу, как чинить.
Ошибка №1. Чанкинг по учебнику: 500 токенов и 10% overlap
На демо вы берёте ровные параграфы, всё отлично режется, контекст не теряется. В проде влетают PDF с колонтитулами, таблицы с разрывами строк и Confluence-страницы с кодом. Ваш прекрасный чанк превращается в кашу из заголовков, буллетов и футеров.
Почему это ломается. Статический чанкинг (RecursiveCharacterTextSplitter с size=500) не понимает семантику документа. Он режет прямо посреди предложения, разделяет заголовок и его содержимое, а в таблице вырывает ячейки из контекста.
Решение: семантический чанкинг + адаптивный размер. Не фиксируйте размер. Используйте детектор границ абзацев (например, LLM для определения логических блоков) или библиотеки вроде semantic-text-splitter. Для таблиц — отдельный пайплайн с парсингом в markdown, где строки не разрываются.
from semantic_splitter import SemanticChunker
from langchain.embeddings import OpenAIEmbeddings
chunker = SemanticChunker(
embeddings=OpenAIEmbeddings(model="text-embedding-3-large"),
buffer_size=3, # объединять до 3 предложений с похожей семантикой
)
chunks = chunker.split_documents(docs)
Такой подход увеличивает рекалл на 15-20% на реальных данных (проверено на бенчмарке BEIR 2025). Если ваш документ — мешанина из текста и таблиц, загляните в статью RAG в 2026: хакеры атакуют, таблицы сопротивляются — там разобраны парсеры для сложных форматов.
Совет: не доверяйте дефолтному TextSplitter. Протестируйте на датасете из 100 страниц реальных документов — увидите, сколько контекста теряется на стыках.
Ошибка №2. Все документы равны, но некоторые равнее
В демо вы грузите только текст. В проде — договоры в PDF с подвалами, Confluence с вложенными таблицами, Excel-отчёты. RAG ищет среди всего этого без разбора: чанк из колонтитула «Конфиденциально» может быть ближе по эмбеддингу к вопросу, чем полезный контент.
Почему это ломается. Эмбеддинги колонтитулов и мусора шумят. Они часто содержат повторяющиеся фразы, которые искажают семантическое пространство. Кроме того, один длинный документ может монополизировать поиск — все топ-K чанков окажутся из одного файла.
Решение: препроцессинг и взвешивание.
- Чистка: удалите колонтитулы, нумерацию страниц, повторяющиеся заголовки. Используйте OCR-пайплайн с фильтром мусора.
- Аннотация: добавьте метаданные (тип документа, дата, источник). В retrieval учитывайте их — например, повышайте вес официальных документов над вики.
- Дедупликация похожих чанков (semantic dedup).
Это напрямую связано с качеством поиска. В материале RAG failed: 3 причины плохого поиска как раз показано, как мусорные чанки убивают MRR.
Ошибка №3. Закэшировали и забыли: данные устаревают быстрее, чем вы думаете
На демо вы проиндексировали одну версию документа и радуетесь. В проде Confluence живёт — страницы правят каждый день, договоры перезаключаются, а старые версии должны быть недоступны. Если индекс не обновляется, RAG отвечает по устаревшей информации.
Почему это ломается. Нет инвалидации кэша. Вы пересчитываете эмбеддинги раз в сутки, но пользователь спрашивает про документ, который изменился час назад. RAG возвращает старый чанк, LLM генерирует уверенный, но ложный ответ.
Решение: event-driven индексация с TTL.
# Псевдокод для обновления по webhook
@app.post("/webhook/confluence")
async def handle_confluence_update(payload):
doc_id = payload["page_id"]
new_text = fetch_page_content(doc_id)
# удалить старые чанки этого документа из векторной БД
vectorstore.delete(ids=[doc_id])
# пересчитать и добавить новые
chunks = chunker.split_text(new_text)
embeddings = embedder.embed_documents(chunks)
vectorstore.add_embeddings(
ids=[f"{doc_id}_{i}" for i in range(len(chunks))],
embeddings=embeddings
)
Ставьте TTL на чанки (например, 24 часа) и пересчитывайте их по триггерам. Для compliance-данных храните версии и указывайте дату актуальности в промпте. Самовосстанавливающийся RAG может частично решить эту проблему — он проверяет факты на лету.
Ошибка прод-уровня: не предусмотреть механизм проверки свежести данных. Если RAG не знает дату документа, он не предупредит пользователя, что информация устарела.
Ошибка №4. Эмбеддинг-модель — серебряная пуля для демо, болото для прода
На демо вы берёте text-embedding-3-large, он отлично понимает общие вопросы. В проде выясняется, что ваши документы на русском с кучей специфичного жаргона (финансовые отчёты, медицинские протоколы), и модель путает «дебет с кредитом».
Почему это ломается. Обученные на общем корпусе эмбеддинги плохо представляют доменную лексику и редкие термины. Семантическое расстояние между «счёт-фактура» и «счет на оплату» может быть большим, хотя для человека это разные документы. Также мультиязычные модели (multilingual-e5-large) могут давать разный рекалл для русского и английского.
Решение: дообучение (fine-tuning) или замена модели.
- Соберите пары (вопрос, релевантный документ) хотя бы 1000 штук из вашего домена.
- Дообучите BGE-M3 или Cohere-embed-english-v3.0 на этих парах с контрастивной потерей.
- Для русского языка я в 2026 году рекомендую intfloat/multilingual-e5-large или BAAI/bge-m3 — они показывают лучший рекалл на RuBQ. Но все равно тестируйте на своих данных.
Подробнее про выбор модели и метрики — в статье Эмбеддинги — слепое пятно RAG. Там же бенчмарки, которые показывают падение точности до 30% при смене домена.
Ошибка №5. Вы верите только в dense retrieval
Демо собрано на одном векторном поиске с cosine similarity. Вы задаёте вопрос про «когда кончается лицензия», dense retrieval находит чанк с точной фразой «срок действия лицензии истекает». Но если пользователь спросит «дата окончания лицензионного договора», а в документе стоит «период действия соглашения» — dense поиск может пролететь мимо, потому что эмбеддинг не уловил синоним.
Почему это ломается. Dense retrieval хорошо работает на семантическое сходство, но плохо на точное совпадение ключевых слов и редких терминов. BM25 (sparse) наоборот — ловит точные вхождения, но игнорирует контекст. В проде вам нужно и то, и другое.
Решение: гибридный поиск (dense + sparse).
from langchain.retrievers import EnsembleRetriever
from langchain_community.retrievers import BM25Retriever
bm25_retriever = BM25Retriever.from_documents(docs)
bm25_retriever.k = 10
dense_retriever = vectorstore.as_retriever(search_kwargs={"k": 10})
ensemble_retriever = EnsembleRetriever(
retrievers=[bm25_retriever, dense_retriever],
weights=[0.4, 0.6]
)
Настройте веса под свой домен. Если важна точная дата — поднимите вес BM25. Если нужен контекст — dense. RAG 2026: От гибридного поиска до production — roadmap даёт полную дорожную карту внедрения с учётом latency и бюджета.
Ещё один нюанс: в проде dense retrieval проседает при длинных запросах (10+ токенов). Если пользователи формулируют вопросы многословно — используйте рерайтер запроса или Agentic RAG, который разбивает сложный запрос на подзапросы.
Вместо заключения: переверните фреймворк
Не пытайтесь докрутить демо до прода. Сделайте наоборот: спроектируйте пайплайн для худшего случая — кривые PDF, устаревшие данные, редкие синонимы. Протестируйте на кастомном датасете из 10 000 вопросов. А демо потом просто отрежьте как подмножество.
Если чувствуете, что теряете контроль над качеством — используйте инструменты диагностики. RAG Doctor прогонит ваш пайплайн по десятку метрик и укажет на слабые места. И помните: идеального RAG не существует, есть RAG, который вы вовремя починили.