ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Computer-Use 与 Browser-Agent 实战:用 Playwright + Accessibility Tree 搭一套可复现的 WebArena 评测骨架

Computer-Use 与 Browser-Agent 实战:用 Playwright + Accessibility Tree 搭一套可复现的 WebArena 评测骨架 1. 为什么 Browser-Agent 评测总在“能跑”和“可复现”之间反复横跳Computer-Use 和 Browser-Agent 这两类任务落到工程里最容易被低估的一环不是模型选型而是评测骨架。你大概率见过这样的场景本地写个 Playwright 脚本点几下按钮、填个表单跑通了截图一发感觉 Agent 已经能操作网页了。可一旦换台机器、换个浏览器版本、或者把任务交给同事复现结果就开始飘——有的步骤点不到元素有的页面加载慢半拍有的干脆因为一个弹窗把整条链路带偏。问题不在模型而在观察空间和动作空间没有钉死。Browser-Agent 的本质是“带 UI 副作用的多步控制器”它需要一套稳定的观察表示Accessibility Tree 就是目前性价比最高的选择和一套可回放的动作记录。WebArena 这类基准之所以被反复提起就是因为它把任务、环境、评测标准打包成了一个可对比的闭环。但很多人只把它当成一个跑分工具忽略了它背后那套“环境准备 → 任务执行 → 结果验证”的最小骨架。这篇内容面向的是想自己搭一套可调试、可复现 Browser-Agent 评测环境的开发者。我会用 Playwright 驱动浏览器把 Accessibility Tree 作为主观察通道串起从启动配置、快照脚本、动作执行到任务验证的完整链路并给出一个统一的 Key/API 接入骨架让模型调用和浏览器控制解耦。你不需要先有 WebArena 的完整镜像跟着步骤就能搭出一个能跑通单任务、能回放、能定位失败点的最小闭环。2. TaoToken 前置把模型通道和浏览器控制拆开在动手写 Playwright 之前先把模型调用这一层理清楚。Browser-Agent 的循环通常是观察页面 → 把观察结果发给模型 → 模型返回动作 → 执行动作 → 再观察。这个循环里模型通道如果和浏览器控制耦合在一起调试会非常痛苦——你分不清是页面没加载好还是模型返回了非法动作。我的做法是先把模型通道独立出来用一个统一的 Key 和 API 入口。TaoToken 在这里扮演的就是这个角色它提供一个兼容常见接口规范的通道你可以在环境变量里配好 Key然后在 Agent 循环里通过一个薄封装去调用。这样浏览器侧只负责“观察”和“执行”模型侧只负责“决策”两边可以分别 mock、分别回放。具体操作上你需要在 TaoToken 控制台创建一个 API Key然后把它写进本地环境变量。注意不要把 Key 硬编码进脚本也不要把带 Key 的请求日志提交到仓库。下面是一个最小配置示例你可以直接复制到.env文件里# .env TAOTOKEN_API_KEYsk-your-key-here TAOTOKEN_BASE_URLhttps://taotoken.net/api然后在 Node.js 侧读取// config.js import dotenv/config; export const config { apiKey: process.env.TAOTOKEN_API_KEY, baseURL: process.env.TAOTOKEN_BASE_URL, model: gpt-4o-mini, // 按你实际可用的模型名替换 };这里有个容易踩的坑很多人会把baseURL写成带路径的形式比如https://taotoken.net/api/v1结果请求 404。正确的做法是只写到/api具体路径由 SDK 或你的请求封装去拼。如果你用的是 OpenAI 兼容的 SDK通常只需要把baseURL指向https://taotoken.net/api即可。提示如果你还没创建 Key可以先到 TaoToken 控制台的 API Keys 页面生成一个。建议按项目分 Key方便后续做用量归因和轮换。模型通道准备好之后浏览器侧就可以专心处理 Playwright 的启动和观察逻辑了。这样拆分的好处是当任务失败时你可以先单独测模型通道是否正常再单独测页面快照是否完整排查路径清晰很多。3. 可复制配置Playwright 启动与 Accessibility Tree 快照3.1 Playwright 启动配置Browser-Agent 的浏览器实例不要复用每个任务开一个独立的 BrowserContext。这样 cookie、localStorage、session 都是隔离的回放时也不会互相污染。下面是一个生产可用的启动配置我把它封装成了一个createBrowserContext函数// browser.js import { chromium } from playwright; export async function createBrowserContext(task) { const browser await chromium.launch({ headless: true, args: [--no-sandbox, --disable-dev-shm-usage], }); const context await browser.newContext({ viewport: { width: 1280, height: 720 }, locale: task.locale || en-US, timezoneId: task.timezone || America/New_York, userAgent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36, }); // 开启 tracing方便后续回放 await context.tracing.start({ screenshots: true, snapshots: true, }); const page await context.newPage(); // 设置默认超时防止挂死 page.setDefaultTimeout(10000); page.setDefaultNavigationTimeout(30000); return { browser, context, page }; }几个参数值得说明。headless: true是默认值但如果你需要调试可以临时改成false看实际渲染。viewport固定成 1280x720 是为了让 Accessibility Tree 的布局相对稳定避免响应式布局导致元素位置漂移。tracing一定要开它会把每一步的截图和 DOM 快照存下来后面做失败复盘时非常有用。3.2 Accessibility Tree 快照脚本Accessibility Tree 是 Browser-Agent 的主观察通道。相比截图它的优势是结构化、token 成本低、元素可引用。Playwright 提供了page.accessibility.snapshot()方法但原始输出比较冗长需要做一层压缩把每个节点映射成一个可引用的ref。下面是一个快照脚本它会把 Accessibility Tree 转成模型友好的扁平结构// snapshot.js export async function captureAccessibilityTree(page) { const snapshot await page.accessibility.snapshot({ interestingOnly: true, }); const nodes []; let refCounter 0; function walk(node, depth 0) { if (!node) return; const ref e${refCounter}; nodes.push({ ref, role: node.role, name: node.name || , value: node.value || , depth, disabled: node.disabled || false, focused: node.focused || false, }); if (node.children) { for (const child of node.children) { walk(child, depth 1); } } } walk(snapshot); return { nodes, text: nodes .map((n) ${n.ref} ${n.role} ${n.name}${n.value ? value${n.value} : }) .join(\n), }; }这个脚本输出的text字段可以直接塞进模型的 prompt 里。每个节点都有一个ref模型返回动作时只需要引用这个ref比如{ type: click, ref: e12 }执行器再根据ref去定位真实元素。这样就避免了让模型输出坐标或 CSS selector稳定性会高很多。注意interestingOnly: true会过滤掉一些纯装饰性节点减少 token 消耗。但如果你的任务涉及自定义组件或 Canvas可能需要关掉这个选项或者额外加截图作为 fallback。3.3 动作执行器有了快照还需要一个执行器把模型返回的动作映射到 Playwright 操作。下面是一个最小实现// executor.js export async function executeAction(page, action, refMap) { const locator refMap.get(action.ref); if (!locator) { throw new Error(Unknown ref: ${action.ref}); } switch (action.type) { case click: await locator.click(); break; case fill: await locator.fill(action.value || ); break; case select: await locator.selectOption(action.value || ); break; case press: await locator.press(action.key || Enter); break; default: throw new Error(Unsupported action: ${action.type}); } // 等待页面稳定 await page.waitForLoadState(networkidle, { timeout: 5000 }).catch(() {}); }这里的关键是refMap。在生成快照时你需要同时把ref和 Playwright 的 Locator 对应起来。一个简单的做法是用page.locator()配合nth或getByRole但更稳妥的方式是在快照阶段就记录每个节点的可定位信息。如果你不想自己维护映射也可以直接用page.getByRole(role, { name })去重新定位但要注意 name 可能重复。4. 验证请求跑通一个 WebArena 风格任务4.1 任务定义我们用一个简化的 WebArena 风格任务来验证整条链路打开一个电商页面搜索指定商品把第一个结果加入购物车然后验证购物车数量变成 1。任务定义如下// task.js export const task { id: cart-add-001, startUrl: https://example-shop.com, goal: Search for wireless mouse and add the first result to cart, maxSteps: 15, allowedOrigins: [https://example-shop.com], };4.2 Agent 循环把前面的模块串起来就是一个最小的 Agent 循环// agent.js import { createBrowserContext } from ./browser.js; import { captureAccessibilityTree } from ./snapshot.js; import { executeAction } from ./executor.js; import { callModel } from ./model.js; export async function runTask(task) { const { browser, context, page } await createBrowserContext(task); const trace []; try { await page.goto(task.startUrl); for (let step 0; step task.maxSteps; step) { const observation await captureAccessibilityTree(page); const action await callModel({ goal: task.goal, observation: observation.text, history: trace, }); trace.push({ step, observation: observation.text, action }); if (action.type done) { break; } await executeAction(page, action, observation.refMap); } // 验证结果 const finalObservation await captureAccessibilityTree(page); const success finalObservation.text.includes(Cart (1)); return { success, trace }; } finally { await context.tracing.stop({ path: traces/${task.id}.zip }); await browser.close(); } }4.3 模型调用封装模型调用这一层用前面配好的 TaoToken 通道// model.js import OpenAI from openai; import { config } from ./config.js; const client new OpenAI({ apiKey: config.apiKey, baseURL: config.baseURL, }); export async function callModel({ goal, observation, history }) { const prompt You are a browser agent. Goal: ${goal} Current page accessibility tree: ${observation} History: ${history.map((h) Step ${h.step}: ${JSON.stringify(h.action)}).join(\n)} Return a JSON action. Supported types: click, fill, select, press, done. Example: {type: click, ref: e12} ; const response await client.chat.completions.create({ model: config.model, messages: [{ role: user, content: prompt }], temperature: 0, }); const text response.choices[0].message.content.trim(); return JSON.parse(text); }跑通之后你会看到 trace 里记录了每一步的观察和动作traces/cart-add-001.zip里也有完整的 Playwright trace。如果任务失败你可以打开 trace 看是哪一步点错了或者把 observation 拿出来单独分析。5. 本篇常见错排查5.1 Accessibility Tree 为空或节点极少最常见的原因是页面还没加载完就抓快照。Playwright 的page.goto默认等待load事件但很多 SPA 在load之后还会异步渲染。解决办法是在抓快照前加一个waitForLoadState(networkidle)或者显式等待某个关键元素出现。另一个原因是interestingOnly: true过滤太狠。如果你的页面大量使用divrole自定义组件可以试着关掉这个选项或者改用page.locator(body).ariaSnapshot()。5.2 ref 定位失败模型返回的ref在执行时找不到对应元素通常是因为快照和执行之间页面发生了变化。比如快照时元素还在执行时因为动画或异步请求被移除了。解决办法是在执行前重新抓一次快照或者给执行器加一个重试机制如果ref找不到重新抓快照并让模型重新决策。还有一种情况是refMap没有正确建立。如果你用的是page.getByRole重新定位要注意 name 可能包含动态内容。建议在快照阶段就把 Locator 存下来而不是只存文本。5.3 模型返回非法 JSON有些模型会在 JSON 外面包一层 markdown 代码块或者加一些解释性文字。解决办法是在解析前先做清洗function extractJSON(text) { const match text.match(/\{[\s\S]*\}/); if (!match) throw new Error(No JSON found); return JSON.parse(match[0]); }如果模型经常返回非法动作可以在 prompt 里加 few-shot 示例或者把temperature设成 0。5.4 任务超时或死循环Browser-Agent 很容易陷入“点不动 → 再点 → 还点不动”的循环。除了设置maxSteps还可以加一个 stuck detector连续三步的 DOM hash 相同就强制终止或转人工。DOM hash 可以用页面 HTML 的简单哈希不需要太精确。5.5 回放时结果不一致回放不一致通常来自几个非确定性来源动画、A/B 测试、时钟、随机验证码。解决办法是在测试环境冻结 feature flag、用 mock clock、拦截第三方脚本。Playwright 的page.route可以用来拦截和 mock 网络请求把不稳定的外部依赖挡掉。6. 接入骨架与后续扩展到这里一个最小的 Browser-Agent 评测骨架就跑通了。它的结构是Playwright 负责浏览器控制Accessibility Tree 负责观察模型通道负责决策trace 负责回放。你可以在这个骨架上继续加东西比如把单任务扩展成任务集批量跑并统计成功率加一个 Verifier在每个写操作后做 DOM 断言而不是只看最终状态把 trace 和 observation 存到对象存储方便后续做数据分析和模型微调用 TaoToken 的 Coding Plan 跑长期编码任务把 Agent 循环和代码生成结合起来。如果你在接入模型通道时遇到问题可以先到 TaoToken 的接入文档里对照请求格式或者直接用模型对话页面手动测一下 Key 是否生效。浏览器侧的调试则更多依赖 Playwright trace 和快照对比建议每跑一个任务就把 trace 存下来失败时第一时间看回放。这套骨架的价值不在于跑分多高而在于每一步都可观察、可回放、可定位。Browser-Agent 的工程化本质上就是把“黑盒点击”变成“白盒控制”。你先把这条最小闭环跑稳后面加多少任务、换什么模型都只是在这个骨架上做增量。
返回列表