Чтобы подключить документацию к LLM, не требуется дообучать модель на каждом обновлении базы знаний. Соберите RAG-пайплайн: загрузите документы, разделите их на чанки, превратите фрагменты в эмбеддинги, сохраните в векторной базе, а перед генерацией ответа найдите релевантный контекст и передайте его модели.
RAG, или Retrieval-Augmented Generation, превращает обычную LLM в ассистента по внутренним регламентам, wiki, FAQ, инструкциям и техническим описаниям. Модель отвечает на основе найденных фрагментов, поэтому знания можно обновлять переиндексацией документов, без нового обучения весов.
Базовая схема выглядит так: документы - чанки - эмбеддинги - векторный поиск - промпт с контекстом - ответ с указанием источников. Ниже разобраны компоненты архитектуры, пример на Python, ограничения семантического поиска и требования к production-системе.
Что такое RAG и зачем он нужен для работы с документацией
Обычная LLM формирует ответ на основе данных, попавших в её обучающую выборку, системного промпта и текущего диалога. Внутренние документы компании, закрытая wiki или свежая версия регламента могут отсутствовать в этих данных. Модель заполнит пробел наиболее вероятным текстом, и именно здесь появляются галлюцинации.
RAG добавляет к генерации отдельный этап поиска. Пользовательский вопрос превращается в вектор, система находит похожие фрагменты в индексе, после чего добавляет их в контекст LLM. Модель получает фактический материал перед ответом и должна опираться на него.
- Актуальность. После изменения инструкции можно пересобрать индекс и сразу использовать новую версию.
- Контролируемость. Ответ связывается с конкретными файлами, разделами и фрагментами.
- Снижение галлюцинаций. Промпт может требовать признать отсутствие ответа, если нужных данных нет в контексте.
- Разделение доступа. Поиск можно фильтровать по отделу, проекту, уровню конфиденциальности или владельцу документа.
RAG подходит для вопросов вроде «какой срок согласования указан в регламенте», «как настроить сервис по внутренней инструкции» или «какие поля обязательны в заявке». В каждом случае системе требуется найти конкретный фрагмент, а затем пересказать его понятным языком.
Этот подход входит в переход от отдельных промптов к полноценным AI-системам, где приходится учитывать память, API-интеграции, деплой, мониторинг, безопасность, лимиты токенов и доступ к данным. Промпт сам по себе не решает проблему устаревших документов или разграничения прав.
| Подход | Где хранятся знания | Как обновить информацию | Типичный сценарий |
|---|---|---|---|
| Обычная LLM | Веса модели и текущий диалог | Нужна новая модель или внешний контекст | Общие вопросы и генерация текста |
| RAG | Внешняя база документов | Обновить документы и индекс | FAQ, регламенты, wiki, техническая документация |
| Fine-tuning | Веса дообученной модели | Новый цикл обучения | Стиль, формат, классификация и устойчивое поведение |
Для часто меняющейся базы знаний RAG обычно практичнее fine-tuning. Дообучение меняет поведение модели и помогает закрепить формат ответов, но плохо подходит для постоянной доставки новых фактов. RAG можно дополнить fine-tuning, если системе нужны одновременно свежие документы и специфический стиль работы.
Архитектура RAG: от документов до ответа
RAG-пайплайн состоит из двух контуров. Первый готовит индекс и запускается при добавлении или изменении документов. Второй обрабатывает пользовательский запрос в реальном времени.
- Индексация: загрузка файлов, очистка текста, разбиение на чанки, создание эмбеддингов и сохранение в векторной базе.
- Retrieval: преобразование вопроса в вектор и поиск близких фрагментов.
- Augmented generation: сборка промпта с найденным контекстом и вызов LLM.
- Проверка: вывод источников, фильтрация ответа, проверка структуры и обработка ситуации, когда данных недостаточно.
Эмбеддинги: как превратить текст в векторы
Эмбеддинг, это числовое представление текста. Модель кодирует смысл фразы в массив чисел, а поиск сравнивает такие массивы по косинусному расстоянию или другой метрике близости. Благодаря этому запрос «как вернуть товар» может найти фрагмент с формулировкой «процедура оформления возврата», даже если слова почти не совпадают.
Для индекса и пользовательского запроса нужно использовать одну embedding-модель. Смена модели без пересборки индекса нарушит сопоставимость векторов. При выборе смотрите на четыре параметра:
- Язык. Для русскоязычной базы нужна русскоязычная или мультиязычная модель. Англоязычная модель может хуже различать падежи, термины и короткие формулировки.
- Качество поиска. Проверяйте модель на собственном наборе вопросов, где для каждого запроса известен правильный документ или фрагмент.
- Размер вектора и скорость. Более крупное представление может требовать больше памяти и времени, но размер сам по себе не гарантирует лучший поиск.
- Стоимость и размещение. Облачный API упрощает старт, локальная Sentence-Transformers-модель дает контроль над данными и расходами на запросы.
Для первого прототипа подойдут OpenAIEmbeddings или мультиязычные модели из экосистемы Sentence-Transformers. Финальный выбор лучше делать по тестовой выборке из 30-100 реальных вопросов, а не по названию модели.
Чанки: как правильно разбивать документы
LLM и векторный поиск работают с фрагментами, поэтому качество разбиения напрямую влияет на ответ. Слишком большой чанк содержит лишний текст и размывает сигнал. Слишком маленький теряет определения, условия и исключения, которые были описаны в соседнем абзаце.
В качестве стартовой настройки используйте 200-500 токенов на чанк и перекрытие 10-20%. Точный размер зависит от документов. Для коротких FAQ нужны небольшие самостоятельные блоки. Для технических регламентов полезнее сохранять заголовок раздела, список условий и связанный пример в одном фрагменте.
- Разбивайте текст по заголовкам, абзацам и пунктам списка, а не посреди предложения.
- Добавляйте в каждый чанк путь документа и заголовки родительских разделов.
- Храните номер страницы, дату версии, тип документа и уровень доступа в metadata.
- Таблицы, схемы и код обрабатывайте отдельными правилами, иначе линейное извлечение текста разрушит связи между строками и столбцами.
- Для ссылок вида «см. раздел 7.2» сохраняйте структуру оглавления и идентификаторы разделов.
В коде ниже размер чанка задается в символах, потому что стандартный RecursiveCharacterTextSplitter считает символы. Значение 1600 символов дает примерно несколько сотен токенов, но реальное соотношение зависит от языка и содержания.
Векторная база данных: где хранить эмбеддинги
Векторная база хранит эмбеддинг, исходный текст и metadata. При запросе она быстро находит ближайшие векторы и возвращает связанные фрагменты. Для прототипа достаточно FAISS или Chroma, которые можно запускать локально. Pinecone и Weaviate подходят для сервисов, где нужны управляемая инфраструктура, фильтрация и горизонтальное масштабирование.
| Решение | Подходит для | Ограничения |
|---|---|---|
| FAISS | Локальный прототип, один сервис, небольшая база | Нужно самостоятельно организовать API, обновление и контроль доступа |
| Chroma | Локальное приложение с metadata и простой интеграцией | Перед production-переходом нужно проверить нагрузку и отказоустойчивость |
| Pinecone | Облачный сервис с управляемым хранением индексов | Зависимость от внешней инфраструктуры и тарификации |
| Weaviate | Развертывание с расширенными возможностями поиска | Потребуются настройка сервера и сопровождение |
Выбор базы не исправит плохой чанкинг. Если нужная мысль потеряна при обработке PDF или попала в большой фрагмент вместе с десятью нерелевантными разделами, другая векторная БД сама по себе не даст точный ответ.
Практический пайплайн на Python: код и пояснения
Ниже приведен минимальный вариант на LangChain и FAISS. Он читает TXT, Markdown и PDF, создает индекс, выполняет поиск и передает контекст в чат-модель. Для production-проекта потребуется добавить обработку DOCX, HTML, таблиц, версий документов и прав доступа.
Подготовка документов и создание индекса
Создайте каталог knowledge_base и положите в него файлы. Установите библиотеки:
pip install -U langchain langchain-community langchain-openai langchain-text-splitters faiss-cpu pypdf
Индекс строится один раз, а затем пересобирается при изменении источников:
import os
from pathlib import Path
from langchain_community.document_loaders import PyPDFLoader, TextLoader
from langchain_community.vectorstores import FAISS
from langchain_openai import OpenAIEmbeddings
from langchain_text_splitters import RecursiveCharacterTextSplitter
DATA_DIR = Path('knowledge_base')
INDEX_DIR = 'faiss_index'
documents = []
for path in DATA_DIR.rglob('*'):
suffix = path.suffix.lower()
if suffix == '.pdf':
documents.extend(PyPDFLoader(str(path)).load())
elif suffix in {'.txt', '.md'}:
documents.extend(TextLoader(str(path), encoding='utf-8').load())
if not documents:
raise RuntimeError('В каталоге knowledge_base нет поддерживаемых файлов')
splitter = RecursiveCharacterTextSplitter(
chunk_size=1600,
chunk_overlap=240,
separators=['\\n\\n', '\\n', '. ', ' ', '']
)
chunks = splitter.split_documents(documents)
for chunk_id, document in enumerate(chunks):
source = document.metadata.get('source', 'unknown')
document.metadata['source'] = Path(source).name
document.metadata['chunk_id'] = chunk_id
embeddings = OpenAIEmbeddings(
model=os.getenv('EMBEDDING_MODEL', 'text-embedding-3-small')
)
vector_store = FAISS.from_documents(chunks, embeddings)
vector_store.save_local(INDEX_DIR)
print(f'Документов: {len(documents)}')
print(f'Чанков: {len(chunks)}')
print(f'Индекс сохранен в: {INDEX_DIR}')
Метаданные здесь ограничены именем файла и номером чанка. Для рабочей базы добавьте document_id, версию, дату публикации, подразделение и ACL. Поле ACL понадобится, чтобы отбрасывать закрытые фрагменты еще на этапе поиска.
PDF часто содержит колонки, колонтитулы, сканы и таблицы. Перед созданием эмбеддингов проверьте несколько извлеченных страниц глазами или отдельным скриптом. Если текст прочитан в неправильном порядке, поиск будет работать с испорченным контекстом.
Поиск релевантного контекста и генерация ответа
На втором шаге загрузите индекс, найдите несколько фрагментов и сформируйте промпт. Параметр k=5 здесь служит стартовой настройкой. Для коротких FAQ может хватить двух-трех чанков, а для многостраничного регламента потребуется больше результатов или отдельный поиск по разделам.
import os
from langchain_community.vectorstores import FAISS
from langchain_core.prompts import ChatPromptTemplate
from langchain_openai import ChatOpenAI, OpenAIEmbeddings
INDEX_DIR = 'faiss_index'
embeddings = OpenAIEmbeddings(
model=os.getenv('EMBEDDING_MODEL', 'text-embedding-3-small')
)
vector_store = FAISS.load_local(
INDEX_DIR,
embeddings,
allow_dangerous_deserialization=True
)
question = 'Какой порядок согласования заявки указан во внутреннем регламенте?'
results = vector_store.similarity_search_with_score(question, k=5)
context_parts = []
for document, distance in results:
source = document.metadata.get('source', 'unknown')
chunk_id = document.metadata.get('chunk_id', 'unknown')
text = document.page_content.strip()
context_parts.append(
f'[Источник: {source}, фрагмент: {chunk_id}] {chr(10)}{text}'
)
context = (chr(10) + chr(10)).join(context_parts)
prompt = ChatPromptTemplate.from_messages([
('system', '''Ты ассистент по внутренней документации.
Отвечай только на основе переданного контекста.
Если контекст не содержит ответа, напиши: «В документации нет достаточных данных для ответа».
Не придумывай сроки, суммы, названия и исключения.
Для каждого существенного утверждения указывай источник в формате [Источник: имя файла, фрагмент: номер].'''),
('human', '''Контекст:
{context}
Вопрос: {question}
Сформируй краткий ответ с конкретными условиями и цитатами источников.''')
])
llm = ChatOpenAI(
model=os.getenv('CHAT_MODEL', 'gpt-4o-mini'),
temperature=0
)
chain = prompt | llm
answer = chain.invoke({
'context': context,
'question': question
})
print(answer.content)
Значение distance в примере не выводится в ответ: у FAISS это расстояние, а не универсальная вероятность релевантности. Его можно использовать для пороговой фильтрации только после калибровки на собственных данных.
Метки источников в контексте помогают модели сформировать ссылки на файлы, но сами по себе не делают цитирование проверяемым. Интерфейс должен уметь открыть исходный документ, страницу или раздел. Иначе пользователь получит название файла, но не сможет быстро проверить утверждение.
Пример ожидаемого формата ответа:
Заявка сначала направляется руководителю подразделения, затем передается ответственному согласующему. Срок и исключения указаны в разделе 3.2 регламента [Источник: регламент_заявок.pdf, фрагмент: 18].
Этот текст служит образцом структуры, а не фактическим ответом по конкретной базе. Реальное содержание зависит от загруженных документов.
Ограничения RAG и как их обойти
RAG не гарантирует точность только потому, что в системе есть векторная база. Ошибка может появиться при извлечении текста, разбиении документа, создании эмбеддинга, поиске, сборке промпта или генерации. Диагностировать нужно каждый этап отдельно.
- Плохой парсинг. Таблица превратилась в набор строк, заголовки потерялись, страницы перемешались.
- Неудачные чанки. Ответ разделен между фрагментами, а каждый из них по отдельности выглядит неполным.
- Слабая embedding-модель. Она не различает близкие термины, аббревиатуры или русские формулировки.
- Низкий recall. Нужный фрагмент не попал в top-k, и LLM физически не получила правильные данные.
- Неправильный промпт. Модель смешивает контекст с собственными знаниями или не сообщает о нехватке данных.
- Конфликт версий. В индекс попали старая и новая редакции документа, а поиск вернул обе.
Пайплайн с четырьмя зонами ошибок, парсингом, запросом, поиском и генерацией, подробно разобран в статье о причинах галлюцинаций в RAG. Такой подход полезен при отладке: сначала нужно выяснить, получил ли ретривер правильный текст, и только затем менять промпт.
Гибридный поиск и переранжирование
Семантический поиск хорошо находит похожие по смыслу формулировки, но может проигрывать точному поиску по артикулам, номерам разделов, названиям API, кодам ошибок и юридическим формулировкам. BM25 учитывает совпадение слов и поэтому дополняет embeddings.
Гибридный поиск объединяет два списка результатов. Упрощенная схема может выглядеть так:
final_score = alpha * vector_score + (1 - alpha) * bm25_score
Перед сложением оценки нужно нормализовать. Коэффициент alpha подбирайте на тестовых вопросах. Для запросов с именами полей и кодами ошибок полезнее увеличить вклад BM25, для описательных вопросов, наоборот, усилить семантическую часть.
Переранжирование добавляет второй этап. Сначала быстрый retriever выбирает, например, 20 кандидатов, затем cross-encoder оценивает пару «вопрос + фрагмент» и оставляет несколько лучших. Такой каскад увеличивает вычисления, поэтому его разумно включать для сложных запросов или крупных коллекций, а не для каждого короткого FAQ.
Еще один способ улучшить recall, расширить запрос синонимами, извлечь сущности и искать по фильтрам metadata. Для вопроса о версии API можно отдельно выделить название сервиса, номер версии и тип операции. В многошаговых сценариях полезно разбить исходный вопрос на подзадачи и выполнить несколько поисков. Такой подход описан в материале о многошаговом семантическом поиске.
Когда RAG не подходит
RAG не стоит использовать как универсальный слой для любой работы с данными. Если ответ требует точного значения из таблицы, SQL-запрос к структурированной базе будет надежнее свободного поиска по тексту. Если задача сводится к проверке фиксированного правила, подойдет обычный код или классификатор.
- Обобщение всей большой коллекции. Поиск нескольких чанков не заменяет анализ полного корпуса. Понадобятся иерархическое суммирование, сжатие контекста или отдельный аналитический пайплайн.
- Многошаговое рассуждение. Один retrieval-вызов может не найти документы для всех этапов задачи. Нужен оркестратор, который планирует поиск и проверяет промежуточные результаты.
- Строгая консистентность. Для финансовых расчетов, остатков, прав доступа и транзакций источником истины должна быть система учета, а LLM может объяснять уже полученный результат.
- Точные соответствия. Регулярные выражения, словари, нормализация и BM25 дешевле и предсказуемее для артикулов, кодов и справочников.
- Закрепление поведения модели. Если нужно стабильно соблюдать формат JSON, стиль или схему классификации, полезнее рассмотреть fine-tuning и программную валидацию.
Практичная система часто использует каскад: сначала точное совпадение и фильтры, затем BM25, embeddings, reranking и вызов LLM только при необходимости. Подходы, которые заменяют RAG более дешевыми NLP-методами для отдельных типов документов, разобраны в этом материале.
Внедрение RAG в production: что учесть
Прототип из двух скриптов показывает механику, но рабочая система требует контроля данных, версий, доступа и качества. RAG входит в более широкую AI-архитектуру, где нужны повторяемые процессы, логирование, деплой и наблюдаемость.
Обновление индекса
- Присваивайте каждому документу стабильный идентификатор.
- Считайте хеш файла и переиндексируйте только изменившиеся документы.
- Удаляйте старые чанки при публикации новой версии.
- Храните дату, номер редакции и статус документа в metadata.
- Переключайте приложение на новый индекс после полной сборки, а не во время записи.
Без контроля версий система может ответить по отмененному регламенту. Для критичных процессов добавьте фильтр по статусу, например published, и запрет на поиск по архивным редакциям.
Мониторинг качества и стоимости
Соберите набор из реальных вопросов и эталонных источников. Для него измеряйте несколько показателей:
- Recall поиска: попал ли нужный фрагмент в выданные результаты.
- Точность контекста: сколько найденных фрагментов действительно относится к вопросу.
- Faithfulness: подтверждается ли ответ переданным контекстом.
- Доля отказов: как часто система корректно сообщает о нехватке данных.
- Задержка: сколько времени занимают поиск, reranking и генерация.
- Расход токенов: размер контекста, промпта и ответа на один запрос.
Логируйте запрос, версии embedding- и чат-модели, идентификаторы найденных чанков, оценки поиска, итоговый ответ и решение проверяющего. Содержимое документов с персональными или коммерческими данными нужно маскировать в логах.
Безопасность и права доступа
Фильтр доступа должен срабатывать до передачи контекста в LLM. Если пользователь не имеет права читать документ, его чанк нельзя отправлять в промпт даже при очень похожем запросе.
- Разделяйте индексы или добавляйте ACL-фильтры для разных групп пользователей.
- Проверяйте доступ повторно перед показом цитаты и ссылки на исходный файл.
- Считайте текст документов недоверенным вводом: внутри может встретиться инструкция, которая пытается изменить правила ассистента.
- Ограничивайте инструменты, доступные модели, особенно если ассистент умеет вызывать API или выполнять команды.
- Храните аудит запросов и ответов с учетом требований к персональным данным.
Практический чек-лист по источникам, метаданным, аудиту и границе между подсказкой и служебным решением приведен в статье о помощнике по ведомственным регламентам.
Деплой и отказоустойчивость
Разделите сервис индексации, сервис поиска и слой генерации. Тогда смена чат-модели не потребует переписывать обработку документов, а переход на другой vector store не затронет интерфейс пользователя.
Предусмотрите тайм-ауты, повторные попытки для временных ошибок API, лимит размера контекста и понятный отказ при пустом результате поиска. Сохраняйте исходный запрос и найденные идентификаторы, чтобы воспроизвести спорный ответ.
Стоимость зависит от числа документов, частоты переиндексации, embedding-модели, количества кандидатов и длины контекста. Сократить расходы помогают кэширование эмбеддингов, индексация только изменившихся файлов, ранняя фильтрация и каскад моделей: простые вопросы обрабатывает более дешевая модель, сложные передаются сильной.
Заключение: RAG как практический инструмент
RAG подключает документацию к LLM через поиск релевантных фрагментов. Для старта нужны четыре базовых компонента: загрузчик документов, чанкер, embedding-модель и векторная база. Затем добавляются retrieval, промпт с правилами цитирования и генератор ответа.
Дообучение не требуется для большинства задач, где меняются факты, регламенты и FAQ. Главные риски лежат в качестве извлечения текста, размере чанков, выборе embedding-модели, точности поиска и смешении версий документов.
Начните с небольшой тестовой базы и 30-100 реальных вопросов. Проверьте, находит ли система нужные фрагменты, корректно ли отказывает при отсутствии ответа и может ли пользователь открыть первоисточник. После этого добавляйте гибридный поиск, reranking, фильтры metadata и многошаговую маршрутизацию там, где простой pipeline не справляется.