Главная
remove check_box_outline_blank close

Опыт создания AI-консультанта (Часть-2)

2026-02-12

Опыт создания AI-консультанта. Часть 2: RAG на «минималках»

В первой части я рассказывал, как прошел путь от попыток дообучения (Fine-tuning) модели Qwen, которые закончились ответами по 90 секунд, к архитектуре RAG (Retrieval-Augmented Generation). Мы научились парсить данные и собирать «цифровой паспорт» товара.

Но собрать данные — это только половина дела. В этой части я расскажу, как научить модель понимать сленг клиентов, и главное — как задеплоить современный AI-стек на VDS с 1 ядром CPU и 1 ГБ оперативной памяти, не потеряв рассудок.


Глава 4: Как научить AI понимать «человеческий» язык

Просто загрузить данные в векторную базу недостаточно. Клиенты не говорят языком технической документации. Они пишут «движок» вместо «двигатель» и «уазик» вместо «УАЗ». Чтобы поиск работал точно, мне пришлось внедрить слой нормализации запросов.

1. Словарь синонимов и «Слово к слову»

Я создал систему маппинга (сопоставления), которую в шутку назвал «слова к слову». Это словари, которые транслируют народные названия в официальные бренды и категории.

Вот как мы учим систему понимать, что Газель — это ГАЗ, а патрубок — это то же самое, что рукав:

# Приводим бренды к единому стандарту
self.brand_mapping = {
    'газ': 'ГАЗ', 'уаз': 'УАЗ', 'волга': 'ГАЗ',
    'газель': 'ГАЗ', 'соболь': 'ГАЗ',
    'ваз': 'ВАЗ', 'lada': 'ВАЗ', 'жигули': 'ВАЗ',
    'камаз': 'КАМАЗ', 'маз': 'МАЗ',
    'ямз': 'ЯМЗ'
}

# Расширенная база синонимов для поиска
self.synonyms = {
    'двигатель': ['мотор', 'движок', 'engine', 'motor'],
    'прокладка': ['уплотнение', 'герметик', 'шайба'],
    'кольцо': ['уплотнитель', 'сальник', 'o-ring'],
    'силикон': ['силиконовый', 'резиновый', 'резина', 'silicone'],
    'охлаждения': ['охлаждение', 'охлождения', 'cooling', 'тосол', 'антифриз']
}

Благодаря этому, когда пользователь пишет "нужен патрубок силикон на газель", система ищет "Рукав/Патрубок материал:Силикон бренд:ГАЗ".

2. Системный промпт: «Ты — менеджер, а не философ»

Чтобы модель не начинала сочинять небылицы, мы жестко ограничили её поведение системным промптом. Мы прописали роль: менеджер оптовых продаж. Если товара нет в найденном контексте, модель честно предлагает связаться с менеджером, а не выдумывает несуществующие запчасти.

Вот как выглядит финальный запрос к LLM, объединяющий все части:

{
  "messages": [
    {
      "role": "system",
      "content": "Ты - профессиональный консультант компании 'ВФ-Авто'. Твоя роль: 1. Помогать клиентам подбирать запчасти. 2. Уточнять параметры (марку, двигатель). 3. Информировать, что продажа ТОЛЬКО ОПТОМ. 4. Если не уверен - направлять в отдел продаж. Запрещено выдумывать несуществующие товары."
    },
    {
      "role": "user",
      "content": "какой лучше герметик для камаз?"
    },
    {
      "role": "assistant",
      "content": "Контекст из базы данных: Товар: Кольцо упл. системы охлаждения ( 172 ) | Описание: Уплотнительное кольцо с артикулом 7405-1303172— это уплотнительный элемент для соединений в системе охлаждения двигателей КАМАЗ. | Характеристики: Артикул: 7405-1303172; Тип: кольцо уплотнительное системы охлаждения; Материал: Силиконовый компаунд (термостойкий эластомер)"
    }
  ]
}

Глава 5: Деплой. Битва за ресурсы

Когда локальные тесты прошли успешно, я перешел к деплою. В моем распоряжении был VDS сервер с очень скромными характеристиками:

  • CPU: 1 ядро
  • RAM: 1 ГБ
  • SSD: ~20 ГБ

Попытка запустить там Docker + Qdrant + FastAPI + Sentence-Transformers превратилась в триллер.

Проблема №1: OOM Killer (Out-Of-Memory)

Как только приложение пыталось загрузить модель эмбеддингов в память, процесс падал с ошибкой Killed. Операционной системе просто не хватало места, и ядро Linux убивало самый «прожорливый» процесс.

Решение: SWAP-файл. Если нет RAM, используем диск. Я создал файл подкачки на 2 ГБ. Это медленная память, но она спасает приложение от краша при пиковых нагрузках.

# Создаем SWAP-файл на 2 ГБ
sudo fallocate -l 2G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile
# Делаем его постоянным
echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab

Проблема №2: Нехватка места на диске при сборке Docker

Стандартная установка PyTorch через pip install torch тянет за собой поддержку CUDA (видеокарт NVIDIA), что весит около 1.5–2 ГБ. На моем сервере это забивало всё свободное место и вызывало ошибку no space left on device.

Решение: CPU-only PyTorch и .dockerignore.

  1. Я настроил .dockerignore, чтобы не копировать в контейнер лишние папки (venv, __pycache__).
  2. В requirements.txt я явно указал ссылку на легкую версию PyTorch только для процессора.
--extra-index-url https://download.pytorch.org/whl/cpu
torch
sentence-transformers
qdrant-client
fastapi

Это сэкономило более гигабайта места и ускорило сборку.

Проблема №3: Таймауты при старте («Ленивая» загрузка)

Тяжелая RAG-система инициализировалась в момент старта сервера (startup event). Это занимало 40–90 секунд. Сервер uvicorn уже принимал соединения, но не мог ответить, и Docker считал, что контейнер завис, перезагружая его.

Решение: Lazy Loading. Я убрал загрузку модели из старта приложения. Теперь инициализация происходит только при первом запросе пользователя.

  • Health check проходит мгновенно.
  • Первый клиент ждет чуть дольше (пока модель грузится в кэш).
  • Все последующие запросы обрабатываются быстро.

Итог второй части

Мы прошли путь от «сырых» данных до работающего API на дешевом сервере.

  • Цена решения: Копейки за VDS.
  • Скорость: Благодаря RAG и маленьким моделям ответ приходит быстро, а не за 90 секунд, как при Fine-tuning.
  • Точность: Синонимы и строгий промпт не дают модели галлюцинировать.

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

arrow_back Назад к списку заметок
home integration_instructions description security design_services person contacts
dark_mode