跳到主要内容

Stagehand — 架构与原理

30 秒导读: Stagehand 是一个"用自然语言 + 代码一起写"的浏览器自动化框架。它对外只暴露四个动词——act(做一个动作)、extract(抽结构化数据)、observe(先看有哪些可点的元素)、agent(多步自主完成任务)。它最核心的一招是:不让模型去"看图猜坐标",而是给模型一棵带唯一 ID 的无障碍树文本,模型只需回一个 ID + 方法名,框架再用这个 ID 反查出精确 XPath,确定性地落到真实元素上。配上"动作缓存 + 自愈",让 AI 写的自动化脚本第二次跑可以不再调用模型、页面变了又能自己修。


1. 这是什么(零基础也能懂)

一句话定义: Stagehand 是一个 AI 浏览器自动化框架——你既能像用 Playwright 那样写精确的代码,也能夹一句自然语言让 AI 替你决定"点哪、填啥"。

解决什么问题 / 给谁用:

假设你要写一个自动化脚本,去 GitHub 上找某个仓库最新的 PR 并抓出作者和标题。用传统工具(Selenium / Playwright)你得手写一堆易碎的 CSS 选择器,页面一改版脚本就挂;纯用"AI agent"又太黑盒、生产环境不敢用。Stagehand 让你按需混用:确定的步骤写代码,不确定的页面交给 AI。

它面向要在生产环境跑浏览器自动化的开发者:爬取、表单填写、端到端测试、以及给 LLM 应用当"手脚"去操作网页。

它能做什么(四个动词):

动词干什么典型调用
act执行一个原子动作(点击/输入)stagehand.act("click the sign in button")
observe只"看"不"做":返回候选可交互元素stagehand.observe("find the login button")
extract按 Zod schema 从页面抽结构化数据stagehand.extract("get the PR title", schema)
agent给一个高层目标,自主多步完成agent.execute("Get to the latest PR")

用起来什么样(摘自 packages/core/README.md / 项目 README.md):

const page = stagehand.context.pages()[0];
await page.goto("https://github.com/browserbase");
await stagehand.act("click on the stagehand repo"); // 一步一步的动作
const agent = stagehand.agent();
await agent.execute("Get to the latest PR"); // 多步自主任务
const { author, title } = await stagehand.extract( // 抽结构化数据
"extract the author and title of the PR",
z.object({ author: z.string(), title: z.string() }),
);

一句话直觉/类比: 把它想成给 LLM 装了一副"读屏眼镜"和一只"精确的手"。眼镜(无障碍树快照)让模型读到页面结构而不用看图猜;手(自研 CDP 引擎)拿着模型给的"元素编号"精确落点。模型只负责"选哪个",落点交给确定性的查表——这就是它比"纯视觉 agent"更可靠的原因。


2. 顶层全景(它大概怎么转)

怎么读下面这张图: 从上到下是一次 act("click the login button") 的完整生命周期。左边一列是引擎/浏览器,右边是AI 推理;注意 LLM 那一步的输入是文本树、输出是元素 ID,不是坐标。

主要部件一句话职责:

部件干什么在哪个文件
V3(即导出的 Stagehand)顶层门面,拿着四个 Handler + 缓存,做参数归一化、缓存判定、历史记录packages/core/lib/v3/v3.ts:154(class V3)
ActHandleract 的大脑:抓快照 → 问 LLM → 落动作 → 两步/自愈packages/core/lib/v3/handlers/actHandler.ts:39
ObserveHandler只观察:抓快照 → 问 LLM 要一批候选元素 → 映射成带 XPath 的 Action[]packages/core/lib/v3/handlers/observeHandler.ts:22
ExtractHandler抽数据:把 a11y 树 + 可选截图喂给 LLM,按 Zod schema 返回packages/core/lib/v3/handlers/extractHandler.ts:65
captureHybridSnapshotgrounding 地基:用 CDP 抓无障碍树 + DOM,产出文本树和 ID→XPath 映射,跨 iframe 拼接packages/core/lib/v3/understudy/a11y/snapshot/capture.ts:58
understudy(CDP 引擎)自研的、不依赖 Playwright 的浏览器控制层,直接讲 Chrome DevTools Protocolpackages/core/lib/v3/understudy/cdp.ts:61(class CdpConnection)
V3AgentHandlerDOM 模式 agent:用 Vercel AI SDK 的工具调用循环,工具就是 act/extract/goto…packages/core/lib/v3/handlers/v3AgentHandler.ts:120
V3CuaAgentHandlercomputer-use 模式 agent:纯坐标动作(截图→模型给 (x,y)→CDP 点)packages/core/lib/v3/handlers/v3CuaAgentHandler.ts:39
ActCache按"指令+URL+变量名"哈希缓存已成功的动作,可无模型重放packages/core/lib/v3/cache/ActCache.ts:20

主线走一遍(高层,不进代码):

  1. 你调 stagehand.act("click the login button"),V3.act 先看能不能命中缓存(同指令+同 URL 之前跑过)——命中就直接重放存好的 XPath,一次模型都不调(v3.ts:1293 起)。
  2. 未命中 → ActHandler.act 让自研 CDP 引擎抓一张混合快照:一棵人类可读的无障碍文本树,加一张**"元素 ID → XPath"字典**(actHandler.ts:152)。
  3. 文本树 + 你的指令喂给 LLM。LLM 不返回坐标,只返回一个 elementId(如 "1-42")、一个 method(如 click)、arguments,外加一个布尔 twoStep(要不要第二步)(inference.ts:410act)。
  4. 框架用 elementId 去字典里反查出精确 XPath(actHandler.ts:467),这一步是确定性查表,不是猜。
  5. performUnderstudyMethod 通过 CDP 把动作落到真实 DOM;成功则把结果写回缓存下次可重放(v3.ts:1346 起)。若失败且开了 selfHeal,重新抓快照再问一次模型,换个 XPath 重试(actHandler.ts:334)。

一个关键认知:V3 是当前架构,understudy 是它的命门。 代码里到处是 v3 前缀;老的 v2 已被取代。整个项目最有价值的设计,都集中在 understudy/(自研 CDP 引擎)+ a11y/snapshot(把网页变成"带 ID 的文本树")。理解这两块,就理解了 Stagehand 为什么"比纯视觉 agent 稳"。


3. 核心机制

3.1 DOM Grounding:把"那个登录按钮"落到真实元素(项目的命门)

它要解决的小问题: 模型说"点登录按钮",可页面上到底哪个 DOM 节点是登录按钮?这一步(grounding,落地)是所有浏览器 agent 最难、最容易碎的地方。

思路/直觉: 大多数视觉系 agent 让模型直接盯着截图输出 (x, y) 坐标——但模型的空间定位并不可靠。Stagehand 反其道而行:

  • 先把整页渲染成一棵无障碍树(accessibility tree)文本,每个可交互节点前面挂一个唯一 ID(形如 frameOrdinal-backendNodeId,如 1-42)。
  • 让模型只从这些 ID 里选一个返回。
  • 框架再拿这个 ID 去一张预先建好的 ID→XPath 字典里查——查表是确定性的,没有"猜坐标"的误差。

一句话:把"视觉定位"降级成"从带编号的目录里挑一项",难题从模型身上卸到了确定性代码里。

真实实现:

  • 快照入口 captureHybridSnapshot(understudy/a11y/snapshot/capture.ts:58)分五步:①可选按 selector 聚焦子树;②每个 CDP session 调一次 DOM.getDocument 建索引;③逐帧抓 AX 树,得到该帧的相对 XPath/tag/url 映射;④走帧树算出每个 iframe 的绝对 XPath 前缀;⑤mergeFramesIntoSnapshot 把所有帧合并成全局的 combinedTree 文本 + combinedXpathMap(capture.ts:808)。跨 iframe 的元素 ID 也能查到 XPath,是靠给每个子帧的 XPath 加上宿主 iframe 的前缀(capture.ts:833prefixXPath)。
  • observe 里 LLM 返回的 elementId 就是这样被翻译成 xpath=... 选择器的:combinedXpathMap[lookUpIndex] 一查即得(observeHandler.ts:154-157)。act 的 normalizeActInferenceElement 做同样的事(actHandler.ts:467)。
  • LLM 侧的返回结构由 zod schema 约束死:elements[].elementId / method / arguments(packages/core/lib/inference.tsobserve,elementId 字段定义在 inference.ts:281)。模型被迫"选 ID + 选方法",而不是自由发挥。

关键细节/坑:

  • shadow DOM 里的元素查不到 XPath 时,会返回一个 method: "not-supported" 的占位,直接被过滤掉(observeHandler.ts:218)——这是它诚实标注"我落不了这个点"的方式。
  • dragAndDrop 特殊:参数里的目标元素也是个 ID,需要二次反查成 XPath(actHandler.ts:475)。

3.2 act 的"两步动作 + 自愈":为什么一次调用可能问模型好几次

它要解决的小问题: 有些"一个动作"其实是两步——比如"从下拉框选『中国』",得先点开下拉、再点选项。而且页面会变,存好的选择器可能失效。

思路/直觉: act 不是"问一次模型、落一个动作"就完。它有两条增强路径:

  • 两步(twoStep): 模型在第一步返回时可以说"我还没做完"(twoStep: true)。框架于是再抓一张快照,和第一张做 diff(diffCombinedTrees)只取变化的部分喂回模型,让它决定第二步(actHandler.ts:198-252)。diff 是为了省 token——下拉框弹出后只有新增的选项是相关的。
  • 自愈(self-heal): 落点动作抛错时,若开了 selfHeal,框架重新抓快照、重新问一次模型要新的可交互元素,用新 XPath 但保留原方法/参数重试(actHandler.ts:334-419)。页面小改版导致老选择器失效时,这一招能自动救回来。

真实实现:

  • 两步的触发点:if (actInferenceResponse?.twoStep !== true) return firstResult;(actHandler.ts:198)。
  • 第二步用 diff 树:diffCombinedTrees(combinedTree, combinedTree2),diff 为空则回退用整棵新树避免上下文空掉(actHandler.ts:209-213)。
  • 自愈重试沿用原 method + resolvedArgs,只换 fallbackAction.selector(actHandler.ts:392-405)。

关键细节/坑: 两步里第二步故意排除了 selectOptionFromDropdown 这个方法(actHandler.ts:220),避免死循环式地一直"再选一次下拉"。

3.3 两种 agent:DOM 工具循环 vs Computer-Use 坐标循环

它要解决的小问题: act 是单步的,但"帮我订张票"是多步任务,需要一个能自己规划、循环执行、知道何时收手的 agent。Stagehand 提供两种风格的 agent,针对不同模型能力。

两种模式对比:

维度DOM 模式(V3AgentHandler)CUA 模式(V3CuaAgentHandler)
落点方式复用 3.1 的无障碍树 grounding纯视觉:模型直接给屏幕 (x, y)
循环驱动Vercel AI SDK 的 generateText 工具循环各 provider 客户端自己的 while 循环
停止条件stopWhen: stepCountIs(maxSteps)while (!completed && currentStep < maxSteps)
工具/动作act / extract / goto / scroll / screenshot…click / type / scroll / screenshot(坐标级)
适用模型任意会 tool-calling 的模型专门的 computer-use 模型(Claude/GPT/Gemini CUA)
代码位置handlers/v3AgentHandler.ts:120handlers/v3CuaAgentHandler.ts:39

DOM 模式怎么转: V3AgentHandler.execute 把一组"工具"(每个工具内部就是调 v3.act / v3.extract 等)交给 AI SDK 的 generateText,由它反复"模型出一个工具调用 → 执行 → 把结果喂回"直到 stepCountIs(maxSteps) 触发或模型调用了 done 工具(v3AgentHandler.ts:458-472)。关键在于:agent 的 act 工具就是重新走一遍 3.1/3.2 的完整 grounding 流程(agent/tools/act.ts:42 直接 await v3.act(...))——所以 DOM agent 天然继承了缓存和自愈。

CUA 模式怎么转: provider 客户端(如 AnthropicCUAClient)自己跑循环:截图 → 发给 computer-use 模型 → 模型回一批 computer_call(带坐标)→ executeAction 用 CDP 落点 → 再截图进下一轮,while (!completed && currentStep < maxSteps)(agent/AnthropicCUAClient.ts:153)。落点是纯坐标的:page.click(x, y) 最终发 CDP 的 Input.dispatchMouseEvent(understudy/page.ts:1867)。

一个巧妙细节: CUA 在"录制模式"下,会把模型给的坐标点击反解成一个 XPath(page.click(x, y, { returnXpath: true }),v3CuaAgentHandler.ts:342),这样一次视觉驱动的运行也能被存成确定性的 XPath 动作、之后不靠模型重放。视觉与 DOM 两条路在"可复用性"上被统一了。


4. 巧妙之处与局限

值得偷师的设计:

  • 用 a11y 树把 grounding 从"视觉猜测"变成"字典查表"。 这是最该学的一点:让模型只从带 ID 的候选集里选,而不是自由生成坐标/选择器。模型的输出空间被压缩到"选一个 ID + 一个方法",既降幻觉又能被 zod schema 校验(inference.ts:410 的 act schema)。
  • 不依赖 Playwright,自研 CDP 引擎(understudy)。 直接讲 Chrome DevTools Protocol(cdp.ts:61CdpConnection 用裸 ws WebSocket),换来对多帧、shadow DOM、截图、坐标输入的完全掌控,以及更轻的依赖。截图、点击、键盘全走 CDP 原语(page.ts:1487 screenshot、page.ts:1867 mouse、page.ts:2132 key)。
  • act 缓存 = "第一次用 AI,之后免 AI"。 缓存键是 指令 + 归一化 URL + 变量名集合 的哈希(ActCache.ts:57buildActCacheKey),命中就重放存好的 XPath 动作,把"AI 驱动的探索"沉淀成"确定性的回放",省钱省时(README 说的 "run without LLM inference")。
  • extract 的 URL 小魔术。 LLM 直接吐 URL 常出错(截断/编造)。extract 先把 schema 里的 z.string().url() 字段换成 z.number(),让模型只返回元素的数字 ID,再用 combinedUrlMap 把真实 URL 注回去(extractHandler.ts:205transformUrlStringsToNumericIds + extractHandler.ts:247injectUrls)。同样是"别让模型生成易错内容,让它选 ID"的思想。
  • 两条执行路:本地 vs 托管 API。 每个动词内部都有 if (this.apiClient) 分支(v3.ts:1242 等),可把 act/extract/observe/agent 全部代理到 Browserbase 的托管服务器执行,本地只当薄客户端。

边界与局限(诚实):

  • 强依赖无障碍树质量。 若页面 a11y 语义差(纯 canvas、大量无 role 的 div、被 ARIA 隐藏),文本树里就没有好的候选,DOM 模式会退化——这时才需要 CUA 视觉模式兜底。
  • shadow DOM 有明确盲区。 查不到 XPath 的 shadow 元素被直接标 not-supported 过滤(observeHandler.ts:218),即"我知道我够不着"。
  • 缓存对 URL 敏感。 缓存键含归一化 URL(ActCache.ts:59);URL 里的动态参数会影响命中率,SPA 内部软导航也可能让"同一页"哈希不同。
  • 每次 act/observe/extract 都要抓一次全页快照。 多帧 + DOM.getDocument + AX 树在大页面上有成本;为此才有 focusSelector 聚焦子树(capture.ts:161tryScopedSnapshot)和两步的 diff 树来控 token。
  • CUA 模式的可靠性回到"模型给的坐标准不准"。 一旦走视觉路,3.1 的确定性优势就不再适用。

代码地图(导航索引)

关注点文件说明
顶层门面 / 四个动词的路由packages/core/lib/v3/v3.tsclass V3(导出为 Stagehand);act/extract/observe/agent 方法、缓存判定、init 里装配各 Handler
act 主流程packages/core/lib/v3/handlers/actHandler.tsActHandler.act / takeDeterministicAction / normalizeActInferenceElement;两步、自愈、ID→XPath
observe 主流程packages/core/lib/v3/handlers/observeHandler.tsObserveHandler.observe;LLM 返回 ID → combinedXpathMap 查表
extract 主流程packages/core/lib/v3/handlers/extractHandler.tsExtractHandler.extract;URL 字段转数字 ID 再回注(transformUrlStringsToNumericIds)
grounding 地基:混合快照packages/core/lib/v3/understudy/a11y/snapshot/capture.tscaptureHybridSnapshot / mergeFramesIntoSnapshot / prefixXPath;产出 combinedTree + combinedXpathMap
自研 CDP 引擎(连接)packages/core/lib/v3/understudy/cdp.tsCdpConnection / CdpSession;裸 WebSocket 讲 DevTools Protocol,无 Playwright
CDP 页面原语packages/core/lib/v3/understudy/page.tsPage.click / screenshot / type;Input.dispatchMouseEvent / Input.dispatchKeyEvent
DOM 模式 agentpackages/core/lib/v3/handlers/v3AgentHandler.tsV3AgentHandler.execute;Vercel AI SDK generateText + stopWhen: stepCountIs
CUA(视觉)模式 agentpackages/core/lib/v3/handlers/v3CuaAgentHandler.tsV3CuaAgentHandler.execute / executeAction;坐标动作、录制反解 XPath
CUA provider 循环packages/core/lib/v3/agent/AnthropicCUAClient.tsexecutewhile (!completed && currentStep < maxSteps) 截图→动作循环
agent 工具集packages/core/lib/v3/agent/tools/index.tscreateAgentTools;act.ts 等工具内部就是回调 v3.act
act 缓存packages/core/lib/v3/cache/ActCache.tsActCache.prepareContext / tryReplay / buildActCacheKey;免模型重放
LLM 推理封装packages/core/lib/inference.tsact / observe / extract;zod schema 约束模型只返回 ID+方法
prompt 组装packages/core/lib/prompt.tsbuildActPrompt / buildStepTwoPrompt / buildObserveSystemPrompt