ИИ-агент на VPS обычно означает программу, которая получает задачи из Telegram, веб-формы или расписания, обращается к языковой модели через API, при необходимости вызывает инструменты (поиск, работа с файлами, запросы к вашим сервисам) и возвращает результат. Сам сервер в такой схеме почти ничего не вычисляет: тяжелую работу делает модель у провайдера API. Поэтому запуск агента на VPS - задача про надежность, безопасность и контроль расходов, а не про мощное железо.

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

Какой VPS нужен ИИ-агенту

Если агент вызывает модель через API, ресурсы сервера уходят на сам процесс агента, сетевые запросы и инструменты. Для одного агента без тяжелых задач достаточно 1 vCPU и 1–2 ГБ памяти. Тариф Micro (2 vCPU, 2 ГБ RAM, 20 ГБ диска) подходит для старта. Если рядом работают база данных, n8n или несколько агентов, комфортнее тариф Start с 4 ГБ памяти.

Важно: на небольшом VPS не запускают локальную языковую модель. Даже сжатая модель среднего размера требует нескольких гигабайт памяти только под веса, а при длинном контексте потребность растет из-за кеша внимания. Без GPU генерация идет медленно, и для агента это обычно неприемлемо. Локальные модели запускают на отдельном сервере с большим объемом RAM или с видеокартой. Для агента, который работает через API, обычного VPS достаточно.

  • Linux-сервер с Python 3.10 или новее;
  • от 1 vCPU и 1 ГБ RAM для одного агента без тяжелых инструментов;
  • от 10 ГБ диска для системы, Python-пакетов и логов;
  • доступ по SSH с ключом, см. статью Как подключиться к VPS по SSH;
  • исходящий доступ по HTTPS к API провайдера модели.

Подготовка пользователя и каталогов

Агента не нужно запускать от root. Если процесс скомпрометируют, ущерб ограничится правами отдельного пользователя. Создайте системного пользователя и каталог проекта:

sudo apt update && sudo apt install -y python3 python3-venv
sudo useradd --system --home-dir /opt/agent --shell /usr/sbin/nologin agent
sudo mkdir -p /opt/agent/app
sudo chown -R agent:agent /opt/agent

Виртуальное окружение и зависимости

Зависимости ставятся в отдельное виртуальное окружение. Так системный Python не засоряется, а версии библиотек фиксируются внутри проекта:

sudo -u agent python3 -m venv /opt/agent/venv
sudo -u agent /opt/agent/venv/bin/pip install --upgrade pip httpx
sudo -u agent /opt/agent/venv/bin/pip freeze | sudo tee /opt/agent/app/requirements.txt > /dev/null

В файл requirements.txt попадут точные версии. Копируйте его вместе с кодом, чтобы повторить установку на другом сервере.

Ключ API: где хранить и как не потерять

Ключ API - секрет, который дает доступ к платному сервису. Его нельзя писать в код, в git, в переписку и в журналы. Храним ключ в файле окружения, который читает только сервисный пользователь:

sudo install -m 600 -o agent -g agent /dev/null /opt/agent/agent.env
sudo nano /opt/agent/agent.env
LLM_API_KEY=your-api-key-here
LLM_API_URL=https://api.provider.example/v1/chat/completions
LLM_MODEL=your-model-name
MAX_TOKENS_PER_REQUEST=512

Права 600 означают, что файл читает только владелец. Если ключ попал в переписку, скриншот или репозиторий, считайте его скомпрометированным: отзовите его в консоли провайдера и выпустите новый. Удалять следы из истории git при этом недостаточно.

Минимальный код агента

Ниже минимальный цикл: получить задачу, отправить ее модели, записать результат. Пример рассчитан на OpenAI-совместимый формат запроса. Если ваш провайдер использует другой формат или заголовок авторизации, поправьте функцию ask_model по его документации. Функция next_task здесь заглушка: замените ее чтением очереди, файла или Telegram-бота.

import logging
import os
import sys
import time

import httpx

API_KEY = os.environ["LLM_API_KEY"]
API_URL = os.environ["LLM_API_URL"]
MODEL = os.environ["LLM_MODEL"]
MAX_TOKENS = int(os.environ.get("MAX_TOKENS_PER_REQUEST", "512"))
MAX_RETRIES = 3

logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s %(levelname)s %(message)s",
    stream=sys.stdout,
)
log = logging.getLogger("agent")


def ask_model(prompt: str) -> str:
    headers = {"Authorization": "Bearer " + API_KEY}
    payload = {
        "model": MODEL,
        "max_tokens": MAX_TOKENS,
        "messages": [{"role": "user", "content": prompt}],
    }
    for attempt in range(1, MAX_RETRIES + 1):
        try:
            r = httpx.post(API_URL, json=payload, headers=headers, timeout=60)
            if r.status_code == 429:
                wait = 2 ** attempt
                log.warning("rate limited, retry in %s s", wait)
                time.sleep(wait)
                continue
            r.raise_for_status()
            return r.json()["choices"][0]["message"]["content"]
        except httpx.HTTPError as exc:
            log.error("request failed, attempt %s, error %s", attempt, type(exc).__name__)
            time.sleep(2 ** attempt)
    raise RuntimeError("model request failed after retries")


def next_task():
    return None  # заглушка: вернуть строку задачи или None


def main() -> None:
    while True:
        task = next_task()
        if task is None:
            time.sleep(5)
            continue
        log.info("task received, length=%d", len(task))
        answer = ask_model(task)
        log.info("answer received, length=%d", len(answer))
        # отправить answer пользователю или записать в очередь


if __name__ == "__main__":
    main()

Обратите внимание на несколько решений. Ответ модели ограничен параметром max_tokens. Повторы при ошибках растут экспоненциально и ограничены тремя попытками, поэтому агент не уходит в бесконечный цикл. В журнал пишутся длины и типы ошибок, но не сам текст ключа и не заголовки запроса.

Для тестов можно загрузить переменные вручную: set -a; . /opt/agent/agent.env; set +a; и запустить /opt/agent/venv/bin/python /opt/agent/app/agent.py от пользователя agent.

Запуск как сервис systemd

Systemd запускает агента при старте системы, перезапускает его после сбоя и ведет журнал. Создайте unit-файл:

[Unit]
Description=AI agent
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
User=agent
Group=agent
WorkingDirectory=/opt/agent/app
EnvironmentFile=/opt/agent/agent.env
ExecStart=/opt/agent/venv/bin/python /opt/agent/app/agent.py
Restart=on-failure
RestartSec=10
NoNewPrivileges=true
ProtectSystem=strict
ReadWritePaths=/opt/agent
PrivateTmp=true
MemoryMax=512M

[Install]
WantedBy=multi-user.target
sudo nano /etc/systemd/system/ai-agent.service
sudo systemctl daemon-reload
sudo systemctl enable --now ai-agent
sudo systemctl status ai-agent

Значение MemoryMax подберите под свой тариф: на сервере с 1 ГБ памяти лимит 512 МБ оставит запас для системы. Если агент начнет выходить за лимит, systemd его остановит, и это лучше, чем незаметная нагрузка на весь сервер.

Логи и диагностика

Журнал сервиса доступен через journalctl. Он не требует отдельной настройки и сохраняет вывод агента между перезагрузками, если включено постоянное хранение journald:

journalctl -u ai-agent -f
journalctl -u ai-agent --since "1 hour ago"
journalctl -u ai-agent -p err
sudo systemctl restart ai-agent

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

Контроль расходов и лимиты

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

  • лимит ответа через max_tokens в каждом запросе;
  • дневной лимит токенов или запросов внутри самого агента, например счетчик в JSON-файле с обнулением раз в сутки;
  • ограничение частоты запросов и exponential backoff при ошибках 429;
  • последовательная обработка очереди для одного агента, без параллельных запросов без необходимости;
  • лимит расходов или уведомления о пороге в консоли провайдера, если такая функция доступна;
  • дешевая модель для простых задач и дорогая только для сложных.

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

Безопасность ключей и самого агента

Агент с доступом к ключам, файлам и внешним сервисам - это привлекательная цель. Базовые правила:

  • создавайте отдельный ключ для каждого проекта и отзывайте его, если он мог утечь;
  • меняйте ключи по графику и после смены людей, которые имели к ним доступ;
  • ограничивайте агента каталогом /opt/agent и не давайте ему права, которые он не использует;
  • не выполняйте команды оболочки, предложенные моделью, без песочницы и явного списка разрешенных действий;
  • помните о prompt injection: текст из страниц, писем и документов может содержать инструкции для агента, поэтому такие инструкции не должны получать право на опасные действия;
  • закройте сервер фаерволом, обновляйте систему и следуйте рекомендациям из статьи Безопасность Linux-сервера.

Готовый ИИ-бот или свой агент

Самостоятельный VPS оправдан, когда нужны собственный код, свои инструменты и полный контроль над логикой агента. Если задача стандартная, а времени на настройку, мониторинг и контроль ключей нет, проще воспользоваться готовым вариантом. В Pulsar для этого есть раздел AI Bot. Для собственного агента подойдет VPS из раздела VPS.

Проверка перед запуском

  1. Запустите агента вручную от пользователя agent и убедитесь, что модель отвечает.
  2. Проверьте, что ключ не виден в журналах, в истории команд и в репозитории.
  3. Перезагрузите сервер и убедитесь, что сервис поднимается сам.
  4. Отправьте тестовый запрос с небольшим max_tokens и проверьте расход в консоли провайдера.
  5. Сымитируйте ошибку API, например неверный ключ, и убедитесь, что агент не уходит в цикл быстрых повторов.

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