Artikel

2. Systemarchitektur

KILinuxMarketingPython

Die Systemarchitektur muss zwei Anforderungen erfüllen: Sie soll einfach genug sein, um schnell produktiv zu werden, aber sauber genug, um später mehrere Plattformen, unterschiedliche Content-Formate und kontrollierte Interaktionen zu unterstützen.

Der wichtigste Grundsatz lautet:

Erzeugen, Prüfen, Freigeben und Veröffentlichen sind getrennte Schritte.

Das verhindert, dass ein fehlerhafter KI-Text direkt veröffentlicht wird. Außerdem bleibt jeder Beitrag nachvollziehbar: Woher kam er, wann wurde er erzeugt, welche Prüfung hat er bestanden, wer oder was hat ihn freigegeben, wann wurde er veröffentlicht?

2.1 Gesamtübersicht

Das System besteht aus einer Pipeline.

Content-Quelle
    ↓
Content-Generator
    ↓
Quality-Gate
    ↓
Review-Queue
    ↓
Publisher
    ↓
Plattform
    ↓
Logging / Screenshot / Statusupdate

Für Interaktionen gilt eine ähnliche Pipeline:

Plattform-Beitrag oder Kommentar
    ↓
Relevanzprüfung
    ↓
Kommentarvorschlag
    ↓
Quality-Gate
    ↓
Freigabe oder Ablehnung
    ↓
Veröffentlichung
    ↓
Logging / Screenshot / Statusupdate

Die Architektur trennt also zwischen zwei Hauptbereichen:

Eigene Inhalte veröffentlichen
Auf fremde oder eigene Interaktionen reagieren

Eigene Inhalte können stärker automatisiert werden. Interaktionen bleiben vorsichtiger, weil sie kontextabhängiger sind.

2.2 Komponenten

Die Anwendung besteht aus mehreren klar getrennten Komponenten.

Config
OpenAIClient
ContentGenerator
QualityGate
ReviewQueue
Publisher
PlatformAdapter
InteractionWorker
Scheduler
Logger
Storage
SafetyGuard

Config

Die Konfiguration enthält Plattformen, Limits, Dateipfade, Zeitfenster und Betriebsmodus.

Beispiel:

mode: "dry_run"

limits:
  max_posts_per_day: 1
  max_comments_per_day: 3
  max_likes_per_day: 10

platforms:
  linkedin:
    enabled: true
    session_path: "storage/sessions/linkedin.json"
    screenshots_path: "storage/screenshots/linkedin"

  instagram:
    enabled: false
    session_path: "storage/sessions/instagram.json"
    screenshots_path: "storage/screenshots/instagram"

OpenAIClient

Der OpenAI-Client kapselt alle API-Aufrufe.

Er ist zuständig für:

Texterzeugung
Textprüfung
Kürzung
Zusammenfassung
Kommentarvorschläge
Dublettenprüfung
strukturierte JSON-Ausgabe
Fehlerbehandlung

Der Rest der Anwendung sollte nicht direkt mit der OpenAI-API sprechen.

ContentGenerator

Der ContentGenerator erzeugt Entwürfe aus Eingangsdaten.

Eingangsdaten können sein:

Buchdaten
Autoreninformationen
Blogartikel
Zitate
Themenlisten
manuelle Notizen
Produktdaten

Aus diesen Daten entstehen PostDraft-Objekte.

QualityGate

Das QualityGate prüft, ob ein Beitrag veröffentlicht werden darf.

Prüfungen:

Zeichenlimit
Dubletten
verbotene Begriffe
zu werblicher Ton
fehlender Themenbezug
leerer Text
fehlender Bildpfad bei Instagram
fehlender Link bei Linkpost
generische KI-Formulierungen

Das QualityGate entscheidet nicht über Geschmack. Es verhindert technische und fachliche Mindestfehler.

ReviewQueue

Die ReviewQueue enthält Beiträge, die noch nicht veröffentlicht wurden.

Typische Status:

draft
approved
rejected
scheduled
publishing
published
failed

Die Queue ist die zentrale Kontrollstelle zwischen Texterzeugung und Veröffentlichung.

Publisher

Der Publisher verarbeitet freigegebene Beiträge.

Er entscheidet nicht selbst, wie LinkedIn, Instagram oder Facebook bedient werden. Dafür nutzt er Plattformadapter.

PlatformAdapter

Jede Plattform erhält einen eigenen Adapter.

LinkedInAdapter
InstagramAdapter
FacebookAdapter

Ein Adapter kann:

Session laden
Plattform öffnen
Postformular öffnen
Text einfügen
Bild hochladen
Beitrag veröffentlichen
Screenshot speichern
Kommentare lesen

Die gemeinsame Schnittstelle verhindert, dass Plattformlogik überall im Code verteilt wird.

InteractionWorker

Der InteractionWorker ist für Reaktionen zuständig.

Er kann:

Kommentare unter eigenen Beiträgen erfassen
Antwortvorschläge erzeugen
fremde Beiträge als Kandidaten bewerten
Interaktionen in eine Queue schreiben
Tageslimits prüfen

Interaktionen sollten nie blind veröffentlicht werden.

Scheduler

Der Scheduler startet wiederkehrende Aufgaben.

Beispiele:

morgens Beiträge erzeugen
mittags freigegebene Beiträge veröffentlichen
abends Kommentare unter eigenen Beiträgen prüfen
nachts Reports erzeugen

Für den Anfang reicht Cron. Später kann ein Python-Scheduler ergänzt werden.

Logger

Der Logger schreibt strukturierte Logs.

Nicht ausreichend:

print("Post published")

Besser:

{
  "timestamp": "2026-05-11T09:30:00+02:00",
  "event": "post_published",
  "platform": "linkedin",
  "post_id": "post_20260511_001",
  "status": "success",
  "screenshot": "storage/screenshots/linkedin/post_20260511_001.png"
}

Storage

Für das MVP reicht JSON.

Später ist SQLite sinnvoller.

Mögliche Speicherstruktur:

data/books.json
data/topics.json
data/drafts.json
data/approved_posts.json
data/published_posts.json
data/interactions.json
data/interaction_log.json

SafetyGuard

Der SafetyGuard stoppt das System bei kritischen Zuständen.

Beispiele:

CAPTCHA erkannt
Login abgelaufen
Plattformwarnung sichtbar
Tageslimit erreicht
zu viele Fehler hintereinander
fehlender Dry-Run bei neuem Adapter

Diese Komponente ist wichtig, weil Playwright sonst stumpf weiterarbeiten würde.

2.3 Datenfluss

Der Datenfluss für eigene Beiträge beginnt mit einer Content-Quelle.

Beispiel: books.json

[
  {
    "id": "book_001",
    "title": "Stolz und Vorurteil",
    "author": "Jane Austen",
    "category": "Klassiker",
    "shop_url": "https://example.com/stolz-und-vorurteil",
    "description": "Ein Gesellschaftsroman über Stolz, Vorurteil, Herkunft und Heirat."
  }
]

Daraus erzeugt der Generator einen Entwurf.

{
  "id": "post_20260511_001",
  "source_id": "book_001",
  "platform": "linkedin",
  "type": "book_intro",
  "status": "draft",
  "text": "Jane Austens Stolz und Vorurteil ist weniger eine romantische Komödie als eine präzise Gesellschaftsbeobachtung...",
  "image_path": null,
  "url": "https://example.com/stolz-und-vorurteil",
  "created_at": "2026-05-11T09:00:00+02:00"
}

Dann prüft das QualityGate den Entwurf.

{
  "post_id": "post_20260511_001",
  "passed": true,
  "checks": {
    "length": true,
    "duplicate": true,
    "tone": true,
    "url": true
  }
}

Nach Freigabe wird daraus ein veröffentlichbarer Beitrag.

{
  "id": "post_20260511_001",
  "status": "approved",
  "approved_at": "2026-05-11T09:10:00+02:00"
}

Der Publisher verarbeitet nur Beiträge mit passendem Status.

approved → publishing → published

Bei Fehler:

approved → publishing → failed

Nach Veröffentlichung werden Status, Screenshot und Log gespeichert.

{
  "id": "post_20260511_001",
  "status": "published",
  "published_at": "2026-05-11T09:30:00+02:00",
  "platform_url": null,
  "screenshot_path": "storage/screenshots/linkedin/post_20260511_001.png"
}

2.4 Statusmodell für Beiträge

Beiträge brauchen ein klares Statusmodell.

Minimal:

draft
approved
scheduled
publishing
published
rejected
failed

Besser mit zusätzlichem Status für technische Sicherheit:

draft
quality_failed
approved
scheduled
publishing
published
rejected
failed
blocked

Status: draft

Der Beitrag wurde erzeugt, aber noch nicht freigegeben.

Erzeugt durch ContentGenerator.
Noch nicht veröffentlichbar.
Kann bearbeitet oder verworfen werden.

Status: quality_failed

Das QualityGate hat den Beitrag abgelehnt.

Gründe:

zu lang
zu kurz
generisch
Dublettenverdacht
fehlende Pflichtdaten
unpassender Ton

Status: approved

Der Beitrag darf veröffentlicht werden.

Freigabe kann erfolgen durch:

manuelle CLI-Freigabe
automatische Regel
vordefiniertes Template

Status: scheduled

Der Beitrag ist freigegeben, soll aber erst später veröffentlicht werden.

Zusätzliche Felder:

{
  "scheduled_at": "2026-05-12T09:30:00+02:00"
}

Status: publishing

Der Publisher arbeitet gerade an diesem Beitrag.

Dieser Status verhindert Doppelveröffentlichungen.

approved → publishing → published
approved → publishing → failed

Status: published

Der Beitrag wurde veröffentlicht.

Pflichtdaten:

published_at
platform
screenshot_path
log_id

Falls die Plattform eine Beitrags-URL zuverlässig liefert, kann auch diese gespeichert werden.

Status: rejected

Der Beitrag wurde verworfen.

Mögliche Gründe:

inhaltlich schwach
nicht passend
doppelt
zu werblich
manuell abgelehnt

Status: failed

Die Veröffentlichung ist fehlgeschlagen.

Beispiele:

Upload fehlgeschlagen
Post-Button nicht gefunden
Session abgelaufen
Timeout
Netzwerkfehler

Status: blocked

Der Beitrag wird nicht erneut versucht, bis ein Mensch ihn prüft.

Das ist sinnvoll nach:

CAPTCHA
Plattformwarnung
Login-Problem
mehrfach fehlgeschlagenem Publishing

Beispiel als Python Enum

from enum import Enum


class PostStatus(str, Enum):
    DRAFT = "draft"
    QUALITY_FAILED = "quality_failed"
    APPROVED = "approved"
    SCHEDULED = "scheduled"
    PUBLISHING = "publishing"
    PUBLISHED = "published"
    REJECTED = "rejected"
    FAILED = "failed"
    BLOCKED = "blocked"

2.5 Statusmodell für Interaktionen

Interaktionen sind riskanter als eigene Beiträge. Deshalb brauchen sie ein eigenes Statusmodell.

Eine Interaktion kann sein:

Kommentar unter eigenem Beitrag
Antwort auf Nutzerkommentar
Kommentar unter fremdem Beitrag
Like
Speichern eines Kandidaten

Minimalstatus:

candidate
analyzed
suggested
approved
executed
rejected
failed
blocked

Status: candidate

Das System hat einen möglichen Interaktionsfall gefunden.

Beispiel:

{
  "id": "interaction_20260511_001",
  "platform": "linkedin",
  "type": "comment_reply",
  "source_url": "https://linkedin.com/...",
  "status": "candidate"
}

Status: analyzed

Der Kandidat wurde geprüft.

Prüfkriterien:

thematische Relevanz
geschäftliche Unbedenklichkeit
Ton
Risiko
Kontextbezug

Status: suggested

OpenAI hat eine Antwort oder einen Kommentar vorgeschlagen.

{
  "status": "suggested",
  "suggested_text": "Der Punkt zur Sichtbarkeit kleiner Anbieter ist im Buchmarkt ähnlich..."
}

Status: approved

Die Interaktion wurde freigegeben.

Bei fremden Beiträgen sollte das normalerweise manuell geschehen.

Status: executed

Die Interaktion wurde ausgeführt.

Gespeichert werden:

Zeitpunkt
Plattform
Art der Interaktion
Text
Screenshot
Quelle

Status: rejected

Die Interaktion wurde abgelehnt.

Gründe:

zu privat
zu politisch
zu werblich
zu generisch
kein echter Mehrwert
nicht relevant

Status: failed

Die Ausführung ist fehlgeschlagen.

Status: blocked

Die Interaktion darf nicht erneut versucht werden.

Beispiele:

Plattformwarnung
Kommentarbereich nicht verfügbar
Beitrag gelöscht
Account-Limit erreicht

Beispiel als Python Enum

from enum import Enum


class InteractionStatus(str, Enum):
    CANDIDATE = "candidate"
    ANALYZED = "analyzed"
    SUGGESTED = "suggested"
    APPROVED = "approved"
    EXECUTED = "executed"
    REJECTED = "rejected"
    FAILED = "failed"
    BLOCKED = "blocked"

2.6 Fehler- und Retry-Konzept

Automatisierung darf bei Fehlern nicht aggressiv weiterprobieren.

Ein gutes Retry-Konzept unterscheidet zwischen temporären und kritischen Fehlern.

Temporäre Fehler

Temporäre Fehler dürfen begrenzt erneut versucht werden.

Beispiele:

Timeout
kurzer Netzwerkfehler
Element noch nicht geladen
Upload dauert länger
OpenAI-Rate-Limit

Mögliche Behandlung:

kurz warten
einmal wiederholen
bei zweitem Fehler abbrechen
Status auf failed setzen
Log schreiben

Kritische Fehler

Kritische Fehler dürfen nicht automatisch wiederholt werden.

Beispiele:

Login-Seite erscheint
CAPTCHA erscheint
Plattformwarnung erscheint
Account eingeschränkt
Postformular nicht mehr auffindbar
ungewöhnliche Weiterleitung

Behandlung:

sofort stoppen
Status auf blocked setzen
Screenshot speichern
Plattform-Kill-Switch aktivieren
manuelle Prüfung erforderlich

Retry-Regeln

Sinnvolle Grundregeln:

maximal 2 Versuche pro Beitrag
keine Endlosschleifen
kein sofortiger Retry bei kritischen Fehlern
kein Retry bei CAPTCHA
kein Retry bei Login-Problemen
kein Retry bei Plattformwarnung
nach 3 Plattformfehlern Plattform deaktivieren

Beispiel Retry-Konfiguration

retry:
  max_attempts_per_post: 2
  max_platform_failures_per_day: 3
  retry_delay_seconds: 30

critical_errors:
  - "captcha_detected"
  - "login_required"
  - "platform_warning"
  - "account_restricted"

Beispiel Fehlerklassen

class PublisherError(Exception):
    pass


class TemporaryPublisherError(PublisherError):
    pass


class CriticalPublisherError(PublisherError):
    pass


class LoginRequiredError(CriticalPublisherError):
    pass


class CaptchaDetectedError(CriticalPublisherError):
    pass


class PlatformWarningError(CriticalPublisherError):
    pass

Beispiel Retry-Logik

from collections.abc import Callable


def run_with_retry(action: Callable[[], None], max_attempts: int) -> None:
    attempt = 1

    while attempt <= max_attempts:
        try:
            action()
            return
        except CriticalPublisherError:
            raise
        except TemporaryPublisherError:
            if attempt === max_attempts:
                raise

            attempt += 1

Hinweis: In Python muss der Vergleich == verwendet werden. Die obige Logik entspricht konzeptionell dem gewünschten Ablauf. Korrekt lautet die betreffende Zeile:

if attempt == max_attempts:
    raise

Praktische Retry-Strategie

Für dieses Projekt reicht:

OpenAI-Aufruf: 2 Versuche
Browser-Navigation: 2 Versuche
Upload: 2 Versuche
Veröffentlichung: kein blinder zweiter Klick
Kommentarveröffentlichung: kein automatischer Retry bei unklarem Status

Besonders wichtig: Nach dem Klick auf „Veröffentlichen“ darf das System nicht einfach erneut klicken, wenn unklar ist, ob der Beitrag bereits veröffentlicht wurde. Sonst entstehen Doppelposts.

Besser:

Nach Veröffentlichungsversuch prüfen.
Screenshot speichern.
Status nur bei eindeutiger Bestätigung auf published setzen.
Bei Unsicherheit Status auf failed_manual_check setzen.

2.7 Logging und Nachvollziehbarkeit

Logging ist kein Zusatz, sondern Kernbestandteil des Systems.

Ohne Logging ist später nicht mehr nachvollziehbar:

welcher Text veröffentlicht wurde
welcher Prompt verwendet wurde
welches Modell verwendet wurde
ob ein Quality-Gate bestanden wurde
wann ein Beitrag gepostet wurde
warum ein Beitrag fehlgeschlagen ist
ob ein Retry stattgefunden hat
welcher Screenshot zum Vorgang gehört

Log-Arten

Sinnvoll sind mehrere Logtypen.

application.log
openai.log
publisher.log
interaction.log
error.log
audit.log

Für den Anfang reicht auch eine JSONL-Datei.

storage/logs/events.jsonl

Jede Zeile enthält ein vollständiges JSON-Objekt.

Beispiel:

{
  "timestamp": "2026-05-11T09:30:00+02:00",
  "level": "info",
  "event": "post_published",
  "platform": "linkedin",
  "post_id": "post_20260511_001",
  "status_before": "publishing",
  "status_after": "published",
  "screenshot_path": "storage/screenshots/linkedin/post_20260511_001.png"
}

Audit-Log

Das Audit-Log ist besonders wichtig.

Es beantwortet:

Was hat das System getan?
Wann?
Warum?
Mit welchen Eingangsdaten?
Mit welchem Ergebnis?

Für jeden Beitrag sollte gespeichert werden:

Post-ID
Quelle
Prompt-Version
OpenAI-Modell
generierter Text
Quality-Gate-Ergebnis
Freigabestatus
Veröffentlichungszeitpunkt
Plattform
Screenshot
Fehlerstatus

Prompt-Versionierung

Prompts sollten nicht nur überschrieben werden.

Besser:

prompts/linkedin_post_v1.txt
prompts/linkedin_post_v2.txt
prompts/quality_check_v1.txt

Oder mit Metadaten:

{
  "prompt_name": "linkedin_post",
  "prompt_version": "1.0.0",
  "model": "gpt-4.1-mini"
}

So lässt sich später nachvollziehen, warum Beiträge zu einem bestimmten Zeitpunkt anders klangen.

Screenshot-Pflicht

Nach jeder wichtigen Browser-Aktion sollte ein Screenshot gespeichert werden.

Mindestens bei:

erfolgreicher Veröffentlichung
Fehler
Login-Problem
CAPTCHA
Plattformwarnung
unklarem Status

Dateinamen:

storage/screenshots/linkedin/post_20260511_001_success.png
storage/screenshots/linkedin/post_20260511_001_error.png
storage/screenshots/linkedin/post_20260511_001_blocked.png

Beispiel Logger

import json
from datetime import datetime
from pathlib import Path
from zoneinfo import ZoneInfo
from typing import Any


class JsonLogger:
    def __init__(self, log_file: Path) -> None:
        self.log_file = log_file

    def write(self, event: str, data: dict[str, Any]) -> None:
        payload: dict[str, Any] = {
            "timestamp": datetime.now(ZoneInfo("Europe/Berlin")).isoformat(),
            "event": event,
        }

        for key, value in data.items():
            payload[key] = value

        with self.log_file.open("a", encoding="utf-8") as file:
            file.write(json.dumps(payload, ensure_ascii=False))
            file.write("\n")

Mindeststandard für Nachvollziehbarkeit

Für ein produktives System sollte jeder veröffentlichte Beitrag diese Daten haben:

{
  "id": "post_20260511_001",
  "platform": "linkedin",
  "type": "book_intro",
  "source_id": "book_001",
  "status": "published",
  "text": "Jane Austens Stolz und Vorurteil ist weniger eine romantische Komödie...",
  "created_at": "2026-05-11T09:00:00+02:00",
  "approved_at": "2026-05-11T09:10:00+02:00",
  "published_at": "2026-05-11T09:30:00+02:00",
  "prompt_name": "linkedin_post",
  "prompt_version": "1.0.0",
  "model": "gpt-4.1-mini",
  "quality_gate_passed": true,
  "screenshot_path": "storage/screenshots/linkedin/post_20260511_001_success.png"
}

Ergebnis dieses Kapitels

Die Architektur ist damit definiert:

klare Pipeline
getrennte Komponenten
eigene Statusmodelle für Beiträge und Interaktionen
begrenzte Retry-Logik
harte Stop-Regeln
strukturierte Logs
Screenshots als Belege
nachvollziehbare Prompt- und Modellnutzung

Damit kann im nächsten Schritt das Projekt technisch aufgesetzt werden.

0 Kommentare