Stagehand + Playwright:用自然语言指令驱动浏览器自动化

Stagehand + Playwright:用自然语言指令驱动浏览器自动化 说实话我第一次看到 Stagehand 这个项目的时候脑子里冒出来的想法是这不就是我一直想要的“浏览器自动化不用再写选择器”的方案吗。Playwright 本身已经是浏览器自动化里很能打的一套框架但用久了你会发现真正耗时间的不是“让浏览器跑起来”而是“让代码找到那个该死的按钮”。如果这时候 AI 能接手“理解页面、决定操作”这一层把 Playwright 当底座去执行体验完全不一样。Stagehand 做的就是这件事在 Playwright 之上加一层自然语言指令接口让浏览器自动化从“写代码”变成“说需求”。这篇文章我结合自己实际跑通的流程把它的核心思路、环境搭建、API 用法、避坑经验都整理出来给正在做 UI 自动化、爬虫或 AI Agent 的同行做个参考。1. Stagehand 到底解决了什么痛点1.1 传统浏览器自动化的“选择器地狱”只要写过一段时间 Playwright 或者类似框架你大概率经历过这种场景。页面结构一改class 变了一个字符串整个脚本就废了。XPath 写得长一点前端加一层 div定位直接失效。SPA 应用里更是煎熬异步渲染、骨架屏、接口延时你以为元素加载完了实际点击的时候还差 100 毫秒。这些问题归纳起来就是一句话我们在用代码去描述“人眼很容易看到的东西”过程繁琐且脆弱。我自己维护过一套爬虫脚本前期写功能只花了一天后面三个月全在修定位。今天等不到元素明天 iframe 没切进去后天按钮从 hover 改成 click 触发了。每次失败都要打开 DevTools 重新看 DOM找到新的 selector改完还得担心有没有别的页面受影响。这种维护成本才是浏览器自动化落地最大的障碍。1.2 Stagehand 的核心思路把“意图”和“执行”分开Stagehand 是 Browserbase 团队开源的一个项目本质上是给 Playwright 套了一层 AI 代理层。它保留了 Playwright 所有的底层能力但不再要求你写 CSS、XPath 或者复杂的等待逻辑。你只需要告诉它“做什么”它自己决定“怎么做”。它的核心思路是LLM 负责理解页面和生成操作计划Playwright 负责稳定地执行这些操作。这个分工很关键。如果让 AI 从头到尾黑盒操作浏览器不确定性太强出了问题也难排查。Stagehand 的做法是让 AI 生成 Playwright 代码然后在真实浏览器里执行并验证结果。指令不对它可以自我修正但底层永远是确定性的代码。举个例子以前你要写page.locator(.post-title a).first().click()现在只需要一句“点击页面上标题为 XXX 的链接”。页面怎么变不重要AI 会重新理解并找到目标。这套设计让它特别适合三类场景AI 驱动的前端测试、动态网页数据提取、需要自主决策的 Agent 应用。我有个做测试平台的朋友说他们用 Stagehand 跑回归用例最惊喜的点是页面样式改版后旧脚本居然还能直接用。2. 为什么底座选 Playwright而不是 Puppeteer 或 Cypress2.1 Playwright 作为“能力底座”的优势既然核心是“AI 生成操作、浏览器负责执行”那这个浏览器执行层必须足够稳、足够全。Playwright 在这方面几乎是现成的最佳选择。它支持 Chromium、Firefox、WebKit 三种内核多标签页、多上下文、iframe 处理都是原生能力。内置的自动等待机制会等元素可交互再操作这部分正好可以抵消 LLM 指令在“时机判断”上的盲区。还有一点容易被忽略Playwright 对现代 Web 特性的支持非常完整尤其是动态渲染的页面。它的 Actionability 检查元素可见、稳定、可点击比很多框架都严格这对 AI 生成的操作特别重要。AI 只会说“点击这个按钮”但按钮到底啥时候出现、出现后能不能点必须靠 Playwright 这层来把关。我实际用过之后最大的感受是Stagehand 的page对象本质上就是 Playwright 的 Page 实例包了一层。这意味着 Playwright 原有的 locator、waitFor、网络拦截、截图等功能全部可以直接混用。你不是在“二选一”而是在“Playwright 之上叠加 AI 能力”。这一点非常重要后面我专门讲怎么混用。2.2 官方说“不背锅”与其他框架的边界很多人会拿 Stagehand 和 Cypress 比。Cypress 是一个很好的集成测试框架但它运行在自己的执行环境中多标签页、跨域场景支持都不如 Playwright 灵活。Puppeteer 虽然也是 Chrome 自动化库但官方只支持 Chromium功能覆盖面比 Playwright 窄一些。Selenium 则太古老API 繁琐定位方式还停留在 WebDriver 时代和 AI 结合的成本更高。我整理了一个对比表方便你快速判断框架多浏览器自动等待iframe/多标签支持与 AI 结合的改造难度适合的角色PlaywrightChromium/Firefox/WebKit完善完善低已有生态可直接复用自动化底座Puppeteer仅 Chromium需手动一般中Chrome 专用场景Cypress浏览器支持有限完善较弱高运行模型特殊前端集成测试Selenium完善需配合库一般较高协议老旧遗留系统兼容所以 Stagehand 选择 Playwright 不是偶然。它需要一个干净、现代、能力全面的浏览器控制层Playwright 恰好把这块做完了Stagehand 只需要专注把“LLM 意图解析”做深做透。两者是互补关系不是替代关系。3. 5 分钟搭好 Stagehand 环境3.1 安装步骤与依赖注意事项Stagehand 是个 Node.js 库要求 Node 18 及以上。第一步先初始化项目然后安装核心包和 Playwright 运行时。Playwright 需要单独安装浏览器内核这一步在 Linux 服务器上尤其容易踩坑。npm init -y npm install browserbasehq/stagehand playwright npx playwright install chromium如果你在 CI 或者 Docker 环境跑建议直接用npx playwright install --with-deps它会把系统依赖库一起装上。我之前在纯净的 Ubuntu 服务器上只执行playwright install结果启动浏览器时报了一堆.so文件缺失后来加上--with-deps才解决。本地 macOS 一般不用操心这个Windows 也顺畅。我习惯把 API Key 写在.env里然后配合dotenv加载。Stagehand 会读取环境变量中的模型 API Key比如OPENAI_API_KEY你也可以在初始化时显式传apiKey。不过建议别硬编码在代码里尤其是准备推到 GitHub 的项目泄露 key 的教训太多了。3.2 模型接入与关键配置项Stagehand 的初始化参数核心就几个env指定运行环境headless控制是否有头模式modelName和modelClient决定调哪个模型。最简配置长这样import { Stagehand } from browserbasehq/stagehand; import dotenv from dotenv; dotenv.config(); const stagehand new Stagehand({ env: LOCAL, headless: false, modelName: gpt-4o, }); await stagehand.init(); console.log(Stagehand 已启动); await stagehand.close();env: LOCAL表示在本地启动浏览器这也是大多数人入门的模式。如果你用 Browserbase 的云浏览器可以改成对应的云环境配置但我个人觉得本地模式调试起来更方便能看到浏览器真实运行状态LLM 出错时也更容易定位。模型选择这块我多说一句Stagehand 通过模型抽象层支持多家模型包括 OpenAI、Anthropic、Google 等。我的经验是指令遵循能力强的模型明显更稳用太小的开源模型跑复杂页面容易出现“看错元素”或者“操作生成不合法”的情况。如果你只是本地测试可以用 ollama 起一个中等规模的模型试试水真正跑业务建议还是上旗舰级模型。模型越好后面 act/extract 的准确率越高调试成本越低。这里没有绝对标准你可以用同一个页面分别跑不同模型对比结果自然能感受到差距。4. 核心 API 拆解act / extract / observe 怎么用4.1 page.act把自然语言指令变成真实点击和输入act是 Stagehand 使用频率最高的接口负责在页面上执行操作。它的用法很直观await stagehand.page.goto(https://news.ycombinator.com); await stagehand.page.act({ instruction: 点击标题为 Show HN: Stagehand 的链接, });这段代码的直观程度比写page.locator(a[href*item?id]).filter({ hasText: Show HN }).click()高了太多。act的底层逻辑是Stagehand 把当前页面的语义化快照可访问性树、文本内容、可交互元素等发给 LLMLLM 根据指令生成一段 Playwright 操作代码然后 Stagehand 在真实页面里执行并校验结果。如果执行失败它会读取错误信息重新生成操作相当于自带一轮“自我修复”。实际使用有几个参数很有用waitUntil可以控制在执行前等待页面状态retries可以控制操作失败后的重试次数。指令写得越具体效果越稳。比如“点击页面上第一个帖子标题链接”就比“点击链接”可靠。我自己踩过的坑是不加约束时 AI 偶尔会点到一个完全不相关的广告区域所以现在写指令都会带上位置、文本关键词这些特征。4.2 page.extract用结构化方式提取页面数据写爬虫的朋友对act可能兴趣一般但extract基本属于“用了就回不去”的功能。它让 AI 从当前页面抽取指定信息并且直接输出成 JSON。最省心的用法是配合schema参数告诉模型你要哪些字段、字段类型是什么const result await stagehand.page.extract({ instruction: 提取页面上第一个新闻帖子的标题、分数和链接, schema: { title: string, points: number, url: string, }, }); console.log(result); // { title: Show HN: Stagehand, points: 342, url: https://... }不传 schema 也能跑模型会自由发挥但输出格式不稳定有时候给你多出几个字段有时候字段名和预期不一致。所以我的建议是尽量传 schema。它本质上是在约束 LLM 的输出格式让后续程序处理数据时不用做太多兼容。extract的底层实现依赖于 LLM 的函数调用能力模型会比较“靠谱”地填充字段。遇到页面信息很多的情况你可以用更细的指令缩小提取范围比如“只看正文部分”或者“忽略页脚内容”。这既能提高准确率也能明显减少 token 消耗。4.3 page.observe先让 AI 帮你看清楚页面observe是个容易被人忽略但很有价值的接口。它不是直接执行操作而是“观察”页面上有哪些可用动作然后返回一个候选列表。我的理解是observe相当于 AI 的侦察兵act相当于行动队。const observations await stagehand.page.observe({ instruction: 页面上有哪些筛选条件可以点击, });返回的结果里会包含元素描述、动作类型这些信息。你可以根据观察结果让用户确认操作或者写代码决定下一步调哪个act。在做 Agent 类应用时这个接口特别实用因为 Agent 的每一步决策都应该是基于“当前页面有什么可操作”来确定的而不是凭空让模型猜。我自己的使用模式是这样的遇到一个复杂的新页面先observe一轮看看 AI 发现了哪些可交互元素对比一下页面真实结构确认没有理解偏差再写后续的act指令。这招能帮你提前发现很多“模型理解错误”的问题省去反复调试的麻烦。5. 一个完整任务的实操全流程5.1 任务拆解与拆步骤思考光讲 API 不落地是耍流氓。这里我用一个典型的“搜索信息并提取”任务走一遍完整流程打开一个搜索页面输入关键词并回车点击第一条结果然后从详情页提取标题和正文第一段。这个任务拆解下来其实是三步操作加一步提取打开搜索页等待网络空闲定位搜索框输入关键词并提交点击第一条搜索结果提取详情页的核心信息。以前用原生 Playwright 写第一步和第二步没什么难度第三步和第四步要处理搜索结果列表的定位策略规则稍微写不好就会点到广告或者推荐内容。现在用 Stagehand指令写起来简单而且 AI 会结合页面语义自己判断“第一条结果”是什么。5.2 完整代码演示下面这段代码是完整可运行的需要提前配好 API Keyimport { Stagehand } from browserbasehq/stagehand; import dotenv from dotenv; dotenv.config(); async function main() { const stagehand new Stagehand({ env: LOCAL, headless: false, modelName: gpt-4o, }); await stagehand.init(); try { // 第一步打开页面等待网络空闲 await stagehand.page.goto(https://example.com/search, { waitUntil: networkidle, }); // 第二步搜索 await stagehand.page.act({ instruction: 在页面的搜索框中输入 playwright stagehand 并按下回车键, }); // 第三步点击第一条搜索结果 await stagehand.page.act({ instruction: 点击搜索结果列表中的第一条结果链接, }); // 第四步提取详情页信息 const data await stagehand.page.extract({ instruction: 提取当前页面的主标题和正文第一段内容, schema: { title: string, firstParagraph: string, }, }); console.log(提取结果, data); } finally { await stagehand.close(); } } main();这段代码我最喜欢的一点是它几乎不需要维护。关键词从 “playwright stagehand” 改成别的搜索结果页结构怎么变它都能凭页面语义重新理解。5.3 多个页面循环与动态内容处理真实项目里很少只跑一个页面。比如抓取一个列表页然后打开每一条详情页提取数据这时用一个循环就能搞定。需要注意的一点是Stagehand 在循环里反复调用act和extract每次都会调用模型所以耗时和 token 消耗要心里有数。const links await stagehand.page.extract({ instruction: 提取当前页面上前5条结果的标题和链接, schema: { items: [ { title: string, url: string } ] } }); for (const item of links.items) { await stagehand.page.goto(item.url, { waitUntil: domcontentloaded }); const detail await stagehand.page.extract({ instruction: 提取正文中的发布日期, schema: { date: string }, }); console.log(item.title, detail.date); await stagehand.page.goBack(); }动态页面处理上我的经验是尽量让goto配合waitUntil: networkidle或domcontentloaded避免还没渲染完就开始操作。如果遇到懒加载内容可以在act指令里加一句“向下滚动页面直到出现更多内容”Stagehand 能处理这个场景。5.4 与原生 Playwright 混用的分层策略前面说了Stagehand 的page就是 Playwright Page 的能力超集。这意味着你可以随心所欲地切换。实际项目中我会把操作分成两类稳定不变的部分用原生 locator 实现变化频繁的部分交给 AI 指令。// 原生 locator 处理登录这种稳定流程 await stagehand.page.goto(https://example.com/login); await stagehand.page.locator(input[nameusername]).fill(my_account); await stagehand.page.locator(input[namepassword]).fill(my_password); await stagehand.page.locator(button[typesubmit]).click(); // AI 处理内容区域这种经常改版的部分 await stagehand.page.act({ instruction: 点击当前页面上关于 方案对比 的标签页, }); await stagehand.page.waitForTimeout(2000); const result await stagehand.page.extract({ instruction: 提取对比表格中前三行的产品名和价格, schema: { rows: [{ product: string, price: string }], }, });为什么要这样分层因为 LLM 调用有成本和时延稳定操作没必要每次都让 AI 判断。反过来页面内容区改版频繁用 locator 写死 selector 就是找罪受。这种混合模式是我目前觉得最省心、性价比最高的用法。你不需要“信任”AI你只需要在它擅长的场景语义理解用它在不擅长的场景精确重复用代码兜底。6. 高频问题排查与避坑清单6.1 常见问题速查表我把自己和身边朋友实际踩过的坑整理成了表格如果你跑 Stagehand 遇到问题直接照着排查现象可能原因处理方式act 点击了错误的元素页面语义被干扰指令模糊指令里补充位置/文本特征先用 observe 看候选改用更具体的描述extract 返回 null 或缺字段页面信息超出上下文窗口schema 字段名与语义不匹配缩小 instruction 范围精简页面信息检查 schema 字段命名是否足够直观启动浏览器失败提示 target closed页面导航中断SPA 路由跳转导致操作句柄失效在 goto 后等待 networkidle操作包 try-catch 并做一次重试headless 模式启动失败报沙箱相关错误Linux 环境缺少系统依赖或沙箱权限限制执行npx playwright install --with-deps可信环境可在 launch 参数加--no-sandboxtoken 消耗大响应慢页面 DOM 过大快照信息量太多先导航到目标区域拆分成多次小范围 extract减少不必要指令轮次6.2 关于 iframe、Electron 等特殊场景的处理有几个场景很容易让新手卡住。第一个是 iframe 嵌套页面。Stagehand 的可访问性快照通常能“看到”大部分 iframe 内容但偶尔会漏。遇到这种情况我推荐直接用 Playwright 原生的frameLocator进入指定 iframe再对那个 frame 执行 Stagehand 操作。因为你拿到的一定是一个可操作的 page 引用混用起来没有障碍。const frame stagehand.page.frameLocator(iframe[namecontent]); await frame.locator(button.submit).click(); // 如果 iframe 内部还要用 AI可以基于主 page 继续 act通常也能处理第二个是 Electron 桌面应用里的浏览器窗口。Playwright 官方支持通过_electron.launch来启动 Electron 应用Stagehand 本身是为网页设计的但理论上你可以先拿到 Electron 里的 BrowserWindow 对应页面再包装给 Stagehand 用。这类场景比较偏如果遇到我建议先查官方 issue 或者直接在仓库里搜 Electron 相关讨论。不要急着让 Stagehand 硬上先用 Playwright 原生 API 跑通链路再考虑加 AI 层。第三个是 Linux 服务器跑 headless 的沙箱问题。我之前帮朋友排查过一个问题Docker 容器里跑 Playwright 一直报target closed最后发现是沙箱权限不够。解决办法是在启动浏览器时给 chromium 加--no-sandbox参数或者干脆用官方 Docker 镜像镜像里已经配好了所有依赖。6.3 成本控制与合规提醒最后说两个不太显眼但很重要的事。第一是成本。Stagehand 每执行一次 act/extract就是一次完整 LLM 推理涉及页面快照渲染和模型调用。页面越复杂、指令越含糊token 消耗越高。想省钱核心思路是“让 AI 只在关键动作上出手”。比如先跳到目标页面、登录、翻页这些重复动作用 Playwright 原生代码完成把 AI 留给最需要语义理解的那一步。我见过有些团队一上来就全部用 act简单页面跑一次任务烧掉几百个 token 还不稳定改成混合模式之后成本直接降了一个数量级。第二是合规。用浏览器自动化去操作网站无论在哪个场景都要先确认目标站点允许这种自动化访问。测试环境或你自己维护的站点完全没问题抓取第三方公开数据时建议先看 robots 协议和网站服务条款。Stagehand 只是个工具怎么用是你的选择。我一直坚持的原则是只对自己拥有权限的系统跑自动化这样既安全又不给自己留隐患。如果你打算把这套方案应用到自己的项目里我这里有一份个人常用配置偏好本地调试开有头模式、关 headless方便第一时间看到浏览器在干什么CI 环境再切换 headless 模式跑回归。模型我习惯保持默认先用旗舰级模型验证全流程确认脚本稳定后再评估是否换更经济的模型跑量。最后再提醒一句Stagehand 还在快速迭代API 细节可能调整遇到怪问题优先看官方更新记录和 GitHub issue。至少现阶段它已经把“浏览器自动化”这件事的体验推进了一大截值得花时间上手。