Собственная облачная LLM на 16 ГБ VRAM — часть 1 - Академия Selectel

Собственная облачная LLM на 16 ГБ VRAM — часть 1

Тирекс
Тирекс Самый зубастый автор
5 марта 2026

В статье покажем, как развернуть собственную облачную LLM, которая укладывается в 16 ГБ видеопамяти, поддерживает инструменты и вызов функций, интегрируется с MCP-серверами и может использоваться как полноценный API-сервис для бэкенд-задач. 

Изображение записи

На фоне ажиотажа вокруг нейросетей все чаще встает вполне приземленный вопрос — сколько стоит содержать собственную LLM.

Современные ИИ-агенты уровня Claude, ChatGPT и DeepSeek уже давно перестали быть «чатами для развлечения». Это сложные системы, которые перед тем как выдать ответ, тратят десятки тысяч токенов на внутренние рассуждения, вызывают внешние функции, взаимодействуют с MCP-серверами и даже работают напрямую с интерфейсом ОС.

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

Статья подготовлена автором Telegram-канала «Легкий путь в Python» Алексеем Яковенко.

Как сократить расходы на использование LLM, не потеряв в качестве 

Самый очевидный ответ — развернуть собственную нейросеть на сервере и использовать ее как облачный API.

Однако здесь нас ждет стоп-фактор. Большинство мощных open-source языковых моделей требуют десятки гигабайт видеопамяти. 24, 48, а иногда и 80 ГБ VRAM — а это уже совсем другой бюджет на инфраструктуру, который подходит далеко не всем. Вся архитектура будет максимально прагматичной и ориентированной на продакшен.

Из каких компонентов состоит решение

Прежде чем писать команды и поднимать сервер, нужно зафиксировать, что именно мы строим. Важно понимать: речь идет не о запуске «локального чата», а о сборке небольшой, но полноценной инфраструктуры.

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

MCP (Model Context Protocol) — открытый протокол, позволяющий подключать к агенту внешние сервисы как инструменты. Вместо того чтобы писать обертки для каждого API вручную, можно использовать готовые MCP-серверы.

Между моделью и внешним миром расположен инференс-движок — слой, отвечающий за производительность, управление видеопамятью и API. Именно на этом уровне используется vLLM, подробнее о нем чуть ниже.

Для корректного доступа к сервису извне применяется reverse proxy. Он отвечает за SSL, доменное имя и создает задел для будущего масштабирования.

Поверх этого располагается FastAPI-приложение — отдельный бэкенд-слой с прикладной логикой. Здесь сосредоточено управление входящими запросами, обработка вызовов инструментов, интеграция MCP-серверов, а также контроль доступа и ограничений.

Такое разделение позволяет воспринимать LLM как обычный бэкенд-сервис, а не как монолит, в который положили все подряд.

Какой стек будем использовать

Возьмем приземленный и знакомый большинству Python-разработчиков стек: 

  • Linux-сервер с GPU; 
  • инференс-движок для LLM, vLLM; 
  • Nginx, FastAPI как основной бэкенд; 
  • MCP-серверы и инструменты — в качестве внешних или встроенных компонентов.

Никаких специфичных ML-фреймворков или редких зависимостей — все легко поддерживается и масштабируется.

Почему именно vLLM, а не классический запуск через transformers?

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

Если к модели обратятся сразу несколько человек, видеопамять начнет тормозить и вам придется вручную прикручивать API и придумать, как не «уронить» сервер при высокой нагрузке.

vLLM создан как раз для таких задач. Он хорошо держит несколько одновременных запросов, а также API, совместимый с OpenAI-стилем. Он справляется со сложными функциями и позволяет засунуть серьезные модели даже в обычную видеокарту на 16 ГБ VRAM. Проще говоря, vLLM — это инференс-движок, заточенный под продакшен.

Подготовка инфраструктуры

Переходим к практике. На этом этапе арендуем VPS с GPU и сразу привязываем к нему домен. Это позволит дальше не тратить время на настройку DNS.

Даже если вы никогда раньше не работали с VPS и доменами — ничего страшного. Повторяя шаги ниже, вы без проблем справитесь.

Домен 

Чтобы наша система могла работать как полноценный «персональный ИИ-инстанс», нам необходимо открыть возможность отправки внешних запросов к нашему Qwen3.

При этом обращаться напрямую к IP-адресу с портом — не лучшая идея. Вместо этого мы будем использовать доменное имя с HTTPS. Без шифрования ваши промты и API-ключи будут лежать в сети в открытом виде, а современные браузеры и сервисы просто откажутся работать с «небезопасным» IP. Плюс, если сервер переедет, вам не придется переписывать конфиги во всех приложениях.

Шаги по регистрации домена:

Регистрируйте аккаунт в панели управления, если у вас его еще нет. Открывайте вкладку Продукты, далее Домены. Переходите в раздел регистрации доменных имен и нажимайте кнопку Зарегистрировать домен.

Скрин из ПУ с регистрацией домена.

Проверьте доступность доменного имени, если имя свободно — нажимайте Оформить.

Скрин из ПУ с регистрацией домена.

Заполните контактные данные администратора домена и завершите регистрацию. После этого доменное имя переходит в ваше распоряжение.

Скрин из ПУ с регистрацией домена.

Далее переходите в раздел Доменные зоны и нажимайте Добавить зону.

Скрин из ПУ с регистрацией домена.

Ожидайте, пока домен получит статус «Делегирован». Изменения вносятся в реестр за один рабочий час, но для полной работы DNS-серверов может потребоваться до 24 часов.

Скрин из ПУ с регистрацией домена.

Этот статус означает, что мы можем полноценно управлять DNS-записями домена.

На этом этапе этих шагов более чем достаточно. К доменному имени мы еще вернемся позже — когда будем настраивать Nginx на VPS-сервере и привязывать домен к нашему vLLM-проекту, превращая его в полноценную точку входа для удаленных запросов.

VPS сервер с GPU 16 ГБ

В рамках статьи я сознательно буду показывать запуск проекта на бюджетной конфигурации с GPU 16 ГБ. Этого более чем достаточно для демонстрации, тестирования и личного использования.

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

Сейчас нам нужно арендовать VPS-сервер с GPU, получить к нему доступ и подготовить его к дальнейшей настройке.

Я остановился на следующем варианте конфигурации облачного сервера:

  • процессор — четыре виртуальных ядра;
  • оперативная память — 32 ГБ;
  • накопитель — 100 ГБ SSD;
  • графический ускоритель — одна видеокарта NVIDIA Tesla T4 с 16 ГБ видеопамяти.

Это сбалансированная и доступная конфигурация, которая отлично подходит для запуска Qwen3-14B в квантованном виде и работы через vLLM.

Как по шагам создать свой VPS

Переходим в в панель управления, открываем раздел Инфраструктура Облачные серверы.

Скрин из как создать свой VPS.

Кликаем на Создать сервер.

Скрин из как создать свой VPS.

В качестве операционной системы выбираем Ubuntu 24.04. Она лучше всего адаптирована под работу с GPU, драйверами NVIDIA и современным ML-стеком (и идет с установленными драйверами на видеокарту).

Скрин из как создать свой VPS.

Выбираем конфигурацию. Я возьму для тестов GPU с Tesla T4 (16 ГБ).

Скрин из как создать свой VPS.

Добавим к стандартным 20 ГБ SSD диска еще 80 ГБ, чтобы хватило места для модели, кэша Hugging Face, а также для тяжелых зависимостей vLLM в виртуальном окружении.

Скрин из как создать свой VPS.

Добавим SSH-ключ для входа на сервер без ввода пароля.

Скрин из как создать свой VPS.

Создание SSH-ключа — инструкция для всех ОС

Инструкция для Windows:

Для современных Windows 10/11 лучше всего использовать встроенный клиент:

# Встроенный OpenSSH (Win 10/11)
ssh-keygen -t ed25519 -C "ваш@email.com"

# Или через PowerShell
New-Item -ItemType Directory -Force ~/.ssh
ssh-keygen -t ed25519 -f ~/.ssh/id_ed25519 -N ""

Альтернатива: PuTTYgen (скачать с putty.org) → Generate → Save public/private key.

Инструкция для macOS / Linux:

Здесь все стандартно, используем терминал:

# Самый безопасный (рекомендую)
ssh-keygen -t ed25519 -C "ваш@email.com"

# Или RSA (совместимость со старыми системами)  
ssh-keygen -t rsa -b 4096 -C "ваш@email.com"

Где лежат ключи:

  • публичный (его мы копируем в панель облака): ~/.ssh/id_ed25519.pub;
  • приватный (это ваш пароль, его нельзя никому показывать): ~/.ssh/id_ed25519.

Как быстро скопировать ключ в буфер обмена:

# macOS
pbcopy < ~/.ssh/id_ed25519.pub

# Linux (xclip)
xclip -sel clip < ~/.ssh/id_ed25519.pub

# Windows PowerShell
Get-Content ~/.ssh/id_ed25519.pub | Set-Clipboard

Готово. Теперь вставляем публичный ключ в панель управления облака.

Скрин из ПУ по созданию SSH-ключа.
Скрин работающего сервера из ПУ.

На этом этапе сервер готов к работе. 

Связываем доменное имя с VPS сервером

Если на данном этапе у вас есть доменное имя и активирована доменная зона, то процесс связки доменного имени с сервером займет пару минут.

Переходим в Доменные зоны.

Скрин из ПУ как связать доменное имя с VPS сервером.

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

Скрин из ПУ как связать доменное имя с VPS сервером.

Оставляем имя группы пустым, если хотите, чтоб вызов нейросети шел по домену второго уровня.

Настройка VPS-сервера

Перед запуском LLM-модели необходимо подготовить VPS-сервер: установить базовые зависимости, проверить драйверы NVIDIA и поставить окружение для запуска vLLM. Приведем сервер в рабочее состояние.

Подключение к серверу

Ввод пароля не требуется, так как мы на предыдущем этапе уже связали свою локальную машину с VPS сервером через публичный ключ.

Скрин консоли с настройкой VPS-сервера.

Обновление системы и установка базовых зависимостей

Для начала обновим пакеты и установим необходимый минимум:

sudo apt update && sudo apt install -y \
    nginx \
    certbot \
    python3 \
    python3-pip \
    python3-venv \
    python3-certbot-nginx
Скрин консоли с настройкой VPS-сервера.

Что здесь важно:

  • Python 3 + venv — основа для запуска vLLM;
  • Nginx и certbot понадобятся позже для HTTPS и проксирования.

Мы закладываем инфраструктуру сразу, без необходимости возвращаться к этому этапу.

Драйверы NVIDIA

Если вы выбирали выделенный сервер с Ubuntu 24 в панели управления Selectel, то необходимости в отдельной установке драйверов на видеокарту у вас не будет. 

Убедимся, что драйверы установлены, командой: nvidia-smi.

Если команда выводит информацию о Tesla T4 — значит, драйвер установлен и видеокарта готова к работе.

Скрин консоли с настройкой VPS-сервера.

Дополнительно установим CUDA Toolkit из репозитория Ubuntu/Debian. Нам это нужно для компиляции CUDA-кода, работы с GPU и выполнения операций на видеокарте.

apt install -y nvidia-cuda-toolkit

Подготовка проекта и виртуального окружения

Создадим рабочую директорию под наш проект и разместим ее в домашнем каталоге:


      cd ../home
mkdir vllm_qwen3
cd vllm_qwen3

Дальше создаем виртуальное окружение Python и активируем его:


      python3 -m venv .venv
source .venv/bin/activate

Использование виртуального окружения позволяет:

  • изолировать зависимости проекта;
  • избежать конфликтов с системными пакетами;
  • упростить дальнейшее сопровождение сервиса.

Устанавливаем инференс-движок vLLM: pip install vllm.

Скрин консоли с подготовкой проекта и виртуального окружения.

Запуск Qwen3 через vLLM на Tesla T4 

Мы запускаем Qwen3-14B-AWQ на доступной Tesla T4 (16 ГБ VRAM) с 4-битной квантовкой AWQ и аккуратно подобранными лимитами памяти и контекста.

Цель — получить продакшн-совместимый OpenAI-API сервер и не прибегнуть к производительным A100/H100.

Генерация API-ключа

Для защиты API потребуется токен, получим его командой: openssl rand -hex 32.

Токен будет выглядеть примерно так: c8a9f7d2e4b1a6c3f9d8e7b2a4c6f1d3e5b7a9c2f4d6e8b1a3c5f7d9e2b4a6c8

Этот ключ потребуется передавать в заголовке:

Authorization: Bearer ВАШ_КЛЮЧ

Команда запуска


      vllm serve Qwen/Qwen3-14B-AWQ \
  --host 0.0.0.0 \
  --port 8002 \
  --gpu-memory-utilization 0.96 \
  --max-model-len 8000 \
  --max-num-seqs 1 \
  --dtype half \
  --enforce-eager \
  --trust-remote-code \
  --enable-prefix-caching \
  --enable-chunked-prefill \
  --quantization awq \
  --served-model-name qwen3-14b \
  --api-key "ВАШ_КЛЮЧ" \
  --disable-log-requests \
  --enable-auto-tool-choice \
  --tool-call-parser hermes \
  --chat-template-content-format string
Скрин консоли с генерацией API-ключа.

Разбор параметров — что и за что отвечает

Модель Qwen/Qwen3-14B-AWQ

Как я уже говорил, используется уже квантованная версия модели в формате AWQ. На T4 это позволяет:

  • уместить 14B модель в 16 ГБ VRAM;
  • сохранить приемлемое качество;
  • получить хорошую скорость инференса.

Сетевые параметры

  • --host 0.0.0.0 — сервис доступен извне (не только localhost);
  • --port 8002 — порт OpenAI-совместимого API.

Память и производительность

--gpu-memory-utilization 0.96

vLLM может использовать до 96% VRAM. Позволяет максимально эффективно задействовать T4, не доводя до OOM.

--max-model-len 8000

Максимальный контекст — 8 000 токенов.

На T4 это практически верхняя безопасная граница при 4-битной модели. Больше — риск нехватки памяти при длинных диалогах.

--max-num-seqs 1

Одновременно обрабатывается один активный запрос. Это стабилизирует потребление памяти, исключает резкие скачки VRAM и подходит для личного сервера или low-load продакшена.

Если планируется многопользовательская нагрузка — параметр, как и мощность железа, нужно будет пересматривать.

--dtype half

Используется FP16 для вычислений (где это применимо). Оптимальный баланс скорости и стабильности для T4.

--enforce-eager

Принудительный eager-режим. На T4 это часто работает стабильнее, чем полностью компилируемый граф.

Оптимизации скорости

--enable-prefix-caching

Кэширует общий префикс запроса и сильно ускоряет диалоги, повторяющиеся системные промты, агентные сценарии.

--enable-chunked-prefill

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

Безопасность и интеграция

--api-key "..."

Ограничивает доступ к серверу. Без ключа запросы будут отклоняться.

--disable-log-requests

Не логирует тексты запросов в stdout. Важно для приватности и продакшн-развертываний.

Инструменты (Tool Calling)

--enable-auto-tool-choice

Позволяет модели автоматически выбирать инструмент.

--tool-call-parser hermes

Используется Hermes-парсер для корректной обработки tool-calls.

Это важно, если планируется:

  • LangChain;
  • агентные сценарии;
  • вызов внешних функций.

Прочее

--trust-remote-code

Разрешает загрузку кастомного кода модели с HuggingFace. Необходим для корректной работы Qwen.

--served-model-name qwen3-14b

Имя, которое будет отображаться в API. Именно его будут видеть клиенты OpenAI-формата.

--chat-template-content-format string

Формат передачи контента в шаблон чата. Обеспечивает совместимость с OpenAI-стилем сообщений.

Что происходит после запуска

Скачивание модели происходит автоматически (8–9 ГБ, один раз), после чего  ее загрузка в VRAM занимает около 20–40 секунд. Первый запрос может выполняться до 1–2 минут из-за нормального процесса компиляции графа. Однако все последующие ответы будут приходить практически мгновенно.

Не пугайтесь долгого первого старта, это стандартное поведение.

Скрин консоли с скачиванием модели.

Что мы получили:

  • 14B модель;
  • 4-битная квантовка;
  • 8 000 токенов контекста;
  • OpenAI-совместимый API;
  • Полная работа на одной Tesla T4.

Без A100, без 80 ГБ VRAM и без космического бюджета.

Теперь модель доступна по адресу:

http://SERVER_IP:8002

Быстрый тест API

Когда сервер поднялся, проверим, что все работает. Для этого необходимо открыть новый терминал и там выполнить вход на VPS сервер. 

Если мы введем команду nvidia-smi, то увидим, что наша модель заняла практически весь размер видеопамяти:

Скрин консоли.

Выполним простой запрос через curl:


      curl http://localhost:8002/v1/chat/completions \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer z64bf3f7f93601wZez6a621381axqfagzaaa0fd79c589aX2eaaadsr1a0cdd0491a2ea" \
  -d '{
    "model": "qwen3-14b",
    "messages": [
      {
        "role": "user",
        "content": "Привет! Кто тебя создал такого?"
      }
    ],
    "max_tokens": 256
  }'
Скрин консоли.

Сразу видим, что модель из коробки поддерживает режим Thinking. То есть кроме обычного ответа и доп данных, мы можем видеть процесс размышления модели. Бывает очень полезно в некоторых сценариях.

Если вы не хотите тратить время на получение размышлений, то можно легко эти самые размышления отключить. 

Пример запроса:


      curl http://localhost:8002/v1/chat/completions \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer z64bf3f7f93601wZez6a621381axqfagzaaa0fd79c589aX2eaaadsr1a0cdd0491a2ea" \
  -d '{
    "model": "qwen3-14b",
    "messages": [
      {
        "role": "user",
        "content": "Привет! Кто ты?"
      }
    ],
    "max_tokens": 256,
    "chat_template_kwargs": {
      "enable_thinking": false
    }
  }'
Скрин консоли.
Ответ получили значительно быстрее.

Запуск через systemd

Для постоянной работы сервиса запускать его вручную — плохая идея. Поэтому сразу оформим все через systemd.

Создаем сервисный файл командой: sudo nano /etc/systemd/system/vllm-qwen3.service.

Пример конфигурации:


      [Unit]
Description=vLLM Qwen3-14B Server
After=network.target
StartLimitIntervalSec=0

[Service]
Type=simple
User=root
WorkingDirectory=/home/vllm_qwen3
Environment="PATH=/home/vllm_qwen3/.venv/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
Environment="CUDA_VISIBLE_DEVICES=0"
ExecStart=/home/vllm_qwen3/.venv/bin/python -m vllm.entrypoints.openai.api_server \
    --model Qwen/Qwen3-14B-AWQ \
    --host 0.0.0.0 \
    --port 8002 \
    --gpu-memory-utilization 0.96 \
    --max-model-len 8000 \
    --max-num-seqs 1 \
    --dtype half \
    --enforce-eager \
    --trust-remote-code \
    --enable-prefix-caching \
    --enable-chunked-prefill \
    --quantization awq \
    --served-model-name qwen3-14b \
    --api-key ВАШ_ТОКЕН\
    --disable-log-requests \
    --enable-auto-tool-choice \
    --tool-call-parser hermes \
    --chat-template-content-format string
Restart=always
RestartSec=10
StandardOutput=journal
StandardError=journal
SyslogIdentifier=vllm-qwen3

[Install]
WantedBy=multi-user.target

Активируем сервис:


      # Перезагрузить systemd
sudo systemctl daemon-reload
# Включить автозапуск
sudo systemctl enable vllm-qwen3.service
# Запустить сервис
sudo systemctl start vllm-qwen3.service
# Проверить статус
sudo systemctl status vllm-qwen3.service
# Посмотреть логи
sudo journalctl -u vllm-qwen3.service -f
Скрин консоли.

Теперь LLM будет автоматически подниматься вместе с сервером и переживать перезапуски.

Настраиваем Nginx и HTTPS

Превратим наш локальный LLM-сервис в настоящую облачную нейросеть, к которой можно подключаться с любого места — точно так же, как к ChatGPT или DeepSeek.

На данный момент у нас уже должно быть доменное имя, к которому привязана А-запись на домен второго уровня с привязкой к IP-адресу нашего сервера.

Скрин из ПУ, группы записей.

Конфигурация Nginx

Создадим конфигурационный файл:

nano /etc/nginx/sites-available/qwen3-vllm.ru

Содержимое:


      server {
    listen 80;
    server_name ВАШ ДОМЕН (в моем случае qwen3-vllm.ru);
    # Важно для долгих ответов и стриминга
    proxy_read_timeout 600s;
    proxy_connect_timeout 600s;
    proxy_send_timeout 600s;
    # Для больших промтов
    client_max_body_size 50M;
    location / {
        proxy_pass http://127.0.0.1:8002;
        proxy_http_version 1.1;
        # WebSocket для стриминга
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        # Отключаем буферизацию для SSE
        proxy_buffering off;
        proxy_cache off;
    }
}
Активация конфига и получение SSL сертификата
# Активируем сайт
ln -s /etc/nginx/sites-available/qwen3-vllm.ru /etc/nginx/sites-enabled/
rm -f /etc/nginx/sites-enabled/default
# Проверяем конфиг
nginx -t
# Перезапускаем Nginx
systemctl restart nginx
Скрин консоли.

Подключаем HTTPS

Устанавливаем certbot (если еще не установлен):

sudo apt install -y certbot python3-certbot-nginx

Получаем SSL-сертификат

certbot --nginx -d ВАШ ДОМЕН  --non-interactive --agree-tos --email your-email@example.com

Например:

certbot --nginx -d qwen3-vllm.ru --non-interactive --agree-tos --email ivanov_invan@gmail.com

Certbot автоматически включит HTTPS, настроит редирект и добавит автообновление сертификата.

Скрин консоли.

Проверяем, что все работает

Открываем в браузере https://ВАШ_ДОМЕН/docs. В моем случае это: https://qwen3-vllm.ru/docs.

FastAPI с Swagger-документацией.

Так как vLLM использует FastAPI под капотом, вы сразу увидите Swagger-документацию. Это отличный знак — значит сервис полностью доступен извне.

На этом этапе у нас есть полностью рабочая облачная LLM с HTTPS-доступом и API, совместимым с OpenAI. Прежде чем двигаться дальше и добавлять прикладную бизнес-логику, имеет смысл проверить, как эта модель ведет себя в реальных сценариях.

Практика: тестируем LLM как API-сервис

Все примеры ниже выполняются на локальном компьютере. GPU, Docker и сложная инфраструктура не требуются — мы работаем с LLM как с обычным внешним API.

Подготовка локального окружения

Создадим отдельное виртуальное окружение для клиентского кода на своей локальной машине:


      mkdir llm-client
cd llm-client
python3 -m venv .venv
source .venv/bin/activate

Установим минимальный набор зависимостей:

pip install langchain langchain-openai langgraph langchain-mcp-adapters pydantic-settings httpx

Или, если вы клонировали репозиторий: pip install -r requirements.txt

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

Конфигурация

Для подключения к LLM нам нужны три параметра: токен, адрес сервера и название модели. Хранить их в коде — плохая практика, поэтому вынесем все в .env-файл:


      CLIENT_TOKEN=ВАШ_ТОКЕН
CLIENT_BASE_URL=https://qwen3-vllm.ru
LLM_MODEL_NAME=qwen3-14b

А считывать будем через pydantic-settings — это дает валидацию и типизацию из коробки: from pydantic_settings import BaseSettings, SettingsConfigDict.


      class Settings(BaseSettings):
    model_config = SettingsConfigDict(
        env_file=".env",
        env_file_encoding="utf-8",
        case_sensitive=False,
    )
    client_token: str
    client_base_url: str
    llm_model_name: str
settings = Settings()

Если забыть указать любую из переменных — приложение падает при старте с понятной ошибкой валидации, а не где-то посреди работы с None вместо токена.

Пример-1: чат-агент

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


      import asyncio
from langchain_openai import ChatOpenAI
from langchain.agents import create_agent
from config import settings
def create_model(
    temperature: float = 0.7,
    max_tokens: int = 1024,
    enable_thinking: bool = False,
) -> ChatOpenAI:
    base_url = settings.client_base_url.rstrip("/")
    extra_body = None
    if not enable_thinking:
        extra_body = {
            "chat_template_kwargs": {"enable_thinking": False}
        }
    return ChatOpenAI(
        model=settings.llm_model_name,
        api_key=settings.client_token,
        base_url=f"{base_url}/v1",
        temperature=temperature,
        max_completion_tokens=max_tokens,
        extra_body=extra_body,
    )

async def main():
    model = create_model()
    agent = create_agent(
        model=model,
        tools=[],
        system_prompt="Ты полезный ассистент. Отвечай кратко и по делу на русском языке.",
    )

    print("Простой агент запущен. Введите 'exit' для выхода.\n")
    while True:
        user_input = input("Вы: ")
        if user_input.strip().lower() in ("exit", "quit", "выход"):
            break
        result = await agent.ainvoke(
            {"messages": [{"role": "user", "content": user_input}]}
        )

        messages = result.get("messages", [])
        for msg in reversed(messages):
            if msg.type == "ai" and msg.content:
                print(f"\nАссистент: {msg.content}\n")
                break
if __name__ == "__main__":
    asyncio.run(main())

Ключевой момент — ChatOpenAI из LangChain работает с любым OpenAI-совместимым API. Мы просто подставляем base_url нашего vLLM-сервера, и все подключение готово. Никаких специальных клиентов или SDK не нужно.

Параметр extra_body с enable_thinking: False отключает «режим размышления» у моделей, которые его поддерживают (например, QwQ). Для простого чата он не нужен, а ответы приходят быстрее.

Запускаем через команду: python -m example.simple_agent.

Скрин консоли.

Пример-2: агент с инструментами

Простой чат — это хорошо, но настоящая сила агентов в том, что они могут использовать инструменты. Модель сама решает, когда и какой инструмент вызвать, на основе запроса пользователя.

Определим несколько инструментов через декоратор @tool:


      from langchain_core.tools import tool
import httpx
from datetime import datetime, timezone, timedelta

@tool
def get_current_time() -> str:
    """Возвращает текущую дату и время по Москве (UTC+3)."""
    moscow_tz = timezone(timedelta(hours=3))
    now = datetime.now(moscow_tz)
    return now.strftime("%d.%m.%Y %H:%M:%S (МСК)")

@tool
async def get_random_joke() -> str:
    """Возвращает случайный анекдот/шутку на английском языке."""
    async with httpx.AsyncClient(timeout=10) as client:
        resp = await client.get("https://official-joke-api.appspot.com/random_joke")
        resp.raise_for_status()
        data = resp.json()
        return f"{data['setup']}\n— {data['punchline']}"

@tool
async def get_random_quote() -> str:
    """Возвращает случайную вдохновляющую цитату."""
    async with httpx.AsyncClient(timeout=10) as client:
        resp = await client.get("https://zenquotes.io/api/random")
        resp.raise_for_status()
        data = resp.json()[0]
        return f'«{data["q"]}»\n— {data["a"]}'

@tool
async def get_random_fact() -> str:
    """Возвращает случайный интересный факт."""
    async with httpx.AsyncClient(timeout=10) as client:
        resp = await client.get(
            "https://uselessfacts.jsph.pl/api/v2/facts/random?language=en"
        )

        resp.raise_for_status()
        data = resp.json()
        return data["text"]

Обратите внимание: docstring каждого инструмента — это не просто документация для разработчика. Именно по этому описанию модель понимает, что делает инструмент и когда его стоит вызвать. Чем точнее описание, тем лучше модель выбирает нужный инструмент.

Теперь передаем инструменты агенту:


      async def main():
    model = create_model()
    tools = [get_current_time, get_random_joke, get_random_quote, get_random_fact]
    agent = create_agent(
        model=model,
        tools=tools,
        system_prompt=(
            "Ты полезный ассистент с доступом к инструментам:\n"
            "- get_current_time — узнать текущее московское время\n"
            "- get_random_joke — получить случайную шутку\n"
            "- get_random_quote — получить случайную цитату\n"
            "- get_random_fact — получить случайный интересный факт\n\n"
            "Используй инструменты когда это уместно. "
            "Данные от API приходят на английском — переводи их на русский в ответе."
        ),
    )

    tool_names = [t.name for t in tools]
    print(f"Агент с инструментами запущен: {', '.join(tool_names)}")
    print("Введите 'exit' для выхода.\n")
    while True:
        user_input = input("Вы: ")
        if user_input.strip().lower() in ("exit", "quit", "выход"):
            break
        result = await agent.ainvoke(
            {"messages": [{"role": "user", "content": user_input}]}
        )

        messages = result.get("messages", [])
        for msg in messages:
            if msg.type == "ai" and hasattr(msg, "tool_calls") and msg.tool_calls:
                for tc in msg.tool_calls:
                    print(f"  🔧 Вызов: {tc['name']}({tc['args']})")
            if msg.type == "tool":
                print(f"  📎 Результат: {msg.content[:200]}")
        for msg in reversed(messages):
            if msg.type == "ai" and msg.content and not msg.tool_calls:
                print(f"\nАссистент: {msg.content}\n")
                break

Логика процесса устроена так: LangChain через LangGraph строит цикл, «модель → инструмент → модель». Когда пользователь спрашивает «Который час?», модель не пытается угадать время — она возвращает вызов get_current_time(). LangGraph выполняет функцию, передает результат обратно модели, и та формирует финальный ответ.

Запуск: python -m example.agent_with_tools.

Попробуйте спросить: «Расскажи шутку и интересный факт» — модель вызовет оба инструмента и скомбинирует результаты в один ответ.

Скрин консоли.

Пример-3: агент с MCP-серверами

В этом примере мы подключаем два MCP-сервера:

  • fetch — загрузка и парсинг веб-страниц по URL;
  • filesystem — чтение, запись и навигация по файловой системе.

И комбинируем их с нашими кастомными инструментами из предыдущего примера.


      from langchain_mcp_adapters.client import MultiServerMCPClient
MCP_SERVERS = {
    "fetch": {
        "command": "uvx",
        "args": ["mcp-server-fetch"],
        "transport": "stdio",
    },
    "filesystem": {
        "command": "npx",
        "args": ["-y", "@modelcontextprotocol/server-filesystem", "/home/user"],
        "transport": "stdio",
    },
}

MCP-серверы запускаются как дочерние процессы и общаются с клиентом по stdio. Библиотека langchain-mcp-adapters автоматически превращает все доступные операции MCP-сервера в LangChain-инструменты:


      async def main():
    mcp_client = MultiServerMCPClient(MCP_SERVERS)
    mcp_tools = await mcp_client.get_tools()
    custom_tools = [get_current_time, get_random_joke, get_random_quote, get_random_fact]
    all_tools = mcp_tools + custom_tools
    model = create_model()
    agent = create_agent(
        model=model,
        tools=all_tools,
        system_prompt=(
            "Ты ассистент с доступом к инструментам.\n\n"
            "MCP инструменты:\n"
            "- fetch — загрузка и чтение веб-страниц по URL\n"
            "- filesystem — чтение, запись, поиск файлов и навигация по каталогам\n\n"
            "Кастомные инструменты:\n"
            "- get_current_time — текущее московское время\n"
            "- get_random_joke — случайная шутка\n"
            "- get_random_quote — случайная цитата\n"
            "- get_random_fact — случайный интересный факт\n\n"
            "Отвечай на русском языке. Данные от API на английском — переводи."
        ),
    )
    …

Запуск командой: python -m example.agent_with_mcp.

Теперь агент может, например, прочитать веб-страницу и пересказать ее содержание, или найти файл на диске и показать его содержимое — все в рамках одного диалога.

Попробуем с запросом: «прочитай эту статью https://ru.wikipedia.org/wiki/Qwen и сделай ее саммари с основными тезисами в файл summary.md».

Скрин консоли.

Заключение

Мы убедились, что LLM доступна по API, корректно отвечает на вопросы, умеет вызывать инструменты и работать с внешними сервисами через MCP. Все это — с локального компьютера, GPU и без сложной настройки.

Следующий логичный шаг — обернуть эту логику в полноценный бэкенд на FastAPI, чтобы агент стал доступен не только из терминала, но и через HTTP-эндпоинты для фронтенда или других сервисов, чем мы и займемся в следующей части.

Поделитесь в комментариях: используете ли вы квантованные модели в продакшене и, если да, какое семейство моделей (Qwen, Llama, Mistral) лучше всего показало себя в ваших задачах?