Guía técnica

Automatización de Navegador con IA Sin BrowserBase: Lo Que Construimos en su Lugar

Cómo construir automatización de navegador con Playwright y LLMs en lugar de herramientas de pago. Pool de instancias, gestión de sesiones, comprensión de página por LLM y comparación de costes.

16 de febrero de 202614 min de lecturaEquipo de Ingeniería Oronts

El mercado de automatización de navegador de pago

BrowserBase, Browserless y servicios similares cobran por minuto o por sesión para navegadores headless gestionados. Para los workflows de IA que necesitan interactuar con páginas web (rellenar formularios, extraer datos estructurados, navegar procesos multi-paso), estos servicios gestionan la infraestructura: instancias de navegador, anti-detección, proxies y gestión de sesiones.

Los costes se acumulan rápido. A 0,10-0,50 $ por minuto de sesión, un workflow que procesa 1.000 páginas por día a 2 minutos cada una cuesta 200-1.000 $ al día. Para un sistema IA que funciona continuamente, son 6.000-30.000 $ al mes solo en infraestructura de navegador.

Construimos una alternativa auto-alojada usando Playwright + LLM para la comprensión de página. Cubre el 90 % de los casos de uso a una fracción del coste. Este artículo cubre la arquitectura. Para entender cómo construimos sistemas de workflows IA e IA agéntica de forma más amplia, esas guías cubren los patrones de nivel superior.

La arquitectura

┌─────────────────────────────────────────────────────────┐
│                  AI Browser Engine                       │
│                                                          │
│  ┌──────────────┐  ┌──────────────┐  ┌──────────────┐  │
│  │  Task Queue   │  │  Instance    │  │  Session      │  │
│  │  (BullMQ)     │  │  Pool        │  │  Manager      │  │
│  │               │  │  (Playwright │  │  (cookies,    │  │
│  │  Prioritized  │  │   browsers)  │  │   localStorage│  │
│  │  Retry logic  │  │              │  │   auth state) │  │
│  └──────┬───────┘  └──────┬───────┘  └──────┬───────┘  │
│         │                 │                  │           │
│         ▼                 ▼                  ▼           │
│  ┌──────────────────────────────────────────────────┐   │
│  │              Page Interaction Layer                │   │
│  │                                                    │   │
│  │  1. Navegar a la URL                              │   │
│  │  2. Esperar la carga de la página                 │   │
│  │  3. Extraer la estructura (árbol de accesibilidad)│   │
│  │  4. Enviar la estructura al LLM                   │   │
│  │  5. El LLM devuelve un plan de acciones           │   │
│  │  6. Ejecutar las acciones vía Playwright          │   │
│  │  7. Extraer datos estructurados del resultado     │   │
│  └──────────────────────────────────────────────────┘   │
│                                                          │
└─────────────────────────────────────────────────────────┘

Pool de instancias

Lanzar un nuevo navegador para cada tarea es caro (arranque en frío: 1-3 segundos, memoria: 200-400 MB por instancia). Un pool reutiliza instancias de navegador entre tareas.

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 }> {
        // Reutilizar una instancia disponible
        if (this.available.length > 0) {
            const browser = this.available.pop()!;
            const id = crypto.randomUUID();
            this.inUse.set(id, browser);
            return { browser, id };
        }

        // Crear una nueva si estamos bajo el límite
        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 agotado: esperar a que se libere una
        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);

        // Limpiar el estado entre tareas
        const pages = browser.contexts();
        for (const context of pages) {
            await context.close();
        }

        // Si alguien está esperando, darle esta instancia
        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);
        }
    }
}

Dimensionamiento del pool

Carga de trabajoTamaño del poolMemoria requerida
Ligera (< 100 páginas/hora)2-3 instancias1-2 GB
Media (100-500 páginas/hora)5-10 instancias3-5 GB
Pesada (500+ páginas/hora)10-20 instancias5-10 GB

Cada instancia de Chromium consume 200-400 MB de RAM. El tamaño del pool determina tu techo de rendimiento y tus requisitos de memoria. Empieza pequeño y ajusta según la carga real.

Gestión de sesiones

Muchos workflows necesitan mantener el estado de conexión entre múltiples interacciones con las páginas. El gestor de sesiones persiste cookies, localStorage y tokens de autenticación entre tareas.

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',
        });

        // Restaurar el estado de sesión anterior si existe
        const existing = this.sessions.get(id);
        if (existing) {
            await context.addCookies(existing.cookies);
            // localStorage restaurado vía page.evaluate después de la navegación
        }

        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(),
        });
    }
}

Comprensión de página dirigida por LLM

La innovación principal: en lugar de escribir selectores CSS o consultas XPath para cada página, enviamos el árbol de accesibilidad de la página a un LLM y dejamos que decida con qué elementos interactuar.

async function extractPageStructure(page: Page): Promise<string> {
    // Obtener el árbol de accesibilidad (representación estructurada y compacta)
    const tree = await page.accessibility.snapshot();

    // Convertir a formato texto que el LLM pueda entender
    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) {
        // Ignorar elementos no interactivos, pero recorrer los hijos
        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;
}

Planificación de acciones por el LLM

Enviamos la estructura de la página al LLM junto con la descripción de la tarea. El LLM devuelve una secuencia de acciones:

async function planActions(pageStructure: string, task: string): Promise<Action[]> {
    const response = await llm.generate({
        model: 'gpt-4o-mini', // Modelo rápido para planificación de acciones
        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);
}

// Ejemplo de tarea: "Rellenar el formulario de contacto con nombre Sara Mustermann y email sara.mustermann@beispiel.de"
// El LLM devuelve:
// [
//   { "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" }
// ]

Resolución de acciones LLM a comandos Playwright

El LLM devuelve targets legibles por humanos ("Name input field"). Un resolver los mapea a selectores de Playwright:

async function resolveAndExecute(page: Page, actions: Action[]): Promise<void> {
    for (const action of actions) {
        // Encontrar el elemento que coincide con la descripción del LLM
        const element = await findElementByDescription(page, action.target);

        if (!element) {
            throw new ActionError(`Could not find element: ${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> {
    // Probar múltiples estrategias para encontrar el elemento
    const strategies = [
        // Por aria-label
        () => page.$(`[aria-label*="${description}" i]`),
        // Por placeholder
        () => page.$(`[placeholder*="${description}" i]`),
        // Por texto visible
        () => page.$(`text=${description}`),
        // Por asociación de label
        () => page.$(`label:has-text("${description}") + input, label:has-text("${description}") input`),
        // Por rol y nombre
        () => 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-detección: lo básico

Algunos sitios web detectan y bloquean navegadores headless. Las contramedidas básicas:

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

    // Rotación de user agents
    userAgent: getRandomUserAgent(),

    // Locale y zona horaria realistas
    locale: 'de-DE',
    timezoneId: 'Europe/Berlin',

    // Geolocalización realista
    geolocation: { latitude: 48.1351, longitude: 11.5820 },
    permissions: ['geolocation'],
});

// Sobreescribir navigator.webdriver (detección de headless)
await page.addInitScript(() => {
    Object.defineProperty(navigator, 'webdriver', { get: () => undefined });
});

Nota: la anti-detección es una carrera armamentística. Para sitios con detección de bots sofisticada (Cloudflare, Akamai), el Playwright auto-alojado acabará siendo detectado. Ahí es donde servicios de pago como BrowserBase aportan valor: invierten continuamente en anti-detección. Para la mayoría de tareas de automatización empresarial (herramientas internas, portales de socios, datos públicos), la anti-detección básica es suficiente.

Cuándo las herramientas de pago SÍ valen la pena

EscenarioAuto-alojadoServicio de pago
Automatización de herramientas internasMejor opción (no necesita anti-detección)Excesivo
Extracción de datos públicos (simple)Bueno (la anti-detección básica funciona)Innecesario
Sitios con detección de botsPosible pero mantenimiento constanteJustificado (ellos gestionan la anti-detección)
Scraping de alto volumen (10K+ páginas/día)Complejo (rotación de proxy, gestión de IP)Justificado (infraestructura gestionada)
Datos regulados (RGPD, cumplimiento normativo)Mejor (los datos se quedan en tu infraestructura)Riesgo (los datos pasan por un tercero)
Migración puntualBueno (carga de trabajo temporal)Coste innecesario

El marco de decisión: si automatizas workflows internos o procesas datos públicos de sitios sin detección de bots agresiva, auto-aloja. Si haces extracción de alto volumen desde sitios con protección nivel Cloudflare, paga por un servicio cuya anti-detección es su negocio principal.

Comparación de costes

ComponenteAuto-alojado (mensual)BrowserBase (mensual)
Compute (5 instancias)50-100 $ (contenedor/VPS)N/A
Llamadas LLM (planificación de acciones)20-50 $ (GPT-4o-mini)N/A
Sesiones BrowserBaseN/A500-2.000 $
Servicio de proxy (si es necesario)50-200 $Incluido
Mantenimiento2-4 horas/mesNinguno
Total (1.000 páginas/día)120-350 $/mes500-2.000 $/mes
Total (10.000 páginas/día)300-800 $/mes3.000-10.000 $/mes

El auto-alojamiento es de 3 a 10 veces más barato a escala. La contrapartida es el tiempo de mantenimiento y la capacidad de anti-detección.

Errores comunes

  1. Sin pool de instancias. Lanzar un nuevo navegador por tarea desperdicia 1-3 segundos en arranque en frío y 200-400 MB de RAM. Usa un pool y reutiliza instancias.

  2. Selectores CSS hardcodeados. Las páginas modifican su estructura DOM regularmente. La identificación de elementos por LLM es más resiliente que selectores hardcodeados.

  3. Sin persistencia de sesión. Los workflows multi-paso que requieren login fallan cuando el estado de sesión se pierde entre pasos.

  4. Ignorar completamente la anti-detección. Incluso las medidas básicas (viewport aleatorio, rotación de user agent, override de webdriver) evitan la detección en la mayoría de sitios.

  5. Usar un modelo grande para la planificación de acciones. GPT-4o-mini o Claude Haiku son lo suficientemente rápidos para la comprensión de página. Un modelo grande añade latencia sin mejorar la precisión para esta tarea.

  6. Sin timeout en la carga de páginas. Algunas páginas cargan indefinidamente (scroll infinito, scripts de terceros lentos). Define un timeout de navegación y gestiónalo.

  7. Ejecutar en producción sin monitorización. Monitoriza la tasa de éxito, el tiempo medio de ejecución y los tipos de error por workflow. Alerta cuando la tasa de éxito baje.

Puntos clave

  • Playwright auto-alojado + LLM cubre el 90 % de los casos de automatización de navegador. Para herramientas internas, portales de socios y datos públicos sin detección de bots agresiva, este es el enfoque correcto.

  • El pool de instancias es esencial. Reutiliza instancias de navegador entre tareas. Los arranques en frío y la asignación de memoria son el mayor cuello de botella de rendimiento.

  • La comprensión de página por LLM reemplaza selectores frágiles. Envía el árbol de accesibilidad a un modelo rápido. Deja que decida con qué elementos interactuar. Más resiliente a cambios de página que selectores CSS hardcodeados.

  • Los servicios de pago valen su coste para la anti-detección. Si tus sitios objetivo tienen Cloudflare o protección similar, BrowserBase invierte continuamente en evadirla. Es su negocio principal. No intentes competir.

  • El auto-alojamiento es de 3 a 10 veces más barato a escala. Pero pagas en tiempo de mantenimiento y limitaciones de anti-detección. Haz el compromiso con conocimiento de causa.

Integramos la automatización de navegador en nuestros sistemas de workflows IA y nuestros proyectos de software a medida. Si necesitas ayuda con la arquitectura de automatización de navegador, habla con nuestro equipo o solicita un presupuesto.

Temas cubiertos

automatización navegador IAnavegador headless TypeScriptPlaywright IAautomatización navegador open sourcealternativa BrowserBaseLLM navegadorscraping web IA

¿Listo para construir sistemas de IA listos para producción?

Nuestro equipo se especializa en sistemas de IA listos para producción. Hablemos de cómo podemos ayudar.

Iniciar una conversación