Один разработчик показывал мне свою RAG-систему. Двадцать гигабайт документации, сотни PDF-файлов, векторная база на миллион чанков. На вопрос «работает?» — честно ответил: «Хуже, чем просто скормить всё в контекст целиком». Я видел такое не раз. RAG строят многие, работают — единицы. Разберёмся почему.
Зачем RAG вообще нужен
Большие языковые модели хороши на общих данных. Но бизнесу нужны ответы про внутренние процессы, продукты, решения из прошлых проектов. Модель об этом не знает. Варианта два: засунуть всё в промпт или научить модель искать саму.
Первый способ быстро упирается в лимит контекста и цену. Платишь за токены, а лишний мусор разбавляет релевантную информацию шумом. RAG обещает другое: достаём только то, что относится к вопросу, и подсовываем модели точечно.
Звучит просто. На деле — зоопарк решений, и половина из них стреляют вхолостую.
Как устроен RAG изнутри
Пять шагов, каждый со своими граблями.
Индексация. Берём документы, режем на куски — чанки. Каждый чанк засовываем в эмбеддинг-модель, получаем вектор. Вектора складываем в базу. И ещё: сохраняем структуру — метаданные, заголовки, источник. Это потом пригодится.
Поиск. Пользователь задаёт вопрос. Вопрос тоже превращается в вектор. Ищем ближайшие в базе по косинусной близости или похожей метрике.
Ранжирование. Нашли топ-K результатов. Дальше или отдаём как есть, или пропускаем через reranker — отдельную модель, которая переранжирует результаты точнее.
Формирование контекста. Найденные чанки склеиваем в промпт, добавляем инструкцию отвечать только по этим данным.
Генерация. LLM формирует ответ.
Каждый шаг можно сделать криво, и система в целом будет работать — но плохо. Без ошибок не бывает, но большинство проблем концентрируются в трёх местах.
Грабли первая: чанкинг
Самая недооценённая часть. Люди ставят чанк-сайз 512 токенов, нарезают по документу поровну — а потом удивляются, почему ответы рваные.
Чанк должен быть семантически связным. Разрезал абзац пополам — второй чанк потерял контекст. Взял 2000 токенов — влезло много лишнего, модель-поиск начинает путаться в деталях.
Я пробовал три подхода. Фиксированный чанк-сайз — просто и дёшево, но качество скачет. Рекурсивное разбиение по логическим единицам — абзацы, потом предложения — заметно лучше, но приходится писать разборщик. И модельные чанки: отдельная LLM решает, где разрезать. Дорого при индексации, зато результат чище.
Ещё полезная штука: добавлять к каждому чанку заголовок раздела и пару предложений до и после. Стоит копейки, а поиск становится намного точнее — модель получает не только ответ, но и его окружение.
Грабли вторая: эмбеддинги
Не все модели одинаково хорошо понимают русский. Многие берут sentence-transformers на английской модели и удивляются, почему поиск по русским документам лажает. Ответ очевиден, но и я сам на это нарывался.
Варианта два. Либо модель, которая хорошо работает на нужном языке — ruBERT, multilingual-e5 и их аналоги, — либо переводить запрос на английский, искать по английской базе, а потом маппить обратно. Второй способ извращённый, но иногда выручает, когда своей хорошей модели нет.
Размерность эмбеддинга тоже влияет. 384 — хороший компромисс между скоростью и точностью. 768 — чуть лучше, но медленнее и больше жрёт память при поиске. 1536 — для случаев, когда у тебя GPU-кластер и задача того стоит.
Грабли третья: поиск сам по себе
Косинусная близость по векторам — это семантический поиск. Он находит документы, где используются другие слова, но тот же смысл. Но плохо работает, когда нужны точные совпадения: номера документов, коды, специфичные термины.
Тут выручает гибридный поиск: семантический плюс булев/BM25. BM25 ищет по точным совпадениям слов, семантика — по смыслу. Совмещаешь через RRF (Reciprocal Rank Fusion) или просто склеиваешь результаты с весами. Я видел случаи, когда гибрид поднимал accuracy с 60% до 85% — чисто за счёт учёта точных совпадений.
Отдельная тема — фильтрация по метаданным. Если база большая, имеет смысл сначала отфильтровать по источнику, дате, типу документа — и искать уже в отфильтрованном множестве. Это сильно сужает область поиска и снижает вероятность мусорных результатов.
Reranker: дорого, но иногда надо
После поиска берёшь топ-20 результатов и отдаёшь в reranker. Это отдельная модель, чаще кросс-энкодер, которая для каждой пары «вопрос — документ» выдаёт оценку релевантности. Медленная, дорогая, зато точнее векторного поиска.
Чаще всего reranker не нужен. Для простых Q&A его можно пропустить и просто увеличить топ-K. Но если запросы сложные и релевантность не сводится к близости векторов — без него будет больно.
Паттерн, который хорошо работает: сначала быстрый векторный поиск (топ-100), потом reranker (топ-10), потом в LLM. Между скоростью и точностью приемлемый баланс.
Что обычно забывают
Query expansion и rewrite. Пользователь спрашивает «как подключить АПИ?», а в базе это называется «интеграция через REST-интерфейс». Модель-поиск может не сопоставить. Можно перед поиском переформулировать запрос через LLM или добавить синонимы. Дёшево и эффективно.
Агрегирование. Нашёл 15 релевантных чанков из 5 документов. LLM не может удержать всё в голове. Хороший подход — сначала кратко сжать каждый чанк (summarize), потом подавать сжатое в основной запрос. Или иерархически: сначала находишь релевантные документы, потом чанки внутри них.
Fallback. Что если ничего не нашлось? Модель не должна выдумывать. Обязательно промпт, который говорит: если релевантная информация не найдена — скажи об этом честно. Без этого RAG-системы галлюцинируют даже больше, чем голая модель. Потому что человек потом винит систему, а не модель.
Сколько это стоит и когда оно того не стоит
Открытые решения (LlamaIndex, LangChain, Chroma, Qdrant) покрывают 80% задач. Платные векторные БД (Pinecone, Weaviate) удобнее, но зачем платить, если у тебя сотни тысяч чанков и свой сервер?
Главное, что я усвоил: RAG не нужен, если у тебя меньше 50 страниц документации и простые вопросы. Скормил в контекст — и достаточно. RAG оправдывает себя, когда данных много, они обновляются, и нужны точные ссылки на источник.
Если система построена криво — она объективно хуже обычного контекста. Поэтому я всегда рекомендую сначала замерить baseline: голая модель на твоей задаче, а потом уже строить RAG и сравнивать.
Конкретный кейс из практики: команда перешла с RAG на fine-tuning для внутренней базы знаний и была довольна. RAG остался для документов, которые обновляются каждый день — там fine-tuning не успевает. Для статичных знаний — fine-tuning часто дешевле и быстрее. Для динамики — без RAG не обойтись.
Это не про технологию ради технологии. Это про задачу: если у тебя она решается проще — решай проще.
