Автономный AI-агент локально: почта, Telegram, systemd — полный гайд | AiManual
AiManual Logo Ai / Manual.
25 Май 2026 Гайд

AI-агент, который не спит: почта, Telegram, systemd и LLM под одной крышей

Собираем полностью автономного AI-агента на локальном сервере: LLM, почта, Telegram, systemd и долговременная память. Подробное руководство с кодом и без облачн

Ты когда-нибудь просыпался в три ночи от мысли «а не грохнулся ли мой облачный AI-агент?» Я — да. Особенно после того, как OpenAI в очередной раз поменял pricing, а Gmail API просто перестал отвечать. В этот момент ты понимаешь: единственный способ быть уверенным в своём цифровом помощнике — забрать его под свой контроль. На свой сервер. На свои рельсы.

Сегодня мы соберём автономного AI-агента, который не зависит ни от каких провайдеров. Он будет читать твою почту, отвечать в Telegram, выполнять сложные цепочки действий и жить под systemd — как настоящий демон. Никаких «облачных блокировок», никакого «превышен лимит запросов». Только твой код, твоя модель и твой контроль.

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

Что мы будем строить? (архитектура без компромиссов)

Представь себе цикл: Событие -> Подумал -> Вызвал инструмент -> Получил результат -> Запомнил -> Снова подумал. Это и есть агентный цикл, только на стероидах. У нашего агента есть четыре слоя:

  • Слой восприятия: Telegram-бот слушает команды, IMAP-клиент проверяет новые письма.
  • Мозг: Локальная LLM (Qwen3, Mistral или Saiga) с поддержкой tool calling и многошагового рассуждения.
  • Руки: Набор инструментов — от парсинга веба до выполнения Python-кода и отправки писем.
  • Память: Векторная база ChromaDB, куда агент складывает результаты своих действий и извлекает их при необходимости.

Вся эта магия завернута в systemd-сервис — стартует при загрузке, перезапускается при падениях, логирует в journald. Никаких супервизоров и docker-compose, только чистый Linux-демон.

💡
Если ты ещё не определился с выбором локальной модели, прочитай гайд по российскому локальному AI-агенту — там я подробно сравниваю Qwen3-Coder-32B, Saiga 3 70B и Mistral-7B под наши задачи.

Шаг 1: Запускаем LLM — почему Ollama, а не «собери сам»

Можно, конечно, компилировать llama.cpp собственноручно, но зачем изобретать велосипед? Ollama — это стандарт де-факто для локального рантайма. На момент мая 2026 года актуальная версия — 0.6.4, и она умеет:

  • Загружать модели в несколько команд.
  • Поддерживать tool calling (function calling) — критично для агентов.
  • Работать с GPU (CUDA, ROCm, Vulkan) и с CPU (квантованные модели 4-bit).
  • Давать совместимый с OpenAI API-эндпоинт.

Для русскоязычного агента я рекомендую Qwen3-32B-Instruct (Q4_K_M) — она отлично понимает русский контекст, умеет в длинные цепочки рассуждений (Chain-of-Thought) и поддерживает tool calling из коробки. Если железа мало — бери Mistral-7B-v0.3 или Saiga-8B.

# Установка Ollama
curl -fsSL https://ollama.com/install.sh | sh
# Скачиваем модель
ollama pull qwen3:32b-q4_K_M
# Проверяем, что работает
ollama run qwen3:32b-q4_K_M "Привет, расскажи о себе"

После этого Ollama будет слушать порт 11434. Мы будем обращаться к нему через http://localhost:11434/v1/chat/completions.

Watch out: Не ставь OLLAMA_HOST=0.0.0.0 без firewall! Ollama не требует аутентификации по умолчанию — любой в локальной сети сможет гонять твои запросы. Я обжёгся на этом, когда забыл закрыть порт.

Шаг 2: Пишем ядро агента — цикл «Думай-Делай-Проверяй»

Теперь самое мясо. Ядро агента — это Python-скрипт, который в бесконечном цикле получает задачу, отправляет её LLM, выполняет инструменты и снова отправляет результат. Без этого любой «агент» остаётся просто чат-ботом.

Вот минимальная реализация. Обрати внимание на tool calling: мы передаём модели список доступных функций в формате, совместимом с OpenAI API. Ollama это поддерживает начиная с версии 0.5.0.

import openai
import json

client = openai.OpenAI(
    base_url="http://localhost:11434/v1",
    api_key="ollama"  # не играет роли, но нужно для заглушки
)

tools = [
    {
        "type": "function",
        "function": {
            "name": "send_telegram_message",
            "description": "Отправить сообщение в Telegram",
            "parameters": {
                "type": "object",
                "properties": {
                    "chat_id": {"type": "string"},
                    "text": {"type": "string"}
                },
                "required": ["chat_id", "text"]
            }
        }
    },
    # Добавь сюда другие инструменты
]

def agent_loop(user_input: str):
    messages = [{"role": "user", "content": user_input}]
    while True:
        response = client.chat.completions.create(
            model="qwen3:32b-q4_K_M",
            messages=messages,
            tools=tools,
            tool_choice="auto"
        )
        choice = response.choices[0]
        if choice.finish_reason == "tool_calls":
            for tc in choice.message.tool_calls:
                tool_name = tc.function.name
                args = json.loads(tc.function.arguments)
                result = execute_tool(tool_name, args)
                messages.append({"role": "tool", "tool_call_id": tc.id, "content": json.dumps(result)})
        else:
            return choice.message.content

def execute_tool(name: str, args: dict):
    # Здесь вызываем реальные функции
    pass

Важный момент: проверяй, что модель не уходит в бесконечный цикл. На практике Qwen3 может начать вызывать инструменты без конечного ответа. Поэтому добавь счётчик итераций (максимум 10) и fallback «Я не справился, извини».

💡
Подробнее о том, как избежать вечных циклов, я писал в статье «Локальные агентные AI: почему Cline и Goose не работают». Спойлер: дело в промптах и ограничении глубины.

Шаг 3: Интеграция с Telegram — уши агента

Telegram — идеальный интерфейс. Агент слушает бота, получает команды и отвечает. Мы используем python-telegram-bot версии 21.x (асинхронный).

Агент должен уметь:

  • Принимать произвольный запрос и запускать агентный цикл.
  • Отвечать в том же чате (или пересылать результат админу).
  • Поддерживать длительные операции — не блокировать бота на время размышлений модели.
from telegram.ext import Application, MessageHandler, filters
import asyncio

async def handle_message(update, context):
    user_text = update.message.text
    # Запускаем агента в фоновом потоке, чтобы не блокировать бота
    loop = asyncio.get_event_loop()
    result = await loop.run_in_executor(None, agent_loop, user_text)
    await update.message.reply_text(result)

app = Application.builder().token("YOUR_BOT_TOKEN").build()
app.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, handle_message))
app.run_polling()

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

Шаг 4: Почтовый ящик — глаза и уши в email

IMAP — старый, но надёжный протокол. Большинство почтовых сервисов (даже Gmail, если включить доступ для менее защищённых приложений) отдают письма по IMAP. Для входящих используем imaplib, для исходящих — smtplib.

Агент может:

  • Читать непрочитанные письма.
  • Парсить тело (текст, HTML, вложения).
  • Отвечать или отправлять новые письма по команде.
  • Анализировать поток писем — например, искать срочные уведомления.
import imaplib, smtplib, email
from email.mime.text import MIMEText

def fetch_unread_emails(imap_server, username, password):
    mail = imaplib.IMAP4_SSL(imap_server)
    mail.login(username, password)
    mail.select("INBOX")
    _, ids = mail.search(None, "UNSEEN")
    emails = []
    for id in ids[0].split():
        _, msg_data = mail.fetch(id, "(RFC822)")
        msg = email.message_from_bytes(msg_data[0][1])
        emails.append({"from": msg["From"], "subject": msg["Subject"], "body": get_body(msg)})
    mail.logout()
    return emails

def send_email(smtp_server, username, password, to, subject, body):
    with smtplib.SMTP_SSL(smtp_server) as s:
        s.login(username, password)
        msg = MIMEText(body, "plain", "utf-8")
        msg["Subject"] = subject
        msg["From"] = username
        msg["To"] = to
        s.send_message(msg)

Ошибка новичка: Хранить пароль от почты в открытом виде в коде. Используй переменные окружения или keyring. И никогда не давай агенту доступ к почте, если он может случайно отправить письмо с паролями.

Кстати, если твой Gmail завязан на 2FA, прочитай гайд по синхронизации cookies Chrome — он поможет агенту авторизоваться без вечного ввода кодов.

Шаг 5: Вечная память — ChromaDB не даёт агенту забыть

Без долговременной памяти агент — как человек с амнезией. Он выполняет задачу, а через час не помнит, что делал. Мы используем ChromaDB — векторную базу данных, которая хранит эмбеддинги текста и позволяет искать по семантической близости.

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

import chromadb
from chromadb.utils import embedding_functions

# Используем эмбеддинги от той же модели (Ollama поддерживает embeddings)
emb_fn = embedding_functions.OllamaEmbeddingFunction(
    model_name="qwen3:32b-q4_K_M",
    url="http://localhost:11434/api/embeddings"
)
client = chromadb.PersistentClient(path="/var/lib/agent-memory")
collection = client.get_or_create_collection("agent_memories", embedding_function=emb_fn)

def save_memory(user_id: str, step: str, result: str):
    collection.add(
        documents=[f"{step}: {result}"],
        metadatas={"user_id": user_id, "timestamp": str(datetime.now())},
        ids=[f"{user_id}_{step}_{int(time.time())}"]
    )

def recall_similar(query: str, n_results=3):
    results = collection.query(query_texts=[query], n_results=n_results)
    return results["documents"][0] if results["documents"] else []
💡
Важно: память нужно чистить от устаревших записей. Агент, который помнит каждую мелочь за год, будет тонуть в шуме. Я настроил TTL — удаляю записи старше 30 дней, если они не помечены как важные.

Шаг 6: Инструменты — чем больше, тем опаснее

Агент силён не моделью, а инструментами. Вот минимальный набор, который я зашиваю в своего демона:

Инструмент Что делает Риски
web_search Парсит поисковую выдачу DuckDuckGo (без API) Может наткнуться на вредоносный контент
read_file Читает локальные файлы (PDF, DOCX, TXT) Доступ к конфиденциальным данным
execute_python Выполняет произвольный Python-код в sandbox RCE, если sandbox слабый
send_email Отправляет письма от имени агента Спам, утечка

Каждый инструмент должен быть wrapped в функцию с валидацией аргументов и максимумом вызовов. Например, execute_python я запускаю в изолированном Docker-контейнере с ограничением CPU/RAM и без сети.

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

Шаг 7: systemd — превращаем скрипт в демона

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

Вот правильный юнит. Обрати внимание на Restart=always, RestartSec=10 и User=agent — мы не запускаем агента от root.

[Unit]
Description=AI Agent Service
After=network-online.target ollama.service
Requires=ollama.service

[Service]
Type=simple
User=agent
WorkingDirectory=/opt/ai-agent
EnvironmentFile=/etc/ai-agent/env.conf
ExecStart=/opt/ai-agent/.venv/bin/python /opt/ai-agent/main.py
Restart=always
RestartSec=10
# Ограничение ресурсов
MemoryMax=4G
CPUQuota=80%
# Логирование
StandardOutput=journal
StandardError=journal

[Install]
WantedBy=multi-user.target

Не забудь создать пользователя agent с ограниченными правами:

sudo useradd -r -s /usr/sbin/nologin -d /opt/ai-agent agent
sudo mkdir -p /opt/ai-agent && sudo chown agent:agent /opt/ai-agent
sudo systemctl enable ai-agent
sudo systemctl start ai-agent

Совет про здоровье: Добавь healthcheck — запусти systemd timer, который каждый час проверяет, отвечает ли агент. Если нет — перезапускай. Я сделал так: timer выполняет curl к внутреннему эндпоинту агента, и если код ответа не 200, то вызывает systemctl restart ai-agent.

Шаг 8: Безопасность — как не выстрелить себе в ногу

Агент с доступом к почте и Telegram — это, по сути, бэкдор. Если злоумышленник или сам агент (по ошибке) решит отправить все твои файлы — он это сделает. Вот что я делаю для защиты:

  • Sandbox для кода: execute_python выполняется в Docker с read-only rootfs, без сети, с лимитом 1 CPU, 512 MB RAM.
  • Белый список команд: Агент может выполнять только заранее разрешённые инструменты. Никакой subprocess.call с произвольной командой.
  • Лимит вызовов: Не больше 5 вызовов инструментов за один запрос. Иначе агент может потратить часы на бесконечные поиски.
  • Валидация вывода: Перед отправкой письма или сообщения в Telegram агент должен показать человеку превью и получить подтверждение (human-in-the-loop).
  • Разделение секретов: Пароли, токены — только в /etc/ai-agent/env.conf с правами 600. Код их никогда не содержит.

Реальный пример ошибки: Однажды я забыл поставить лимит итераций. Агент начал искать «лучший рецепт борща» и за 40 минут сделал 1500 вызовов к веб-поиску. Счётчик потраченного трафика порадовал только провайдера. Теперь я ставлю жёсткий лимит.

Шаг 9: Типичные ошибки и как их чинить

  • Агент уходит в бесконечный цикл tool calling. Проверь, что модель поддерживает функцию stop_tool_calls или добавил счётчик итераций в коде. Qwen3 склонна переусердствовать с вызовами.
  • Telegram-бот падает при длительных запросах. Используй run_in_executor или asyncio.to_thread. Иначе бот зависнет на всё время работы модели.
  • Почта не подключается (SSL ошибка). Многие IMAP-серверы (Mail.ru, Yandex) требуют современные шифры. Обнови OpenSSL или проверь сертификаты.
  • Память ChromaDB разрастается. Добавь автоматическую очистку по TTL или удаление записей, которые не использовались дольше N дней.
  • systemd не стартует после перезагрузки. Проверь After=network.target ollama.service и что юнит агента не зависит от сети до её поднятия.

FAQ: то, о чём обычно спрашивают

Какую модель выбрать, если у меня 16GB RAM?

Бери Mistral-7B или Qwen3-7B в Q4 квантовании. Они потянут даже на CPU, но медленно. На GPU (6GB VRAM) заработает Qwen3-14B.

Можно ли запустить это на Raspberry Pi?

Технически да, но модели >3B там будут думать минутами. Лучше используй Ollama на сервере, а на RPi поставь только клиента для Telegram.

Агент отправил письмо не тому адресату. Как защититься?

Добавь human-in-the-loop: перед отправкой агент пишет в Telegram «Я хочу отправить письмо X с текстом Y. Подтверждаешь?» и ждёт ответа.

Стоит ли давать агенту доступ к bash?

Нет. Никогда. Даже с sandbox. Если очень нужно — используй ограниченный shell (rbash) или контейнер, но я бы не советовал.

Финальное слово: не оставляй агента без присмотра

Локальный агент — это мощно. Он не зависит от облаков, не платит за токены, не боится блокировок. Но он — как щенок, который может разгрызть тапки, если не следить. Я настоятельно рекомендую сначала поставить режим «только для чтения»: пусть агент анализирует почту и отвечает в Telegram, но не отправляет письма и не выполняет код. Через неделю, когда убедишься, что он не сошёл с ума, можно постепенно открывать доступ.

Кстати, если ты думаешь, что проще взять готовый n8n и не париться с кодом, — прочитай гайд по развёртыванию агента с n8n. Но имей в виду: n8n — это конструктор, а наш код — это полный контроль. Выбор за тобой.

И последнее: не забывай обновлять модели и библиотеки. Qwen3 вышла в марте 2026, но уже в мае появился Qwen3.1 с улучшенным tool calling. Следи за новостями. А я пошёл кормить своего демона новыми промптами.

Подписаться на канал