本节摘要:本节把 Stagehand 的三个核心 API 讲到能上手写代码的程度。初始化:Stagehand 实例的 env 选项区分本地浏览器与 Browserbase 云端会话,init 之后拿到的 page 兼容 Playwright 接口——这是「三原语是语义层」在 API 面的直接体现。act:指令书写三原则展开成好指令对照表,一次 act 一个意图单元。extract:指令加 zod 数据模式,模式设计的三条纪律(宁窄勿宽、字段自描述、嵌套分层)。observe:候选清单的两种消费法(人工检视与选中后执行)。错误处理:超时、候选为空、指令歧义三类失败的捕获与带退避的重试封装,以及「失败后降级到 observe」的排查路径。本节所有代码为 ES2020 模块形态(可直接做语法检查),类型以 JSDoc 标注;API 细节以官方文档为准。
第 0.2 节已装好依赖(@browserbasehq/stagehand@3.7.3 与 zod)。初始化的最小形态:
// stagehand_init.mjs —— Stagehand 初始化骨架(写法示意,以官方文档为准) import { Stagehand } from "@browserbasehq/stagehand"; const stagehand = new Stagehand({ env: "LOCAL", modelName: "gpt-4o", }); await stagehand.init(); const page = stagehand.page; // 兼容 Playwright 的页面对象 await page.goto("http://127.0.0.1:8080/demo_todo.html"); console.log("标题:", await page.title()); await stagehand.close();
四个要点:env 取 LOCAL 用本机浏览器、取 BROWSERBASE 走云端会话(账号与计费在 Browserbase,基建细节第 7 章);模型通过 modelName 与模型客户端选项指定,密钥走环境变量(第 0.2 节纪律);stagehand.page 兼容 Playwright page 接口——goto、title 这些普通调用照常可用,三原语是加在它上面的语义层,不剥夺原生能力(第 1.3 节结论的 API 证据);用完 close,无头模式下忘了关会残留浏览器进程。
第 1.1 节给过三原则(说目标不说路径、引用可见文本、一次一个意图),这里落成对照表:
| 坏指令 | 病灶 | 好指令 |
|---|---|---|
| 点击页面右下角的蓝色按钮 | 位置加外观,改版即失效 | 点击「添加」按钮 |
| 提交表单 | 无语义锚,候选过多 | 点击「登录」按钮 |
| 填写用户名、密码然后登录再关掉弹窗 | 一句塞三个分支意图 | 拆成三次 act,各自单一意图 |
| 在框里输入牛奶 | 「框」歧义 | 在「新待办」输入框填入 买牛奶 |
「一次一个意图单元」需要澄清:像「在输入框填入买牛奶,然后点击添加」这样的紧耦合序列算一个意图单元(填与点共同完成「添加待办」),可以合并;但「登录然后处理弹窗再跳转设置页」是三个意图单元,必须拆开——判据是任何一步都可能失败并需要单独重试。指令里引用的文本必须是页面上真实可见的(「添加」而不是你代码里的变量名),因为模型是按用户视角找元素的(第 1.2 节)。
// act_basic.mjs —— act 的基本形态(写法示意,以官方文档为准) import { Stagehand } from "@browserbasehq/stagehand"; const stagehand = new Stagehand({ env: "LOCAL", modelName: "gpt-4o" }); await stagehand.init(); const page = stagehand.page; await page.goto("http://127.0.0.1:8080/demo_todo.html"); await page.act("在「新待办」输入框填入 买牛奶,然后点击「添加」按钮"); await stagehand.close();
extract 的调用形态是「instruction + schema」,schema 用 zod 表达(序列化后随指令发给模型,约束输出结构):
// extract_basic.mjs —— extract 的数据模式设计(写法示意,以官方文档为准) import { Stagehand } from "@browserbasehq/stagehand"; import { z } from "zod"; const stagehand = new Stagehand({ env: "LOCAL", modelName: "gpt-4o" }); await stagehand.init(); const page = stagehand.page; await page.goto("http://127.0.0.1:8080/demo_todo.html"); const data = await page.extract({ instruction: "提取待办列表的全部条目,以及每条是否包含日期信息", schema: z.object({ items: z.array( z.object({ text: z.string().describe("条目原文"), hasDate: z.boolean().describe("是否包含日期"), }) ), }), }); console.log(JSON.stringify(data, null, 2)); await stagehand.close();
模式设计三纪律,与第 1.1 节一脉相承并加一条:宁窄勿宽——类型写死,别用任意类型兜底;字段自描述——describe 注释写清字段含义,它会被模型读到,等于给抽取加了说明书;嵌套分层——复杂数据拆成对象数组,别让模型往一个字符串里塞结构化内容再让你解析。指令侧同样要具体:「提取待办列表的全部条目」优于「提取页面内容」——extract 的指令是裁剪刀,范围说得越准,返回越干净。
observe 返回候选动作数组(第 1.3 节见过产物形态)。两种消费法:
// observe_use.mjs —— observe 的两种消费(写法示意,以官方文档为准) import { Stagehand } from "@browserbasehq/stagehand"; const stagehand = new Stagehand({ env: "LOCAL", modelName: "gpt-4o" }); await stagehand.init(); const page = stagehand.page; await page.goto("http://127.0.0.1:8080/demo_todo.html"); // 消费法一:人工检视(干跑排障、上线前确认命中面) const candidates = await page.observe("添加待办相关的操作"); for (const item of candidates) { console.log("-", JSON.stringify(item)); } // 消费法二:选中后执行(把裁决权拿回代码里) if (candidates.length > 0) { await page.act(candidates[0]); } await stagehand.close();
消费法二值得强调:observe 把「候选」从模型手里拿到代码手里,代码挑中第几个再交给 act 执行——裁决逻辑(拿第一个、按条件过滤、随机挑)完全由你写。这是 3.3 节缓存机制的直接前提:候选里的定位信息可以被代码持有与复用,而不再每步问模型。
act 与 extract 会失败,失败分三类,处理各不同:
| 失败类 | 现象 | 处理 |
|---|---|---|
| 超时 | 页面未就绪或模型往返过慢 | 捕获后按退避重试 |
| 候选为空 | 指令无命中(页面变了或指令写偏) | 降级到 observe 检视,修正指令 |
| 指令歧义 | 命中多个目标,执行结果不稳 | 收紧指令(加可见文本锚),或 observe 后代码裁决 |
通用的带退避重试封装(本书 TS 代码块 JSDoc 形态的完整示例):
// act_with_retry.mjs —— 带退避的重试与降级(写法示意,以官方文档为准) import { Stagehand } from "@browserbasehq/stagehand"; /** @param {import("@browserbasehq/stagehand").Page} page 页面对象 @returns {Promise} 成功 resolve 为 true,失败为 false */ async function actWithRetry(page, instruction, attempts) { for (let i = 0; i < attempts; i++) { try { await page.act(instruction); return true; } catch (err) { console.warn(`第 ${i + 1} 次尝试失败: ${err.message}`); if (i === attempts - 1) { return false; } await new Promise((resolve) => setTimeout(resolve, 1000 * (i + 1))); } } return false; } const stagehand = new Stagehand({ env: "LOCAL", modelName: "gpt-4o" }); await stagehand.init(); const page = stagehand.page; await page.goto("http://127.0.0.1:8080/demo_todo.html"); const ok = await actWithRetry(page, "点击「添加」按钮", 3); if (!ok) { // 降级路径:observe 检视现场,把候选打出来人工研判 const candidates = await page.observe("页面上的可点击元素"); console.log("降级检视,候选数:", candidates.length); for (const item of candidates) { console.log("-", JSON.stringify(item)); } } await stagehand.close();
两个封装纪律:重试要有上限与退避(线性或指数退避,别裸 for 三连击——连续失败大概率不是瞬时抖动);重试耗尽要降级而不是抛完就跑——降级到 observe 是最便宜的现场保全,日志里留着它,第 6 章的验证策略会在此基础上判断「该重试、该换路径还是该上报人工」。
武器齐了,但按判定树,act 的每一步都在花模型钱。下一节讲怎么把钱省下来:从 act 起步,经 observe 检视与缓存,逐步把稳定步骤固化成纯代码——以及哪些步骤永远不该固化。