Browser Agent con Claude: Automazione Web con l’AI
Perché i Browser Agent nel 2026
L’automazione web tradizionale — selettori CSS fragili, XPath da incubo, script che si rompono al primo redesign — ha i giorni contati. I browser agent con AI rappresentano il salto evolutivo successivo: invece di dire al programma dove cliccare, gli dici cosa fare, e lui capisce da solo come farlo.
Nel 2026, Claude di Anthropic ha raggiunto una maturità tale da poter interpretare l’albero DOM, ragionare sulla struttura di una pagina e prendere decisioni autonome su come navigarla. Combinato con Playwright — il framework di automazione browser più robusto oggi disponibile — ottieni uno stack capace di fare cose che fino a poco tempo fa richiedevano un umano davanti allo schermo.
Se hai già lavorato con Playwright per automazione e scraping, sai quanto sia potente. Ma aggiungere un LLM al loop trasforma uno script rigido in un agente adattivo. Parliamo di: compilare form complessi interpretando il contesto, navigare siti con anti-bot senza riscrivere il codice ogni volta, estrarre dati strutturati da layout imprevedibili.
In questa guida costruiamo un browser agent funzionante da zero: architettura, tool use con Claude, gestione degli stati di navigazione, errori e casi limite. Tutto in TypeScript, tutto eseguibile nel tuo terminale oggi.
Architettura di un Browser Agent: il Loop Percezione—Ragionamento—Azione
Un browser agent segue un ciclo continuo: osserva lo stato della pagina, ragiona sul passo successivo, agisce tramite il browser. Questo loop si ripete fino al completamento del task o all’esaurimento dei tentativi.
La parte cruciale è come si passa lo “stato della pagina” a Claude. Non puoi inviare screenshot da 2MB a ogni step (caro e lento). La strategia migliore è estrarre uno snapshot accessibile del DOM: elementi interattivi, testi visibili, ruoli ARIA. Claude ragiona benissimo su questo tipo di rappresentazione testuale compatta.
// browser-agent/types.ts
export interface PageSnapshot {
url: string;
title: string;
interactiveElements: InteractiveElement[];
visibleText: string;
}
export interface InteractiveElement {
id: string; // ref univoco per l'agente
type: "button" | "link" | "input" | "select" | "textarea";
label: string; // testo visibile o aria-label
value?: string; // valore attuale (per input)
disabled: boolean;
selector: string; // CSS selector per Playwright
}
export interface AgentAction {
type: "click" | "type" | "navigate" | "scroll" | "wait" | "done" | "error";
elementId?: string;
value?: string;
url?: string;
reason: string; // Claude spiega il suo ragionamento
}
La funzione di snapshot DOM è il cuore del sistema. Usa Playwright per estrarre solo ciò che serve, mantenendo i payload piccoli e i costi API sotto controllo. Se utilizzi già Claude API, sai quanto impatta la lunghezza dei prompt — qui trovi approfondimenti sul Prompt Caching in Claude API per ottimizzare ulteriormente i costi.
// browser-agent/snapshot.ts
import { Page } from "playwright";
import { PageSnapshot, InteractiveElement } from "./types";
export async function capturePageSnapshot(page: Page): Promise<PageSnapshot> {
const url = page.url();
const title = await page.title();
const elements = await page.evaluate(() => {
const results: InteractiveElement[] = [];
let counter = 0;
const selectors = [
"button:not([disabled])",
"a[href]",
"input:not([type='hidden'])",
"select",
"textarea",
"[role='button']",
"[role='link']",
];
document.querySelectorAll(selectors.join(", ")).forEach((el) => {
const rect = el.getBoundingClientRect();
if (rect.width === 0 || rect.height === 0) return; // elemento nascosto
const label =
el.getAttribute("aria-label") ||
el.getAttribute("placeholder") ||
(el as HTMLElement).innerText?.trim().slice(0, 80) ||
el.getAttribute("name") ||
el.getAttribute("id") ||
"elemento senza label";
const id = `el_${counter++}`;
el.setAttribute("data-agent-id", id);
results.push({
id,
type: el.tagName.toLowerCase() as any,
label,
value: (el as HTMLInputElement).value || undefined,
disabled: (el as HTMLButtonElement).disabled || false,
selector: `[data-agent-id="${id}"]`,
});
});
return results;
});
// Testo visibile principale (limitato a 1500 char per contenere i token)
const visibleText = await page.evaluate(() => {
const body = document.body.cloneNode(true) as HTMLElement;
body.querySelectorAll("script, style, nav, footer").forEach((el) => el.remove());
return (body as HTMLElement).innerText?.slice(0, 1500) || "";
});
return { url, title, interactiveElements: elements, visibleText };
}
Tool Use con Claude: Definire le Azioni del Browser
Il vero motore del browser agent è il tool use di Claude. Definisci un set di azioni che l’agente può eseguire, e Claude decide quale usare in base allo stato della pagina. Se non hai ancora familiarità con questo pattern, ti consiglio di leggere prima il tutorial su Tool Use con Claude API.
La chiave è definire tool con descrizioni precise: Claude legge queste descrizioni per capire cosa fare. Sii esplicito sui parametri richiesti e sui casi d’uso di ciascun tool.
// browser-agent/tools.ts
import Anthropic from "@anthropic-ai/sdk";
export const BROWSER_TOOLS: Anthropic.Tool[] = [
{
name: "click_element",
description: "Clicca su un elemento interattivo della pagina (bottone, link, ecc.). Usa l'elementId dallo snapshot.",
input_schema: {
type: "object",
properties: {
elementId: { type: "string", description: "ID dell'elemento (es: el_3)" },
reason: { type: "string", description: "Perché stai cliccando questo elemento" },
},
required: ["elementId", "reason"],
},
},
{
name: "type_text",
description: "Digita testo in un campo input o textarea. Clicca prima sull'elemento, poi usa questo tool.",
input_schema: {
type: "object",
properties: {
elementId: { type: "string", description: "ID del campo input" },
text: { type: "string", description: "Testo da digitare" },
clearFirst: { type: "boolean", description: "Svuota il campo prima di digitare (default: true)" },
reason: { type: "string", description: "Perché stai digitando questo testo" },
},
required: ["elementId", "text", "reason"],
},
},
{
name: "navigate_to",
description: "Naviga a un URL specifico. Usa solo se necessario, preferisci cliccare i link della pagina.",
input_schema: {
type: "object",
properties: {
url: { type: "string", description: "URL di destinazione (assoluto)" },
reason: { type: "string", description: "Perché navighi a questo URL" },
},
required: ["url", "reason"],
},
},
{
name: "task_completed",
description: "Segnala che il task assegnato è stato completato con successo. Chiama SOLO quando hai finito.",
input_schema: {
type: "object",
properties: {
summary: { type: "string", description: "Riepilogo di cosa è stato fatto" },
extractedData: { type: "object", description: "Dati estratti (opzionale)", additionalProperties: true },
},
required: ["summary"],
},
},
];
Il Loop Principale dell’Agente
Con snapshot e tool definiti, possiamo scrivere il loop principale. L’architettura è quella classica degli agenti: accumula la cronologia dei messaggi, manda lo stato aggiornato a Claude, esegui l’azione richiesta, ripeti. Questo pattern è simile a quello descritto in Creare un Agente AI con JavaScript e Claude, ma specializzato per il browser.
// browser-agent/agent.ts
import Anthropic from "@anthropic-ai/sdk";
import { chromium, Browser, Page } from "playwright";
import { capturePageSnapshot } from "./snapshot";
import { BROWSER_TOOLS } from "./tools";
const client = new Anthropic();
async function executeTool(
page: Page,
toolName: string,
toolInput: Record<string, any>
): Promise<{ success: boolean; message: string; data?: any }> {
try {
switch (toolName) {
case "click_element": {
const snapshot = await capturePageSnapshot(page);
const el = snapshot.interactiveElements.find((e) => e.id === toolInput.elementId);
if (!el) return { success: false, message: `Elemento ${toolInput.elementId} non trovato` };
await page.click(el.selector);
await page.waitForLoadState("domcontentloaded", { timeout: 5000 }).catch(() => {});
return { success: true, message: `Cliccato: ${el.label}` };
}
case "type_text": {
const snapshot = await capturePageSnapshot(page);
const el = snapshot.interactiveElements.find((e) => e.id === toolInput.elementId);
if (!el) return { success: false, message: `Elemento ${toolInput.elementId} non trovato` };
if (toolInput.clearFirst !== false) await page.fill(el.selector, "");
await page.type(el.selector, toolInput.text, { delay: 50 });
return { success: true, message: `Digitato in ${el.label}: ${toolInput.text}` };
}
case "navigate_to": {
await page.goto(toolInput.url, { waitUntil: "domcontentloaded", timeout: 15000 });
return { success: true, message: `Navigato a: ${toolInput.url}` };
}
case "task_completed": {
return { success: true, message: "DONE", data: toolInput };
}
default:
return { success: false, message: `Tool sconosciuto: ${toolName}` };
}
} catch (err: any) {
return { success: false, message: `Errore: ${err.message}` };
}
}
export async function runBrowserAgent(task: string, startUrl: string): Promise<any> {
const browser: Browser = await chromium.launch({ headless: false }); // headless: true in prod
const page: Page = await browser.newPage();
await page.goto(startUrl, { waitUntil: "domcontentloaded" });
const messages: Anthropic.MessageParam[] = [];
let maxSteps = 15;
let result: any = null;
const systemPrompt = `Sei un browser agent preciso. Hai accesso a un browser web tramite tool.
Il tuo compito: completare il task assegnato navigando la pagina.
Ragioni passo per passo: osservi lo snapshot della pagina, scegli l'azione ottimale, la esegui.
Usa task_completed SOLO quando hai finito. Se impossibile, spiega il motivo nel summary.`;
while (maxSteps > 0) {
maxSteps--;
// Cattura stato attuale
const snapshot = await capturePageSnapshot(page);
const snapshotText = JSON.stringify({
url: snapshot.url,
title: snapshot.title,
elements: snapshot.interactiveElements.map((e) => ({
id: e.id,
type: e.type,
label: e.label,
value: e.value,
})),
visibleText: snapshot.visibleText,
}, null, 2);
const userMessage = messages.length === 0
? `Task: ${task}
Stato pagina iniziale:
${snapshotText}`
: `Stato pagina aggiornato dopo l'ultima azione:
${snapshotText}`;
messages.push({ role: "user", content: userMessage });
const response = await client.messages.create({
model: "claude-opus-4-5",
max_tokens: 1024,
system: systemPrompt,
tools: BROWSER_TOOLS,
messages,
});
// Aggiungi risposta di Claude alla history
messages.push({ role: "assistant", content: response.content });
// Processa tool calls
let hasToolCall = false;
const toolResults: Anthropic.ToolResultBlockParam[] = [];
for (const block of response.content) {
if (block.type === "tool_use") {
hasToolCall = true;
console.log(`[AGENT] Tool: ${block.name} | Motivo: ${(block.input as any).reason || ""}`);
const execResult = await executeTool(page, block.name, block.input as Record<string, any>);
console.log(`[AGENT] Risultato: ${execResult.message}`);
if (block.name === "task_completed") {
result = execResult.data;
await browser.close();
return result;
}
toolResults.push({
type: "tool_result",
tool_use_id: block.id,
content: JSON.stringify(execResult),
});
}
}
if (toolResults.length > 0) {
messages.push({ role: "user", content: toolResults });
}
if (!hasToolCall || response.stop_reason === "end_turn") break;
}
await browser.close();
return result;
}
Casi d’Uso Reali: Scraping Adattivo e Form Filling
Vediamo due scenari concreti dove il browser agent supera l’automazione tradizionale.
Scraping adattivo: hai un e-commerce che cambia layout ogni trimestre. Invece di aggiornare i selettori, l’agente capisce da solo dove trovare prezzo e titolo. Form filling intelligente: un form di registrazione con campi condizionali (“se sei un’azienda, compila la P.IVA”) — l’agente legge il contesto e decide cosa compilare.
// Esempio d'uso: scraping adattivo
import { runBrowserAgent } from "./agent";
async function scrapeProduct(url: string) {
const result = await runBrowserAgent(
`Estrai le seguenti informazioni da questa pagina prodotto:
- nome prodotto
- prezzo attuale (e prezzo scontato se presente)
- disponibilità (in stock / esaurito)
- valutazione media (stelle) se visibile
Inserisci tutto nel campo extractedData di task_completed.`,
url
);
console.log("Dati estratti:", result?.extractedData);
return result?.extractedData;
}
// Esempio d'uso: form filling automatizzato
async function autoRegister(formUrl: string, userData: Record<string, string>) {
const task = `Compila e invia il form di registrazione con questi dati:
${Object.entries(userData).map(([k, v]) => `- ${k}: ${v}`).join("
")}
Gestisci campi condizionali se appaiono.
Clicca il bottone di submit finale.
NON accettare cookie o popup: chiudili se appaiono.`;
return await runBrowserAgent(task, formUrl);
}
// Esecuzione
(async () => {
const productData = await scrapeProduct("https://example-shop.com/prodotto-123");
console.log(JSON.stringify(productData, null, 2));
})();
Questo approccio si integra perfettamente con pipeline di automazione più complesse. Se usi n8n, puoi richiamare l’agente come subprocess e passare i dati estratti a step successivi — un pattern descritto in dettaglio in Automatizzare la Gestione Email con n8n e Claude AI.
Gestione Errori, Anti-Bot e Fallback
Un browser agent in produzione deve saper gestire: captcha (rileva e ferma l’esecuzione), popup di consenso cookie (chiude automaticamente), timeout di rete (retry con backoff), pagine che cambiano struttura durante la navigazione.
La strategia migliore per i siti con anti-bot è usare un browser in modalità stealth (playwright-extra con il plugin stealth) e aggiungere ritardi casuali tra le azioni, mimando il comportamento umano. Claude aiuta qui: siccome ragiona su ogni passo, i gap temporali tra un’azione e l’altra sono naturalmente più lunghi di uno script che esegue in sequenza rigida.
// browser-agent/resilience.ts
import { Page } from "playwright";
// Chiude automaticamente i banner cookie comuni
export async function dismissCookieBanners(page: Page): Promise<void> {
const cookieSelectors = [
"[id*='cookie'] button[id*='accept']",
"[class*='cookie'] button[class*='accept']",
"button[data-testid*='accept']",
"#onetrust-accept-btn-handler",
".cc-accept",
"[aria-label*='Accept cookies']",
];
for (const selector of cookieSelectors) {
try {
const el = page.locator(selector).first();
if (await el.isVisible({ timeout: 500 })) {
await el.click();
console.log("[RESILIENCE] Banner cookie chiuso:", selector);
await page.waitForTimeout(500);
return;
}
} catch { /* continua */ }
}
}
// Rileva presenza di CAPTCHA
export async function detectCaptcha(page: Page): Promise<boolean> {
const captchaIndicators = [
"iframe[src*='recaptcha']",
"iframe[src*='hcaptcha']",
"[class*='captcha']",
"#captcha",
];
for (const selector of captchaIndicators) {
if (await page.locator(selector).count() > 0) return true;
}
return false;
}
// Wrapper con retry e delay umano
export async function humanLikeDelay(min = 500, max = 2000): Promise<void> {
const delay = Math.random() * (max - min) + min;
await new Promise((resolve) => setTimeout(resolve, delay));
}
Ottimizzare Costi e Performance in Produzione
Il costo principale di un browser agent è l’API di Claude: ogni step consuma token. Con task complessi da 10-15 step puoi arrivare a 20-30k token per esecuzione. Ecco come contenere i costi senza sacrificare qualità:
- Snapshot compatti: limita
visibleTexta 800-1000 caratteri, includi solo elementi interattivi visibili. - Modello giusto per il task: per task semplici (click su un bottone specifico) usa
claude-haiku-4-5, molto più economico. Riserva Opus per task complessi con ragionamento. - Prompt caching: il system prompt non cambia — marcalo come
cache_control: {type: "ephemeral"}per riutilizzarlo tra i step. Il risparmio può arrivare al 90% sui token del sistema. - Limita la history: tieni solo gli ultimi 6-8 messaggi invece della conversazione completa. Claude ha bisogno del contesto recente, non di tutta la cronologia.
- Parallellizzazione: se devi elaborare 100 URL, lancia 5-10 agenti in parallelo con pool di browser separati.
Per un’architettura RAG che integra dati storici nel ragionamento dell’agente (utile per agenti che devono ricordare sessioni precedenti), esplora il pattern descritto in RAG con Claude API.
FAQ e Domande Frequenti
Un browser agent può bypassare i sistemi anti-bot professionali come Cloudflare?
No, e non dovresti provare a farlo. Cloudflare Bot Management, PerimeterX e simili rilevano pattern a livello di TLS fingerprint, timing e comportamento mouse — cose che nemmeno il browser più stealth supera con certezza. L’approccio corretto per siti che non vogliono essere scraperati è usare le loro API ufficiali. Il browser agent brilla su siti che non hanno protezioni agressive, o su applicazioni interne dove hai il controllo dell’ambiente.
Qual è la differenza tra un browser agent e un semplice script Playwright?
Uno script Playwright è deterministico e fragile: sa esattamente cosa fare ma rompe al primo cambiamento di layout. Un browser agent è adattivo: capisce l’intenzione del task e trova da solo il modo di eseguirlo, anche se la pagina è diversa da come si aspettava. Il prezzo è la velocità (ogni step chiama l’API) e il costo. Per task stabili e ripetitivi, Playwright puro è meglio. Per task complessi o su siti che cambiano spesso, l’agente AI vale il costo.
Posso usare questo pattern con modelli open source invece di Claude?
Sì, tecnicamente. Llama 3.3 70B e Mistral Large supportano il tool use. In pratica, la qualità del ragionamento su DOM complessi con Claude Opus è significativamente superiore agli open source nel 2026. Per task semplici (click su bottoni chiari, compilazione form standard) un modello 7-13B può bastare. Per navigazione su siti reali con layout arbitrario, la differenza di qualità si sente: gli open source tendono a ciclare sullo stesso elemento o a perdere il contesto dopo pochi step.
Come gestisco le sessioni autenticate (login) nei browser agent?
Playwright ha un meccanismo nativo: storageState. Prima esegui il login manualmente (o con uno script dedicato) e salvi lo stato con await context.storageState({ path: "auth.json" }). Poi ogni agente parte con browser.newContext({ storageState: "auth.json" }) e trova la sessione già attiva. Per sessioni che scadono, aggiungi un check all’inizio di ogni run: se l’agente rileva la pagina di login, esegui il re-auth prima di procedere con il task principale.
Conclusione
I browser agent con Claude rappresentano una frontiera concreta dell’automazione web nel 2026 — non fantascienza, ma stack funzionante che puoi deployare oggi. La combinazione Playwright + Claude Tool Use ti dà un agente che pensa prima di agire, adattandosi a layout imprevedibili senza richiedere manutenzione continua del codice.
Il costo è reale: ogni step costa token, e task complessi da 10-15 step su Claude Opus pesano sul budget. Ma per casi d’uso ad alto valore — automazione di processi business critici, scraping su siti che cambiano spesso, test end-to-end intelligenti — il ROI è netto. Inizia da task semplici, misura i costi, poi scala.
Il prossimo step naturale è integrare questo agente in un workflow più ampio: un trigger n8n, una coda di task, output salvato su database. L’architettura è già pronta — serve solo cominciare.
Suggerimenti e Risorse
🔧 Tool: Usa
playwright-extracon il pluginpuppeteer-extra-plugin-stealth(compatibile con Playwright) per ridurre le probabilità di rilevamento bot su siti consumer.
💡 Pro tip: Aggiungi un tool
take_screenshotal tuo agente e loggalo su file. Quando l’agente si comporta in modo strano, guarda gli screenshot per capire cosa “vede” Claude ad ogni step — debug molto più veloce.
🎯 Strategia: Prima di costruire un browser agent, verifica se il sito target ha un’API non documentata (ispeziona le chiamate di rete nel DevTools). Spesso è più semplice chiamare direttamente l’API JSON che parsare il DOM.

