Свой Telegram-бот на локальной модели Ollama
Личный ассистент в Telegram, который отвечает на вашей собственной модели, а не гоняет запросы через чужой API, — это буквально час работы: BotFather, полсотни строк на Python и Ollama на порту 11434. Отдельная головная боль — не дать боту стать бесплатным чат-ассистентом для всех, кто найдёт его юзернейм, поэтому ниже сразу разберём ограничение доступа по chat_id, а не оставим это «на потом».
Содержание
Обсудить статью, задать вопрос или начать новую тему
Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.
Перейти в сообщество →Что понадобится
Схема простая: Telegram Bot API получает сообщения от пользователей, ваш Python-скрипт их обрабатывает и пересылает в Ollama по HTTP на localhost, Ollama генерирует ответ локальной моделью и отдаёт его обратно скрипту, а тот — пользователю в Telegram.
Перед началом нужно:
- сервер (VPS или выделенный) с уже установленной и запущенной Ollama — если её ещё нет, разверните по шагам из статьи про локальный запуск LLM через Ollama;
- загруженная модель, например
ollama pull llama3.1:8b— если сомневаетесь, какая модель влезет по памяти, свериться можно с таблицей RAM для моделей Ollama; - Python 3.10+ и токен бота от BotFather;
- права на создание systemd-юнита (root или sudo).
Для теста, что Ollama вообще отвечает, достаточно:
curl http://localhost:11434/api/generate -d '{
"model": "llama3.1:8b",
"prompt": "Привет, как дела?",
"stream": false
}'
Если в ответ пришёл JSON с полем response — можно двигаться дальше.
Создаём бота через BotFather
Открываем в Telegram диалог с @BotFather и выполняем:
/newbot- Вводим отображаемое имя бота, например «My Ollama Assistant».
- Вводим юзернейм — обязательно оканчивается на
bot, напримерmy_ollama_assistant_bot. - BotFather присылает токен вида
123456789:AAExampleTokenDoNotUseThisOne. Сохраните его — это единственный ключ доступа к боту, при компрометации сразу делайте/revoke.
Дополнительно стоит настроить через BotFather команду /setprivacy → Disable, если планируете добавлять бота в групповые чаты и хотите, чтобы он видел все сообщения, а не только адресованные ему через /. Для личного ассистента в личке это не требуется — оставляйте Enable по умолчанию, это безопаснее.
Токен не кладите в код — только в переменные окружения или systemd EnvironmentFile, об этом ниже.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Развернуть OllamaПишем Python-скрипт
Библиотека python-telegram-bot (версия 20+, асинхронная) — самый предсказуемый вариант для такой задачи. Устанавливаем окружение:
mkdir -p /opt/ollama-bot && cd /opt/ollama-bot
python3 -m venv venv
source venv/bin/activate
pip install "python-telegram-bot>=21,<22" httpx python-dotenv
Файл .env с секретами (права 600, владелец — сервисный пользователь):
TELEGRAM_TOKEN=123456789:AAExampleTokenDoNotUseThisOne
OLLAMA_MODEL=llama3.1:8b
OLLAMA_URL=http://localhost:11434/api/generate
ALLOWED_CHAT_IDS=111111111,222222222
Сам бот, bot.py:
import os
import logging
import httpx
from dotenv import load_dotenv
from telegram import Update
from telegram.ext import (
ApplicationBuilder, MessageHandler, CommandHandler,
ContextTypes, filters,
)
load_dotenv()
TOKEN = os.environ["TELEGRAM_TOKEN"]
OLLAMA_URL = os.environ.get("OLLAMA_URL", "http://localhost:11434/api/generate")
MODEL = os.environ.get("OLLAMA_MODEL", "llama3.1:8b")
ALLOWED = {
int(x) for x in os.environ.get("ALLOWED_CHAT_IDS", "").split(",") if x.strip()
}
logging.basicConfig(level=logging.INFO)
log = logging.getLogger("ollama-bot")
def is_allowed(chat_id: int) -> bool:
return not ALLOWED or chat_id in ALLOWED
async def start(update: Update, context: ContextTypes.DEFAULT_TYPE):
await update.message.reply_text(f"Ваш chat_id: {update.effective_chat.id}")
async def ask_ollama(prompt: str) -> str:
async with httpx.AsyncClient(timeout=120) as client:
resp = await client.post(OLLAMA_URL, json={
"model": MODEL,
"prompt": prompt,
"stream": False,
})
resp.raise_for_status()
return resp.json().get("response", "").strip()
async def on_message(update: Update, context: ContextTypes.DEFAULT_TYPE):
chat_id = update.effective_chat.id
if not is_allowed(chat_id):
log.warning("Отказ chat_id=%s", chat_id)
await update.message.reply_text("Доступ закрыт.")
return
text = update.message.text
await context.bot.send_chat_action(chat_id=chat_id, action="typing")
try:
answer = await ask_ollama(text)
except httpx.HTTPError as e:
log.error("Ollama error: %s", e)
answer = "Модель сейчас недоступна, попробуйте позже."
await update.message.reply_text(answer or "Пустой ответ от модели.")
def main():
app = ApplicationBuilder().token(TOKEN).build()
app.add_handler(CommandHandler("start", start))
app.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, on_message))
app.run_polling()
if __name__ == "__main__":
main()
Команда /start специально отвечает вашим chat_id — так вы узнаёте его без сторонних ботов и сразу вписываете в ALLOWED_CHAT_IDS. После первого запуска команду можно закомментировать или оставить — она безобидна, если у бота уже нет открытого доступа к Ollama для чужих chat_id.
Обратите внимание на stream: false в запросе к Ollama — это упрощает код: ответ приходит одним куском, без сборки чанков. Для длинных генераций это означает, что пользователь не увидит текст, пока модель не закончит целиком, — это осознанный компромисс ради простоты, при желании его можно доработать позже (см. раздел «Что доработать»).
Ограничение доступа по chat_id
Без этого шага любой человек, который найдёт юзернейм бота, получает бесплатный доступ к вашей модели и вашим ресурсам CPU/GPU — а юзернейм легко находится через поиск в Telegram или случайную пересылку ссылки. Проверка is_allowed() в коде выше — это минимально достаточная защита для личного или семейного использования, но у неё есть нюансы:
ALLOWED_CHAT_IDSпустой = бот открыт для всех. Это осознанное поведение для этапа отладки — не забудьте заполнить список перед тем, как оставлять бота работать без присмотра.- chat_id личных чатов стабилен, но если бот добавлен в группу — там свой отдельный
chat_id(отрицательное число), его тоже нужно вписать отдельно, если хотите разрешить именно эту группу. - Для более гибкой схемы (несколько уровней доступа, лимит запросов в час на пользователя) есть смысл хранить список разрешённых ID не в
.env, а в SQLite и добавлять администратора отдельной командой/allow <id>с проверкой, что её вызывает только владелец бота — сравнивайтеchat_idвызывающего с заранее зашитымOWNER_ID.
Минимальный вариант с лимитом запросов, если не хотите городить базу — простой словарь в памяти со счётчиком и сбросом раз в час; для одного сервера этого достаточно, персистентность между перезапусками не критична.
Запуск как systemd-сервис
Чтобы бот работал в фоне и поднимался после перезагрузки сервера, оформляем его как systemd-юнит вместо screen/nohup — так вы получите автоперезапуск при падении и нормальные логи через journalctl.
Создаём пользователя без прав входа (не обязательно, но правильнее, чем гонять бота от root):
useradd -r -s /usr/sbin/nologin -d /opt/ollama-bot botuser
chown -R botuser:botuser /opt/ollama-bot
chmod 600 /opt/ollama-bot/.env
Файл /etc/systemd/system/ollama-bot.service:
[Unit]
Description=Telegram bot on local Ollama model
After=network.target ollama.service
Wants=ollama.service
[Service]
Type=simple
User=botuser
Group=botuser
WorkingDirectory=/opt/ollama-bot
EnvironmentFile=/opt/ollama-bot/.env
ExecStart=/opt/ollama-bot/venv/bin/python /opt/ollama-bot/bot.py
Restart=on-failure
RestartSec=5
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=multi-user.target
After=network.target ollama.service и Wants=ollama.service нужны, чтобы бот стартовал после того, как поднялась сама Ollama — иначе первый запрос после перезагрузки сервера может упасть с ошибкой соединения.
Включаем и запускаем:
systemctl daemon-reload
systemctl enable --now ollama-bot.service
systemctl status ollama-bot.service
journalctl -u ollama-bot.service -f
Если сервис падает в цикл рестартов — почти всегда это неверный TELEGRAM_TOKEN, недоступная Ollama на 11434 или опечатка в пути ExecStart; всё это видно в journalctl за первые секунды после старта.
Что можно доработать
Рабочий вариант выше сознательно простой — реальные проекты обычно дорастают до следующего:
| Что доработать | Зачем |
|---|---|
stream: true + постепенное редактирование сообщения | ответ виден по мере генерации, а не одним куском в конце |
| История диалога (контекст) | модель помнит предыдущие сообщения в рамках чата, а не отвечает на каждое как на новое |
| Очередь запросов | если модель одна, а пользователей несколько — без очереди параллельные запросы будут конкурировать за CPU/GPU и тормозить друг друга |
| Таймауты и ретраи к Ollama | генерация на CPU может занимать десятки секунд, дефолтный timeout в HTTP-клиенте это нужно закладывать заранее |
| Логирование в файл с ротацией | journalctl хранит логи ограниченное время, для истории обращений лучше отдельный файл |
Приоритет обычно такой: сначала контекст диалога (без него бот бесполезен для сколько-нибудь связной беседы), потом очередь, стриминг — уже опционально, ради удобства. Если вместо ручной проверки токенов и лимитов хочется готовый шлюз с логированием и биллингом на несколько моделей сразу, посмотрите в сторону LiteLLM — это отдельная история, вынесенная в статью про частые ошибки LiteLLM на сервере.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Развернуть OllamaОбсудить статью, задать вопрос или начать новую тему
Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.
Перейти в сообщество →Частые вопросы
Ollama и бот обязательно должны быть на одном сервере?
Нет, но проще и быстрее — да: локальный HTTP-запрос на 127.0.0.1:11434 не упирается в сеть. Если разносите на разные машины, замените OLLAMA_URL на адрес другого сервера и закройте порт 11434 файрволом от внешнего доступа — Ollama по умолчанию не требует авторизации.
Что если бот не отвечает, а Ollama работает?
Проверьте journalctl -u ollama-bot.service -f во время отправки сообщения — если запрос вообще не долетает до скрипта, вероятная причина в токене или в том, что Telegram API недоступен из региона сервера без прокси. Если запрос доходит, но зависает — увеличьте timeout в httpx.AsyncClient, генерация на слабом CPU может быть медленной.
Можно ли обойтись без python-telegram-bot и написать проще?
Да, через прямые запросы к Telegram Bot API (getUpdates/sendMessage по HTTP) без сторонних библиотек — но тогда самому придётся реализовывать long polling, обработку ошибок сети и повторные попытки, а python-telegram-bot уже это делает за вас.
ALLOWED_CHAT_IDS — это надёжная защита?
Для личного использования и небольшой группы доверенных людей — да, если токен бота не утёк и порт Ollama не торчит наружу. Для публичного продукта нужна полноценная авторизация с базой пользователей, а не список ID в .env.
Как посмотреть, сколько ресурсов ест модель во время диалога?
Общая нагрузка на CPU/RAM в реальном времени видна через htop или nvidia-smi (если модель на GPU); для отдельного мониторинга под нагрузкой от Ollama есть гайд по мониторингу нагрузки локальной LLM.
Нужны сами нейросети для контента?
Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.