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.
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
| Workload | Pool-Größe | Benötigter Speicher |
|---|---|---|
| Leicht (< 100 Seiten/Stunde) | 2-3 Instanzen | 1-2 GB |
| Mittel (100-500 Seiten/Stunde) | 5-10 Instanzen | 3-5 GB |
| Schwer (500+ Seiten/Stunde) | 10-20 Instanzen | 5-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
| Szenario | Self-Hosted | Kostenpflichtiger Dienst |
|---|---|---|
| Interne Tool-Automatisierung | Beste Wahl (keine Anti-Detection nötig) | Overkill |
| Öffentliche Datenextraktion (einfach) | Gut (grundlegende Anti-Detection reicht) | Unnötig |
| Websites mit Bot-Erkennung | Möglich, aber ständige Wartung | Lohnt 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 Migration | Gut (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
| Komponente | Self-Hosted (monatlich) | BrowserBase (monatlich) |
|---|---|---|
| Compute (5 Instanzen) | $50-100 (Container/VPS) | N/A |
| LLM-Aufrufe (Aktionsplanung) | $20-50 (GPT-4o-mini) | N/A |
| BrowserBase Sessions | N/A | $500-2.000 |
| Proxy-Dienst (bei Bedarf) | $50-200 | Inklusive |
| Wartung | 2-4 Stunden/Monat | Keine |
| 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
-
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.
-
Hardcodierte CSS-Selektoren. Seiten ändern ihre DOM-Struktur regelmäßig. LLM-basierte Element-Identifikation ist widerstandsfähiger als hardcodierte Selektoren.
-
Keine Session-Persistenz. Mehrstufige Workflows, die einen Login erfordern, scheitern, wenn der Session-Zustand zwischen den Schritten verloren geht.
-
Anti-Detection komplett ignorieren. Selbst grundlegende Maßnahmen (zufälliger Viewport, User-Agent-Rotation, Webdriver-Override) verhindern die Erkennung auf den meisten Websites.
-
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.
-
Kein Timeout bei Seitenladungen. Manche Seiten laden endlos (Infinite Scrolling, langsame Third-Party-Skripte). Setze einen Navigation-Timeout und behandle den Fall.
-
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
Verwandte Guides
Agent Memory, das sich wirklich erinnert: Knowledge Graphs und Context Assembly
Ein tiefgehender technischer Guide zu Agent Memory in Produktion: Recent Windows, Semantic Recall, Working-Memory-Templates, mandantenspezifische Knowledge Graphs und mehrschichtige Context Assembly.
Guide lesenUnternehmenshandbuch zu Agentischen KI-Systemen
Technischer Leitfaden zu agentischen KI-Systemen in Unternehmen. Erfahre mehr ueber Architektur, Faehigkeiten und Anwendungen autonomer KI-Agenten.
Guide lesenAgentic Commerce: Wie du KI-Agenten sicher einkaufen lässt
Wie du gesteuerten, KI-initiierten Handel designst. Policy Engines, HITL-Freigabe-Gates, HMAC-Quittungen, Idempotenz, Tenant-Scoping und das vollständige Agentic Checkout Protocol.
Guide lesenBauen Sie gerade so etwas?
Wir entwerfen und betreiben Produktionssysteme wie das in diesem Leitfaden. Sprechen Sie mit den Ingenieuren, die ihn geschrieben haben, ohne Verkaufsgespräch.
Gespräch starten