Technischer Leitfaden

KI-Browser-Automatisierung ohne BrowserBase: Was wir stattdessen gebaut haben

Wie du KI-gestützte Browser-Automatisierung mit Playwright und LLMs statt kostenpflichtiger Tools baust. Instance Pooling, Session-Management, LLM-basiertes Seitenverständnis und Kostenvergleich.

16. Februar 202614 Min. LesezeitOronts Engineering Team

Der Markt für kostenpflichtige Browser-Automatisierung

BrowserBase, Browserless und ähnliche Dienste berechnen pro Minute oder pro Session für verwaltete Headless-Browser. Für KI-Workflows, die mit Webseiten interagieren müssen (Formulare ausfüllen, strukturierte Daten extrahieren, mehrstufige Prozesse navigieren), übernehmen diese Dienste die Infrastruktur: Browser-Instanzen, Anti-Detection, Proxies und Session-Management.

Die Kosten summieren sich schnell. Bei $0,10-0,50 pro Session-Minute kostet ein Workflow, der 1.000 Seiten pro Tag bei 2 Minuten pro Seite verarbeitet, $200-1.000 pro Tag. Für ein KI-System im Dauerbetrieb sind das $6.000-30.000 pro Monat allein für Browser-Infrastruktur.

Wir haben eine selbst gehostete Alternative mit Playwright + LLM für Seitenverständnis gebaut. Sie deckt 90% der Anwendungsfälle zu einem Bruchteil der Kosten ab. Dieser Artikel beschreibt die Architektur. Wie wir KI-Workflow-Systeme und agentische KI grundsätzlich bauen, behandeln die verlinkten Guides im Detail.

Die Architektur

┌─────────────────────────────────────────────────────────┐
│                  AI Browser Engine                       │
│                                                          │
│  ┌──────────────┐  ┌──────────────┐  ┌──────────────┐  │
│  │  Task Queue   │  │  Instance    │  │  Session      │  │
│  │  (BullMQ)     │  │  Pool        │  │  Manager      │  │
│  │               │  │  (Playwright │  │  (cookies,    │  │
│  │  Prioritized  │  │   browsers)  │  │   localStorage│  │
│  │  Retry logic  │  │              │  │   auth state) │  │
│  └──────┬───────┘  └──────┬───────┘  └──────┬───────┘  │
│         │                 │                  │           │
│         ▼                 ▼                  ▼           │
│  ┌──────────────────────────────────────────────────┐   │
│  │              Page Interaction Layer                │   │
│  │                                                    │   │
│  │  1. URL aufrufen                                  │   │
│  │  2. Auf Seitenladevorgang warten                  │   │
│  │  3. Seitenstruktur extrahieren (Accessibility Tree)│  │
│  │  4. Struktur an LLM zum Verständnis senden        │   │
│  │  5. LLM gibt Aktionsplan zurück (click, type, select)│ │
│  │  6. Aktionen via Playwright ausführen             │   │
│  │  7. Strukturierte Daten aus Ergebnis extrahieren  │   │
│  └──────────────────────────────────────────────────┘   │
│                                                          │
└─────────────────────────────────────────────────────────┘

Instance Pooling

Einen neuen Browser für jede Aufgabe zu starten ist teuer (Cold Start: 1-3 Sekunden, Speicher: 200-400 MB pro Instanz). Ein Pool verwendet Browser-Instanzen über Aufgaben hinweg wieder.

class BrowserPool {
    private available: Browser[] = [];
    private inUse = new Map<string, Browser>();
    private maxInstances: number;

    constructor(options: { maxInstances: number }) {
        this.maxInstances = options.maxInstances;
    }

    async acquire(): Promise<{ browser: Browser; id: string }> {
        // Verfügbare Instanz wiederverwenden
        if (this.available.length > 0) {
            const browser = this.available.pop()!;
            const id = crypto.randomUUID();
            this.inUse.set(id, browser);
            return { browser, id };
        }

        // Neue Instanz erstellen, wenn unter dem Limit
        if (this.inUse.size < this.maxInstances) {
            const browser = await chromium.launch({
                headless: true,
                args: [
                    '--no-sandbox',
                    '--disable-setuid-sandbox',
                    '--disable-dev-shm-usage',
                    '--disable-gpu',
                    '--single-process',
                ],
            });
            const id = crypto.randomUUID();
            this.inUse.set(id, browser);
            return { browser, id };
        }

        // Pool erschöpft: warten, bis eine Instanz freigegeben wird
        return new Promise((resolve) => {
            this.waitQueue.push(resolve);
        });
    }

    async release(id: string): Promise<void> {
        const browser = this.inUse.get(id);
        if (!browser) return;

        this.inUse.delete(id);

        // Zustand zwischen Aufgaben bereinigen
        const pages = browser.contexts();
        for (const context of pages) {
            await context.close();
        }

        // Wenn jemand wartet, diese Instanz zuweisen
        if (this.waitQueue.length > 0) {
            const resolve = this.waitQueue.shift()!;
            const newId = crypto.randomUUID();
            this.inUse.set(newId, browser);
            resolve({ browser, id: newId });
        } else {
            this.available.push(browser);
        }
    }
}

Pool-Dimensionierung

WorkloadPool-GrößeBenötigter Speicher
Leicht (< 100 Seiten/Stunde)2-3 Instanzen1-2 GB
Mittel (100-500 Seiten/Stunde)5-10 Instanzen3-5 GB
Schwer (500+ Seiten/Stunde)10-20 Instanzen5-10 GB

Jede Chromium-Instanz braucht 200-400 MB RAM. Die Pool-Größe bestimmt deinen Durchsatz und Speicherbedarf. Fang klein an und skaliere basierend auf der tatsächlichen Last.

Session-Management

Viele Workflows erfordern einen persistenten Login-Zustand über mehrere Seiteninteraktionen hinweg. Der Session Manager speichert Cookies, localStorage und Authentifizierungs-Tokens zwischen Aufgaben.

class SessionManager {
    private sessions = new Map<string, SessionState>();

    async createSession(id: string, options: SessionOptions): Promise<BrowserContext> {
        const context = await browser.newContext({
            viewport: { width: 1280, height: 720 },
            userAgent: options.userAgent || this.getRandomUserAgent(),
            locale: options.locale || 'en-US',
            timezoneId: options.timezone || 'Europe/Berlin',
        });

        // Vorherigen Session-Zustand wiederherstellen, falls vorhanden
        const existing = this.sessions.get(id);
        if (existing) {
            await context.addCookies(existing.cookies);
            // localStorage wird nach Navigation via page.evaluate wiederhergestellt
        }

        return context;
    }

    async saveSession(id: string, context: BrowserContext): Promise<void> {
        const cookies = await context.cookies();
        const pages = context.pages();
        let localStorage = {};

        if (pages.length > 0) {
            localStorage = await pages[0].evaluate(() => {
                const data: Record<string, string> = {};
                for (let i = 0; i < window.localStorage.length; i++) {
                    const key = window.localStorage.key(i);
                    if (key) data[key] = window.localStorage.getItem(key) || '';
                }
                return data;
            });
        }

        this.sessions.set(id, {
            cookies,
            localStorage,
            lastUsed: Date.now(),
        });
    }
}

LLM-basiertes Seitenverständnis

Die zentrale Innovation: Statt CSS-Selektoren oder XPath-Abfragen für jede Seite zu schreiben, sendest du den Accessibility Tree der Seite an ein LLM und lässt es entscheiden, mit welchen Elementen interagiert werden soll.

async function extractPageStructure(page: Page): Promise<string> {
    // Accessibility Tree abrufen (strukturierte, kompakte Darstellung)
    const tree = await page.accessibility.snapshot();

    // In ein Textformat konvertieren, das das LLM versteht
    return formatAccessibilityTree(tree, {
        maxDepth: 5,
        includeRoles: ['button', 'link', 'textbox', 'combobox', 'checkbox', 'heading'],
        includeText: true,
        includeLabels: true,
    });
}

function formatAccessibilityTree(node: any, options: any, depth = 0): string {
    if (depth > options.maxDepth) return '';
    if (!options.includeRoles.includes(node.role) && depth > 1) {
        // Nicht-interaktive Elemente überspringen, aber in Kinder rekursieren
        return (node.children || []).map(c => formatAccessibilityTree(c, options, depth + 1)).join('');
    }

    const indent = '  '.repeat(depth);
    let result = `${indent}[${node.role}] ${node.name || ''}`;
    if (node.value) result += ` value="${node.value}"`;
    result += '\n';

    for (const child of node.children || []) {
        result += formatAccessibilityTree(child, options, depth + 1);
    }
    return result;
}

LLM-Aktionsplanung

Die Seitenstruktur wird mit der Aufgabenbeschreibung an das LLM gesendet. Das LLM gibt eine Sequenz von Aktionen zurück:

async function planActions(pageStructure: string, task: string): Promise<Action[]> {
    const response = await llm.generate({
        model: 'gpt-4o-mini', // Schnelles Modell für Aktionsplanung
        messages: [
            {
                role: 'system',
                content: `You are a browser automation assistant. Given a page structure and a task,
                return a JSON array of actions to accomplish the task.
                Available actions: click(selector), type(selector, text), select(selector, value),
                wait(ms), extract(selector).
                Use the element text/labels to identify targets, not CSS selectors.`,
            },
            {
                role: 'user',
                content: `Page structure:\n${pageStructure}\n\nTask: ${task}`,
            },
        ],
        responseFormat: 'json',
    });

    return JSON.parse(response.text);
}

// Beispielaufgabe: "Kontaktformular ausfüllen mit Name Sara Mustermann und E-Mail sara.mustermann@beispiel.de"
// LLM gibt zurück:
// [
//   { "action": "type", "target": "Name input field", "value": "Sara Mustermann" },
//   { "action": "type", "target": "Email input field", "value": "sara.mustermann@beispiel.de" },
//   { "action": "click", "target": "Submit button" }
// ]

LLM-Aktionen in Playwright-Befehle auflösen

Das LLM gibt menschenlesbare Ziele zurück ("Name input field"). Ein Resolver bildet sie auf Playwright-Selektoren ab:

async function resolveAndExecute(page: Page, actions: Action[]): Promise<void> {
    for (const action of actions) {
        // Element finden, das der LLM-Beschreibung entspricht
        const element = await findElementByDescription(page, action.target);

        if (!element) {
            throw new ActionError(`Element nicht gefunden: ${action.target}`);
        }

        switch (action.action) {
            case 'click':
                await element.click();
                await page.waitForLoadState('networkidle');
                break;
            case 'type':
                await element.fill(action.value);
                break;
            case 'select':
                await element.selectOption(action.value);
                break;
            case 'wait':
                await page.waitForTimeout(action.value);
                break;
            case 'extract':
                const text = await element.textContent();
                results.push({ field: action.target, value: text });
                break;
        }
    }
}

async function findElementByDescription(page: Page, description: string): Promise<ElementHandle | null> {
    // Mehrere Strategien zum Finden des Elements
    const strategies = [
        // Per aria-label
        () => page.$(`[aria-label*="${description}" i]`),
        // Per placeholder
        () => page.$(`[placeholder*="${description}" i]`),
        // Per sichtbarem Text
        () => page.$(`text=${description}`),
        // Per Label-Zuordnung
        () => page.$(`label:has-text("${description}") + input, label:has-text("${description}") input`),
        // Per Rolle und Name
        () => page.getByRole('textbox', { name: new RegExp(description, 'i') }).first().elementHandle(),
        () => page.getByRole('button', { name: new RegExp(description, 'i') }).first().elementHandle(),
    ];

    for (const strategy of strategies) {
        try {
            const element = await strategy();
            if (element) return element;
        } catch {
            continue;
        }
    }

    return null;
}

Anti-Detection Grundlagen

Manche Websites erkennen und blockieren Headless-Browser. Grundlegende Gegenmaßnahmen:

const context = await browser.newContext({
    // Viewport randomisieren
    viewport: {
        width: 1280 + Math.floor(Math.random() * 200),
        height: 720 + Math.floor(Math.random() * 100),
    },

    // User Agents rotieren
    userAgent: getRandomUserAgent(),

    // Realistische Locale und Zeitzone setzen
    locale: 'de-DE',
    timezoneId: 'Europe/Berlin',

    // Realistische Geolocation
    geolocation: { latitude: 48.1351, longitude: 11.5820 },
    permissions: ['geolocation'],
});

// navigator.webdriver überschreiben (Headless-Erkennung)
await page.addInitScript(() => {
    Object.defineProperty(navigator, 'webdriver', { get: () => undefined });
});

Hinweis: Anti-Detection ist ein Wettrüsten. Für Websites mit ausgefeilter Bot-Erkennung (Cloudflare, Akamai) wird selbst gehostetes Playwright irgendwann erkannt. Hier liefern kostenpflichtige Dienste wie BrowserBase einen echten Mehrwert: Sie investieren kontinuierlich in Anti-Detection. Für die meisten geschäftlichen Automatisierungsaufgaben (interne Tools, Partnerportale, öffentliche Daten) reicht grundlegende Anti-Detection aus.

Wann sich kostenpflichtige Tools LOHNEN

SzenarioSelf-HostedKostenpflichtiger Dienst
Interne Tool-AutomatisierungBeste Wahl (keine Anti-Detection nötig)Overkill
Öffentliche Datenextraktion (einfach)Gut (grundlegende Anti-Detection reicht)Unnötig
Websites mit Bot-ErkennungMöglich, aber ständige WartungLohnt sich (Anti-Detection ist deren Kerngeschäft)
Hohes Volumen (10K+ Seiten/Tag)Komplex (Proxy-Rotation, IP-Management)Lohnt sich (verwaltete Infrastruktur)
Regulierte Daten (DSGVO, Compliance)Besser (Daten bleiben auf deiner Infrastruktur)Risiko (Daten gehen durch Drittanbieter)
Einmalige MigrationGut (temporäre Arbeitslast)Unnötige Kosten

Die Entscheidungsregel: Wenn du interne Workflows automatisierst oder öffentliche Daten von Websites ohne aggressive Bot-Erkennung verarbeitest, hoste selbst. Wenn du hohes Volumen von Websites mit Cloudflare-Level-Schutz extrahierst, zahle für einen Dienst, der Anti-Detection als Kerngeschäft betreibt.

Kostenvergleich

KomponenteSelf-Hosted (monatlich)BrowserBase (monatlich)
Compute (5 Instanzen)$50-100 (Container/VPS)N/A
LLM-Aufrufe (Aktionsplanung)$20-50 (GPT-4o-mini)N/A
BrowserBase SessionsN/A$500-2.000
Proxy-Dienst (bei Bedarf)$50-200Inklusive
Wartung2-4 Stunden/MonatKeine
Gesamt (1.000 Seiten/Tag)$120-350/Monat$500-2.000/Monat
Gesamt (10.000 Seiten/Tag)$300-800/Monat$3.000-10.000/Monat

Self-Hosting ist 3-10x günstiger bei Skalierung. Der Trade-off ist Wartungsaufwand und eingeschränkte Anti-Detection-Fähigkeiten.

Häufige Fehler

  1. Kein Instance Pooling. Einen neuen Browser pro Aufgabe zu starten verschwendet 1-3 Sekunden Cold Start und 200-400 MB RAM. Instanzen poolen und wiederverwenden.

  2. Hardcodierte CSS-Selektoren. Seiten ändern ihre DOM-Struktur regelmäßig. LLM-basierte Element-Identifikation ist widerstandsfähiger als hardcodierte Selektoren.

  3. Keine Session-Persistenz. Mehrstufige Workflows, die einen Login erfordern, scheitern, wenn der Session-Zustand zwischen den Schritten verloren geht.

  4. Anti-Detection komplett ignorieren. Selbst grundlegende Maßnahmen (zufälliger Viewport, User-Agent-Rotation, Webdriver-Override) verhindern die Erkennung auf den meisten Websites.

  5. Großes Modell für Aktionsplanung verwenden. GPT-4o-mini oder Claude Haiku sind schnell genug für Seitenverständnis. Ein großes Modell fügt Latenz hinzu, ohne bei dieser Aufgabe genauer zu sein.

  6. Kein Timeout bei Seitenladungen. Manche Seiten laden endlos (Infinite Scrolling, langsame Third-Party-Skripte). Setze einen Navigation-Timeout und behandle den Fall.

  7. Ohne Monitoring in Produktion betreiben. Tracke Erfolgsrate, durchschnittliche Ausführungszeit und Fehlertypen pro Workflow. Alarmiere, wenn die Erfolgsrate sinkt.

Zentrale Erkenntnisse

  • Self-Hosted Playwright + LLM deckt 90% der Browser-Automatisierungsfälle ab. Für interne Tools, Partnerportale und öffentliche Daten ohne aggressive Bot-Erkennung ist das der richtige Ansatz.

  • Instance Pooling ist unverzichtbar. Browser-Instanzen über Aufgaben hinweg wiederverwenden. Cold Starts und Speicherzuweisung sind die größten Performance-Engpässe.

  • LLM-Seitenverständnis ersetzt fragile Selektoren. Sende den Accessibility Tree an ein schnelles Modell. Lass es entscheiden, mit welchen Elementen interagiert werden soll. Widerstandsfähiger gegen Seitenänderungen als hardcodierte CSS-Selektoren.

  • Kostenpflichtige Dienste verdienen ihr Geld bei Anti-Detection. Wenn deine Zielseiten Cloudflare oder ähnlichen Schutz haben, investiert BrowserBase kontinuierlich in die Umgehung. Das ist deren Kerngeschäft. Versuche nicht, damit zu konkurrieren.

  • Self-Hosting ist 3-10x günstiger bei Skalierung. Aber du zahlst mit Wartungszeit und Anti-Detection-Einschränkungen. Triff den Trade-off bewusst.

Wir bauen Browser-Automatisierung in unsere KI-Workflow-Systeme und Custom-Software-Projekte ein. Wenn du Hilfe mit Browser-Automatisierungs-Architektur brauchst, sprich mit unserem Team oder fordere ein Angebot an.

Behandelte Themen

Browser-Automatisierung KIHeadless Browser TypeScriptPlaywright KIBrowser-Automatisierung Open SourceBrowserBase AlternativeLLM BrowserWeb Scraping KI

Bereit, produktionsreife KI-Systeme zu bauen?

Unser Team ist spezialisiert auf produktionsreife KI-Systeme. Lass uns besprechen, wie wir deinem Unternehmen helfen können.

Gespräch starten