
Всем привет!
Предисловие
В этой статье я хочу поделиться своим опытом создания приложения на базе 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.jsx2.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 headings3.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 sections3.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
