Zum Inhalt

Architektur

LLM-Chat ist als containerisierte Webanwendung mit klarer Schichtentrennung aufgebaut. Eine Gradio-basierte UI nimmt Eingaben entgegen und gibt Streaming-Antworten aus; eine modulare Geschäftslogik kapselt Sitzungs-, Dokument- und Bildverarbeitung sowie die Anbindung an den Sprachmodell-Server. Die Anbindung an das Sprachmodell ist asynchron implementiert und über Modell-Profile parametrisiert. Konfigurations- und Verbindungsparameter werden vollständig über Umgebungsvariablen gesetzt.

Auf einen Blick

  • Webanwendung in Python 3.12, ausgeliefert als Container-Image, Bereitstellung über einen einzelnen HTTP-Port.
  • UI-Schicht mit Gradio (MultimodalTextbox, Chatbot, Sidebar, Accordion); ausgelagertes CSS und ein eigenes Theme.
  • Geschäftslogik strikt getrennt von der UI in Modulen für Sitzungen, Dokumente, Bilder, Streaming und Export.
  • Asynchrone LLM-Anbindung mit AsyncOpenAI und Streaming-Verarbeitung; Sampling- und Reasoning-Parameter pro Modell-Profil konfigurierbar.
  • Dokumenten-Pipeline mit zweistufiger Extraktion (schneller, format­spezifischer Extraktor → Universal-Fallback) und nachgelagerter Bereinigung.
  • Anwendungs-State pro Browser-Tab über gr.State; Dokumente sind sitzungs­übergreifend, aber tab-lokal.
  • Konfiguration vollständig über Umgebungsvariablen mit dokumentierten Defaults; lokale Verarbeitung ohne externe Drittanbieter-Ressourcen.

Architekturüberblick

Die Anwendung gliedert sich in vier Bereiche: die UI-Schicht, den tab-lokalen Anwendungs-State, die Geschäftslogik mit ihren Verarbeitungs-Pipelines sowie die externe Anbindung an einen Sprachmodell-Server.

Komponenten und Datenfluss

flowchart TB
    User([Browser])

    subgraph UI["UI-Schicht (Gradio)"]
        Input["Multimodale Eingabe"]
        ChatUI["Chat-Bereich"]
        Sidebar["Sidebar mit Sitzungen und Dokumenten"]
        Export["Word-Export"]
    end

    subgraph Logic["Geschäftslogik (pro Browser-Tab)"]
        SessMgr["SessionManager"]
        DocMgr["DocumentManager"]
        ImgProc["ImageProcessor"]
        DocProc["DocumentProcessor"]
        StreamChat["StreamingChat"]
    end

    subgraph Pipeline["Dokumenten-Pipeline"]
        Fast["Schnelle Extraktoren"]
        Universal["Universal-Parser"]
        Clean["Bereinigung und Tokenzählung"]
    end

    Profiles[("Modell-Profile<br/>Qwen3 / Qwen3.5 / Kimi / GLM / Default")]
    LLM[("LLM-Endpunkt<br/>OpenAI-kompatibel")]

    User --> Input
    User --> Sidebar
    Input --> ImgProc
    Input --> DocProc
    DocProc --> Fast
    Fast -- "bei Fehlschlag" --> Universal
    Fast --> Clean
    Universal --> Clean
    Clean --> DocMgr
    ImgProc --> SessMgr
    Sidebar --> SessMgr
    Sidebar --> DocMgr
    SessMgr --> StreamChat
    DocMgr --> StreamChat
    Profiles --> StreamChat
    StreamChat -- "Streaming-Anfrage" --> LLM
    LLM -- "Antwort-Stream" --> StreamChat
    StreamChat --> ChatUI
    SessMgr --> ChatUI
    SessMgr --> Export

UI-Schicht

Die UI-Schicht ist mit Gradio realisiert. Eine multimodale Eingabezeile akzeptiert Text und Datei-Anhänge gemeinsam; die Klassifikation als Bild oder Dokument erfolgt über die Dateierweiterung. Die Sidebar zeigt den Sitzungsverlauf, geladene Dokumente mit Token-Auslastung sowie eine zusätzliche Upload-Zone und enthält den Auslöser für den Word-Export. Markdown- und LaTeX-Rendering erfolgen direkt im Chat-Bereich; das CSS ist in einem eigenen Modul zentralisiert.

Anwendungs-State

Pro Browser-Tab wird ein AppState über gr.State instanziiert. Er bündelt die Session-Verwaltung, die Dokumentenverwaltung, die Prozessoren für Dokumente und Bilder, den Streaming-Client sowie die geltende Konfiguration. Diese Aufteilung gewährleistet, dass mehrere parallele Browser-Tabs einander nicht beeinflussen, während innerhalb eines Tabs der Dokumentenpool sitzungsübergreifend erhalten bleibt.

Dokumenten-Pipeline

Eingehende Dokumente durchlaufen eine zweistufige Pipeline. Zunächst wird je nach Dateiformat ein leichtgewichtiger Extraktor versucht: PDF über pdfminer.six, DOCX über python-docx, PPTX über python-pptx, XLSX über openpyxl, HTML über einen eigenen Tag-Parser, reine Textformate über eine Mehrfach-Encoding-Lesung. Schlägt der schnelle Pfad fehl oder existiert für das Format kein entsprechender Extraktor (DOC, PPT, XLS), übernimmt unstructured mit format­spezifischen Partitionern; für PDF ist über die Konfiguration zwischen einer schnellen Variante und einer hochauflösenden Layout-Analyse wählbar. Im Anschluss werden mehrfache Leerzeilen und Whitespaces reduziert, optional Seitenzahlen entfernt und zu kurze Absätze ausgefiltert. Der Token-Bedarf wird approximiert und gegen das konfigurierte Budget geprüft, bevor das Dokument an den DocumentManager übergeben wird.

Bild-Pipeline

Bilder werden vom ImageProcessor validiert (Format, Dateigröße), in der EXIF-Orientierung korrigiert, bei Bedarf auf eine maximale Kantenlänge skaliert und in den RGB-Farbraum konvertiert. Die Übergabe an das Sprachmodell erfolgt als Base64-kodierte JPEG-Datei innerhalb der image_url-Struktur der OpenAI-API. Eine persistente Kopie wird zusätzlich für die Anzeige im Chat-Verlauf zwischengespeichert.

LLM-Anbindung und Modell-Profile

Die Anbindung an das Sprachmodell ist im Modul StreamingChat gekapselt und nutzt AsyncOpenAI mit aktivem Streaming. Vor jedem Aufruf wird das aktive Modell-Profil ermittelt: zunächst über eine explizite Konfigurationsvariable, andernfalls über einen Substring-Match auf den Modellnamen, ansonsten über das Default-Profil. Pro Profil sind getrennte Parametersätze für Reasoning- und Instruct-Modus hinterlegt; sie umfassen reguläre OpenAI-Parameter (Temperature, top-p, Penalty-Werte), vLLM-spezifische Zusatzparameter (top-k, min-p, repetition-penalty) und chat_template_kwargs zur Steuerung des Reasoning-Verhaltens. Vorkonfiguriert sind Profile für Qwen3, Qwen3.5, Kimi und GLM; weitere Modelle werden über das Default-Profil bedient.

Während des Streamings werden die eingehenden Chunks fortlaufend auf <think>-Markierungen geprüft. Solange ein Reasoning-Block geöffnet ist (auch bei nur teilweise empfangenem Tag), wird die UI mit einer kompakten Statusanzeige aktualisiert; sobald der Block geschlossen ist, wird er aus der sichtbaren Antwort entfernt und der reguläre Text wieder zeichenweise ausgegeben.

Workflow einer Anfrage

Eine Nutzereingabe wird wie folgt verarbeitet: Beim Absenden werden Text und Anhänge zwischengespeichert und das Eingabefeld geleert (Anti-Blink-Pattern). Die Anhänge werden klassifiziert und parallel verarbeitet — Dokumente durchlaufen die Extraktions-Pipeline und werden dem DocumentManager zugeführt, Bilder werden vom ImageProcessor aufbereitet und der aktuellen Nachricht zugeordnet. Liegt kein Eingabetext vor, wird ein kontextabhängiger Standard-Prompt verwendet (Bildbeschreibung beziehungsweise Dokumentenfrage). Die ChatSession setzt anschließend die vollständige Nachrichtenfolge zusammen: System-Prompt mit Datumshinweis und gegebenenfalls aktuellem Dokumentkontext, gefolgt vom bisherigen Chat-Verlauf und der neuen Eingabe. Diese Liste wird an StreamingChat übergeben, dort werden die profil­abhängigen Parameter angewendet und die Anfrage als Streaming-Aufruf an den LLM-Endpunkt gesendet. Die zurückkommenden Chunks werden gefiltert und stückweise an die UI weitergereicht. Nach der ersten Antwort einer Sitzung wird zusätzlich ein Kurztitel über einen separaten, nicht-streamenden Aufruf erzeugt.

Nebenläufigkeit und Robustheit

Die LLM-Anbindung ist asynchron; eine laufende Generierung kann über ein Stop-Signal zeitnah unterbrochen werden, ohne den restlichen State zu beschädigen. Verbindungsfehler werden in benutzerfreundlich formulierte Hinweise übersetzt (Server überlastet, Modell nicht gefunden, keine Verbindung) und unterbrechen den Chat nicht dauerhaft. Bei der Dokument­extraktion wird der Fallback-Pfad nur bei tatsächlich leerem oder fehlgeschlagenem Schnellpfad eingeschlagen und protokolliert.

Konfiguration und Deployment

Sämtliche Konfiguration erfolgt über Umgebungsvariablen, die in einer dataclass-basierten AppConfig gebündelt werden. Dies umfasst Verbindungsparameter zum LLM-Endpunkt, Token-Budgets, Bild- und Dokumentlimits, Verarbeitungs­optionen sowie Server- und Pfadangaben. Eine optionale .env-Datei wird automatisch geladen; ohne sie greifen dokumentierte Defaults. Die Auslieferung erfolgt als Container-Image auf Basis von python:3.12-slim mit den für unstructured benötigten System-Bibliotheken (libmagic, poppler, OpenGL/glib). Die Anwendung lauscht standardmäßig auf einem konfigurierbaren Port und ist über einen ebenfalls konfigurierbaren URL-Pfad einbindbar.

Technologie-Übersicht

  • Sprache und Laufzeit: Python 3.12.
  • UI: Gradio.
  • LLM-Client: openai Python-SDK (AsyncOpenAI) gegen einen OpenAI-kompatiblen Endpunkt.
  • Modell-Profile: vorkonfiguriert für Qwen3, Qwen3.5, Kimi und GLM; Default-Profil für weitere Modelle.
  • Schnelle Dokumentenextraktion: pdfminer.six (PDF), python-docx (DOCX), python-pptx (PPTX), openpyxl (XLSX), eigener HTML-Parser.
  • Universal-Extraktion: unstructured mit format­spezifischen Partitionern.
  • Bildverarbeitung: Pillow.
  • Word-Export: python-docx mit eigenständigem Markdown-Renderer.
  • Konfiguration: Umgebungsvariablen, optional über python-dotenv.
  • Deployment: Docker (python:3.12-slim) mit System-Abhängigkeiten für unstructured[pdf].