RAG на пальцах: бот, который отвечает по нашим документам
Как устроен чат-бот поверх внутренних регламентов: эмбеддинги, векторная база, разбиение на куски. И где эта конструкция ломается.
Задача звучала буднично: «сделайте бота, который отвечает на вопросы по нашим регламентам». Модель про наши регламенты ничего не знает — она училась на публичных данных. Дообучать её после каждой правки документа дорого и медленно.
Поэтому — RAG. Retrieval-Augmented Generation, генерация с дополненным поиском. Идея объясняется за минуту.
Экзамен с конспектом
Представь студента, которому разрешили пользоваться конспектом. Он не помнит всё наизусть, но умеет читать и формулировать. Перед ответом находит нужные две страницы и отвечает по ним.
RAG делает ровно это:
- пользователь задаёт вопрос;
- мы ищем в своей базе документов куски, относящиеся к вопросу;
- подкладываем найденное в промпт: «ответь, опираясь только на этот текст»;
- модель формулирует ответ.
Никакого дообучения. Обновился регламент — переиндексировали документ, и бот отвечает уже по новой версии. Это, кстати, главный аргумент в разговоре с бизнесом: правки в документах не требуют разработчика.
Как искать по смыслу
Обычный текстовый поиск ищет слова. Вопрос «сколько дней отпуска на испытательном» не найдёт документ, где написано «в период испытательного срока предоставляется…» - слова другие.
Тут работают эмбеддинги. Модель превращает текст в вектор - массив чисел, который кодирует смысл. Близкие по смыслу тексты дают близкие векторы, близость меряется косинусным расстоянием:
const v1 = await embed('сколько дней отпуска на испытательном сроке');
const v2 = await embed('в период испытательного срока отпуск предоставляется…');
cosineSimilarity(v1, v2); // ≈ 0.87 - близко, хотя слова разные
Векторы всех кусков документации складываем в векторную базу (мы брали Qdrant), она быстро находит ближайших соседей к вектору вопроса.
Разбиение на куски - самое важное решение
Целый документ в промпт не влезет, да и не нужен: чем больше лишнего текста, тем хуже ответ.
Что выяснилось на практике:
Резать надо по смыслу, а не по символам. По заголовкам, разделам, пунктам. Кусок, оборванный на середине предложения, отравляет поиск - его вектор не про что.
Размер 300-800 токенов с перекрытием в сотню-полторы. Перекрытие спасает, когда ответ лежит ровно на границе кусков.
Метаданные хранить обязательно: источник, раздел, дата, права доступа:
await qdrant.upsert('docs', {
points: [{
id, vector,
payload: { text: chunk, source: 'regulation_hr.pdf', section: '4.2', updated: '2026-03-01' },
}],
});
Права доступа - не мелочь. Бот, процитировавший зарплатный документ не тому сотруднику, это инцидент, а не забавный баг. И фильтровать надо на этапе поиска, а не просить модель «не отвечать, если нельзя».
Как выглядит запрос
const qVec = await embed(question);
const found = await qdrant.search('docs', {
vector: qVec,
limit: 5,
filter: { must: [{ key: 'access', match: { any: userRoles } }] },
});
const context = found
.map(f => `[${f.payload.source} §${f.payload.section}]\n${f.payload.text}`)
.join('\n\n');
const answer = await llm.complete(`
Отвечай только на основе контекста ниже. Если ответа в контексте нет - так и скажи.
Обязательно указывай источник в формате [файл §раздел].
Контекст:
${context}
Вопрос: ${question}
`);
Две строчки про «только по контексту» и «если нет - скажи» убирают большую часть выдумок. Не все, но большую часть.
Где ломается
Поиск нашёл не то. Самая частая причина плохих ответов - не модель, а retrieval. Прежде чем менять модель, посмотри, какие куски вообще нашлись. В половине случаев проблема в разбиении.
Вопросы вида «а что изменилось за год». RAG видит пять кусков, а не всю картину, и на обзорные вопросы отвечает плохо. Это закрывается отдельно, обычными запросами к структурированным данным.
Противоречивые документы. Лежат старый и новый регламент - модель честно процитирует оба. Лечится не промптом, а гигиеной источников и фильтром по дате.
Нет оценки качества. Без набора из полусотни контрольных вопросов с эталонными ответами любая правка промпта - это гадание. Мы такой набор завели и прогоняли после каждого изменения, а основной метрикой была доля ответов со ссылкой на верный документ.
Ещё сильно помогает гибридный поиск. Векторный плохо ищет точные обозначения - номера статей, артикулы, коды ошибок. Связка «векторный плюс обычный полнотекстовый» стабильно лучше любого по отдельности.
В сухом остатке: RAG - это не магия и не революция. Это хорошая поисковая система, к которой приделали умение формулировать. И качество итога определяется качеством поиска, то есть качеством ваших документов.
Ссылки
- Lewis et al., 2020 - статья, где предложили подход
- Документация Qdrant и гибридный поиск
- Contextual Retrieval у Anthropic - как улучшить извлечение кусков
- Гайд по эмбеддингам