Skip to content
BotServBotServ
TelegramBotBot IAOllamaaiogramPythonSelf-Hosting

Bot de Telegram con IA Local

Crea un bot de Telegram con Ollama: BotFather, Polling vs. Webhook, teclados inline, grupos, RAG y Docker.

S

schutzgeist

9 min read
Bot de Telegram con IA Local

Bot de Telegram con IA local

Qué cubre este artículo

  • Cómo construir un bot de Telegram usando la API oficial de Bot y Ollama.
  • Configuración de BotFather, Polling vs. Webhooks y gestión de tokens.
  • Ejemplos de código completos con aiogram (Python).
  • Extensiones: teclados inline, moderación de grupos, RAG, imágenes y mensajes de voz.
  • Despliegue con Docker, servicio systemd y operación continua.

Introducción

Telegram es la plataforma más amigable para bots: la API de Bot es gratuita, está oficialmente documentada y no requiere cuenta de negocio, aprobaciones ni certificados. Creas un bot en dos minutos con @BotFather y listo, puede recibir mensajes, responderlos y ejecutar funciones de IA propias.

Combinado con Ollama obtienes un asistente de chat que corre completamente en tu servidor: responder preguntas, resumir textos, buscar en documentos (RAG), moderar grupos, sin que los contenidos se envíen a OpenAI u otros proveedores en la nube.

Casos de uso típicos

  • Asistente personal de IA: chatea con tu LLM local desde el móvil, en cualquier lugar, sin necesidad de VPN al interfaz web.
  • Soporte comunitario: responde automáticamente preguntas recurrentes en tu grupo de Telegram.
  • Bot de notificaciones: alertas de monitoreo, estado de copias de seguridad o eventos de hogar inteligente directo en Telegram.
  • Asistente de documentos: envía un PDF al bot y lo resume (RAG + OCR).
  • Moderación: detección de spam, bienvenida de nuevos miembros, respuestas automáticas ante infracciones de reglas.
  • Puerta de entrada para agentes: Telegram como interfaz para tus agentes de IA, dispara tareas por chat.

Requisitos previos

  • Cuenta de Telegram
  • Servidor con Ollama (al menos un modelo cargado, p. ej. llama3.1:8b)
  • Python 3.10+
  • Opcional: Docker para operación, dominio + reverse proxy para webhooks

Paso 1: crear el bot en BotFather

Abre Telegram y escribe a @BotFather:

/newbot

BotFather te pide un nombre (nombre mostrado, p. ej. “Mi Asistente IA”) y usuario (debe terminar en bot, p. ej. mi_bot_ia). Recibirás un token:

123456789:AAEhBOweik5ad9rQXMENKJBI...

Nunca escribas el token en el código, siempre como variable de entorno o a través de gestión de secretos. Quien tenga el token controla el bot.

Comandos útiles de BotFather para después:

/setdescription     # Descripción en el perfil del bot
/setcommands        # Lista de comandos (/start, /help, ...)
/setprivacy         # Importante para grupos: Disable = bot lee todos los mensajes
/setjoingroups      # Si el bot puede agregarse a grupos

Paso 2: bot mínimo con aiogram

aiogram es el framework Python actualmente mantenido para la API de Bot de Telegram (async, type hints, concepto de router). python-telegram-bot es la alternativa, ambos funcionan.

pip install aiogram ollama python-dotenv

Archivo .env:

TELEGRAM_TOKEN=123456789:AAEhBOweik5ad9rQXMENKJBI...
OLLAMA_URL=http://localhost:11434
OLLAMA_MODEL=llama3.1:8b

bot.py:

import asyncio
import os
from dotenv import load_dotenv
from aiogram import Bot, Dispatcher, F
from aiogram.filters import Command
from aiogram.types import Message
import ollama

load_dotenv()

bot = Bot(token=os.environ["TELEGRAM_TOKEN"])
dp = Dispatcher()
ollama_client = ollama.Client(host=os.environ["OLLAMA_URL"])
MODEL = os.environ.get("OLLAMA_MODEL", "llama3.1:8b")

SYSTEM_PROMPT = """Eres un asistente útil en Telegram.
Responde de forma breve y precisa. Máximo 3-4 párrafos.
Usa lenguaje simple, sin tecnicismos sin explicar."""


@dp.message(Command("start"))
async def cmd_start(message: Message):
    await message.answer(
        "¡Hola! Soy un bot de IA con modelo local. "
        "Simplemente escríbeme un mensaje o usa /help."
    )


@dp.message(Command("help"))
async def cmd_help(message: Message):
    await message.answer(
        "Comandos:\n"
        "/start - Iniciar el bot\n"
        "/reset - Reiniciar la conversación\n"
        "/model - Muestra el modelo actual\n"
        "Simplemente escribe para obtener respuestas de IA."
    )


@dp.message(Command("model"))
async def cmd_model(message: Message):
    await message.answer(f"Modelo activo: `{MODEL}`", parse_mode="Markdown")


# Contexto de conversación por usuario (almacenamiento en memoria simple)
conversations: dict[int, list] = {}

@dp.message(Command("reset"))
async def cmd_reset(message: Message):
    conversations.pop(message.from_user.id, None)
    await message.answer("Conversación reiniciada.")


@dp.message(F.text)
async def handle_message(message: Message):
    user_id = message.from_user.id

    # Mostrar indicador de "escribiendo..."
    await bot.send_chat_action(message.chat.id, "typing")

    # Construir contexto (máximo 10 últimos mensajes)
    history = conversations.setdefault(user_id, [])
    history.append({"role": "user", "content": message.text})
    history = history[-10:]

    try:
        response = ollama_client.chat(
            model=MODEL,
            messages=[{"role": "system", "content": SYSTEM_PROMPT}] + history,
        )
        answer = response["message"]["content"]
        history.append({"role": "assistant", "content": answer})
        conversations[user_id] = history

        # Telegram limita mensajes a 4096 caracteres
        for i in range(0, len(answer), 4000):
            await message.answer(answer[i:i + 4000])

    except Exception as e:
        await message.answer("Error al generar la respuesta. ¿Ollama está disponible?")
        print(f"Error de Ollama: {e}")


async def main():
    await dp.start_polling(bot)

if __name__ == "__main__":
    asyncio.run(main())

Inicia:

python bot.py

Escribe un mensaje al bot en Telegram, te responderá con el modelo local. Así de simple, la integración de plataforma más fácil.

Polling vs. Webhook

ModoCómo funcionaCuándo usarlo
PollingEl bot pregunta a Telegram cada pocos segundos por nuevos mensajesDesarrollo, servidor local sin dominio, detrás de NAT
WebhookTelegram envía actualizaciones a tu endpoint HTTPSProducción con dominio, alto volumen de mensajes, respuestas más rápidas

Polling suele ser suficiente para self-hosters y es mucho más simple, sin reverse proxy ni certificados TLS. Webhook tiene sentido con muchos usuarios o múltiples bots en un servidor.

Configuración de webhook con aiogram:

from aiogram.webhook.aiohttp_server import SimpleRequestHandler, setup_application
from aiohttp import web

async def main():
    await bot.set_webhook(
        "https://bots.tu-dominio.de/telegram/webhook",
        secret_token=os.environ["WEBHOOK_SECRET"],
    )
    app = web.Application()
    SimpleRequestHandler(dispatcher=dp, bot=bot).register(app, path="/telegram/webhook")
    setup_application(app, dp, bot=bot)
    web.run_app(app, host="127.0.0.1", port=8080)

Más un reverse proxy (Nginx/Caddy) con TLS, ver Reverse Proxy.

Extensiones

Teclados inline: botones bajo los mensajes

Los teclados sirven para opciones de selección, como “cambiar modelo” o “calificar resultado”:

from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton
from aiogram.filters import Command

@dp.message(Command("modelchoice"))
async def cmd_modelchoice(message: Message):
    keyboard = InlineKeyboardMarkup(inline_keyboard=[
        [InlineKeyboardButton(text="Llama 3.1 (8B)", callback_data="model:llama3.1:8b")],
        [InlineKeyboardButton(text="Qwen2.5 (14B)", callback_data="model:qwen2.5:14b")],
        [InlineKeyboardButton(text="DeepSeek-R1 (8B)", callback_data="model:deepseek-r1:8b")],
    ])
    await message.answer("¿Qué modelo?", reply_markup=keyboard)


@dp.callback_query(F.data.startswith("model:"))
async def model_chosen(callback):
    model = callback.data.split(":")[1]
    user_models[callback.from_user.id] = model
    await callback.message.edit_text(f"Modelo configurado: `{model}`", parse_mode="Markdown")
    await callback.answer()

Operación en grupos y moderación

Para que el bot lea todos los mensajes en grupos (no solo /commands), debes configurar en BotFather /setprivacy en Disable. Después:

@dp.message(F.text & F.chat.type.in_({"group", "supergroup"}))
async def group_handler(message: Message):
    # Solo responder si se menciona al bot
    if f"@{BOT_USERNAME}" in message.text:
        await handle_message(message)
        return

    # Moderación sencilla con IA: clasificar el mensaje
    check = ollama_client.chat(
        model=MODEL,
        messages=[{
            "role": "user",
            "content": f"Klasifiziere diese Nachricht als OK oder SPAM. "
                       f"Antworte nur mit einem Wort.\n\n{message.text}"
        }],
    )
    if check["message"]["content"].strip().upper() == "SPAM":
        await message.delete()
        await message.answer(f"@{message.from_user.username}: Nachricht entfernt.")

RAG: El bot responde preguntas sobre tus documentos

El bot busca en una base de datos vectorial antes de responder. La estructura es como en RAG local:

from qdrant_client import QdrantClient

qdrant = QdrantClient(host="localhost", port=6333)

def retrieve_context(query: str) -> str:
    embedding = ollama_client.embeddings(model="bge-m3", prompt=query)["embedding"]
    hits = qdrant.search(collection_name="dokumente", query_vector=embedding, limit=3)
    return "\n\n".join(h.payload["text"] for h in hits)


@dp.message(F.text)
async def handle_message(message: Message):
    context = retrieve_context(message.text)
    response = ollama_client.chat(
        model=MODEL,
        messages=[
            {"role": "system", "content": SYSTEM_PROMPT +
             f"\n\nNutze diesen Kontext zur Antwort:\n{context}"},
            {"role": "user", "content": message.text},
        ],
    )
    await message.answer(response["message"]["content"])

Imágenes y mensajes de voz

Telegram distribuye archivos mediante getFile/download. Los mensajes de voz se pueden enviar a Whisper, las imágenes a un modelo de visión como llava o minicpm-v:

@dp.message(F.voice)
async def handle_voice(message: Message):
    file = await bot.get_file(message.voice.file_id)
    path = await bot.download_file(file.file_path)
    text = whisper_transcribe(path.name)   # Whisper local
    await message.answer(f"Verstanden: „{text}"\n\n")
    # después tratar como mensaje de texto normal


@dp.message(F.photo)
async def handle_photo(message: Message):
    file = await bot.get_file(message.photo[-1].file_id)
    path = await bot.download_file(file.file_path)
    response = ollama_client.chat(
        model="minicpm-v",
        messages=[{
            "role": "user",
            "content": "Beschreibe dieses Bild.",
            "images": [path.name],
        }],
    )
    await message.answer(response["message"]["content"])

Respuestas con streaming

Ollama permite streaming, y los mensajes de Telegram se pueden editar posteriormente:

sent = await message.answer("…")
text = ""
async for chunk in ollama_client.chat(model=MODEL, messages=msgs, stream=True):
    text += chunk["message"]["content"]
    if len(text) % 80 == 0:          # no enviar cada token (límite de rate!)
        await sent.edit_text(text)
await sent.edit_text(text)

Operación: Docker y systemd

Dockerfile:

FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY bot.py .
CMD ["python", "bot.py"]

docker-compose.yml (bot + Ollama + Qdrant en el mismo stack):

services:
  telegram-bot:
    build: .
    restart: always
    env_file: .env
    depends_on:
      - ollama
    networks: [botnet]

  ollama:
    image: ollama/ollama:latest
    restart: always
    volumes:
      - ollama_data:/root/.ollama
    networks: [botnet]
    # deploy.resources para GPU ver artículo Docker

volumes:
  ollama_data:

networks:
  botnet:

Importante: en .env configura OLLAMA_URL=http://ollama:11434: los contenedores se alcanzan entre sí usando nombres de servicio, no localhost.

Seguridad y protección de datos

  • Protección de tokens: guarda el token en .env/Secrets, nunca en el repositorio Git. Si se filtra, usa /revoke en BotFather.
  • Restricción de acceso: permite solo IDs de usuario específicas, de lo contrario cualquiera puede usar tu bot (y tu Ollama):
ALLOWED_USERS = {123456789}  # Tu ID de usuario en Telegram

@dp.message(F.text)
async def handle_message(message: Message):
    if message.from_user.id not in ALLOWED_USERS:
        return
    # ...
  • Aviso de privacidad: los mensajes van a los servidores de Telegram (encriptados cliente-servidor, no E2E). Para chats realmente confidenciales, bots Matrix o bots Signal son mejores opciones.
  • Rate limiting: Telegram permite alrededor de 30 mensajes por segundo, controla el límite en ediciones con streaming.
  • Control de costos: cada solicitud consume CPU en tu servidor, con bots públicos establece límites por usuario. Ver Control de costos.

Errores comunes

  • El bot no responde en grupos: coloca /setprivacy en Disable y vuelve a añadir el bot al grupo.
  • 409 Conflict al iniciar: hay una segunda instancia escuchando el mismo token, termina los procesos/contenedores antiguos.
  • Mensaje demasiado largo: límite de Telegram 4096 caracteres, divide las respuestas (ver código anterior).
  • Errores de Markdown: parse_mode="Markdown" falla con caracteres sin balancear del LLM, usa parse_mode=None o captura errores.
  • La memoria crece: contexto in-memory por usuario sin límite causa bloqueos en operación continua. Establece un límite (ver código) o desplázalo a Redis/BD.

Enlaces relacionados

Puntos clave:

  • Telegram es la plataforma bot más simple: BotFather → Token → aiogram → listo.
  • Polling es suficiente para self-hosters, webhook solo con alta carga o múltiples bots.
  • Extensiones: teclados inline, moderación en grupos, RAG, visión, Whisper para voz.
  • Restringe el acceso a tus IDs de usuario, trata los tokens como secretos.
  • Sin cifrado E2E, para chats confidenciales Matrix o Signal son mejores.

FAQ

¿Tiene costo un bot de Telegram?

No. La API de bots es gratuita e ilimitada (con rate limits). Solo pagas por tu hardware de servidor. El bot no necesita Telegram Premium.

¿Polling o Webhook?

Para self-hosters: polling, sin certificado, sin proxy inverso, funciona detrás de NAT. Webhook solo con alta carga o si ejecutas múltiples bots en un servidor.

¿Por qué el bot no responde en grupos?

Modo privacidad: los bots solo ven /commands y menciones en grupos por defecto. En BotFather establece /setprivacy en Disable y vuelve a añadir el bot al grupo.

¿aiogram o python-telegram-bot?

Ambos son buenos. aiogram es completamente async con un concepto de router limpio y desarrollo activo, para proyectos nuevos la mejor opción. python-telegram-bot es más antiguo y tiene más tutoriales heredados.

¿Pueden varios usuarios usar el bot simultáneamente?

Sí, pero la GPU local es el cuello de botella. Ollama procesa solicitudes en serie, con muchos usuarios usa vLLM o encola respuestas. Ver Múltiples agentes.

¿Cómo recuerda el bot el contexto?

Telegram no proporciona historial: tú almacenas los últimos mensajes por usuario (en el ejemplo un dict en memoria, en producción Redis o SQLite). Sin almacenamiento propio el bot solo conoce el mensaje actual.

¿Son privados los chats?

Parcialmente. Tu backend de IA permanece local, pero todos los mensajes pasan por servidores de Telegram (sin E2E en chats normales). Para máxima privacidad: usa Matrix o Signal.

¿Puede el bot publicar en canales?

Sí, añade el bot como admin al canal y entonces publica automáticamente (ej: reportes de monitoreo, resúmenes de noticias). En canales solo lee comentarios, no posts.

Fuentes y lecturas complementarias

Volver al blog
Share:

Entradas relacionadas