1.3 最小示例走查:本地测试页上的三原语


1.3 最小示例走查:本地测试页上的三原语

本节摘要:本节把第 1.1 节的语义与第 1.2 节的机制在靶场上合流。任务选一个不平凡但可控的:观察本地测试页有哪些可操作元素,添加一条待办「买牛奶」,再抽取整个列表验证结果。三步各占一节:observe 一步摊开候选动作清单这个中间产物;act 一步执行并讨论指令写法与幂等问题(重复运行会重复添加,测试要清场);extract 一步用指令加数据模式取出结构化列表。随后给两版对照:Stagehand 指令式完整脚本(全书 TypeScript 代码块的统一形态说明也在本节),与纯 Playwright 代码式脚本——对照的意义在于看清「三原语是语义层,其下仍是普通浏览器驱动」。最后一张排查表对应三步各自的期望产物与失败信号。

学习目标

  • 在靶场上独立跑通 observe、act、extract 三步并解读每步中间产物。
  • 按本书统一形态书写与运行 TypeScript 代码块(ES2020、JSDoc 标注类型)。
  • 对照指令式与代码式两种写法,说出三原语「语义层」的确切含义。
  • 用期望产物排查三步中任何一步的失败。

一、靶场与任务定义

复用第 0.2 节的本地测试页(http://127.0.0.1/demo_todo.html),元素结构沿用第 0.2 节的结构表:标题「待办清单演示」、输入框「新待办」、按钮「添加」、列表、链接「关于」。本节任务分三步,正好对应三原语:

步骤 原语 目标 期望产物
一 observe 看清页面可操作什么 候选动作清单
二 act 添加待办「买牛奶」 页面列表出现新条目
三 extract 取出整个列表 结构化 JSON

一个先说破的坑:这个任务不幂等——每跑一遍就多一条「买牛奶」。自动化测试对幂等的标准处理是「前置清场或断言数量」,第 6 章会把这上升为验证纪律;本节阶段,手动刷新页面即可清场。

二、第一步 observe:候选动作清单

// step1_observe.mjs —— 用 Stagehand 观察页面(写法示意,以官方文档为准) 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("这个页面上可以做的操作"); console.log("候选动作数:", candidates.length); for (const item of candidates) { console.log("-", JSON.stringify(item)); } await stagehand.close(); ​

期望输出形如(示意,字段名以官方文档为准):

# step1_output.txt —— observe 输出示意(节选) 候选动作数: 3 - {"instruction":"在「新待办」输入框输入内容","method":"fill", ... } - {"instruction":"点击「添加」按钮","method":"click", ... } - {"instruction":"点击「关于」链接","method":"click", ... } ​

注意两点:其一,这条清单就是第 1.2 节「编号点击」清单的同构物——每项携带定位信息(方法、目标),只欠执行;其二,observe 全程没有改页面一个字节,你此刻拥有零成本否决权:候选里若混进「删除全部」这类危险项,在这里就能看见并改指令。

三、第二步 act:执行添加

// step2_act.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(); ​

act 指令刻意按第 1.1 节三原则书写:说目标(添加待办)、引用可见文本(「新待办」「添加」)、复合但不分支(填与点两个子意图,交给框架解析)。若失败,回到 observe 检查候选——多半是输入框的 label 关联没做对(第 0.2 节结构表的「用 label 正确关联」正是为这一步铺路),或页面尚未渲染完就执行了。

act 的每一步都是一次模型调用,成本与延迟随指令数线性增长——这句话是第 3 章整章的引子。

四、第三步 extract:结构化抽取

// step3_extract.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.string()) }), }); console.log(JSON.stringify(data, null, 2)); await stagehand.close(); ​

若第二步刚执行过,期望输出:

# step3_output.txt —— extract 输出示意 { "items": [ "买牛奶" ] } ​

模式刻意写窄(只要一个字符串数组):第 1.1 节讲过,宽模式诱发模型脑补。若此处结果为空数组而页面上明明有「买牛奶」,嫌疑按序排查:列表是否真的渲染(手工打开页面确认)、抽取是否发生在渲染前(加等待)、指令是否被理解成别的列表(把指令里的锚文本写具体)。

五、两版对照:语义层之下是什么

把同一个三步任务用纯 Playwright 写一遍(不含任何模型调用):

# three_steps_playwright.py —— 纯代码完成同样三步(对照版) import asyncio from playwright.async_api import async_playwright async def main() -> None: async with async_playwright() as p: browser = await p.chromium.launch(headless=True) page = await browser.new_page() await page.goto("http://127.0.0.1:8080/demo_todo.html") # 观察:枚举可交互元素(第 1.2 节机制的手工版) print("标题:", await page.title()) print("按钮数:", await page.get_by_role("button").count()) # 执行:语义定位,填表并提交 await page.get_by_label("新待办").fill("买牛奶") await page.get_by_role("button", name="添加").click() # 抽取:读取列表 items = await page.get_by_role("listitem").all_inner_texts() print("待办:", items) await browser.close() asyncio.run(main()) ​
维度 指令式(Stagehand) 代码式(Playwright)
定位依据 指令文本,模型现场裁决 写死的角色与文本匹配
模型调用 每原语一次 零
页面结构变化耐受 高(语义不变即成立) 中(可见文本不变即成立)
执行一致性 概率路径 完全确定
适合 动态页面、一次性任务 稳定流程、回归测试

对照的结论一句话:三原语是架在浏览器驱动之上的语义层,不是替代品。Stagehand 内部正是用 Playwright 驱动浏览器;browser-use 同样构建在 Playwright 之上。语义层买适应性、付模型成本;代码层反之——两层之间如何逐块换料,是第 3.3 节渐进式固化的全部内容。

顺带交代本书 TypeScript 代码块的统一形态:为兼顾无编译直接检查语法,代码块写成 ES2020 模块形态(.mjs,可直接用 node 做语法检查),类型信息以 JSDoc 注释标注而非 TS 语法,如:

// jsdoc_shape.mjs —— 本书 TS 代码块形态:JSDoc 标注类型 /** @param {string} instruction 自然语言指令 @returns {Promise} 成功 resolve 为 true,失败为 false */ async function safeAct(page, instruction) { try { await page.act(instruction); return true; } catch (err) { console.warn("act 失败:", err.message); return false; } } ​

六、每一步的中间产物与排查表

步骤 期望产物 失败信号 第一反应
observe 3 项左右候选清单 候选为 0 或大量无关项 检查页面渲染与 label 关联
act 列表出现「买牛奶」 报错或列表未变 回 observe 看候选;检查指令锚文本
extract items 数组含新条目 空数组或字段缺失 确认渲染完成;收窄模式;具体化指令

三步各自独立成文件便于分段排障;合并成一个脚本的完整版(含清场与错误处理)留作本章练习思路:goto 后先刷新重开页面保证清场,三步之间插入显式等待,extract 结果与预期做一次显式比对——这个比对动作就是第 6 章「验证」的最小雏形。

本节要点回顾

  • 三原语走查闭环:observe 出候选、act 改页面、extract 取结果,每步中间产物都可摊开检查。
  • 任务不幂等时先清场再断言,这是验证纪律的雏形(第 6 章展开)。
  • 指令式与代码式对照说明:三原语是语义层,其下仍是 Playwright 这类浏览器驱动。
  • 本书 TS 代码块统一 ES2020 模块形态加 JSDoc 类型标注,可直接做语法检查。

单步会做了,语义层与代码层的边界也看清了。下一章把语义层推到极致:browser-use 的 Agent 循环——谁在替你编排三原语、页面状态如何反馈给模型、循环什么时候停。

作者与出处
原作者:灏天文库
整理人: 灏天文库整理
本站整理收录,版权归原作者/开源协议所有;欢迎通过原文链接访问源仓库。
发布者: 作者: 灏天文库 转发
评论区 (0)
U