基于 DOM 信息进行页面操作的 page-agent

使用 AI Agent 进行页面操作的实践已经挺多了,但更多的是以浏览器插件形式存在的。最近阿里发布了 alibaba/page-agent: JavaScript in-page GUI agent. Control web interfaces with natural language. 可以直接在页面内嵌入脚本来进行 AI Agent 的页面操作。

关于 page-agent 的实现,我们简单过一下执行流程,然后重点关注两个功能:一个是如何让 Agent 准确理解页面内容,另一个是如何让 Agent 可以准确的对页面进行操作

执行流程

page-agent 的执行流程与普通的 agent 区别不大:

  1. 业务代码创建 new PageAgent(config)
  2. PageAgent 内部创建 PageController
  3. PageAgent 继承 PageAgentCore
  4. PageAgentCore.execute(task) 启动任务循环:
    1. 每一步先通过 pageController.getBrowserState() 获取当前页面状态
    2. Core 组装 system prompt + user prompt
    3. LLM.invoke() 调用模型,并强制模型调用宏工具 AgentOutput
    4. 宏工具里包了一层“反思 + 动作”结构
    5. Core 从动作中找到具体 tool
    6. tool 再调用 PageController 的 DOM 方法执行动作
    7. 执行结果进入 history,继续下一步,直到 done

对应关系如下:

1
2
3
4
5
6
7
PageAgent
-> PageAgentCore.execute(task)
-> PageController.getBrowserState()
-> LLM.invoke(messages, { AgentOutput })
-> AgentOutput.execute(reflection + action)
-> internal tool.execute(...)
-> PageController.click/input/select/scroll/...

页面内容提取

首先,我们来看页面内容的提取。现在主流实现 browser use 有两种方向:

  1. 一种是对页面进行截图,让多模态模型通过视觉理解返回要操作的坐标位置,进行对应的操作。类似的有字节的 Midscene - Vision-Driven UI Automation
  2. 另一种是直接获取 DOM 的信息,文本化后传给模型。这么做的好处是不依赖多模态模型,任何一个纯文本模型都可以进行识别和操作。但坏处就是它只能理解 DOM 信息,对于复杂的交互,比如 Canvas、SVG 这种就不是很好好操作了。并且如果一个页面它的布局跟 DOM 流不是严格对应的,比如用了过多的 absolute 和 fix 布局,那么通过 DOM 信息来获取的页面内容可能会不太准确。

page-agent 是通过将 DOM 信息文本化来提取页面内容的,这使得它可以方便的集成在页面中,同时也不要求必须使用多模态模型。

我们使用以下例子来快速了解 page-agent 是如何文本化 DOM 信息的,假设原页面是一个带顶部导航、筛选表单、结果列表和分页器的后台页面,文本化后的内容大致为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
[0]<a aria-label=Company Home />
[1]<button aria-label=Open user menu aria-haspopup=menu aria-expanded=false />
[2]<a >Dashboard />
[3]<a >Orders />
[4]<a >Customers />

Order Management
Review and process customer orders

[5]<input type=text placeholder=Search by order ID, customer, or phone />
[6]<button aria-label=Search />
[7]<button aria-label=Reset filters />

Status
[8]<button aria-haspopup=listbox aria-expanded=true aria-controls=status-menu>Pending Review />
[9]<div role=option aria-checked=true>Pending Review />
[10]<div role=option aria-checked=false>Paid />
[11]<div role=option aria-checked=false>Shipped />
[12]<div role=option aria-checked=false>Completed />

Date Range
[13]<input type=text placeholder=Start date data-date-format=YYYY-MM-DD />
[14]<input type=text placeholder=End date data-date-format=YYYY-MM-DD />

Advanced Filters
[15]<button aria-expanded=false aria-controls=advanced-panel>Expand />

Results: 128 orders

[16]<input type=checkbox checked=true aria-label=Select all visible orders />

[17]<a >SO-20260329-001
Alice Chen
138****2211
Pending Review
USD 299.00 />

[18]<button >Approve />
[19]<button >Reject />
[20]<button aria-haspopup=menu aria-expanded=false>More actions />

[21]<a >SO-20260329-002
Bob Lee
bob@example.com
Paid
USD 89.00 />

[22]<button >Ship />
[23]<button >Refund />

[24]<a >SO-20260329-003
Carol Zhang
VIP Customer
Completed
USD 1,249.00 />

Showing 1-20 of 128 results

[25]<button aria-label=Previous page disabled=true />
[26]<button aria-current=page>1 />
[27]<button >2 />
[28]<button >3 />
[29]<button aria-label=Next page />

可以看到 page-agent 以文本形式结构化了 DOM 信息,每个元素前面都加上了固定的 ID,其内部会维护每个 ID 与实际 DOM 元素的对应关系。

同时我们还注意到 page-agent 会把 DOM 的信息添加在文本信息中,比如是否已经选中了,是否可以滚动等等。

操作动作执行

模型每一步不是直接输出动作,而是必须调用一个宏工具,输入结构类似:

1
2
3
4
5
6
{
evaluation_previous_goal?: string
memory?: string
next_goal?: string
action: { [toolName]: toolInput }
}

也就是说,模型被要求:

  • 先评估上一动作效果
  • 再写短期记忆
  • 再说明下一目标
  • 最后选择一个实际动作

packages/core/src/tools/index.ts 定义了内置工具集合,本质上是:

  • PageAgentTool
  • Map<string, PageAgentTool>

目前主要工具有:

  • done
  • wait
  • ask_user
  • click_element_by_index
  • input_text
  • select_dropdown_option
  • scroll
  • scroll_horizontally
  • execute_javascript

这些工具会对 DOM 元素进行精细的操作。

Prompt

从实现上看,每一步发给模型的是两条 message:

  • system
  • user

system message 来源有两种:

  • 默认的 packages/core/src/prompts/system_prompt.md
  • config.customSystemPrompt 完全覆盖默认 prompt

system prompt 负责定义 Agent 的全局行为规则,而不是当前页面状态。

user message 由 #assembleUserPrompt() 拼装,包含四大块:

  1. <instructions>

    这是可选块,由 #getInstructions() 生成,可能包含:

    • <system_instructions> 来自 config.instructions.system
    • <page_instructions> 来自 config.instructions.getPageInstructions(url)
    • <llms_txt> 如果开启 experimentalLlmsTxt,会抓当前站点 /llms.txt

    这块的作用是给默认 system prompt 追加“部署方自己的规则”和“页面级特殊说明”。

  2. 这块描述当前任务状态,主要包含:

    • <user_request> 用户原始任务文本
    • <step_info> 当前是第几步,最大允许步数,当前时间

    它解决的是“当前任务在什么阶段”这个问题。

  3. `

    这块是 Agent 的短期记忆。

    对于之前的每个 step,会写入:

    • Evaluation of Previous Step
    • Memory
    • Next Goal
    • Action Results

    此外还会写入系统 observation,例如:

    • 页面 URL 变化
    • 剩余 step 太少
    • 等待时间过长
    • 用户 takeover
  4. 这是重点,当前页面快照,来自 pageController.getBrowserState(),包括:

    • header
      • 当前页面标题和 URL
      • viewport/page 尺寸
      • 当前滚动位置
      • 页面上下方还剩多少内容
    • content
      • 文本化 DOM 主体
    • footer
      • 底部滚动提示

    如果配置了 transformPageContent,则 content 会在送进 prompt 前先被改写。

最终可以把每一步发给模型的内容抽象成这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
system:
[default system_prompt.md or customSystemPrompt]

user:
<instructions>...</instructions>
<agent_state>...</agent_state>
<agent_history>...</agent_history>
<browser_state>...</browser_state>

tools:
AgentOutput {
evaluation_previous_goal?: string
memory?: string
next_goal?: string
action: one of current tools
}

总结

1
2
3
4
5
6
7
8
User Task
-> PageAgent
-> PageAgentCore
-> observe: PageController.getBrowserState()
-> think: LLM.invoke()
-> act: tools -> PageController actions
-> history/activity events
-> Panel