Blog

Cómo montar un bot de Telegram con IA local en tu servidor con Ollama y Python

Abres Telegram, escribes una pregunta en el chat privado de tu bot y, a los pocos segundos, responde un modelo que se ejecuta en tu propio VPS. Ninguna API externa ha leído esa conversación y ningún SaaS la guarda en su cola. La pieza que hace posible ese aislamiento es el long polling: el bot solo abre conexiones salientes, así que no necesitas dominio, certificados ni reglas de firewall. Si ya tienes un VPS con Docker, el stack completo son dos servicios en un compose y un script de Python corto.

Arquitectura del bot: long polling antes que webhook

Un bot de Telegram tiene dos formas de recibir mensajes y la elección condiciona todo el despliegue. El webhook invierte el flujo: Telegram hace un POST HTTPS a la URL pública que registres, por lo que necesitas dominio, proxy inverso con certificado y el puerto 443 abierto. El long polling mantiene el flujo natural: tu script pregunta a la API y Telegram retiene la respuesta hasta que llega algo. Para un primer despliegue en un VPS personal, la segunda opción es la elección sensata.

Cómo funciona getUpdates y el offset

El método getUpdates acepta un parámetro timeout en segundos. Con un timeout alto, Telegram mantiene la petición HTTP abierta hasta que llega un update o vence el plazo, y la respuesta trae todos los mensajes acumulados. Tu código procesa ese lote, guarda el update_id del último, le suma uno y vuelve a llamar con ese offset. El resultado es un bucle casi en tiempo real con una única conexión saliente hacia api.telegram.org.

Montado dentro de un bucle while con un try/except generoso, el bot sobrevive a cortes de red: registra el error, espera unos segundos y reintenta. El servidor no escucha nada hacia Internet y la superficie expuesta queda reducida al token del bot.

Cuándo tendría sentido un webhook

El webhook brilla cuando el bot atiende a mucha gente en paralelo, porque cada mensaje llega al momento y no mantienes conexiones permanentes abiertas. Para un bot personal o de grupo pequeño, esa infraestructura extra es deuda técnica: más piezas que vigilar y un punto de entrada público que proteger. Hay un detalle práctico que conviene conocer desde el principio: la API no permite ambos métodos a la vez, así que si pruebas un webhook, tendrás que llamar a deleteWebhook antes de que getUpdates vuelva a responder.

El stack en Docker Compose: Ollama y el bot

El stack son dos servicios: ollama, que sirve el modelo en el puerto interno 11434, y bot, que ejecuta tu script de Python. Compose crea una red interna entre ambos contenedores y su DNS resuelve el nombre ollama, de modo que tu código llama a http://ollama:11434 sin publicar ese puerto al exterior. La única conexión que sale del servidor es el HTTPS del bot hacia Telegram, y el modelo queda aislado dentro de la red de Docker.

El docker-compose.yml mínimo

Este compose es suficiente para arrancar:

services:
  ollama:
    image: ollama/ollama
    volumes:
      - modelos:/root/.ollama
    restart: unless-stopped

  bot:
    build: ./bot
    env_file: .env
    depends_on:
      - ollama
    restart: unless-stopped

volumes:
  modelos:

Fíjate en que ningún servicio declara ports: nada escucha hacia Internet y el aislamiento lo resuelve la red interna de Compose. El volumen persiste los modelos descargados entre reinicios, y la directiva restart: unless-stopped levanta ambos contenedores si el VPS se apaga y enciende, que es justo lo que exige un servicio siempre disponible. El Dockerfile del bot no necesita nada exótico: parte de una imagen python:3.12-slim, instala requests con pip y lanza el script.

Variables de entorno y token del bot

El token lo genera BotFather en Telegram con el comando /newbot, y conviene guardarlo en un archivo .env fuera del repositorio. Define también en ese archivo el nombre del modelo que usará el bot, algo como MODELO=llama3.2:3b, y así cambias de modelo sin tocar código. Evita escribir el token en los logs: quien lo obtenga puede controlar tu bot por completo.

El bucle de mensajes con la API de Ollama

La llamada a /api/chat con requests

La API de Ollama expone /api/chat con un JSON que contiene el modelo y la lista de mensajes con roles system, user y assistant. Cada llamada es sin estado: tu bot reenvía el historial completo en cada turno, porque Ollama no conserva conversaciones entre peticiones. Esta es la llamada mínima con requests:

import json
import requests

OLLAMA = "http://ollama:11434/api/chat"

def responder(historial, modelo):
    r = requests.post(
        OLLAMA,
        json={"model": modelo, "messages": historial, "stream": False},
        timeout=120,
    )
    r.raise_for_status()
    return r.json()["message"]["content"]

El timeout alto no es casualidad: la primera petición tras arrancar carga el modelo en RAM y en un VPS sin GPU puede tardar bastante más que las siguientes. Ollama mantiene el modelo cargado unos cinco minutos después de cada uso, un comportamiento configurable con el parámetro keep_alive, de modo que las respuestas en caliente llegan rápido.

Streaming con stream: true

Si pasas stream en true, Ollama responde en NDJSON: una línea JSON por fragmento de texto y una última línea con done en true. Con el parámetro stream de requests activado, tu bot itera esas líneas y acumula el contenido del campo message:

def responder_stream(historial, modelo):
    payload = {"model": modelo, "messages": historial, "stream": True}
    with requests.post(OLLAMA, json=payload, stream=True, timeout=300) as r:
        r.raise_for_status()
        for linea in r.iter_lines():
            if linea:
                yield json.loads(linea)["message"]["content"]

Para reproducir el efecto de escritura en vivo, edita el mensaje de Telegram con editMessageText cada dos o tres fragmentos acumulados; editarlo con cada token quema cuota de peticiones sin aportar nada visible. La opción más simple es enviar la acción typing con sendChatAction mientras el modelo genera y publicar el texto completo al terminar. Empieza por la segunda y añade el streaming editado cuando el bot ya funcione.

Memoria de conversación por chat

Historial en RAM y ventana de contexto

La memoria más simple es un diccionario con el chat_id como clave y la lista de mensajes como valor. Antes de cada llamada añades el turno del usuario y, tras la respuesta, el turno del assistant. El límite lo marca la ventana de contexto del modelo: cuando el historial crece demasiado, recorta los turnos antiguos y conserva el prompt de sistema junto a los últimos intercambios. Fijar un máximo de turnos es más sencillo y robusto que contar tokens para un bot personal.

Persistencia ligera con SQLite

Un diccionario en RAM muere con cada redeploy del contenedor, así que vuelca el historial a SQLite con una tabla de mensajes que guarde chat_id, role, content y fecha. Monta la base de datos en un volumen de Docker y sobrevivirá a los rebuilds del contenedor del bot. Añade un comando /reset que borre las filas de ese chat: quien conversa agradece poder empezar de cero y tú controlas el crecimiento de la tabla.

Aísla por chat desde el primer día y evita mandar logs con contenido de mensajes a servicios externos: el contexto debe quedarse en tu servidor. Si dudas entre este enfoque y delegar en un proveedor, la comparativa entre IA local y SaaS deja claro quién controla los datos en cada caso.

Límites de Telegram y rate limiting

Qué límites documenta la API de Bot

Telegram no publica una tabla cerrada de límites, pero su FAQ de bots marca dos reglas concretas: no enviar más de un mensaje por segundo al mismo chat y mantener los envíos masivos por debajo de unos 30 mensajes por segundo. A eso se suma un límite duro de 4096 caracteres por mensaje de texto, que obliga a trocear las respuestas largas antes de enviarlas. Son cifras de la documentación oficial, aunque Telegram advierte de que los límites dinámicos varían con la carga del servidor.

Cola de envíos y manejo del 429

Cuando superas el ritmo permitido, la API responde con el código 429 y un campo parameters que incluye retry_after en segundos. Tu código debe leer ese valor, esperar y reintentar, no machacar la API. Para un bot personal basta con centralizar los envíos en una función que espere lo necesario entre llamada y llamada y gestione el 429 en un solo sitio. Si prefieres no rodar tu propia cola, la librería python-telegram-bot encapsula el polling, las colas y los reintentos, aunque con requests y una función corta llega de sobra.

Despliegue en VPS: del compose al servidor

Qué hardware necesita este stack

Puesta en marcha y mantenimiento

Este diseño mantiene el ciclo completo dentro de tu servidor: los mensajes descansan en tu SQLite, el modelo se ejecuta en tu RAM y la única huella externa es el tráfico con Telegram. El mismo compose admite crecer hacia un vector store para buscar en tus propios documentos, como en la guía de IA local en WordPress con Ollama y Qdrant, o hacia un agente que ejecuta tareas, como los agentes locales con Hermes. Ninguna de esas extensiones obliga a reescribir el bot: son servicios más en la misma red.


Para el primer despliegue, entra por SSH en tu servidor, crea el directorio del compose y ejecuta docker compose up -d: en un minuto tendrás el bot escuchando mensajes. Si aún no tienes máquina, puedes desplegar tu bot en un VPS y dejarlo corriendo las 24 horas; un plan básico aguanta el stack de dos contenedores sin despeinarse. Para dimensionarlo con números, la guía de Self-hosting de IA: VPS, costes y hardware compara las opciones por patrón de uso.

Pruébalo en 3 minutos

Instala Ollama + Open WebUI en tu ordenador, gratis y en privado.