За 8 дней частичной занятости я собрал RAG-систему на NestJS + PostgreSQL (pgvector), которая обрабатывает ~11 000 чанков документов.
Первая версия отвечала около 4 минут, после оптимизации - 40–60 секунд.
Главный вывод: RAG - это не «векторный поиск + LLM», а в первую очередь подготовка данных, фильтрация контекста и аккуратная работа с промптами.


Зачем я это делал

Главной целью проекта было создать RAG-систему, которая могла бы отвечать на вопросы на основе моих знаний и опыта, это позволило понять реальную работу с большим количеством документов.

RAG-система была интегрирована с моим сайтом-визиткой site15.ru. Там я показываю и описываю часть проектов за последние 10 лет: скачивания, звёзды, просмотры, npm-библиотеки, счётчики групп, посты и карму на Habr и dev.to.

Технически это реализовано так: фронтенд site15.ru → backend site15.ru → сервер rag-system. Backend передаёт специальный API-ключ, что хотя бы частично защищает сайт от лишних запросов.

Форма ввода API-ключа на фронтенде админки, демонстрирующая защиту доступа
Форма ввода API-ключа на фронтенде админки, демонстрирующая защиту доступа

Таким образом, site15.ru выступает как демо и интерфейс для взаимодействия с RAG.

Фронтенд чата на site15.ru, пример запроса к RAG и ответа
Фронтенд чата на site15.ru, пример запроса к RAG и ответа

Почему RAG оказался сложнее, чем казалось

На старте проект выглядел просто:

RAG = векторный поиск + LLM

На практике оказалось, что большая часть времени уходит на:

  • подготовку и сегментацию данных,

  • фильтрацию контекста,

  • формирование и согласование промптов.

Первая версия системы делала всё последовательно - и отвечала около 4 минут даже на простой вопрос.


Архитектура системы

Источники данных (Telegram переписки, статьи, портфолио, резюме)
            ↓
Backend (NestJS)
 ├─ LLM-модуль
 ├─ PostgreSQL + pgvector
 ├─ RxJS асинхронный pipeline
 ├─ Dialog Manager
 └─ API контроллер для site15.ru
            ↓
RAG-компоненты
 ├─ Question Transformer
 ├─ Фильтрация документов и секций
 ├─ Векторный поиск
 └─ Формирование промпта
            ↓
LLM-провайдеры
(OpenAI / Groq / Ollama)
Swagger документация от backend, демонстрирующая API для RAG
Swagger документация от backend, демонстрирующая API для RAG

Выбор стека

Backend - NestJS
Я постоянно пишу бэкенды на NestJS, поэтому выбор очевиден.

Main файл nestjs
Main файл nestjs

Frontend - React Admin
Нужна была админка, чтобы управлять данными и промптами.

Cписок embedding документов
Cписок embedding документов
Embedding документ
Embedding документ

PostgreSQL + pgvector
Одна система для обычных данных и векторов - проще и надежнее, чем раздельные хранилища.

Таблицы в базе данных
Таблицы в базе данных

Несколько LLM-провайдеров
Поддержка разных провайдеров позволяет использовать бесплатные лимиты и легко переключаться при необходимости.

Список нейронных сетей
Список нейронных сетей

Ключевое архитектурное решение: иерархическая фильтрация

Главная идея - не отправлять в LLM всё подряд.
Pipeline обработки запроса:

Запрос пользователя (frontend site15.ru)
  ↓
Backend site15.ru
  ↓
RAG-сервер: нормализация вопроса
  ↓
Фильтрация по метаданным      (11 000 → ~500)
  ↓
Фильтрация по секциям/заголовкам (500 → ~200)
  ↓
Векторный поиск                (200 → 5–10)
  ↓
Один оптимизированный запрос в LLM
Визуализация pipeline запросов/ответов в админке или диаграмма архитектуры
Визуализация pipeline запросов/ответов в админке или диаграмма архитектуры

Так удалось значительно сократить объём данных, отправляемых в LLM, и ускорить ответы.


Самая большая техническая проблема

Создание метаданных для 11 005 чанков документов.
Так как я хотел, чтобы всё работало, используя только бесплатные лимиты LLM, я не мог создавать метаданные в облачных LLM — там я быстро упирался в ограничения, поэтому пришлось запускать локально через LM Studio с моделью qwen2.5-7b-instruct и обработка заняла 2 дня на RTX 2060 SUPER.

LM Studio
LM Studio

Почему первая версия была медленной

Первая версия: 8 промежуточных промптов, последовательные вызовы LLM → ~4 минуты на ответ.

После оптимизации: 4 согласованных промпта, часть этапов параллельна, асинхронная очередь на RxJS → 40–60 секунд.


Промпты и ошибки

Промпты оказались самым сложным моментом. Примеры:

Утечка контекста

Пользователь: "Расскажи про NestJS"
LLM: "NestJS - отличный фреймворк. Кстати, у меня есть телеграм-бот для кофе..."

Конфликт инструкций

Промпт 1: отвечай кратко
Промпт 2: приводи подробные примеры

Вывод: меньше, но согласованных промптов - лучше.


Использование ИИ при разработке

Около 70% кода писалось с помощью ИИ-ассистентов, но без моей архитектурной правки и отладки он не работал бы.

Код сгенерированный AI
Код сгенерированный AI

Безопасность

  • проверка разрешённого IP,

  • проверка API-ключа для запросов с backend site15.ru.

Проект не production-ready, но это позволяет хоть как-то защитить основной сайт от лишних запросов.

AuthGuard - проверка IP / API-ключа в админке
AuthGuard - проверка IP / API-ключа в админке

Развёртывание

  • RAG-сервер на отдельном VPS,

  • docker-compose для PostgreSQL и Ollama,

  • backend через pm2,

  • интеграция с site15.ru.

Docker-compose
Docker-compose

Текущий статус

Экспериментальный проект, не MVP, не production-ready.
Site15.ru выступает как интерфейс для демонстрации работы RAG и статистики проектов.


Планы на будущее

  • Написание тестов пользовательских сценариев (проверка корректности ответов и pipeline).

  • Рефакторинг LLM-модуля в стиле NestJS.

  • Добавление аналитики: время обработки, количество вызовов LLM, расход токенов.

  • Автоматизация деплоя через Docker / Kubernetes.

  • Улучшение точности и скорости через оптимизацию фильтрации контекста.


Выводы

RAG - это сложный инженерный процесс: подготовка данных, фильтрация контекста и аккуратная работа с промптами важнее, чем сама LLM.

За 8 дней частичной занятости удалось собрать рабочую систему, интегрировать её с site15.ru и получить реальный опыт работы с RAG.


Ссылки

https://github.com/site15/rag-system - Репозиторий проекта
https://site15.ru - Мой сайт визитка с чатом поддержки

Комментарии (13)


  1. confident_action
    26.01.2026 17:05

    Запрос до сих пор в обработке.

    Толку от всех этапов нет, если одно слово все впечатление ломает, нехватает на входе. быстрого валидатора


    1. kaufmanendy Автор
      26.01.2026 17:05

      да, спасибо за комментарий, вижу что там все бесплатные запросы кончились у всех активных провайдеров, поэтому оно так и повисло, ща вот пишу заглушку чтобы оно хотя-бы уведомляло об этом в обратку


    1. kaufmanendy Автор
      26.01.2026 17:05

      Добавил различные промежуточные тексты, чтобы было понятнее что происходит и вывожу провайдера и модель которая используется


  1. KonstantinTokar
    26.01.2026 17:05

    11000 чанков это сколько в байтах?


    1. kaufmanendy Автор
      26.01.2026 17:05

      вот эти файлы на куски разбиты https://github.com/site15/rag-system/tree/main/sources

      файлов: 450, размер общий: 16.5 MiB (17,311,147 bytes)

      куски разной длины


  1. obir
    26.01.2026 17:05

    Хороший проект, но у меня думает, к сожалению, 2-3 минуты на вопрос. Я RAG собирал на векторах из PDF-ов ~50 Mb на llama 3.2 и он был очень быстр


    1. kaufmanendy Автор
      26.01.2026 17:05

      Тут я больше изучал как вообще можно строить эти RAG системы и какие компоненты там могут быть

      Я добавил там на сайте возможность посмотреть какие именно промпты запускались и там теперь видно почему долго

      Запускается ведь не один промпт, а много, для того чтобы из 11000 чанков сделать 5 и уже в 5 искать нужные данные, все зависит еще и от самого вопроса, чем он ближе к данным которые в базе тем быстрее отработают все логики

      Так как я использую бесплатные лимиты, я стараюсь не делать параллельных запросов - у некоторых провайдеров на это тоже стоят ограничения. Но если делать production-ready RAG-систему и купить платный доступ к нейросетям или развернуть их на собственном железе, то многие задачи можно распараллелить и существенно ускорить поиск ответа.


  1. Gigantazavr
    26.01.2026 17:05

    Автор, рекомендую погрузиться в сложный мир обработки данных, и, вместо 8 промтов нейросетям, использовать собственно-написанные parser, chunker, и далее - embeddings и retrieval. Это позволит вам сократить скорость с 40 секунд на ответ до 2 секунд.

    Разобраться в этом стоит, потому что тогда промт для вас станет вишенкой на торте, а не сложнейшим этапом.


    1. kaufmanendy Автор
      26.01.2026 17:05

      Там уже это все есть, в итоге после фильтрации из 11000 записей емебедниг чанков по входному запросу получаем 5 чанков с контекстом и есть входной запрос от пользователя

      Запрос пользователя нужно привести к нужному нам формату - это делается через дополнительные запросы к нейронке

      Потом этот "нормализованный" запрос + история переписки прогоняем для каждого чанка и используем специальный промпт который определяет есть ли ответ на заданный вопрос в текущем контексте

      После прогона по всем чанкам и нахождения ответа мы формируем ответ пользователю тоже через специальный промпт который дополнительно трансформирует ответ используя историю переписки

      На вход приходят разные виды документов: переписки из чата, посты из хабра, резюме с хедхантер, спарсенный сайт визитки

      Документ можно разделить на разные категории, документ бьется на чанки и у каждого чанка своя категория, запрос от пользователя имеет свою категорию, история может иметь другую отличную от последнего запроса категорию

      В зависимости от категорий мы используем разные промпты при трансформации входных, промежуточных и выходных данных

      export enum Category {
        telegram = 'telegram',
        /**
         *  рекламные, массовые или нерелевантные сообщения;
         */
        spam = 'spam',
        /**
         * предложения о работе на полный рабочий день (штат, фултайм, постоянная занятость);
         */
        job = 'job',
        /**
         * вопросы о найме, формировании или усилении команды;
         */
        hiring = 'hiring',
        /**
         * вопросы о собеседованиях, подготовке к ним, оценке кандидатов;
         */
        interview = 'interview',
        ...
      }


      1. Gigantazavr
        26.01.2026 17:05

        Рекомендую посмотреть, что такое cosine similarity - она решит вашу проблему. Вы можете заменить ваши промты одним надежным алгоритмом.


        1. kaufmanendy Автор
          26.01.2026 17:05

          Я глубоко не копал RAG, просто выделил неделю на изучение и не больше, и поделился тем что узнал и сделал с 0 знаниями

          Сейчас есть база с векторными колонками, и я ищу совпадения вектора входного поискового запроса с векторами чанков а также с специальной графовой версий чанка, там под капотом тоже происходит поиск синусом

          И после фильтрации я и получаю вместо 11000 чанков 5


          1. Gigantazavr
            26.01.2026 17:05

            Вы пытаетесь top 5 чанков отфильтровать через 5 запросов к нейронке, находя "один самый точный". - это не та стратегия для RAG.

            Вы можете либо сразу закинуть эти 5 чанков (найденных путем ретривера) в основную модель, либо, если хотите, взять одну модель реранкера - она сравнит векторы и семантику, и отправит самый точный вариант.

            LLM, даже локальная на 8B параметров, хорошо может из 5 вариантов выбрать 1 верный - не стоит недооценивать ее возможности


            1. kaufmanendy Автор
              26.01.2026 17:05

              Спасибо, в следующий раз когда когда вернусь к этой задаче попробую все чанки в один запрос положить