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
| Modo | Cómo funciona | Cuándo usarlo |
|---|---|---|
| Polling | El bot pregunta a Telegram cada pocos segundos por nuevos mensajes | Desarrollo, servidor local sin dominio, detrás de NAT |
| Webhook | Telegram envía actualizaciones a tu endpoint HTTPS | Producció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/revokeen 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
/setprivacyen 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, usaparse_mode=Noneo 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
- IRC-Coding.de: tutoriales de programación avanzada: arquitecturas de bots, Python async, manejo de eventos y más proyectos bot.
- Fundamentos de chatbot: basados en reglas vs. IA, agentes vs. chatbots.
- Discord bot fundamentos: plataforma alternativa.
- API de Ollama: la interfaz HTTP en detalle.
- RAG local: conocimiento de documentos para el bot.
- Docker: fundamentos de contenedores.
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?
¿Polling o Webhook?
¿Por qué el bot no responde en grupos?
¿aiogram o python-telegram-bot?
¿Pueden varios usuarios usar el bot simultáneamente?
¿Cómo recuerda el bot el contexto?
¿Son privados los chats?
¿Puede el bot publicar en canales?
Fuentes y lecturas complementarias
- Telegram Bot API: Referencia oficial.
- aiogram: Documentación del framework de Python.
- Ollama: Servidor de modelos local.
- IRC-Coding.de: Tutoriales de programación para desarrollo de bots.


