Skip to content
BotServBotServ
TelegramBotKI-BotOllamaaiogramPythonSelf-Hosting

Telegram-Bot mit lokaler KI

Telegram-Bot mit Ollama bauen: BotFather, Polling vs. Webhook, Inline-Keyboards, Gruppen, RAG und Docker-Deployment.

S

schutzgeist

8 min read
Telegram-Bot mit lokaler KI

Telegram-Bot mit lokaler KI

Was dieser Artikel behandelt

  • Wie Du einen Telegram-Bot mit der offiziellen Bot-API und Ollama baust.
  • BotFather-Setup, Polling vs. Webhooks und Token-Verwaltung.
  • Komplette Code-Beispiele mit aiogram (Python).
  • Erweiterungen: Inline-Keyboards, Gruppen-Moderation, RAG, Bilder und Sprachnachrichten.
  • Docker-Deployment, systemd-Service und Betrieb im Dauerbetrieb.

Einleitung

Telegram ist die bot-freundlichste Plattform überhaupt: Die Bot-API ist kostenlos, offiziell dokumentiert und braucht kein Business-Konto, keine Genehmigung und keine Zertifikate. Einen Bot legst Du in zwei Minuten mit dem @BotFather an, danach kann er Nachrichten empfangen, beantworten und eigene KI-Funktionen ausführen.

Kombiniert mit Ollama bekommst Du einen Chat-Assistenten, der komplett auf Deinem Server läuft: Fragen beantworten, Texte zusammenfassen, Dokumente durchsuchen (RAG), Gruppen moderieren, ohne dass Chat-Inhalte an OpenAI oder andere Cloud-Anbieter gehen.

Typische Anwendungsszenarien

  • Persönlicher KI-Assistent: Chatte mit Deinem lokalen LLM vom Handy, von unterwegs, ohne VPN zum Webinterface.
  • Community-Support: Beantworte wiederkehrende Fragen in Deiner Telegram-Gruppe automatisch.
  • Benachrichtigungs-Bot: Monitoring-Alerts, Backup-Status oder smarte Home-Events direkt in Telegram.
  • Dokumenten-Assistent: Schick dem Bot ein PDF, er fasst es zusammen (RAG + OCR).
  • Moderation: Spam-Erkennung, Begrüßung neuer Mitglieder, automatische Antworten auf Regelverstöße.
  • Eingangskanal für Agenten: Telegram als Frontend für Deine KI-Agenten: Aufgaben per Chat auslösen.

Voraussetzungen

  • Telegram-Account
  • Server mit Ollama (mindestens ein Modell geladen, z. B. llama3.1:8b)
  • Python 3.10+
  • Optional: Docker für den Betrieb, Domain + Reverse Proxy für Webhooks

Schritt 1: Bot beim BotFather anlegen

Öffne Telegram und schreibe @BotFather an:

/newbot

BotFather fragt nach Name (Anzeigename, z. B. „Mein KI-Assistent”) und Username (muss auf bot enden, z. B. mein_ki_bot). Du bekommst einen Token:

123456789:AAEhBOweik5ad9rQXMENKJBI...

Den Token niemals in den Code schreiben, immer als Umgebungsvariable oder über ein Secret-Management. Wer den Token hat, kontrolliert den Bot.

Nützliche BotFather-Befehle für später:

/setdescription     # Beschreibung im Bot-Profil
/setcommands        # Befehlsliste (/start, /hilfe, ...)
/setprivacy         # Wichtig für Gruppen: Disable = Bot liest alle Nachrichten
/setjoingroups      # Ob der Bot zu Gruppen hinzugefügt werden darf

Schritt 2: Minimaler Bot mit aiogram

aiogram ist das aktuell gepflegte Python-Framework für die Telegram Bot API (async, Type-Hints, Router-Konzept). python-telegram-bot ist die Alternative, beide funktionieren.

pip install aiogram ollama python-dotenv

.env-Datei:

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 = """Du bist ein hilfreicher Assistent auf Telegram.
Antworte kurz und präzise. Maximal 3-4 Absätze.
Nutze einfache Sprache, keine Fachbegriffe ohne Erklärung."""


@dp.message(Command("start"))
async def cmd_start(message: Message):
    await message.answer(
        "Hallo! Ich bin ein KI-Bot mit lokalem Modell. "
        "Schreib mir einfach eine Nachricht oder nutze /hilfe."
    )


@dp.message(Command("hilfe"))
async def cmd_help(message: Message):
    await message.answer(
        "Befehle:\n"
        "/start - Bot starten\n"
        "/reset - Konversation zurücksetzen\n"
        "/modell - Zeigt das aktuelle Modell\n"
        "Einfach losschreiben für KI-Antworten."
    )


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


# Konversations-Kontext pro User (einfacher In-Memory-Speicher)
conversations: dict[int, list] = {}

@dp.message(Command("reset"))
async def cmd_reset(message: Message):
    conversations.pop(message.from_user.id, None)
    await message.answer("Konversation zurückgesetzt.")


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

    # "Schreibt..."-Indikator anzeigen
    await bot.send_chat_action(message.chat.id, "typing")

    # Kontext aufbauen (max. letzte 10 Nachrichten)
    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 limitiert Nachrichten auf 4096 Zeichen
        for i in range(0, len(answer), 4000):
            await message.answer(answer[i:i + 4000])

    except Exception as e:
        await message.answer("Fehler beim Generieren der Antwort. Ist Ollama erreichbar?")
        print(f"Ollama-Fehler: {e}")


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

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

Starten:

python bot.py

Schreib dem Bot in Telegram eine Nachricht, er antwortet mit dem lokalen Modell. Das war’s: Die einfachste Plattform-Integration überhaupt.

Polling vs. Webhook

ModusWie es funktioniertWann nutzen
PollingBot fragt Telegram alle paar Sekunden nach neuen NachrichtenEntwicklung, Home-Server ohne Domain, hinter NAT
WebhookTelegram pusht Updates an Deinen HTTPS-EndpointProduktion mit Domain, hohe Nachrichtenlast, schnellere Antwortzeiten

Polling ist für Self-Hoster meist ausreichend und deutlich einfacher, kein Reverse Proxy, kein TLS-Zertifikat nötig. Webhook lohnt sich bei vielen Nutzern oder wenn Du mehrere Bots auf einem Server betreibst.

Webhook-Setup mit aiogram:

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

async def main():
    await bot.set_webhook(
        "https://bots.deine-domain.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)

Dazu ein Reverse Proxy (Nginx/Caddy) mit TLS, siehe Reverse Proxy.

Erweiterungen

Inline-Keyboards: Buttons unter Nachrichten

Keyboards eignen sich für Auswahlmöglichkeiten, etwa „Modell wechseln” oder „Ergebnis bewerten”:

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

@dp.message(Command("modellwahl"))
async def cmd_modellwahl(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("Welches Modell?", 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"Modell gesetzt: `{model}`", parse_mode="Markdown")
    await callback.answer()

Gruppen-Betrieb und Moderation

Damit der Bot in Gruppen alle Nachrichten liest (nicht nur /commands), muss im BotFather /setprivacy auf Disable stehen. Danach:

@dp.message(F.text & F.chat.type.in_({"group", "supergroup"}))
async def group_handler(message: Message):
    # Nur antworten, wenn der Bot erwähnt wird
    if f"@{BOT_USERNAME}" in message.text:
        await handle_message(message)
        return

    # Einfache KI-Moderation: Nachricht klassifizieren lassen
    check = ollama_client.chat(
        model=MODEL,
        messages=[{
            "role": "user",
            "content": f"Klassifiziere 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: Bot beantwortet Fragen aus Deinen Dokumenten

Der Bot durchsucht eine Vektordatenbank, bevor er antwortet. Aufbau wie in Lokales RAG:

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"])

Bilder und Sprachnachrichten

Telegram liefert Dateien über getFile/download. Sprachnachrichten lassen sich an Whisper schicken, Bilder an ein Vision-Modell wie llava oder 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)   # lokales Whisper
    await message.answer(f"Verstanden: „{text}“\n\n")
    # danach wie normale Textnachricht behandeln


@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"])

Streaming-Antworten

Ollama kann streamen; Telegram-Nachrichten lassen sich nachträglich editieren:

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:          # nicht jedes Token senden (Rate-Limit!)
        await sent.edit_text(text)
await sent.edit_text(text)

Betrieb: Docker und 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 im selben 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 für GPU siehe Docker-Artikel

volumes:
  ollama_data:

networks:
  botnet:

Wichtig: In .env dann OLLAMA_URL=http://ollama:11434: Container erreichen sich über Service-Namen, nicht localhost.

Sicherheit und Datenschutz

  • Token-Schutz: Token in .env/Secrets, nie im Git-Repo. Bei Leak beim BotFather /revoke.
  • Zugriffsbeschränkung: Erlaube nur bestimmte User-IDs, sonst kann jeder Deinen Bot (und Dein Ollama) nutzen:
ALLOWED_USERS = {123456789}  # Deine Telegram-User-ID

@dp.message(F.text)
async def handle_message(message: Message):
    if message.from_user.id not in ALLOWED_USERS:
        return
    # ...
  • Datenschutz-Hinweis: Nachrichten gehen an Telegrams Server (Client-Server-verschlüsselt, nicht E2E). Für wirklich vertrauliche Chats eignen sich Matrix-Bots oder Signal-Bots besser.
  • Rate-Limiting: Telegram erlaubt ~30 Nachrichten/Sekunde; bei Streaming-Edits Drossel einbauen.
  • Kostenkontrolle: Jeder Aufruf kostet Rechenzeit auf Deinem Server, bei öffentlichen Bots Limits pro User setzen. Siehe Kostenkontrolle.

Typische Stolpersteine

  • Bot antwortet in Gruppen nicht: /setprivacy auf Disable stellen, Bot neu zur Gruppe hinzufügen.
  • 409 Conflict beim Start: Eine zweite Instanz pollt denselben Token, alte Prozesse/Container killen.
  • Nachricht zu lang: Telegram-Limit 4096 Zeichen: Antworten splitten (siehe Code oben).
  • Markdown-Fehler: parse_mode="Markdown" wirft Fehler bei unbalancierten Zeichen aus dem LLM, zur Not parse_mode=None oder Fehler abfangen.
  • Speicher wächst: In-Memory-Kontext pro User nie begrenzen → Langzeitbetrieb bläht RAM auf. Limit setzen (siehe Code) oder auf Redis/DB auslagern.

Key Takeaways:

  • Telegram ist die einfachste Bot-Plattform: BotFather → Token → aiogram → fertig.
  • Polling reicht für Self-Hoster; Webhook nur bei Last oder mehreren Bots.
  • Erweiterungen: Inline-Keyboards, Gruppen-Moderation, RAG, Vision, Whisper für Sprache.
  • Zugriff auf eigene User-IDs beschränken, Token als Secret behandeln.
  • Nicht E2E-verschlüsselt, für vertrauliche Chats besser Matrix oder Signal.

FAQ

Kostet ein Telegram-Bot etwas?

Nein. Die Bot-API ist kostenlos und unbegrenzt nutzbar (mit Rate-Limits). Du zahlst nur Deine Server-Hardware. Telegram Premium braucht der Bot nicht.

Polling oder Webhook?

Für Self-Hoster: Polling, kein Zertifikat, kein Reverse Proxy, funktioniert hinter NAT. Webhook nur bei hoher Last oder wenn Du mehrere Bots auf einem Server betreibst.

Warum antwortet der Bot in Gruppen nicht?

Privacy Mode: Bots sehen in Gruppen standardmäßig nur /commands und Erwähnungen. Im BotFather /setprivacy auf Disable setzen und den Bot neu zur Gruppe hinzufügen.

aiogram oder python-telegram-bot?

Beide sind gut. aiogram ist komplett async mit sauberem Router-Konzept und aktiver Entwicklung, für neue Projekte die bessere Wahl. python-telegram-bot ist älter und hat mehr Legacy-Tutorials.

Können mehrere Nutzer den Bot gleichzeitig nutzen?

Ja, aber die lokale GPU ist der Flaschenhals. Ollama verarbeitet Requests seriell; bei vielen Nutzern vLLM einsetzen oder Antworten queuen. Siehe Mehrere Agenten.

Wie merkt sich der Bot den Kontext?

Telegram liefert keinen Verlauf: Du speicherst die letzten Nachrichten pro User selbst (im Beispiel ein In-Memory-Dict, für Produktion Redis oder SQLite). Ohne eigenen Speicher kennt der Bot nur die aktuelle Nachricht.

Sind die Chats privat?

Teilweise. Dein KI-Backend bleibt lokal, aber alle Nachrichten laufen über Telegrams Server (keine E2E in Normal-Chats). Für maximale Privacy: Matrix oder Signal nutzen.

Kann der Bot in Kanälen posten?

Ja, den Bot als Admin in den Kanal aufnehmen, dann sendet er automatisch (z. B. Monitoring-Reports, News-Zusammenfassungen). Lesen kann er in Kanälen nur Kommentare, keine Posts.

Quellen und weiterführende Literatur

Zurück zum KI Blog
Share:

Ähnliche Beiträge