Сижу, тестирую очередного бота на базе LLM. Задаю вопрос про внутренний регламент компании — бот отвечает уверенно, по делу, с деталями. Единственная проблема: регламент мы сменили полгода назад. Бот выдумал ответ целиком. Это называется галлюцинация, и именно от неё спасает RAG.
RAG — Retrieval-Augmented Generation. Звучит как термин из презентации венчурного фонда, но суть простая: LLM отвечает не из своих весов, а из твоей базы знаний. Подсунул документы — получил ответ по документам. Но вот беда: построить эту базу знаний так, чтобы она реально работала, — задача с кучей подводных камней. Я разбирался недели две, и вот что понял.
Документы — это не данные для модели
Первая ошибка, которую я совершил: запихал в базу целые PDF-ки, по 50 страниц каждый. Логика понятная — там же всё есть. На практике модель получает фрагмент, в котором нужный ответ размыт между таблицами, колонтитулами и сносками. Результат предсказуемый: «Извините, в предоставленных документах информации не найдено».
Причина в том, как работает поиск. Пользователь задаёт вопрос, система ищет в базе похожий фрагмент и подсовывает его модели. Если фрагмент большой — в нём много шума. Если маленький — в нём может не быть контекста для ответа.
Правильная единица — чанк (chunk). Это кусок текста, который загружается в базу как одна запись. Выбор размера чанка — первое критическое решение.
Тут нет идеального числа. Я видел рекомендации от 500 до 2000 токенов, но всё зависит от задачи.
Если база — это юридические договоры, чанки должны быть поменьше, потому что один пункт договора не должен смешиваться с другим. Берёшь 300–500 токенов, и всё чисто.
Если это статьи или документация, можно брать 800–1200 токенов — хватит для контекста, но не настолько много, чтобы нахватать лишнего.
С таблицами и списками беда. Модель не всегда понимает структуру таблицы, если видит её как строку текста. Лучше разбивать на отдельные строки или обрабатывать как картинки.
Второй параметр — overlap, перекрытие между чанками. Ставишь 10–20% — и фраза, разрезанная на два чанка, всё равно найдётся целиком. Без перекрытия ответы обрываются на полуслове.
Эмбеддинги — про что вообще спрашивают
Чтобы искать в базе, нужно перевести текст в числа. Этим занимаются модели-эмбеддеры. Ты берёшь чанк, прогоняешь через эмбеддер — получаешь вектор, набор из сотен чисел. Похожие тексты дают похожие векторы. Дальше поиск — это «найди ближайшие векторы к вектору запроса».
Я начал с text-embedding-ada-002 от OpenAI — дёшево, быстро, вроде работает. Потом попробовал Cohere и BGE от FlagEmbedding. Разница есть, особенно на русском. Ada-002 хорош на английском, а с русскими документами иногда находит совсем не то. BGE-multilingual-GGUF — открытая модель, работает локально, на русском держит смысл значительно лучше.
Для продакшена я бы смотрел в сторону E5 или BGE — они специально заточены под задачу «найди релевантный документ». Ada-002 больше про «найди похожий текст», а это не всегда одно и то же.
Векторная база — куда всё складывать
Самые популярные — pgvector (добавка к PostgreSQL), Qdrant, Chroma и Weaviate. У каждого свои плюсы.
pgvector удобен, если у тебя уже есть PostgreSQL. Не нужно поднимать отдельный сервис, миграции те же, JOIN-ы работают. Для старта — отличный вариант.
Qdrant я выбрал для проекта, где нужна фильтрация по метаданным. Можно написать «ищи среди чанков, где author = Петров и год = 2023», и это сработает. Скорость приличная, есть облачная версия.
Chroma — совсем простой, буквально одна библиотека. Поднимается за минуту. Для прототипов и экспериментов — самое то. Для продакшена с миллионами чанков уже не хватит.
Weaviate — здоровый, с поддержкой гибридного поиска. Он умеет смешивать векторный поиск и обычный BM25. Но порог входа выше, разбираться дольше.
Гибридный поиск — когда просто вектора недостаточно
Векторный поиск хорош на нечётких запросах: «что там написано про расторжение договора» — найдёт и «расторжение», и «прекращение действия», и «отказ от исполнения». Но на точных запросах он проигрывает классическому.
Когда я ищу «пункт 3.2 договора» — мне нужен именно этот пункт, а не «похожий». BM25 справится идеально. А «что делать если сотрудник не выходит на связь три дня» — это уже задача для векторного поиска.
Гибридный поиск — это когда ты берёшь результаты из обоих источников и комбинируешь. Стандартная схема: релевантность = alpha × vector_score + (1 - alpha) × bm25_score. Alpha подбирается под задачу. Для юридических баз я ставлю 0.3–0.5 — побольше веса классическому поиску. Для базы знаний общего назначения — 0.7–0.8.
Дальше — re-ranking. Результаты поиска часто нужно переупорядочить. Модель-реранкер (Cohere Rerank, BGE Reranker) берёт top-k кандидатов и пересортирует их по реальной релевантности. Это последний фильтр перед тем, как чанки уходят в LLM. Треть лишнего шума убирает без проблем.
Что я получил в итоге
Две недели возни, и система из «отвечает правдоподобно, но неправильно» превратилась в «отвечает по делу и почти не галлюцинирует». Не идеально, но приемлемо для внутреннего ассистента.
Главное, что я для себя вынес: RAG — это не «взял документы, загрузил в базу, работает». Это инженерная задача с кучей точек настройки. Chunk size, overlap, модель эмбеддинга, база, гибридный поиск, реранкер — каждый параметр влияет на результат, и без тестирования не поймёшь, что именно правильно для твоей базы.
Но базовый рабочий вариант собирается за пару вечеров. Особенно если не пытаться сразу делать идеально.
