Dein persönlicher KI-Chatbot für Blogs: Open Source & lokal

Ein Konsolen-Fenster vor einem bunten Farbwirbel

Immer wieder stoße ich im Internet auf das Angebot, mir eine ‚eigene KI‘ zu programmieren. Dahinter verbergen sich allerdings höchst unterschiedliche Dinge. In den meisten Fällen wird nicht tatsächlich ein KI-Modell programmiert. Stattdessen werden bestimmte Einstellungen zum Chatten festgelegt (z.B. ‚Gib möglichst kurze Antworten‘ oder ‚Sprich mich mit Du und Nele an‘) und/oder bestimmte Quellen festgelegt, auf die sich ein genutzter Chatbot in der Antwort beziehen soll.

Der Fachbegriff für letzteres lautet: Retrieval Augmented Generation, abgekürzt RAG. Solch eine RAG-Herangehensweise ermöglicht es, einem KI-Modell eine bestimmte Datenbasis zur Verfügung zu stellen, auf die sich der Chatbot in seinen Antworten dann vorrangig beziehen soll.

Ich habe RAG bereits vielfach auf meinem eigenen KI-Server ausprobiert. Meistens habe ich das aber auf einzelne Inhalte (z.B. auf einen bestimmten Blogbeitrag oder ein Transkript) bezogen, nicht auf umfangreichere Quellen wie z.B. alle Beiträge eines bestimmten Blogs. Genau dafür suchte ich jetzt nach einer Lösung. In diesem Zusammenhang wollte ich zugleich eine Lösung entwickeln, die auch andere möglichst ohne Kosten und umfangreiche technische Vorkenntnisse für sich nutzen können und die mit Open Source Modellen sowie unabhängig von proprietären KI-Plattformen funktioniert. Das Vorhaben ist somit – neben dem praktischen Nutzen – vor allem auch ein Projekt für mehr digitale Mündigkeit. Mein Vorgehen habe ich als Schritt-für Schritt Anleitung aufgeschrieben.

Das klingt für dich interessant? Dann lies weiter und probiere die Anleitung sehr gerne für dich aus. Vielleicht hast du ja auch Lust, sie weiter zu verbessern!

Grundsätzliche Schritte

Unser Ziel ist ein RAG-Chatbot für Blog-Websites im Terminal deines Rechners (= dem schwarzen Fenster, in das sich Befehle eintippen lassen). Du kannst den Chatbot auch dann bauen, wenn du bisher noch nicht mit dem Terminal gearbeitet hast.

Wir gehen in mehreren Schritten vor: Nach ein paar Vorbereitungen nutzen wir zuerst ein Programm, mit dem wir alle Beiträge von den von uns gewünschten Blogs herunterladen. Dabei überprüfen wir natürlich zunächst, ob die Blog-Inhaber*innen solch ein Scraping untersagen. Wenn nein, dann versuchen wir die Beiträge entweder über die REST-API (das funktioniert oft bei WordPress-Blogs; es ist ein maschinenlesbarer Zugang zu den Daten der Website) oder ansonsten über den RSS-Feed oder die Sitemap zu erhalten. Wir müssen dazu lediglich die URL (= die Internetadresse) der gewünschten Blogs eingegeben.

Aus den eingesammelten Beiträgen gestalten wir dann – mit einem weiteren Open Source Programm – eine Vektor-Datenbank. Das ist eine komprimierte und maschinenlesbare Variante der Inhalte, die als Datenbasis von KI-Sprachmodellen genutzt werden kann. Diese Datenbank verbinden wir mit dem Open Source KI-Modell Mistral.

Im letzten Schritt nutzen wir dann ein weiteres Programm – den eigentlichen Chatbot – um in die Kommunikation über die Beiträge in den Blogs zu gehen. Das funktioniert dann weiterhin über das Terminal, aber du wirst sehen, dass du im Ergebnis dann einen Bot gestaltet hast, der ähnlich wie andere KI-Chatbots menschliche Sprache auswerten und mit passenden Reaktionen – in unserem Fall mit den Inhalten basierend auf den eingepflegten Blogbeiträgen – antworten kann.

Die folgende Anleitung stellt dir den Code für das ‚Beitragseinsammel-Programm‘ und das ‚Chatbot-Programm‘ zum direkten Weiternutzen zur Verfügung. Ich empfehle dir, erst einmal damit zu arbeiten. Später kannst du aber natürlich diese Programme auch anpassen und deinen Chatbot so noch sehr viel spezifischer für deine Bedürfnisse gestalten.

Wozu das Ganze?

Ich finde dieses Projekt aus mehreren Gründen ziemlich cool:

  1. Es ist ein offenes Projekt, das dir die Potenziale von Open Source Software zeigt und dir eine Alternative zu den proprietären KI-Plattformen ermöglicht.
  2. Es ist ein Projekt für mehr digitale Mündigkeit. Du verstehst und kannst erkunden, wie KI-Chatbots unter der Haube funktionieren.
  3. Du bekommst eine sehr nützliche Anwendung, mit der du Online-Inhalte aus von dir ausgewählten Blogs über eine Chatoberfläche in einem vernetzten Sinne ‚lesen‘ kannst.

Schritt-für-Schritt Anleitung

Insgesamt wirst du für die folgenden Schritte wahrscheinlich ungefähr eine Stunde Zeit brauchen. Du kannst jederzeit zwischendrin aufhören und wieder neu einsteigen.

(Einschränkend möchte ich anmerken, dass ich die Schritte auf meinem Linux-Laptop durchgeführt habe. Ich gebe auch die entsprechenden Befehle für Windows und Mac an, aber habe diese nicht selbst getestet. Entstanden ist die Anleitung basierend auf meiner Ideen dazu, allgemeiner Internet-Recherche und Interaktion mit KI-Chatbots)

Schritt 1: Vorbereitungen

Um solch einen RAG-Chatbot für Blog-Websites im Terminal selbst zu basteln, sind zunächst drei Vorbereitungen nötig:

  1. Du solltest grundlegende Befehle im Terminal kennen.
  2. Du solltest dir Python installieren.
  3. Du solltest dir einen Mistral-API-Key besorgen.

1. Grundlegende Befehle im Terminal kennen

Zunächst zu den grundlegenden Befehlen im Terminal: Diese unterscheiden sich, je nachdem, ob du Linux, Windows oder einen Mac nutzt. Du findest das Terminal mit diesen Eingaben – oder auch einfach über eine Programmsuche:

Linux:

  • Tastenkombination Strg + Alt + T oder
  • Suche nach „Terminal/ Konsole“ in deinen Anwendungen

Windows:

  • Tastenkombination Windows-Taste + R, dann cmd eingeben und Enter drücken oder
  • Suche nach „Eingabeaufforderung“ bzw. „PowerShell“ im Startmenü

Mac:

  • Tastenkombination Cmd + Leertaste, dann „Terminal“ eingeben oder
  • Gehe zu Programme → Dienstprogramme → Terminal

Es öffnet sich dann ein kleines Konsolen-Fenster, über das du an deinen Rechner bestimmte Befehle senden kannst.

(Ich habe die Terminal-Nutzung in den letzten Monaten übrigens sehr viel in Interaktion mit KI-Chatbots gelernt. Vor allem die Anthropic-Modelle funktionieren dazu aus meiner Sicht gut. Du kannst dann einfach im KI-Chat fragen, was der richtige Befehl für das Terminal ist, diesen dann im Zweifel noch einmal im Internet gegenchecken, dann eingeben – und wenn du dann einen irritierenden Output bekommst oder irgend etwas nicht funktioniert, diesen wiederum kopieren, mit dem KI-Chatbot teilen und dir erklären lassen.)

Hier kommt eine kleine Basis-Übung, um das Prinzip des Terminals zu verstehen.

Du brauchst dazu diese Befehle:

Linux/Mac:

  • mkdir ~/Verzeichnis – um ein Verzeichnis zu erstellen
  • cd ~/Verzeichnis – um in das Verzeichnis zu wechseln
  • nano Dateiname – um eine Datei zu erstellen und/oder zu öffnen
  • Nach dem Eintragen dann Strg+O und Enter und Strg+X, um das Programm zu speichern und zu schließen

Windows (PowerShell):

  • mkdir ~/Verzeichnis – um ein Verzeichnis zu erstellen
  • cd ~/Verzeichnis – um in das Verzeichnis zu wechseln
  • notepad Dateiname.txt – um eine Datei zu erstellen und/oder zu öffnen
  • Nach dem Eintragen über Datei → Speichern und dann Fenster schließen

Windows (CMD):

  • mkdir %USERPROFILE%\Verzeichnis – um ein Verzeichnis zu erstellen
  • cd %USERPROFILE%\Verzeichnis – um in das Verzeichnis zu wechseln
  • notepad Dateiname.txt – um eine Datei zu erstellen und/oder zu öffnen

Mit diesen Befehlen kannst du die Terminal-Nutzung testen:

Für Linux/Mac gib im Terminal ein:

mkdir ~/mein-testverzeichnis

Dann drücke Enter.

cd ~/mein-testverzeichnis

Dann drücke Enter.

nano testdatei.txt

Dann drücke Enter.

Für Windows (PowerShell) gib im Terminal ein:

mkdir ~/mein-testverzeichnis

Dann drücke Enter.

cd ~/mein-testverzeichnis

Dann drücke Enter.

notepad testdatei.txt

Dann drücke Enter.

Für Windows (CMD) gib im Terminal ein:

mkdir %USERPROFILE%\mein-testverzeichnis

Dann drücke Enter.

cd %USERPROFILE%\mein-testverzeichnis

Dann drücke Enter.

notepad testdatei.txt

Dann drücke Enter.

Es öffnet sich ein Eingabefeld. Bei Linux/Mac ist das direkt im Terminal (nano-Editor), bei Windows öffnet sich der Notepad-Editor. Schreibe irgend etwas rein. Zum Beispiel:

Hallo, ich erstelle eine Datei mithilfe des Terminals :-)

Bei Linux/Mac: Dann Strg+O und Enter und Strg+X

Bei Windows: Speichere über Datei → Speichern und schließe das Notepad-Fenster

Nach dem Schließen gelangst du wieder zum ursprünglichen Terminal-Fenster zurück. Dass deine Befehle erfolgreich waren, siehst du, wenn du deine Ordnerverwaltung öffnest und darin tatsächlich den erstellten Ordner ‚mein-testverzeichnis‘ mit der Datei testdatei.txt und deinem Inhalt darin findest.

(Ich fand das ziemlich großartig, als ich das zum ersten Mal gemacht habe. Wenn es auch dein erstes Mal ist, dann kannst du dich erst einmal freuen, dass du jetzt einen ziemlich coolen Weg kennst, um mit deinem Gerät zu kommunizieren! Wir werden das im Folgenden nutzen!)

2. Python installieren

Mit diesem Basiswissen können wir uns dann direkt an die Installation von Python machen. Das ist eine Programmiersprache, in der die Programme geschrieben sein werden, mit denen wir arbeiten.

Ob Python bei dir ohnehin schon installiert ist, kannst du überprüfen mit diesem Befehl im Terminal:

python --version

oder

python3 --version

Wenn dann eine Versionsnummer zurückkommt (z.B. Python 3.13.11), ist Python installiert.

Wenn nicht, dann kannst du es folgendermaßen installieren:

Linux (Ubuntu/Debian):

sudo apt update
sudo apt install python3 python3-pip

Mac:

brew install python3

(Falls Homebrew nicht installiert ist, folge dieser Anleitung.)

Windows:

  1. Gehe auf https://www.python.org/downloads/
  2. Lade die neueste Version herunter
  3. Führe den Installer aus und setze unbedingt den Haken bei „Add Python to PATH“
  4. Klicke auf „Install Now“

3. Mistral API-Key besorgen

Als letztes benötigst du noch einen Mistral API Key. Mistral ist ein europäisches Open Source KI-Modell. Wir nutzen im Folgenden Mistral-small, was in einem begrenzten, aber für den persönlichen Gebrauch erfahrungsgemäß mehr als ausreichendem Umfang für Entwickler*innen kostenfrei zur Verfügung steht.

(Mit dieser sehr umfangreichen, kostenfreien Entwicklungsmöglichkeit mit der Mistral-API verfolgt das Unternehmen übrigens eine aus meiner Sicht sehr stimmige Open Source Strategie: Je mehr Menschen mit der Mistral-API erkunden und ausprobieren, desto mehr Ideen und Produkte werden auf dieser Basis auch entwickelt, von denen einige dann später einen kostenpflichtigen Plan in Anspruch nehmen werden.)

Um dir einen Mistral-API-Key zu besorgen, öffnest du diese Website, registrierst dich, gehst auf API und erstellst dir einen API-Key und speicherst ihn ab. Du kannst ihn z.B. „Blog-Chatbot“ nennen. Den API Key speicherst du dir ab, so dass du ihn später nutzen kannst.

Wichtig: Du solltest den API-Key nicht öffentlich teilen!

Schritt 2: Projekt aufsetzen

In diesem Schritt sind wir weiter vorbereitend tätig, aber du kannst direkt deine Learnings aus Schritt 1 anwenden. Wir benötigen nämlich eine Projektumgebung (= einen Ordner auf deinem Rechner, in dem du deinen KI-Chatbot und die Inhalte speicherst). In diesem Ordner speicherst du dann deinen Mistral-API-Key und die weiteren, benötigten Programme.

1. Projektverzeichnis erstellen

Zunächst erstellen wir die Projektumgebung:

Linux/Mac:

mkdir ~/blog-chatbot-projekt
cd ~/blog-chatbot-projekt

Windows (PowerShell):

mkdir ~/blog-chatbot-projekt
cd ~/blog-chatbot-projekt

Windows (CMD):

mkdir %USERPROFILE%\blog-chatbot-projekt
cd %USERPROFILE%\blog-chatbot-projekt

2. Virtuelle Umgebung erstellen

Dann erstellen wir eine virtuelle Umgebung. Das ist sinnvoll, weil damit alle installierten Programme nur für dieses Projekt gelten und nicht mit anderen Python-Projekten auf deinem Rechner in Konflikt kommen können.

Linux/Mac:

python3 -m venv venv
source venv/bin/activate

Windows (PowerShell):

python -m venv venv
.\venv\Scripts\Activate.ps1

(Falls du eine Fehlermeldung bekommst, führe einmalig aus: Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser)

Windows (CMD):

python -m venv venv
venv\Scripts\activate.bat

Nach der Aktivierung siehst du (venv) am Anfang deiner Kommandozeile – das zeigt, dass die virtuelle Umgebung aktiv ist.

3. API-Key speichern

Jetzt speichern wir deinen Mistral-API-Key in einer Datei, die wir so erstellen, wie du es eingangs mit der Testdatei gelernt hast:

Linux/Mac:

nano .env

Windows:

notepad .env

Trage dann folgendes ein (ersetze YOUR_API_KEY durch deinen tatsächlichen Key, den du dir auf der Mistral-Website erstellt hast):

MISTRAL_API_KEY=dein_api_key_hier

Bei Linux/Mac: Speichere mit Strg+O, Enter, Strg+X

Bei Windows: Speichere über Datei → Speichern und schließe Notepad

4. Benötigte Programme installieren

Jetzt installieren wir die Programme, die wir für unseren Chatbot brauchen:

  • chromadb – Die Vektor-Datenbank, in der die Blog-Beiträge gespeichert werden
  • sentence-transformers – Für die semantische Suche und Einbettung der Texte
  • requests – Um Webseiten abzurufen
  • mistralai – Das Programm, um mit der Mistral-API zu kommunizieren
  • python-dotenv – Um den API-Key aus der .env-Datei zu laden
  • feedparser – Um RSS-Feeds zu lesen
  • beautifulsoup4 – Um HTML-Inhalte zu verarbeiten

Bei all diesen Programmen handelt es sich um Open Source Programme. Die Installation funktioniert über eine Befehlseingabe zur Installation (und etwas Zeit).

Für alle Betriebssysteme (wenn virtuelle Umgebung aktiviert ist) gib diesen Befehl im Terminal ein:

pip install chromadb sentence-transformers requests mistralai python-dotenv feedparser beautifulsoup4

Dieser Installationsvorgang kann einige Minuten dauern. Lass das Terminal einfach arbeiten, bis du wieder die ursprüngliche Eingabeaufforderung siehst.

Schritt 3: Blogs einsammeln und Vektordatenbank erstellen

Mit der eingerichteten Basis aus Schritt 2 können wir jetzt das Blog-Einsammel-Programm erstellen und ausführen, so dass damit unsere Vektor-Datenbank erstellt wird. Ich nenne dieses Programm im Folgenden ‚Verwaltungsprogramm‘.

1. Das Verwaltungsprogramm erstellen

Du gehst wie folgt vor:

Linux/Mac:

nano blog_verwalten.py

Windows:

notepad blog_verwalten.py

Dann trägst du diesen Inhalt ein:

import requests
import chromadb
from chromadb.utils import embedding_functions
from bs4 import BeautifulSoup
import feedparser
from urllib.parse import urlparse
from urllib.robotparser import RobotFileParser

# Vektordatenbank-Setup
client = chromadb.PersistentClient(path="./chroma_db")
embedding_fn = embedding_functions.SentenceTransformerEmbeddingFunction(
    model_name="paraphrase-multilingual-MiniLM-L12-v2"
)
collection = client.get_or_create_collection(
    name="blog_posts",
    embedding_function=embedding_fn
)

def ist_gueltige_wahl(eingabe, optionen):
    """Prüft ob Eingabe eine gültige Menü-Option ist"""
    return eingabe in optionen

def blog_existiert(blog_url):
    """Prüft ob Blog bereits in Datenbank ist"""
    alle_daten = collection.get()
    for meta in alle_daten['metadatas']:
        if meta['blog_url'] == blog_url:
            return True
    return False

def pruefe_robots_txt(blog_url):
    """Prüft, ob Scraping erlaubt ist"""
    print("[1/5] Prüfe robots.txt... ", end="", flush=True)
    try:
        rp = RobotFileParser()
        rp.set_url(f"https://{blog_url}/robots.txt")
        rp.read()
        if rp.can_fetch("*", f"https://{blog_url}/"):
            print("✅ Scraping erlaubt")
            return True
        else:
            print("❌ Blog erlaubt kein automatisches Auslesen")
            print("\n💡 Respektiere die Vorgaben des Blogs!")
            print("   Alternative: XML-Export hochladen (falls du Zugriff hast)\n")
            return False
    except:
        # Keine robots.txt = erlaubt
        print("✅ Keine robots.txt (= erlaubt)")
        return True

def hole_via_wordpress_api(blog_url):
    """Versucht Beiträge über WordPress REST API zu holen"""
    print("[2/5] WordPress REST API... ", end="", flush=True)
    alle_posts = []
    seite = 1
    try:
        while True:
            url = f"https://{blog_url}/wp-json/wp/v2/posts?per_page=100&page={seite}"
            response = requests.get(url, timeout=10)
            if response.status_code != 200 or not response.json():
                break
            alle_posts.extend(response.json())
            seite += 1

        if alle_posts:
            print(f"✅ {len(alle_posts)} Beiträge gefunden")
            return [(p['title']['rendered'],
                    BeautifulSoup(p['content']['rendered'], 'html.parser').get_text(),
                    p['link'])
                   for p in alle_posts]
        else:
            print("❌ nicht verfügbar")
            return []
    except:
        print("❌ nicht verfügbar")
        return []

def hole_via_rss(blog_url):
    """Versucht Beiträge über RSS-Feed zu holen"""
    print("[3/5] RSS-Feed Standard... ", end="", flush=True)
    feed_urls = [
        f"https://{blog_url}/feed/",
        f"https://{blog_url}/rss/",
        f"https://{blog_url}/feed.xml",
        f"https://{blog_url}/rss.xml"
    ]

    for feed_url in feed_urls:
        try:
            feed = feedparser.parse(feed_url)
            if feed.entries:
                print(f"✅ {len(feed.entries)} Beiträge gefunden")
                return [(e.title,
                        BeautifulSoup(e.get('content', [{}])[0].get('value', e.get('summary', '')), 'html.parser').get_text(),
                        e.link)
                       for e in feed.entries]
        except:
            continue

    print("❌ nicht verfügbar")
    return []

def hole_via_rss_pagination(blog_url):
    """Versucht RSS mit Pagination"""
    print("[4/5] RSS mit Pagination... ", end="", flush=True)
    alle_posts = []
    seite = 2  # Seite 1 hatten wir schon
    try:
        while seite <= 50:  # Max 50 Seiten
            feed_url = f"https://{blog_url}/feed/?paged={seite}"
            feed = feedparser.parse(feed_url)
            if not feed.entries:
                break
            alle_posts.extend(feed.entries)
            seite += 1

        if alle_posts:
            print(f"✅ weitere {len(alle_posts)} Beiträge")
            return [(e.title,
                    BeautifulSoup(e.get('content', [{}])[0].get('value', e.get('summary', '')), 'html.parser').get_text(),
                    e.link)
                   for e in alle_posts]
        else:
            print("❌ nicht verfügbar")
            return []
    except:
        print("❌ nicht verfügbar")
        return []

def blog_hinzufuegen(blog_url):
    """Holt alle Beiträge von einem Blog"""
    # Prüfen ob Blog schon existiert
    if blog_existiert(blog_url):
        print(f"\n⚠️  Blog {blog_url} ist bereits gespeichert!")
        print("💡 Tipp: Erst löschen (Option 3), dann neu hinzufügen.\n")
        return

    print(f"\n📥 Versuche Beiträge von {blog_url} zu holen...\n")

    # 1. robots.txt prüfen
    if not pruefe_robots_txt(blog_url):
        return

    # 2-4. Verschiedene Methoden probieren
    alle_beitraege = []

    beitraege = hole_via_wordpress_api(blog_url)
    if beitraege:
        alle_beitraege.extend(beitraege)
    else:
        beitraege = hole_via_rss(blog_url)
        if beitraege:
            alle_beitraege.extend(beitraege)
            # Versuche noch mehr zu holen
            mehr = hole_via_rss_pagination(blog_url)
            if mehr:
                alle_beitraege.extend(mehr)

    print(f"[5/5] Zusammenfassung... ", end="", flush=True)

    if not alle_beitraege:
        print("")
        print("\n❌ Keine Beiträge gefunden!\n")
        print("Mögliche Gründe:")
        print("• API/Feed sind deaktiviert")
        print("• URL ist nicht erreichbar")
        print("• Blog nutzt ungewöhnliche Struktur")
        print("\n💡 Alternative: XML-Export hochladen\n")
        return

    print(f"✅ {len(alle_beitraege)} Beiträge insgesamt")
    print("\n💾 Speichere in Datenbank...")

    for i, (titel, inhalt, link) in enumerate(alle_beitraege):
        collection.add(
            documents=[inhalt],
            metadatas=[{
                "title": titel,
                "link": link,
                "blog_url": blog_url
            }],
            ids=[f"{blog_url}_{i}"]
        )
        if (i+1) % 50 == 0:
            print(f"  {i+1} Beiträge verarbeitet...")

    print(f"✓ Fertig! {len(alle_beitraege)} Beiträge von {blog_url} gespeichert.\n")

def blogs_anzeigen():
    """Zeigt alle gespeicherten Blogs"""
    alle_daten = collection.get()
    if not alle_daten['metadatas']:
        print("\n❌ Noch keine Blogs gespeichert!\n")
        return

    blogs = {}
    for meta in alle_daten['metadatas']:
        blog = meta['blog_url']
        blogs[blog] = blogs.get(blog, 0) + 1

    print(f"\n📚 Gespeicherte Blogs ({len(blogs)}):")
    for blog, anzahl in sorted(blogs.items()):
        print(f"  • {blog} ({anzahl} Beiträge)")
    print()

def blog_loeschen(blog_url):
    """Löscht alle Beiträge eines Blogs"""
    alle_daten = collection.get()
    ids_to_delete = [
        alle_daten['ids'][i] 
        for i, meta in enumerate(alle_daten['metadatas']) 
        if meta['blog_url'] == blog_url
    ]

    if not ids_to_delete:
        print(f"\n❌ Blog {blog_url} nicht gefunden!\n")
        return

    collection.delete(ids=ids_to_delete)
    print(f"\n{len(ids_to_delete)} Beiträge von {blog_url} gelöscht.\n")

def menu():
    while True:
        print("=" * 50)
        print("BLOG-CHATBOT VERWALTUNG")
        print("=" * 50)
        print("1 - Blog hinzufügen")
        print("2 - Blogs anzeigen")
        print("3 - Blog löschen")
        print("0 - Beenden")
        print("=" * 50)

        wahl = input("\nDeine Wahl: ").strip()

        if not ist_gueltige_wahl(wahl, ['0', '1', '2', '3']):
            print("\n❌ Ungültige Eingabe! Bitte wähle eine der Optionen (0-3).\n")
            continue

        if wahl == "1":
            url = input("Blog-URL (z.B. ebildungslabor.de): ").strip()
            blog_hinzufuegen(url)
        elif wahl == "2":
            blogs_anzeigen()
        elif wahl == "3":
            url = input("Welchen Blog löschen? ").strip()
            blog_loeschen(url)
        elif wahl == "0":
            print("\nTschüss! 👋\n")
            break

if __name__ == "__main__":
    menu()

Bei Linux/Mac: Speichere mit Strg+O, Enter, Strg+X

Bei Windows: Speichere über Datei → Speichern und schließe Notepad

2. Programm ausführen

Jetzt kannst du das Programm starten:

Für alle Betriebssysteme (virtuelle Umgebung muss aktiv sein):

python blog_verwalten.py

Mit der Eingabe dieses Befehls braucht es nur ein paar Sekunden bis das Verwaltungsprogramm im Terminal aktiv wird. Du wirst dann gefragt, was du machen willst. Am sinnvollsten ist es, hier jetzt erst einmal Blogs hinzuzufügen (Option 1).

Wenn du diese Option auswählst, wirst du aufgefordert, die URL des gewünschten Blogs einzugeben, von dem die Inhalte geholt und gespeichert werden sollen.

Manchmal wirst du die Meldung bekommen, dass Scraping bei diesem Blog nicht erlaubt ist. Das solltest du respektieren.

Du kannst so viele Blogs einsammeln, wie du möchtest. Ich würde an deiner Stelle erst einmal mit 3-5 Blogs beginnen.

Insgesamt ist dieser Schritt eher langweilig, weil du nicht siehst, was im Hintergrund passiert. Wenn du neugierig bist, kannst du aber über den Datei-Manager deine angelegte Projektumgebung öffnen. Darin siehst du den Ordner chroma_db mit einer SQLite-Datenbank und einem Ordner mit mehreren Vektoren, was aber für dich alles nicht direkt lesbar und verwendbar ist. Ich fand es trotzdem spannend, mir mal direkt anzuschauen, wie so etwas aussieht.

Schritt 4: Deinen Chat-Bot gestalten

Die angelegte Vektor-Datenbank und der eingetragene Mistral-API-Key sind nun die Basis, mit der du deinen Blog-Chatbot erstellst. Grundsätzlich lässt sich dieser sehr unterschiedlich gestalten. Im Bot-Programm, das ich dir im folgenden vorschlage, wirst du beim Start des Chats immer erst gefragt, auf welche Blogs du dich bei deiner Frage beziehen willst. Dann kannst du einen oder mehrere der eingespeicherten Blogs auswählen, dann eine Frage stellen und daraufhin dann eine möglichst hilfreiche Antwort erhalten. Mit dem Befehl ‚quellen‘ erreichst du, dass die Links zu den durchsuchten Beiträgen angezeigt werden. Mit dem Befehl ‚mehr‘ werden nicht nur 3, sondern 10 Beiträge für die Antwort verwendet. (Du kannst diese Zahlen natürlich auch hochsetzen, dann kostet dich das mehr Tokens. Für mich passt es so ganz gut).

In dem Chatbot-Programm ist auch ein Systemprompt enthalten, den du ebenfalls ändern kannst, wenn du das möchtest. Aber auch hier ist es wahrscheinlich ganz sinnvoll, erst einmal diese Vorlage auszuprobieren. Ich habe eine ganze Weile experimentiert und war dann mit der jetzt entstandenen Version recht zufrieden!

So gehst du vor, um das Chat-Programm einzutragen:

Linux/Mac:

nano chatbot.py

Windows:

notepad chatbot.py

Du trägst diesen Inhalt ein:

import os
from dotenv import load_dotenv
import chromadb
from chromadb.utils import embedding_functions
from mistralai import Mistral

# Lade API-Key
load_dotenv()
api_key = os.getenv("MISTRAL_API_KEY")
client_mistral = Mistral(api_key=api_key)

# Lade Vektordatenbank
client_chroma = chromadb.PersistentClient(path="./chroma_db")
embedding_fn = embedding_functions.SentenceTransformerEmbeddingFunction(
    model_name="paraphrase-multilingual-MiniLM-L12-v2"
)
collection = client_chroma.get_collection(
    name="blog_posts",
    embedding_function=embedding_fn
)

# Speichere letzte Quellen und Frage
letzte_quellen = []
letzte_frage = ""

def hole_verfuegbare_blogs():
    """Zeigt alle gespeicherten Blogs"""
    alle_daten = collection.get()
    if not alle_daten['metadatas']:
        return []

    blogs = set(meta['blog_url'] for meta in alle_daten['metadatas'])
    return sorted(list(blogs))

def blog_auswahl():
    """Lässt Nutzer*in Blogs auswählen"""
    blogs = hole_verfuegbare_blogs()

    if not blogs:
        print("\n❌ Keine Blogs gespeichert!")
        print("💡 Führe erst 'python blog_verwalten.py' aus.\n")
        return None

    print("\n📚 Verfügbare Blogs:")
    print("0 - Alle Blogs durchsuchen")
    for i, blog in enumerate(blogs, 1):
        print(f"{i} - {blog}")

    while True:
        wahl = input("\nWelche Blogs durchsuchen? (Zahl oder mehrere mit Komma, z.B. 1,3): ").strip()

        if wahl == "0":
            return None  # None = alle Blogs

        try:
            nummern = [int(n.strip()) for n in wahl.split(",")]
            ausgewaehlte = [blogs[n-1] for n in nummern if 1 <= n <= len(blogs)]
            if ausgewaehlte:
                return ausgewaehlte
            else:
                print("❌ Ungültige Auswahl!")
        except:
            print("❌ Ungültige Eingabe!")

def extrahiere_keywords(frage):
    """Extrahiert wichtige Schlüsselwörter aus der Frage"""
    stopwords = ['was', 'ist', 'sind', 'der', 'die', 'das', 'ein', 'eine', 'mit', 'zu', 'im', 'in',
                 'von', 'für', 'und', 'oder', 'wie', 'wo', 'wann', 'warum', 'welche', 'welcher',
                 'gibt', 'es', 'dazu', 'darüber', 'findest', 'du', 'infos', 'informationen',
                 'schreibt', 'blog', 'beitrag', 'dem', 'den', 'des', 'an', 'auf', 'bei',
                 'nach', 'über', 'aus', 'hier', 'da', 'haben', 'hat', 'wird', 'werden', 'kann',
                 'können', 'soll', 'sollen', 'muss', 'müssen']

    woerter = frage.lower().split()
    keywords = [w.strip('?.,!') for w in woerter if w.lower() not in stopwords]
    return keywords

def keyword_suche(keywords, blog_filter=None):
    """Sucht direkt nach Keywords in Titeln und Texten"""
    alle_daten = collection.get()
    treffer = []

    for i, meta in enumerate(alle_daten['metadatas']):
        # Blog-Filter anwenden
        if blog_filter and meta['blog_url'] not in blog_filter:
            continue

        titel_lower = meta['title'].lower()
        text_lower = alle_daten['documents'][i].lower()

        # Zähle Keyword-Matches
        titel_matches = sum(1 for kw in keywords if kw in titel_lower)
        text_matches = sum(1 for kw in keywords if kw in text_lower)

        if titel_matches > 0 or text_matches > 1:  # Mind. 1 im Titel ODER 2 im Text
            treffer.append({
                'id': alle_daten['ids'][i],
                'doc': alle_daten['documents'][i],
                'meta': meta,
                'score': titel_matches * 3 + text_matches
            })

    # Nach Score sortieren
    treffer.sort(key=lambda x: x['score'], reverse=True)
    return treffer

def hybrid_suche(frage, blog_filter=None, anzahl_beitraege=3):
    """Kombiniert Keyword-First mit semantischer Suche"""
    # 1. Keywords extrahieren
    keywords = extrahiere_keywords(frage)

    # 2. Keyword-Suche
    keyword_treffer = keyword_suche(keywords, blog_filter)

    # 3. Semantische Vektorsuche
    if blog_filter:
        vektor_results = collection.query(
            query_texts=[frage],
            n_results=anzahl_beitraege * 2,
            where={"blog_url": {"$in": blog_filter}}
        )
    else:
        vektor_results = collection.query(
            query_texts=[frage],
            n_results=anzahl_beitraege * 2
        )

    # 4. Kombiniere: Keyword-Treffer zuerst, dann Vektor-Results
    finale_results = []
    verwendete_ids = set()

    # Keyword-Treffer haben Priorität
    for treffer in keyword_treffer[:anzahl_beitraege]:
        finale_results.append({
            'doc': treffer['doc'],
            'meta': treffer['meta'],
            'ist_keyword_match': True
        })
        verwendete_ids.add(treffer['id'])

    # Fülle auf mit Vektor-Results
    if vektor_results and vektor_results['documents'][0]:
        for i in range(len(vektor_results['documents'][0])):
            if len(finale_results) >= anzahl_beitraege:
                break

            meta = vektor_results['metadatas'][0][i]

            # Prüfe ob schon dabei
            if any(r['meta']['link'] == meta['link'] for r in finale_results):
                continue

            finale_results.append({
                'doc': vektor_results['documents'][0][i],
                'meta': meta,
                'ist_keyword_match': False
            })

    if not finale_results:
        return None

    return {
        'documents': [[r['doc'] for r in finale_results]],
        'metadatas': [[r['meta'] for r in finale_results]],
        'keyword_flags': [r['ist_keyword_match'] for r in finale_results]
    }

def chat(frage, blog_filter=None, konversation_history=None, anzahl_beitraege=3):
    """Beantwortet Frage basierend auf Blog-Inhalten"""
    global letzte_quellen, letzte_frage

    if konversation_history is None:
        konversation_history = []

    # Frage speichern
    letzte_frage = frage

    # Hybrid-Suche
    results = hybrid_suche(frage, blog_filter, anzahl_beitraege)

    if not results or not results['documents'][0]:
        return "❌ Keine passenden Beiträge gefunden!", konversation_history

    # Quellen speichern
    letzte_quellen = []
    for i, meta in enumerate(results['metadatas'][0], 1):
        marker = "🎯" if results['keyword_flags'][i-1] else ""
        letzte_quellen.append(f"   {marker}{i}. {meta['title']}\n      {meta['link']}")

    # Kontext zusammenbauen
    kontext_teile = []
    for i, doc in enumerate(results['documents'][0], 1):
        meta = results['metadatas'][0][i-1]
        kontext_teile.append(f"[Beitrag {i}: {meta['title']}]\n{doc[:2000]}")

    kontext = "\n\n---\n\n".join(kontext_teile)

    # System-Prompt
    system_prompt = """Du bist ein hilfreicher Assistent, der auf Basis von Blogbeiträgen antwortet.

REGELN:
1. Nutze ALLE Informationen aus den gegebenen Texten
2. Lies die Texte VOLLSTÄNDIG und GENAU
3. Wenn ein Text die Antwort enthält, NUTZE sie
4. Antworte natürlich und gesprächig
5. KEINE Quellenangaben in der Antwort (nur auf explizite Nachfrage)
6. Wenn du etwas nicht weißt: Sage es ehrlich"""

    # Konversationshistorie aufbauen
    messages = [{"role": "system", "content": system_prompt}]

    # Bisherige Konversation
    for msg in konversation_history:
        messages.append(msg)

    # Aktuelle Frage mit Kontext
    user_message = f"""VERFÜGBARE BLOGBEITRÄGE:
{kontext}

---

FRAGE: {frage}"""

    messages.append({"role": "user", "content": user_message})

    # API-Aufruf
    response = client_mistral.chat.complete(
        model="mistral-small-latest",
        messages=messages,
        temperature=0.3
    )

    antwort = response.choices[0].message.content

    # Konversation aktualisieren
    konversation_history.append({"role": "user", "content": frage})
    konversation_history.append({"role": "assistant", "content": antwort})

    return antwort, konversation_history

def zeige_quellen():
    """Zeigt die durchsuchten Beiträge"""
    global letzte_quellen

    if not letzte_quellen:
        print("\n❌ Keine Quellen verfügbar (stelle erst eine Frage).\n")
        return

    print("\n📖 Diese Beiträge wurden als relevant erachtet und durchsucht:")
    print("   (🎯 = Keyword-Match im Titel oder Text)\n")
    for quelle in letzte_quellen:
        print(quelle)
    print()

def main():
    global letzte_frage

    print("\n" + "=" * 50)
    print("BLOG-CHATBOT")
    print("=" * 50)

    # Blog-Auswahl
    blog_filter = blog_auswahl()

    if blog_filter is None:
        print("\n🔍 Durchsuche: ALLE Blogs")
    else:
        print(f"\n🔍 Durchsuche: {', '.join(blog_filter)}")

    print("\n💬 Stelle deine Fragen!")
    print("\n📋 Befehle:")
    print("   • 'mehr'     → Durchsucht 10 statt 3 Beiträge zur letzten Frage")
    print("   • 'quellen'  → Zeigt die durchsuchten Beiträge")
    print("   • 'wechseln' → Ändert die Blog-Auswahl")
    print("   • 'exit'     → Beendet das Programm\n")

    # Konversationshistorie
    konversation = []

    # Chat-Loop
    while True:
        eingabe = input("")

        if eingabe.lower() in ['exit', 'quit', 'tschüss', 'bye']:
            print("\nBis bald! 👋\n")
            break

        if eingabe.lower() == 'quellen':
            zeige_quellen()
            continue

        if eingabe.lower() == 'mehr':
            if not letzte_frage:
                print("\n❌ Stelle erst eine Frage!\n")
                continue
            print(f"\n🔎 Durchsuche nun 10 Beiträge zur Frage: '{letzte_frage}'\n")
            antwort, konversation = chat(letzte_frage, blog_filter, konversation, anzahl_beitraege=10)
            print(f"\n💡 {antwort}\n")
            continue

        if eingabe.lower() == 'wechseln':
            blog_filter = blog_auswahl()
            konversation = []
            letzte_frage = ""
            if blog_filter is None:
                print("\n🔍 Durchsuche: ALLE Blogs\n")
            else:
                print(f"\n🔍 Durchsuche: {', '.join(blog_filter)}\n")
            continue

        if not eingabe.strip():
            continue

        antwort, konversation = chat(eingabe, blog_filter, konversation, anzahl_beitraege=3)
        print(f"\n💡 {antwort}\n")

if __name__ == "__main__":
    main()

Bei Linux/Mac: Speichere mit Strg+O, Enter, Strg+X

Bei Windows: Speichere über Datei → Speichern und schließe Notepad

Damit hast du es geschafft und kannst deinen Chatbot jetzt nutzen.

Schritt 5: Deinen Chatbot nutzen!

Mit diesem Befehl startest du deinen Chatbot:

Für alle Betriebssysteme (virtuelle Umgebung muss aktiv sein):

python chatbot.py

Es braucht wahrscheinlich ein paar Sekunden. Wie oben beschrieben stellt der Chatbot dir dann in der oben geteilten Version des Bot-Programms zunächst die Frage, welche eingespeicherten Blogs du als Basis nutzen willst. Dann kannst du deine Frage eingeben. Wenn du noch tiefer recherchieren willst, dann gib mehr ein. Wenn du andere Blogs als Grundlage nehmen willst, dann nutze den Befehl wechseln. Mit quellen siehst du, welche Beiträge der Bot durchsucht hat.

Hier ist ein exemplarischer Chatverlauf, bei dem ich den Bot auf die eBildungslabor-Beiträge als RAG fokussiert habe:

❓ Wie definiert Nele Kreativität?

💡 Nele definiert Kreativität im pädagogischen Kontext als die Fähigkeit, Neues denken zu können. Dabei geht es nicht primär um künstlerisches Tätigsein, sondern darum, über das Bestehende hinauszudenken und innovative Lösungen zu finden. Diese Definition steht im Rahmen der 4K-Kompetenzen, die auch Kommunikation, Kollaboration und kritisches Denken umfassen. Kreativität wird hier also als eine Schlüsselkompetenz für den digitalen Wandel und die aktive Gestaltung unserer Gesellschaft verstanden.

❓ Was schreibt Nele zu H5P?

💡 Nele schreibt über H5P, dass es ein großartiges und offenes Tool zur Gestaltung von interaktiven Online-Materialien zum Lernen und Lehren ist. Sie schätzt besonders, dass es immer wieder neue Inhaltstypen gibt, die viel Raum für eine gezielte Nutzung im jeweiligen pädagogischen Setting bieten. Nele erwähnt, dass viele ihrer Lieblings-Inhaltstypen von Oliver, einem freiberuflichen Programmierer für H5P, entwickelt wurden.

Sie stellt verschiedene Anwendungen und Möglichkeiten von H5P vor, wie den Phrase-Randomizer, der für kreative Ideenentwicklung genutzt werden kann, oder die Game Map, mit der interaktive Karten gestaltet werden können. Nele beschreibt auch, wie sie H5P genutzt hat, um aus Fotokollen interaktive Lerninhalte zu erstellen, indem sie den Inhaltstyp „Image Hotspots“ verwendet hat. Dabei werden Hintergrundbilder mit zusätzlichen Informationen und Erläuterungen versehen, um so einen vertiefenden Lerninhalt zu schaffen.

Insgesamt zeigt Nele auf, wie vielseitig und nützlich H5P für die Gestaltung interaktiver und kreativer Bildungsmaterialien sein kann.

❓ quellen

📖 Diese Beiträge wurden als relevant erachtet und durchsucht:
(🎯 = Keyword-Match im Titel oder Text)

🎯1. Praxis-Tipp: Inspirationsduschen mit H5P
https://ebildungslabor.de/blog/praxis-tipp-inspirationsduschen-mit-h5p/
🎯2. H5P-Hacking: Mit der Game Map interaktive Online-Karten gestalten
https://ebildungslabor.de/blog/h5p-hacking-mit-der-game-map-interaktive-online-karten-gestalten/
🎯3. Schnelle Idee für H5P: Image Hotspots statt ‚Fotokoll‘
https://ebildungslabor.de/blog/schnelle-idee-fuer-h5p-image-hotspots-statt-fotokoll/

Ich finde: Ziemlich gut und nützlich!

Mit exit kommst du aus dem Bot wieder raus und kannst dich dann daran machen, z.B. die Bot-Datei zu überarbeiten. (Du weißt ja jetzt, wie es geht: nano chatbot.py (Linux/Mac) bzw. notepad chatbot.py (Windows) eingeben und Änderungen vornehmen.)

Wenn du das Terminal geschlossen hast und irgendwann später den Bot wieder öffnen willst, dann öffnest du wie in Schritt 2 beschrieben mit dem cd Befehl wieder das Verzeichnis, aktivierst anschließend die virtuelle Umgebung und startest dann das Botprogramm (mit dem Befehl: python chatbot.py)

Fazit

Ich habe mir erste Blogs eingespeichert und mit dem entwickelten Bot eine Weile gechattet. Komplexere Anfragen (Wie definiert Melanie im Vergleich zu Nele den Begriff Kreativität?) sind mit diesem Basis-Setup nicht möglich. Um sich aber einen Überblick über Blog-Inhalte zu verschaffen und zusammenfassende Verbindungen zu nutzen sowie Hinweise auf weiterführende Quellen zu erhalten, finde ich diese selbst gebastelte Anwendung richtig hilfreich. Ich kann dir das Nachbasteln somit sehr empfehlen.

Viel Freude beim Erkunden und Ausprobieren!

Bonus: Einen RAG-Chatbot online bringen!

So wie du einen RAG-Chatbot in deinem Terminal gestalten kannst, kannst du solch eine Anwendung natürlich auch auf einen Online-Server übertragen und dann anderen öffentlich zur Verfügung stellen. Das wird im Internet ja zuhauf angeboten. Man kann es sich eben aber auch selbst gestalten.

Ich habe das mit einer leicht angepassten Version des obigen Bot-Programms ausprobiert, in dem ich darin nur die Inhalte von meiner eigenen Website eingespeist habe und dazu einen Bot erstellt habe. Der Server kostet ca. 4 Euro / Monat. Ich lasse ihn auf eine Subdomain meiner Website verweisen. Ich habe außerdem (um meine Mistral-Tokens nicht alle direkt aufzubrauchen) eine Begrenzung auf 25 Fragen bei der öffentlichen Nutzung eingestellt. Als Interface habe ich die Open Source Software Streamlit genutzt (anstelle des Terminals auf dem jeweils eigenen Rechner). Das Ergebnis kannst du auf bot.ebildungslabor.de ausprobieren.

(Wenn wir mit einer lokalen und damit dezentralen Nutzung vorankommen, so wie ich es in meinem obigen Blogbeitrag beschrieben habe, dann sind solche Website-Bots bald überflüssig. Schließlich ist es ja viel sinnvoller, sich selbst genau die Anwendung mit den Inhalten, Einstellungen und Systemprompts zu bauen, die man gerade braucht!)


Beitrag weiternutzen und teilen

Dieser Beitrag steht unter der Lizenz CC BY 4.0 und kann somit gerne offen weitergenutzt und geteilt werden. Hier kannst du dir den Beitragslink und/oder den Lizenzhinweis kopieren. Wenn du den Beitragslink in das Suchfeld im Fediverse (z.B. bei Mastodon) eingibst, wird er dir dort angezeigt und du kannst ihn kommentieren.


Erschienen in der Kategorie:
Reaktionen im Fediverse
Anregungen zum Weiterlesen