Ты когда-нибудь просыпался в три ночи от мысли «а не грохнулся ли мой облачный AI-агент?» Я — да. Особенно после того, как OpenAI в очередной раз поменял pricing, а Gmail API просто перестал отвечать. В этот момент ты понимаешь: единственный способ быть уверенным в своём цифровом помощнике — забрать его под свой контроль. На свой сервер. На свои рельсы.
Сегодня мы соберём автономного AI-агента, который не зависит ни от каких провайдеров. Он будет читать твою почту, отвечать в Telegram, выполнять сложные цепочки действий и жить под systemd — как настоящий демон. Никаких «облачных блокировок», никакого «превышен лимит запросов». Только твой код, твоя модель и твой контроль.
Важно: Это не игрушка. Агент с доступом к почте и Telegram может натворить дел, если его плохо настроить. Перед запуском прочитай раздел о безопасности. И да, все данные остаются локально — никаких утечек.
Что мы будем строить? (архитектура без компромиссов)
Представь себе цикл: Событие -> Подумал -> Вызвал инструмент -> Получил результат -> Запомнил -> Снова подумал. Это и есть агентный цикл, только на стероидах. У нашего агента есть четыре слоя:
- Слой восприятия: Telegram-бот слушает команды, IMAP-клиент проверяет новые письма.
- Мозг: Локальная LLM (Qwen3, Mistral или Saiga) с поддержкой tool calling и многошагового рассуждения.
- Руки: Набор инструментов — от парсинга веба до выполнения Python-кода и отправки писем.
- Память: Векторная база ChromaDB, куда агент складывает результаты своих действий и извлекает их при необходимости.
Вся эта магия завернута в systemd-сервис — стартует при загрузке, перезапускается при падениях, логирует в journald. Никаких супервизоров и docker-compose, только чистый Linux-демон.
Шаг 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 «Я не справился, извини».
Шаг 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 []
Шаг 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. Следи за новостями. А я пошёл кормить своего демона новыми промптами.