
Всем привет!
Предисловие
В этой статье я хочу поделиться своим опытом создания приложения на базе Retrieval-Augmented Generation (RAG) системы, которая превращает кучу документов в удобную интерактивную базу знаний. По сути, наша система хранит документы в виде эмбеддингов в векторной базе Qdrant и позволяет задавать к ним вопросы. Когда вы что-то спрашиваете, запрос вместе с контекстом из документов отправляется языковой модели (LLM), которая формирует вам чётко структуированный ответ. Я покажу, как всё устроено изнутри: архитектуру, ключевые компоненты и «кухню», благодаря которой это работает. Если вы только начинаете знакомство с RAG или просто интересно, как может выглядеть ИИ-помощник для документов, эта статья даст наглядное представление о работе такой системы.
Содержание
Введение
1. Что такое RAG?
2. Обзор архитектуры
2.1. База данных
2.2. Бэкенд
2.3. Фронтенд
3. Обработка метаданных документов
3.1. Извлечение заголовков
3.2. Формирование секций
3.3. Пример промпта (тест метаданных)
4. Сложности и возможные улучшения
5. Примечание о конфиденциальности данных
Заключение
Введение
В своей работе я часто сталкивался с проблемой эффективного использования большого объёма документов. Представьте себе огромную библиотеку инструкций, заметок, отчётов и файлов в разных форматах, из которых вам нужно выудить из этого всего лишь один кусочек информации именно тогда, когда он нужен. Раньше я тратил часы на поиск ответов, заставляя клиентов ждать, пока я проверю детали или найду нужный ответ на их вопросы.
В какой-то момент я понял, что должен быть лучший способ. Тогда пришла идея: а что если создать локальное приложение, которое автоматически будет находить нужные фрагменты информации из моей библиотеки документов и отправлять их вместе с моим запросом в LLM? Цель была проста: пусть ИИ берёт на себя тяжёлую работу и выдаёт мне чёткий, структурированный ответ на основе моих собственных данных.
К моему удивлению, это сработало замечательно. Система не только сэкономила огромное количество времени, но и сделала моё общение с клиентами быстрее и точнее. Воодушевлённый этими результатами, я решил поделиться своим опытом того, как я создал это приложение на основе RAG, и кратко объяснить, как оно работает «под капотом». Независимо от того, интересуетесь ли вы RAG или просто хотите собрать собственного интеллектуального помощника для работы с документами, я надеюсь, что эта история поможет вам сделать первые шаги.
1. Что такое RAG?
Представьте, что вы задаёте другу сложный вопрос, а он вместо того, чтобы гадать, просто быстро пролистывает свои заметки и сразу даёт точный ответ. Вот примерно так и работает RAG. Эта штука позволяет языковым моделям «подсматривать» информацию перед тем, как генерировать ответ. То есть вместо того чтобы работать только с тем, чему LLM уже научилась, ей предоставляется нужная информация из внешних источников — например, из документов компании или базы знаний.
Представьте офис, где все время спрашивают про HR: «Сколько у меня отпускных дней?» или «Как оформить возврат расходов?» Ассистент на RAG мгновенно ищет ответы в HR-документах, справочниках или внутреннем вики и выдаёт точные ответы. Сотрудники получают нужную инфу моментально, HR экономит кучу времени на повторяющихся вопросах. Получается, что наша система — это как суперэффективный офисный помощник, который ничего не забывает.
2. Обзор архитектуры
Вот графическая схема компонентов моего приложения, включая в себя фронтенд и бэкенд:

Чтобы показать, как работает приложение, я закинул туда четыре документа: три про возобновляемую энергетику и два про ИИ-агентов в форматах .txt, .docx и .pdf. Так образом система обладает базой знаний из разных областей, чтобы было наглядно видно, как она справляется с поиском информации по разным темам.
Сервис поддерживает несколько популярных форматов файлов, что даёт гибкость при загрузке документов:
self.docx = ".docx"
self.xlsx = ".xlsx"
self.txt = ".txt"
self.pdf = ".pdf" # image-only PDFs are supported through
# built-in OCR (tesseract)
2.1. База данных
Qdrant — это векторная база данных, созданная для эффективного хранения и поиска эмбеддингов. В RAG-системе каждый текстовый фрагмент из документов преобразуется в высокоразмерный вектор, который отражает его смысловое содержание. В моём приложении я использую Qdrant для хранения всех загруженных частей документов, чтобы ИИ мог мгновенно их искать каждый раз, когда пользователь отправляет запрос.
Qdrant известен:
Быстрым поиском по схожести
Qdrant может мгновенно сравнивать вектор запроса с миллионами векторов документов и возвращать ближайшие совпадения за миллисекунды.Работой с большими объёмами данных
Будь то сотни PDF или тысячи фрагментов документов, Qdrant легко масштабируется, обеспечивая быстрый и надёжный поиск.Гибкой поддержкой метаданных
Каждый фрагмент может хранить дополнительную информацию: заголовки, наименования файлов, которую наш сервис использует для более точной фильтрации и ранжирования результатов поиска.Интеграцией с ИИ-пайплайнами
Qdrant отлично работает с различными моделями эмбеддингов, позволяя нашему сервису создавать, хранить и искать фрагменты документов с минимальными затратами ресурсов.
Используя Qdrant в приложении, мы превращаем сырые документы в живую, доступную для поиска базу знаний, благодаря чему ИИ может выдавать ответы, основанные на реальном содержимом, а не только на своих предварительно обученных данных.
2.2. Бэкенд
Бэкенд на FastAPI — это сердце всего приложения, которое держит всё в рабочем состоянии. FastAPI я выбрал за его скорость, современность и асинхронность: сервис легко справляется с кучей запросов одновременно, не тормозя. Плюс он сам проверяет типы данных и легко интегрируется с многопоточными задачами, вроде обработки документов и запросов к Qdrant.
В бэкенде есть пять основных эндпоинтов, которые отвечают за ключевой рабочий процесс:
2.2.1. POST /qdrant/upload-documents
Загружает несколько документов в Qdrant и сохраняет их векторные эмбеддинги для последующего поиска.
Чтобы закидывать файлы сразу пачкой, загрузка документов работает с многопоточностью. Файл читается и обрабатывается в фоне, так что сервер не тормозит на тяжёлых задачах вроде парсинга или создания эмбеддингов.
async def upload_document(self,
util_service: UtilService,
file: UploadFile):
content = await file.read()
def process_file():
filename = file.filename
logger.info(f"Uploading {filename}...")
# Extracting content and chunking here
await run_in_threadpool(process_file)
После того как файл прочитан, сервис извлекает из него текст и делит его на логические секции. Крупные секции дополнительно разбиваются на маленькие "чанки", чтобы эмбеддинги работали точнее, а поиск информации был эффективнее.
text = util_service.extract_text_from_file(content, filename)
headings = util_service.extract_headings(text)
sections = util_service.get_sections(text, headings, filename)
chunks = []
for section in sections:
if len(section.page_content) > CHUNK_SIZE:
chunks.extend(self.splitter.split_documents([section]))
else:
chunks.append(section)
После разбиения на "чанки" каждая секция преобразуется в векторные эмбеддинги с помощью выбранной модели и сохраняется в Qdrant. Благодаря этому система может «находить» нужный текст, когда пользователь задаёт вопрос. Каждый загруженный документ превращается в коллекцию векторов внутри Qdrant, готовую к поиску и поддержке запросов с учётом контекста.
self.embeddings_model = HuggingFaceEmbeddings(
model_name=EMBEDDING_MODEL, # intfloat/multilingual-e5-large
cache_folder=CACHE_FOLDER)
##
Qdrant.from_documents(
documents=chunks,
embedding=self.embeddings_model,
url=self.qdrant_url,
collection_name=filename)
2.2.2. GET /qdrant/get-documents
Возвращает список всех коллекций документов, которые сейчас хранятся в Qdrant, по сути, вашу текущую библиотеку. Это быстрый способ посмотреть, с чем ИИ-ассистент может работать и откуда брать ответы на вопросы.
2.2.3. DELETE /qdrant/delete-document
Удаляет коллекцию документов по имени, удаляя все её эмбеддинги из Qdrant (иногда нужно убрать устаревшие или тестовые документы из векторной базы).
2.2.4. PUT /ai/update-model
Обновляет языковую модель, которую сервис использует для генерации ответов. Это позволяет выбирать нужный баланс скорости и сложности, а также контролировать, насколько подробными или быстрыми будут ответы.
2.2.5. POST /ai/get-answer
Отправляет запрос (промпт) ИИ-ассистенту, который забирает релевантный контекст из Qdrant и возвращает сгенерированный LLM ответ.
Сервис использует векторные эмбеддинги, чтобы понимать смысл вашего запроса и текста в документах. Каждый запрос преобразуется в высокоразмерный вектор, отражающий его семантику. При этом каждый текстовый "чанк" в Qdrant тоже хранится в виде вектора. Система затем выполняет поиск по схожести, фактически измеряя, насколько близок вектор запроса к векторам документов в пространстве эмбеддингов.
В итоге выбираются только TOP_K наиболее релевантных разделов, которые обрабатываются LLM, чтобы ответ был основан на реальном, контекстно значимом содержании.
async def search_collection(c):
results = await run_in_threadpool(
self.client.search,
collection_name=c.name,
query_vector=query_embedding,
limit=TOP_K * TOP_K_COEF)
for r in results:
payload = r.payload
metadata = payload.get(self.metadata, {})
all_results.append((
r.score,
payload.get(self.page_content, ""),
metadata.get(self.heading, ""),
metadata.get(self.filename, "")))
await asyncio.gather(*(search_collection(c) for c in collections))
# Selecting top documents here
return "\n".join([text for _, text, _ in top_docs])
ИИ-помощник берёт ваш запрос и добавляет к нему найденный контекст из самых подходящих кусочков документов из Qdrant, чтобы сформировать запрос локальной LLM для формирования ответа. Эти кусочки дают модели точную и нужную информацию, так что ей не приходится полагаться только на то, что она «помнит» из обучения. Благодаря этому ИИ может отвечать более точно, подробно и с учётом контекста.
Другими словами, модель не просто угадывает, она «читает» самые релевантные куски ваших документов и использует их, чтобы дать осмысленный ответ. В этом и заключается суть RAG: объединение вашей базы знаний с рассуждениями языковой модели.
self.prompt_template = PromptTemplate(
input_variables=["context", "prompt"],
template=(
"Вы — методолог. Дайте подробный ответ, строго опираясь "
"на предоставленный контекст.\n\n"
"Контекст:\n{context}\n\n"
"Вопрос:\n{prompt}\n\n"
"Если в контексте нет информации для ответа, ответьте: "
"В предоставленных документах нет информации для ответа на этот вопрос."))
self.llm = Llama.from_pretrained(
repo_id=LLM_MODEL, # from 'PUT /ai/update-model'
filename=LLM_FILENAME,
cache_dir=CACHE_FOLDER)
##
async def get_answer(self,
qdrant_service: QdrantService,
prompt: str) -> str:
context = await qdrant_service.get_context(prompt)
# LLM answer generation here
return str(raw_answer)
2.3. Фронтэнд
Созданный на React фронтенд — это «лицо» приложение, обеспечивающее плавный и интерактивный пользовательский опыт. React был выбран из-за своей скорости и компонентной архитектуры, что делает его идеальным для создания динамичных интерфейсов, которые мгновенно обновляются, когда ИИ возвращает ответы или загружаются документы. Он также предлагает богатую экосистему библиотек, повторно используемых UI-компонентов и простую интеграцию с бэкендом на FastAPI через REST API. Фронтенд отвечает за основные взаимодействия с пользователем: отправку запросов, отображение ответов ИИ, загрузку документов, переключение языковых моделей и управление базой знаний.
2.3.1. Компоненты
Фронтенд включает пять основных компонентов, каждый из которых отвечает за конкретную часть пользовательского интерфейса:
ChatPanel.jsx
Dashboard.jsx (container for all other components)
DocumentList.jsx
ModelSelector.jsx
UploadForm.jsx
2.3.2. Обработка Markdown
Для отображения ответов от LLM в удобочитаемом виде фронтенд использует рендеринг Markdown. Это позволяет модели возвращать текст с переносами строк, форматированием и даже простыми таблицами или списками, которые пользователь видит именно так, как задумано.
Для этого используются два основных пакета:
react-markdown: отображает содержимое Markdown как React-компоненты;
remark-breaks: сохраняет переносы строк без необходимости добавлять двойные пробелы.
import ReactMarkdown from "react-markdown";
import remarkBreaks from "remark-breaks";
//
<div className="border p-3 rounded bg-gray-50 text-xl whitespace-pre-wrap">
<ReactMarkdown remarkPlugins={[remarkBreaks]}>
{safeResponse}
</ReactMarkdown>
</div>
3. Обработка метаданных документов
Чтобы сделать процесс поиска информации удобнее, система разбивает каждый загруженный документ на разделы по заголовкам. Заголовки определяются автоматически с помощью набора регулярных выражений, которые умеют распознавать разные форматы: нумерованные списки, заголовки прописными буквами, Markdown-заголовки и даже римские цифры.
Такой подход позволяет разбивать большие документы (отчёты, инструкции или научные статьи) на осмысленные части, сохраняя их структуру и контекст для последующего поиска.
3.1. Извлечение заголовков
Метод extract_headings() просматривает текст построчно и проверяет, соответствует ли каждая строка одному из нескольких шаблонов заголовков. В качестве заголовков разделов считаются только те строки, которые достаточно короткие (меньше max_words_heading) и подходят под один из заранее определённых типов заголовков.
def extract_headings(self,
text: str) -> List[str]:
headings = []
for line in text.splitlines():
line = line.strip()
if not line:
continue
words = line.split()
if len(words) > self.max_words_heading:
continue
if (self.numbered_heading.match(line) or \
self.uppercase_heading.match(line) or \
self.markdown_heading.match(line) or \
self.roman_heading.match(line)):
headings.append(line)
return headings
3.2. Формирование секций
Как только заголовки определены, метод get_sections() разбивает текст документа на части, каждая из которых связана с конкретным заголовком. Если заголовки не найдены, весь текст сохраняется как один раздел.
Каждый раздел содержит метаданные, такие как исходное имя файла и путь заголовков (последовательность заголовков от верхнего уровня до текущего раздела), которые при поиске помогают показать, откуда именно взят тот или иной ответ.
def get_sections(self,
text: str,
headings: List[str],
filename: str) -> List[Document]:
sections = []
if not headings:
cleaned_text = self.remove_page_numbers(text.strip())
sections.append(Document(
page_content=cleaned_text,
metadata={self.heading: self.full_text,
self.filename: filename}))
return sections
# Heading-based sectioning here
sections.append(Document(
page_content=content,
metadata={self.heading: accumulated_heading,
self.filename: filename}))
return sections
3.3. Пример промпта (тест метаданных)
Например, мы знаем заголовок и наименование файла, по которым хотим получить информацию. Рассмотрим этот фрагмент из файла ии_на_службе_бизнеса.pdf:

Давайте взглянем на логи:
INFO: MATCH:
5.2 бизнес и производство 5.2.2 оптимизация цепочек поставок и
операционный менеджмент
INFO: PROMPT:
О чем рассказывает 5.2.2 оптимизация цепочек поставок
в ии_на_службе_бизнеса.pdf?
INFO: CONTEXT (truncated):
5.2.2 Оптимизация цепочек поставок и операционный менеджмент
ИИ-агенты революционизируют управление цепочками поставок,
обеспечивая эффективное прогнозирование, контроль запасов и
оптимизацию логистики. Сложный и динамичный характер современных
цепочек поставок делает их идеальной областью для применения ИИ,
поскольку такие системы способны обрабатывать огромные объемы данных,
выявлять закономерности, предсказывать сбои и предлагать корректирующие
действия [74]. Применение предиктивной аналитики позволяет компаниям с
высокой точностью прогнозировать сп...
INFO: 172.18.0.1:51408 - "POST /api/ai/get-answer HTTP/1.1" 200 OK
Теперь давайте посмотрим на ответ нашего ИИ-ассистента:
Раздел 5.2.2 описывает, как ИИ-агенты трансформируют управление цепочками поставок за счёт следующих аспектов:
- Сквозная оптимизация:
Системы ИИ обрабатывают большие, сложные и динамичные данные цепочек поставок, чтобы выявлять закономерности, предсказывать сбои и предлагать корректирующие действия [74].
- Предиктивное прогнозирование спроса:
Аналитика на основе прогнозов повышает точность оценки спроса, снижая перепроизводство и дефицит товаров, что приводит к значительной экономии затрат.
- Оптимизация логистики и маршрутов:
Алгоритмы учитывают такие факторы, как трафик, погодные условия и сроки доставки, чтобы вычислять наиболее эффективные маршруты. Это снижает расход топлива и время доставки, а также способствует достижению целей устойчивого развития [75][76].
- Автоматизация складов:
Автономные роботы, использующие машинное обучение и технологии компьютерного зрения, выполняют задачи сортировки, упаковки и инвентаризации, сокращая количество ошибок, повышая производительность и улучшая общую операционную эффективность [77].В целом, ИИ-агенты рассматриваются как незаменимый инструмент для прогнозирования, контроля запасов, оптимизации логистики и автоматизации складских операций в современных цепочках поставок.
Также стоит отметить, что совпадения и по заголовку, и по имени файла имеют более высокий приоритет, чем просто совпадения по заголовку без наименования файла.
4. Сложности и возможные улучшения
Создавать RAG-сервис, который объединяет FastAPI, Qdrant и React в единую рабочую цепочку, было одновременно интересно и непросто. По ходу работы возникали идеи, которые расширяли пространство для будущих улучшений.
Работа с разными форматами файлов
Поддержка разных форматов вроде.txt,.docx,.xlsxи.pdf(в том числе PDF с изображениями) оказалась сложнее, чем ожидалось. Каждому формату нужна была своя стратегия извлечения, чтобы текст для эмбеддингов был консистентным. OCR на tesseract иногда давал шум из-за качества сканов.
Потенциальные улучшения: использование гибридного подхода OCR-LLM для более чистого извлечения текста.Разбитие на "чанки" и извлечение контекста
Подобор подходящих размеров "чанков" оказался одной из самых сложных задач. Слишком маленькие — контекст распадается; слишком большие — эмбеддинги теряют точность. Сейчас система использует разбиение по заголовкам, что хорошо работает для структурированных отчётов.
Потенциальные улучшения: добавление семантического или динамического разбиения, которое подстраивается под структуру документа и плотность содержимого.Пользовательский интерфейс и ответы
Фронтенд на React сделал взаимодействие удобным и понятным, но больше обратной связи в реальном времени могло бы улучшить опыт пользователя. Например, потоковая подача частичных ответов ИИ или отображение источников при поиске.
Потенциальные улучшения: добавление подсветки цитат и потоковой подачи ответов, чтобы процесс поиска и формирования ответа был более прозрачным.
5. Примечание о конфиденциальности данных
Стоит иметь в виду, что, когда вы отправляете фрагменты документов во внешний ИИ-сервис (например, ChatGPT или другие облачные модели), вы делитесь их содержимым с третьей стороной. То есть любая конфиденциальная или чувствительная информация из файлов может потенциально «выйти» за пределы вашей локальной среды.
Прежде чем загружать или запрашивать что-то из частных документов, убедитесь, что понимаете риски для конфиденциальности. Подумайте, стоит ли анонимизировать данные, отфильтровать их или же просто как в нашем случае обрабатывать их на локальной модели, которую, кстати, можно и дообучить под свои нужды, и обращаться к которой можно даже без сети Интернет. Всегда осторожно обращайтесь с секретной информацией: облачные ИИ могут сильно помочь, но сами по себе — это небезопасные места для хранения данных.
Заключение
То, что начиналось как простая идея разобраться с растущей кучей документов, превратилось в полностью рабочее приложение на основе Retrieval-Augmented Generation (RAG). В процессе я понял, что RAG — это не просто набор компонентов, а способ связать знания, структуру и интеллект. Метаданные, разбиение на фрагменты и эмбеддинги звучат пугающе и технически, но вместе они составляют основу инструмента, который реально помогает людям в работе.
Впереди ещё куча возможностей для улучшений: более умное разбиение на фрагменты, потоковые ответы, более продуманный алгоритм ранжирования и новые интеграции моделей. Но даже в нынешнем виде проект ясно показывает одно: с правильной архитектурой ИИ-агент может полностью изменить то, как мы работаем с собственной информацией.
Контакты
Telegram: @yelis_txt
Email: arselidex@yandex.ru
Комментарии (4)

NeoCode
21.10.2025 06:29Интересно а имеет смысл RAG без ИИ (ну нет у меня дорогой современной видеокарты)? Просто для поиска. Или в этом случае проще написать обычный текстовый поиск чуть поумнее - скажем поиск слов, заданных в строке поиска через пробел, и сортировка найденного по частоте вхождений?

AZverg
21.10.2025 06:29ну нет у меня дорогой современной видеокарты
Правильно говорить не "нет у меня дорогой современной видеокарты", а "кластер с видеокартами на текущий момент не предусмотрен в инфраструктуре заказчика". И тогда приводится обоснование закупки и либо кластер покупается/арендуется либо не покупается.
Для себя для использования моделей можно взять что то из старенького с рук типа 3090 на 24gb, цены более чем приемлемы. Большинство моделей запускаются и достаточно шустро. Для обучения моделей чтобы поднять свои навыки тоже хватит.
Для обучения моделей которые пойдут в прод все равно (в начале пути или бюджеты на железо до 10 млн.р) выгоднее использовать облака. При обучении моделей для бота на сайте или базы знаний первой линии поддержки на арендованных мощностях некоторые вкладываются в 5-10 тр за итерацию. Обычно хватает 2-3 итерации, и потом будет по одной итерации на дообучение обычно раз в пару месяцев. И тут не последним будет вопрос можно ли на облака передать наборы данных для обучения.
Поиск при использовании RAG у нас пока по качеству сильно проигрывает Elasticsearch. Но если цель генерировать ответы или скрипты для общения с пользователями, то большого выбора уже нет. Схема когда генерим ответы и потом их вычитывают технологи/аналитики показала себя в разы производительнее чем было до этого.
beswalod
Только на работе возникла подобная идея - и тут же статья на Хабре. Спасибо!