Browser Agents: Playwright + LLM, Browser-Use и дерево доступности


title: "Browser Agents: Playwright + LLM, Browser-Use и дерево доступности" slug: browser-agents-playwright-llm-2026-ru date: 2026-02-21 lang: ru tags: [browser-agents, playwright, llm, automation, browser-use, accessibility-tree] description: "Глубокий разбор production browser agents: библиотека Browser-Use, Playwright + Claude/GPT-4o vision, навигация по дереву доступности, заполнение форм, многошаговые веб-задачи, восстановление после ошибок и обработка аутентификации."

Browser Agents: Playwright + LLM, Browser-Use и дерево доступности

Ключевые факты

  • playwright==1.58.0 (последняя версия по состоянию на март 2026, требует установки системного браузера через playwright install chromium)
  • browser-use==0.12.0 (открытая высокоуровневая библиотека для LLM-управляемой автоматизации браузера)
  • langchain-anthropic==1.3.4 (интеграция Claude с Browser-Use)
  • langchain-community==0.4.1 (базовый пакет LangChain)
  • anthropic==0.84.0 (официальный Anthropic SDK для прямых вызовов Claude API)
  • Цены Claude Opus 4: ~$15 за 1M входящих токенов, ~$75 за 1M исходящих токенов (смешанная стоимость ~$0.015 за 1000 токенов)
  • Цены GPT-4o vision: ~$2.50 за 1M входящих токенов, ~$10 за 1M исходящих токенов
  • Видимая область по умолчанию: 1920×1080 для desktop-агентов, 1280×720 для более быстрой обработки скриншотов
  • Типичные значения timeout: загрузка страницы 30000ms, взаимодействие с элементом 5000ms, networkidle 10000ms
  • Размер скриншота: качество JPEG 85, только видимая область (не полная страница) уменьшает с ~200KB до ~50KB
  • Эффективность токенов дерева доступности: 1200 токенов на страницу против 8500 токенов для vision-подхода
  • Задержка Vision: ~2800ms на страницу против ~650ms для дерева доступности
  • Аргументы Headless Chrome: --no-sandbox, --disable-dev-shm-usage, --disable-blink-features=AutomationControlled
  • User-Agent для скрытности: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36
  • Максимум шагов для ReAct цикла: 30-50 итераций перед timeout/отказом
  • Задержка решения reCAPTCHA: 15-120 секунд через сервисы anti-captcha
  • Стоимость Anti-captcha API: ~$2.99 за 1000 решённых CAPTCHA
  • Сохранение сессии: сохраняйте JSON storage_state() чтобы избежать повторной аутентификации
  • Ограничение скорости: 1-2 запроса в секунду для избежания обнаружения агрессивного скрейпинга
  • Стоимость за 1000 спарсенных страниц: ~$0.32 (дерево доступности) vs ~$2.40 (vision)
  • Browser agent vs scraper: используйте агентов когда структура страницы часто меняется или требуется многошаговая навигация
  • Browser agent vs RPA: агенты адаптируются к изменениям UI без хрупких селекторов; RPA лучше для pixel-perfect рабочих процессов
  • Production отказы (по частоте): timeout навигации, элемент не найден, CAPTCHA вызовы, истечение аутентификации
  • Облачные sandbox опции: Browserbase ($49-199/mo), Steel ($29-99/mo), Hyperbrowser ($79-199/mo)

Что такое Browser Agent

Browser agent — это ReAct-style (Reason + Act) управляющий цикл, который объединяет LLM с фреймворком автоматизации браузера (обычно Playwright). В отличие от традиционных веб-скрейперов, которые опираются на фиксированные CSS селекторы или XPath выражения, browser agents понимают намерение, адаптируются к изменениям структуры страницы, восстанавливаются после ошибок и выполняют многошаговые рабочие процессы, указанные на естественном языке.

Основная архитектура:

  1. Observe: Захватите текущее состояние браузера через скриншот (vision-based) или дерево доступности (structure-based)
  2. Think: Передайте наблюдение LLM с целью и историей действий
  3. Act: LLM возвращает структурированное действие (click, type, navigate, scroll, extract)
  4. Execute: Playwright выполняет действие в реальном браузере
  5. Repeat: Продолжайте до достижения цели или превышения максимума шагов

Screenshot-Action цикл (Vision-Based):

Browser page → Screenshot (base64 JPEG) → LLM vision model →
Structured action {action: "click", x: 450, y: 200} →
Playwright executes → New page state → Loop

Accessibility Tree цикл (Structure-Based):

Browser page → Accessibility tree extraction → Compact text representation →
LLM text model → Structured action {action: "click", element_id: 12} →
Playwright executes → New page state → Loop

Vision-based агенты работают на любой странице (включая canvas элементы, изображения, плохо доступные SPA) но стоят в 7 раз дороже за страницу и имеют в 4 раза более высокую задержку. Агенты на основе дерева доступности быстрее и дешевле, но не работают на canvas-базированных приложениях (Figma, Google Maps) и React приложениях с грязной разметкой без ARIA атрибутов.

Фреймворк решений

Когда использовать Browser Agent:

  • Структура страницы часто меняется (страницы конкурентов с ценами, e-commerce сайты)
  • Многошаговые рабочие процессы с условной логикой (если товар нет в наличии, выберите альтернативу)
  • Заполнение форм с динамической валидацией (поля появляются/исчезают на основе предыдущих вводов)
  • Требуется человеческое рассуждение ("найти самый дешёвый план с минимум 10GB памяти")

Когда использовать вместо этого Selenium/Playwright scraper:

  • Статическая структура страницы, которая редко меняется (страница компании, о нас)
  • Простая экстракция данных из известных селекторов (цена из #product-price)
  • Высокообъёмный скрейпинг где стоимость LLM за страницу неприемлема (>10,000 страниц/день)
  • Требования по задержке менее 500ms на страницу

Когда использовать вместо этого RPA (UI.Vision, Automation Anywhere):

  • Десктопные приложения, а не только веб-браузеры
  • Требуется pixel-perfect сопоставление скриншотов для визуальной верификации
  • Рабочий процесс должен выполняться идентично каждый раз (требования соответствия)
  • Наследованные системы без API или доступа к структурированному HTML

Матрица решений:

  • Статическая структура страницы + известные селекторы → Playwright scraper
  • Структура страницы часто меняется → Browser agent (дерево доступности)
  • Требуются canvas/визуальные элементы → Browser agent (vision)
  • Требуется автоматизация десктопа → RPA
  • Многошаговый с условной логикой → Browser agent

Таблица справочных параметров

Параметр Типичное значение Примечания
viewport.width 1920 Стандартная ширина desktop, используйте 1280 для более быстрых скриншотов
viewport.height 1080 Стандартная высота desktop, используйте 720 для более быстрых скриншотов
headless True Production по умолчанию; False для отладки
timeout (page load) 30000 Миллисекунды; увеличивайте до 60000 для медленных сайтов
timeout (element) 5000 Миллисекунды; ожидание появления элемента
screenshot_quality 85 JPEG качество (0-100); более низкое уменьшает количество токенов
screenshot_type 'jpeg' Используйте 'png' для pixel-perfect захватов (редко)
full_page False True захватывает всю прокручиваемую область (дорого)
max_steps 30-50 ReAct цикл итерации перед отказом
temperature 0.0 LLM детерминизм; используйте 0.0 для production агентов
max_tokens 1024-4096 LLM output; 1024 достаточно для действий, 4096 для экстракции
user_agent Mozilla/5.0...Chrome/120.0.0.0 Скрытность; избегайте обнаружения как бота
locale 'en-US' Локаль браузера для согласованного рендеринга
timezone_id 'America/New_York' Избегайте fingerprinting по временной зоне
wait_until (navigate) 'networkidle' Опции: 'load', 'domcontentloaded', 'networkidle'
delay (typing) 50 Миллисекунды между нажатиями клавиш; имитирует человеческую печать
slow_mo (debug) 500 Миллисекунды; замедляет все действия для визуальной отладки

Частые ошибки

Ошибка 1: Использование полностраничных скриншотов для каждого действия

Симптом: Высокая задержка (3-5 секунд на действие), чрезмерные стоимости LLM, превышение лимитов токенов

Влияние: Стоимость в 7 раз выше за страницу, задержка в 4 раза выше, отказ на длинных страницах, превышающих лимиты vision токенов

Неправильно:

async def capture_screenshot(page: Page) -> str:
    screenshot_bytes = await page.screenshot(
        full_page=True,  # НЕПРАВИЛЬНО: захватывает всю прокручиваемую страницу
        type='png',      # НЕПРАВИЛЬНО: PNG в 3-5 раз больше JPEG
        quality=100,     # НЕПРАВИЛЬНО: ненужное качество
    )
    return base64.b64encode(screenshot_bytes).decode('utf-8')

Правильно:

async def capture_screenshot(page: Page) -> str:
    screenshot_bytes = await page.screenshot(
        full_page=False,  # Только видимая область: 50KB vs 200KB
        type='jpeg',      # JPEG сжатие
        quality=85,       # Оптимальный компромисс качество/размер
    )
    return base64.b64encode(screenshot_bytes).decode('utf-8')

Ошибка 2: Клик по пиксельным координатам на vision-based агентах

Симптом: Клики промахиваются по целевым элементам, особенно на отзывчивых страницах или разных размерах viewport

Влияние: Высокий процент отказов действий на coordinate-based кликах, требует retry логика, тратит вызовы LLM

Неправильно:

async def execute_action(page: Page, action: dict) -> str:
    if action.get('action') == 'click':
        # НЕПРАВИЛЬНО: пиксельные координаты, предоставленные LLM, неточны
        await page.mouse.click(action['x'], action['y'])
        return f"Clicked at ({action['x']}, {action['y']})"

Правильно:

async def execute_action(page: Page, action: dict) -> str:
    if action.get('action') == 'click':
        # Используйте индекс дерева доступности или текстовый локатор
        if 'element_id' in action:
            selector = element_map[action['element_id']]
            locator = page.locator(selector).first
            await locator.scroll_into_view_if_needed()
            await locator.click(timeout=5000)
        elif 'text' in action:
            locator = page.get_by_text(action['text'], exact=False).first
            await locator.click(timeout=5000)
        return f"Clicked element: {action.get('element_id', action.get('text'))}"

Ошибка 3: Несохранение состояния аутентификации между сессиями

Симптом: Агент входит заново на каждый запуск, вызывает ограничения скорости, встречает CAPTCHA, тратит 10-30 секунд на сессию

Влияние: Выполнение в 2-3 раза медленнее, вызовы CAPTCHA, блокировка аккаунтов от подозрительных паттернов входа

Неправильно:

async def run_agent_task(task: str, credentials: dict) -> str:
    async with async_playwright() as p:
        browser = await p.chromium.launch(headless=True)
        page = await browser.new_page()

        # НЕПРАВИЛЬНО: свежий вход каждый раз
        await page.goto("https://example.com/login")
        await page.fill('#email', credentials['email'])
        await page.fill('#password', credentials['password'])
        await page.click('[type="submit"]')
        await page.wait_for_url('**/dashboard')

        # ... выполнить задачу

Правильно:

async def run_agent_task(task: str, auth_state_path: str) -> str:
    async with async_playwright() as p:
        browser = await p.chromium.launch(headless=True)

        # Загрузите сохранённое состояние аутентификации
        if Path(auth_state_path).exists():
            context = await browser.new_context(
                storage_state=auth_state_path
            )
            page = await context.new_page()
            await page.goto("https://example.com/dashboard")

            # Проверьте, что аутентификация всё ещё действительна
            if await page.query_selector('a[href="/logout"]'):
                # ... выполнить задачу
                return

        # Вход только если аутентификация истекла
        context = await browser.new_context()
        page = await context.new_page()
        # ... выполнить вход один раз
        await context.storage_state(path=auth_state_path)

Ошибка 4: Использование wait_for_load_state('load') после навигации

Симптом: Содержимое страницы неполное, динамические элементы отсутствуют, действия отказывают сразу после навигации

Влияние: Значительно более высокий процент отказов на SPA, требует retry логика, непостоянное поведение

Неправильно:

async def navigate_and_act(page: Page, url: str):
    await page.goto(url, wait_until='load')  # НЕПРАВИЛЬНО: срабатывает когда HTML загружен
    # На этом этапе JavaScript может всё ещё инициализироваться
    await page.click('#dynamic-button')  # ОТКАЗ: элемент ещё не отрендерен

Правильно:

async def navigate_and_act(page: Page, url: str):
    await page.goto(url, wait_until='networkidle')  # Все сетевые запросы завершены
    # Или ожидайте конкретного элемента:
    await page.wait_for_selector('#dynamic-button', state='visible', timeout=10000)
    await page.click('#dynamic-button')

Ошибка 5: Отсутствие обработки неожиданных попапов и диалогов

Симптом: Агент застревает ожидая действие, не обнаруживает модальные окна, блокирующие взаимодействие

Влияние: Значительная доля production запусков прерывается cookie баннерами, newsletter попапами и chat виджетами

Неправильно:

async def execute_step(page: Page, action: dict):
    # НЕПРАВИЛЬНО: предполагает что страница готова к взаимодействию
    await page.click(action['selector'])

Правильно:

async def execute_step(page: Page, action: dict):
    # Проверьте наличие блокирующих диалогов перед любым действием
    dialog_selectors = [
        '[role="dialog"]',
        '[role="alertdialog"]',
        '[aria-modal="true"]',
        '.modal.show',
        '#cookie-banner',
    ]

    for selector in dialog_selectors:
        dialog = await page.query_selector(selector)
        if dialog and await dialog.is_visible():
            # Попытайтесь закрыть
            close_btn = await dialog.query_selector(
                'button[aria-label*="Close"], button[aria-label*="close"], .close, [data-dismiss]'
            )
            if close_btn:
                await close_btn.click()
                await asyncio.sleep(0.5)
            else:
                await page.keyboard.press('Escape')
                await asyncio.sleep(0.5)

    # Теперь выполните предполагаемое действие
    await page.click(action['selector'])

Реализация

Browser-Use: Высокоуровневая библиотека

Browser-Use предоставляет production-ready абстракцию над Playwright + LLM интеграцией:

pip install browser-use==0.12.0 playwright==1.58.0 langchain-anthropic==1.3.4
playwright install chromium

Базовое использование:

import asyncio
from browser_use import Agent
from langchain_anthropic import ChatAnthropic

async def run_browser_agent(task: str) -> str:
    """Запустите browser agent для данной задачи."""
    llm = ChatAnthropic(
        model="claude-opus-4-6",
        temperature=0,
        max_tokens=4096,
    )

    agent = Agent(
        task=task,
        llm=llm,
        use_vision=False,           # Дерево доступности по умолчанию
        vision_fallback=True,       # Используйте vision если tree недостаточно
    )

    result = await agent.run()
    return result

# Экстрактируйте данные ценообразования с сайта конкурента
result = asyncio.run(run_browser_agent(
    "Go to competitor.com/pricing, extract all plan names and prices as JSON"
))

Пользовательские действия с Browser-Use:

from browser_use import Agent, Controller
from browser_use.browser.context import BrowserContext
from pydantic import BaseModel

controller = Controller()

class LoginCredentials(BaseModel):
    username: str
    password: str

@controller.action(
    "Login to the application with provided credentials",
    param_model=LoginCredentials
)
async def login_action(
    params: LoginCredentials,
    browser: BrowserContext
) -> str:
    """Пользовательское действие входа с правильной обработкой учётных данных."""
    page = await browser.get_current_page()

    await page.wait_for_selector('#username', timeout=10000)
    await page.fill('#username', params.username)
    await page.fill('#password', params.password)
    await page.click('[type="submit"]')
    await page.wait_for_load_state('networkidle')

    if 'dashboard' in page.url:
        return "Successfully logged in"
    else:
        error = await page.text_content('.error-message')
        return f"Login failed: {error}"

agent = Agent(
    task="Login with username='admin@corp.com' and execute task",
    llm=llm,
    controller=controller,
)

Прямая интеграция Playwright + Claude

Для максимального контроля, строите напрямую на Playwright:

import base64
import json
import asyncio
from playwright.async_api import async_playwright, Page
import anthropic

client = anthropic.Anthropic()

async def capture_screenshot(page: Page) -> str:
    """Захватите скриншот видимой области как base64 JPEG."""
    screenshot_bytes = await page.screenshot(
        full_page=False,
        type='jpeg',
        quality=85,
    )
    return base64.b64encode(screenshot_bytes).decode('utf-8')

async def get_llm_action(
    goal: str,
    screenshot_b64: str,
    url: str,
    history: list[dict],
    step: int,
) -> dict:
    """Спросите Claude какое действие выполнить дано текущее состояние браузера."""

    system = """You are a browser automation agent. Given a screenshot and goal,
    determine the next action. Return JSON only:
    - action: one of [click, type, navigate, scroll, extract, done]
    - x, y: coordinates for click
    - text: text to type
    - url: URL to navigate
    - scroll_direction: up/down
    - data: extracted data (if extract or done)
    - reasoning: brief explanation"""

    messages = [{
        "role": "user",
        "content": [
            {
                "type": "text",
                "text": f"Goal: {goal}\nCurrent URL: {url}\nStep: {step}\n\nPrevious actions: {json.dumps(history[-5:])}\n\nWhat is the next action?"
            },
            {
                "type": "image",
                "source": {
                    "type": "base64",
                    "media_type": "image/jpeg",
                    "data": screenshot_b64,
                }
            }
        ]
    }]

    response = client.messages.create(
        model="claude-opus-4-6",
        max_tokens=1024,
        system=system,
        messages=messages,
    )

    return json.loads(response.content[0].text)

async def execute_action(page: Page, action: dict) -> str:
    """Выполните LLM-указанное действие."""
    action_type = action.get('action')

    if action_type == 'click':
        await page.mouse.click(action['x'], action['y'])
        await page.wait_for_load_state('networkidle', timeout=5000)
        return f"Clicked at ({action['x']}, {action['y']})"

    elif action_type == 'type':
        await page.keyboard.type(action['text'], delay=50)
        return f"Typed: {action['text']}"

    elif action_type == 'navigate':
        await page.goto(action['url'], wait_until='networkidle')
        return f"Navigated to {action['url']}"

    elif action_type == 'scroll':
        direction = action.get('scroll_direction', 'down')
        delta = 500 if direction == 'down' else -500
        await page.mouse.wheel(0, delta)
        await asyncio.sleep(0.5)
        return f"Scrolled {direction}"

    elif action_type in ('extract', 'done'):
        return f"Data: {action.get('data', {})}"

    return f"Unknown action: {action_type}"

async def run_vision_agent(goal: str, start_url: str, max_steps: int = 30) -> dict:
    """Запустите vision-based browser agent."""
    history = []

    async with async_playwright() as p:
        browser = await p.chromium.launch(
            headless=True,
            args=['--no-sandbox', '--disable-dev-shm-usage']
        )
        context = await browser.new_context(
            viewport={'width': 1280, 'height': 720},
            user_agent='Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36'
        )
        page = await context.new_page()

        await page.goto(start_url, wait_until='networkidle')

        for step in range(max_steps):
            screenshot = await capture_screenshot(page)
            url = page.url

            action = await get_llm_action(goal, screenshot, url, history, step)

            if action.get('action') == 'done':
                await browser.close()
                return {
                    'success': True,
                    'steps': step + 1,
                    'data': action.get('data', {}),
                    'history': history,
                }

            result = await execute_action(page, action)
            history.append({
                'step': step,
                'action': action,
                'result': result,
            })

        await browser.close()

    return {
        'success': False,
        'steps': max_steps,
        'reason': 'Max steps reached',
        'history': history,
    }

Экстракция дерева доступности

Подход на основе дерева доступности обеспечивает снижение стоимости в 7 раз и улучшение задержки в 4 раза:

async def get_accessibility_tree(page: Page) -> str:
    """Экстрактируйте компактное представление дерева доступности."""

    tree = await page.accessibility.snapshot(
        interesting_only=True,  # Фильтруйте интерактивные/content элементы
    )

    def format_node(node: dict, depth: int = 0, index: list = [0]) -> str:
        indent = "  " * depth
        role = node.get('role', '')
        name = node.get('name', '')
        value = node.get('value', '')

        # Пропускайте чисто структурные узлы
        if role in ('none', 'generic') and not name:
            children = node.get('children', [])
            return '\n'.join(format_node(c, depth, index) for c in children)

        current_idx = index[0]
        index[0] += 1

        parts = [f"[{current_idx}] {role}"]
        if name:
            parts.append(f"'{name}'")
        if value:
            parts.append(f"value='{value}'")

        line = f"{indent}{' '.join(parts)}"

        children = node.get('children', [])
        child_text = '\n'.join(
            format_node(c, depth + 1, index)
            for c in children
        )

        return f"{line}\n{child_text}" if child_text else line

    return format_node(tree)

# Пример вывода:
# [0] RootWebArea 'Google'
#   [1] combobox 'Search' value=''
#   [2] button 'Google Search'
#   [3] button "I'm Feeling Lucky"

Умное наблюдение (гибридный подход):

async def smart_observation(page: Page) -> dict:
    """Используйте дерево доступности по умолчанию, переходите на vision когда нужно."""

    tree = await get_accessibility_tree(page)
    interactive_elements = tree.count('[')

    # Если меньше 5 интерактивных элементов, дерево, вероятно, бесполезно
    if interactive_elements < 5:
        screenshot = await capture_screenshot(page)
        return {
            'mode': 'vision',
            'screenshot': screenshot,
            'tree': None,
            'reason': 'Poor accessibility structure'
        }

    # Проверьте наличие canvas элементов (vision нужна)
    has_canvas = await page.evaluate(
        "() => document.querySelectorAll('canvas').length > 0"
    )
    if has_canvas:
        screenshot = await capture_screenshot(page)
        return {
            'mode': 'vision',
            'screenshot': screenshot,
            'tree': tree,
            'reason': 'Canvas elements detected'
        }

    return {
        'mode': 'accessibility',
        'screenshot': None,
        'tree': tree,
        'reason': 'Good accessibility structure'
    }

Облачные Sandbox опции

Запуск browser agents в облаке избегает управления инфраструктурой и обеспечивает ротирующиеся IP-адреса, evasion отпечатков и решение CAPTCHA.

Таблица сравнения

Провайдер Цены Функции Лучше всего для
Browserbase $49-199/mo Режим скрытности, запись сессии, инструменты отладки, резидентные прокси Production скрейпинг в масштабе
Steel $29-99/mo Playwright-совместимый API, экспорт скриншотов/PDF, простые цены Мелкие и средние нагрузки
Hyperbrowser $79-199/mo Продвинутый evasion отпечатков, решение CAPTCHA, гео-таргетинг Высокозащищённый скрейпинг (банкинг, e-commerce)

Интеграция Browserbase

pip install browserbase-sdk
from browserbase import Browserbase

bb = Browserbase(api_key="YOUR_API_KEY")

# Создайте сессию
session = bb.sessions.create()

# Подключите Playwright к удалённому браузеру
from playwright.async_api import async_playwright

async with async_playwright() as p:
    browser = await p.chromium.connect_over_cdp(session.ws_endpoint)
    page = await browser.new_page()

    # Используйте browser agent обычно
    agent = Agent(task="...", llm=llm)
    result = await agent.run(page=page)

Интеграция Steel

pip install steel-sdk
from steel import Steel

steel = Steel(api_key="YOUR_API_KEY")

# Запустите удалённую сессию браузера
session = steel.sessions.create(
    proxy_mode="residential",  # Ротирующиеся резидентные IP-адреса
    solve_captcha=True,        # Автоматическое решение CAPTCHA
)

# Подключите Playwright
async with async_playwright() as p:
    browser = await p.chromium.connect_over_cdp(session.connect_url)
    page = await browser.new_page()
    # ... используйте agent

Production-соображения

Ограничение скорости и Anti-Detection

Конфигурация скрытности:

class BrowserConfig:
    """Production профили конфигурации браузера."""

    @staticmethod
    def stealth_production() -> dict:
        """Максимальная скрытность для production скрейпинга."""
        return {
            'headless': True,
            'args': [
                '--no-sandbox',
                '--disable-dev-shm-usage',
                '--disable-blink-features=AutomationControlled',
                '--disable-infobars',
                '--window-size=1920,1080',
                '--disable-extensions',
                '--disable-gpu',
            ],
            'ignore_default_args': ['--enable-automation'],
        }

    @staticmethod
    def stealth_context() -> dict:
        """Настройки контекста браузера чтобы избежать обнаружения."""
        return {
            'viewport': {'width': 1920, 'height': 1080},
            'user_agent': (
                'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 '
                '(KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
            ),
            'locale': 'en-US',
            'timezone_id': 'America/New_York',
            'geolocation': {'latitude': 40.7128, 'longitude': -74.0060},
            'permissions': ['geolocation'],
            'extra_http_headers': {
                'Accept-Language': 'en-US,en;q=0.9',
                'Accept-Encoding': 'gzip, deflate, br',
            }
        }

Управление сессией аутентификации

import json
from pathlib import Path
from playwright.async_api import async_playwright, BrowserContext

class AuthManager:
    """Управляйте состоянием аутентификации браузера для агентов."""

    def __init__(self, auth_dir: str = "/tmp/browser_auth"):
        self.auth_dir = Path(auth_dir)
        self.auth_dir.mkdir(exist_ok=True)

    def _state_path(self, domain: str) -> Path:
        safe_domain = domain.replace('https://', '').replace('/', '_')
        return self.auth_dir / f"{safe_domain}_state.json"

    async def save_auth_state(self, context: BrowserContext, domain: str):
        """Сохраните состояние аутентификации браузера на диск."""
        state = await context.storage_state()
        state_path = self._state_path(domain)
        state_path.write_text(json.dumps(state))

    async def load_auth_state(
        self,
        playwright,
        domain: str,
        headless: bool = True
    ) -> BrowserContext:
        """Загрузите сохранённое состояние аутентификации или вызовите свежий вход."""
        state_path = self._state_path(domain)
        browser = await playwright.chromium.launch(headless=headless)

        if state_path.exists():
            context = await browser.new_context(
                storage_state=str(state_path)
            )

            # Проверьте что состояние аутентификации всё ещё действительно
            page = await context.new_page()
            await page.goto(domain)

            if await self._is_authenticated(page, domain):
                return context

            # Аутентификация истекла, очистите
            state_path.unlink()
            await context.close()

        # Свежий контекст для нового входа
        context = await browser.new_context()
        return context

    async def _is_authenticated(self, page, domain: str) -> bool:
        """Проверьте если текущая сессия аутентифицирована."""
        indicators = [
            '[data-user-id]',
            '.user-avatar',
            '[aria-label*="User menu"]',
            'a[href*="/logout"]',
        ]

        for selector in indicators:
            if await page.query_selector(selector):
                return True

        if any(keyword in page.url for keyword in ['/login', '/signin', '/auth']):
            return False

        return True

Обработка CAPTCHA

class CaptchaHandler:
    """Обрабатывайте вызовы CAPTCHA в browser agents."""

    def __init__(self, anticaptcha_key: str = None):
        self.anticaptcha_key = anticaptcha_key

    async def detect_captcha(self, page: Page) -> dict:
        """Обнаружьте какой тип CAPTCHA присутствует."""
        captcha_info = await page.evaluate("""
        () => {
            // Проверьте reCAPTCHA
            if (document.querySelector('.g-recaptcha') ||
                document.querySelector('[data-sitekey]')) {
                const el = document.querySelector('[data-sitekey]');
                return {
                    type: 'recaptcha',
                    sitekey: el ? el.getAttribute('data-sitekey') : null
                };
            }

            // Проверьте hCaptcha
            if (document.querySelector('.h-captcha')) {
                const el = document.querySelector('[data-hcaptcha-sitekey]');
                return {
                    type: 'hcaptcha',
                    sitekey: el ? el.getAttribute('data-hcaptcha-sitekey') : null
                };
            }

            // Проверьте Cloudflare Turnstile
            if (document.querySelector('.cf-turnstile')) {
                return { type: 'turnstile' };
            }

            return { type: null };
        }
        """)

        return captcha_info

    async def solve_recaptcha_v2(self, page: Page, sitekey: str) -> bool:
        """Решите reCAPTCHA v2 используя anti-captcha сервис."""
        if not self.anticaptcha_key:
            return False

        import aiohttp

        # Отправьте задачу на решение
        async with aiohttp.ClientSession() as session:
            task_response = await session.post(
                'https://api.anti-captcha.com/createTask',
                json={
                    "clientKey": self.anticaptcha_key,
                    "task": {
                        "type": "NoCaptchaTaskProxyless",
                        "websiteURL": page.url,
                        "websiteKey": sitekey,
                    }
                }
            )
            task_data = await task_response.json()
            task_id = task_data.get('taskId')

            if not task_id:
                return False

            # Опрашивайте результат (макс 120 секунд)
            for _ in range(24):
                await asyncio.sleep(5)

                result_response = await session.post(
                    'https://api.anti-captcha.com/getTaskResult',
                    json={
                        "clientKey": self.anticaptcha_key,
                        "taskId": task_id,
                    }
                )
                result_data = await result_response.json()

                if result_data.get('status') == 'ready':
                    token = result_data['solution']['gRecaptchaResponse']

                    # Внедрите токен в страницу
                    await page.evaluate(f"""
                    () => {{
                        document.getElementById('g-recaptcha-response').innerHTML = '{token}';
                    }}
                    """)
                    return True

        return False

Мониторинг и наблюдаемость

import time
from dataclasses import dataclass
from typing import Optional
import logging

logger = logging.getLogger(__name__)

@dataclass
class AgentMetrics:
    task_id: str
    start_time: float
    end_time: Optional[float] = None
    steps_taken: int = 0
    llm_calls: int = 0
    tokens_used: int = 0
    screenshots_taken: int = 0
    success: Optional[bool] = None
    error: Optional[str] = None

    @property
    def duration_seconds(self) -> float:
        if self.end_time:
            return self.end_time - self.start_time
        return time.time() - self.start_time

    @property
    def estimated_cost_usd(self) -> float:
        # Цены Claude Opus 4 (смешанные)
        return self.tokens_used * 0.000015

class InstrumentedAgent:
    """Browser agent со встроенной наблюдаемостью."""

    def __init__(self, task: str, llm, task_id: str = None):
        self.task = task
        self.llm = llm
        self.task_id = task_id or f"task_{int(time.time())}"
        self.metrics = AgentMetrics(
            task_id=self.task_id,
            start_time=time.time(),
        )

    async def run(self) -> tuple[str, AgentMetrics]:
        """Запустите agent и верните результат с метриками."""
        try:
            result = await self._run_with_instrumentation()
            self.metrics.success = True
            return result, self.metrics
        except Exception as e:
            self.metrics.success = False
            self.metrics.error = str(e)
            self.metrics.end_time = time.time()

            logger.error(
                f"Agent task {self.task_id} failed after "
                f"{self.metrics.duration_seconds:.1f}s: {e}",
                extra={'metrics': self.metrics.__dict__}
            )
            raise
        finally:
            self.metrics.end_time = time.time()

            logger.info(
                f"Agent task completed",
                extra={
                    'task_id': self.task_id,
                    'duration_s': self.metrics.duration_seconds,
                    'steps': self.metrics.steps_taken,
                    'cost_usd': self.metrics.estimated_cost_usd,
                    'success': self.metrics.success,
                }
            )

    async def _run_with_instrumentation(self) -> str:
        agent = Agent(task=self.task, llm=self.llm)
        result = await agent.run()
        return result

Производительность и бенчмарки

Примечание: Приведённые ниже цифры являются иллюстративными оценками на основе типичных production-конфигураций, а не измерениями конкретной системы.

Сравнение задержек (на страницу):

  • Vision-based агент: ~2800ms (1200ms скриншот + 1600ms LLM вывод)
  • Агент дерева доступности: ~650ms (150ms экстракция дерева + 500ms LLM вывод)
  • Традиционный Playwright скрейпер: ~150ms (без вызова LLM)

Сравнение стоимости (за 1000 страниц, используя Claude Opus 4):

  • Vision-based: ~$2.40 (8500 токенов/страницу × $0.015/1K токенов × 1000 страниц)
  • Дерево доступности: ~$0.32 (1200 токенов/страницу × $0.015/1K токенов × 1000 страниц)
  • Традиционный скрейпер: $0 (нет вызовов LLM)

Точность на типичных страницах e-commerce продуктов:

  • Vision-based: 87% процент успеха задачи
  • Дерево доступности: 79% процент успеха задачи (отказывает на div-soup React приложениях)
  • Традиционный скрейпер: 95% процент успеха (но требует ручного обновления селекторов при изменении страницы)

Разбор потребления токенов (подход дерева доступности):

  • Среднее представление страницы: 1200 токенов
  • История действий (последние 5 шагов): 300 токенов
  • System prompt: 200 токенов
  • LLM вывод (действие): 50-150 токенов
  • Итого за шаг: ~1750 токенов

Распределение production отказов:

  • Timeout навигации (40%): медленные страницы, проблемы сети
  • Элемент не найден (30%): динамическое содержимое, плохие селекторы
  • Вызов CAPTCHA (20%): anti-bot детекция сработала
  • Аутентификация истекла (10%): timeout сессии, инвалидация cookie

Стоимость в масштабе (10,000 страниц/день):

  • Дерево доступности: $3.20/день ($96/месяц)
  • Vision-based: $24/день ($720/месяц)
  • Cloud sandbox (Browserbase): $199/месяц (неограниченные сессии)
  • Anti-captcha сервис (5% CAPTCHA доля): $1.50/день ($45/месяц)
  • Итого (дерево доступности + облако + CAPTCHA): ~$240/месяц
  • Итого (vision + облако + CAPTCHA): ~$965/месяц