引言
"一行 script 标签,自然语言控制你的 Web 应用。不需要截图,不需要浏览器扩展,不需要后端服务,不需要 Python。"
这是"每日一个开源项目"系列的第143篇文章 。今天的主角是 page-agent------阿里巴巴开源的客户端 GUI Agent 库。
让 AI 自动操作浏览器,现有的主流方案几乎都走同一条路:截屏 → 多模态 LLM 识别页面元素 → 执行操作。这条路有两个代价:需要多模态模型(贵),需要在服务器端或无头浏览器里运行(复杂)。
page-agent 的回答是:把 DOM 树序列化成文本就够了。交互元素编上索引,LLM 读文本、返回"点击索引 3",在浏览器里直接执行。整个过程不离开页面,不需要截图,不需要多模态能力。
你将学到什么
- 文本化 DOM 的原理:如何把页面变成 LLM 可读的带索引结构
- ReAct 循环架构:Observe → Think(反思 + 行动)→ Act 的完整实现
- 反思模型(Reflection-Before-Action):每步先评估上步效果再决策
- 内置工具系统:点击、输入、滚动、执行 JS 的工具定义方式
- 单包 vs 多包:
page-agent(含 UI)和@page-agent/core(纯核心) - Chrome 扩展 + MCP Server 的跨页面能力扩展
前置知识
- 了解 LLM 的工具调用(Function Calling)机制
- 了解 DOM 和浏览器事件的基本概念
- 使用过 OpenAI SDK 或类似 API
项目背景
项目简介
page-agent 是一个纯客户端的 GUI Agent 库,把 LLM 的推理能力嵌入到网页里,让 AI 通过文本化 DOM 理解页面结构并执行操作。
核心设计决定:不用截图,用 DOM 文本。浏览器的 DOM 本身就完整地描述了页面上有什么元素、它们是什么类型、它们现在是否可交互。把这些信息序列化成文本,比截图更精确(像素级截图里按钮文字有时模糊),更便宜(不需要多模态模型),更快(DOM 读取是同步操作)。
项目致谢了 browser-use(Python 端服务器侧浏览器自动化库),page-agent 的定位是它的客户端版本:在页面内运行,而不是在服务器端控制无头浏览器。
作者/团队介绍
- 组织: Alibaba(阿里巴巴)
- 主要语言: TypeScript
- License: MIT
- npm 包 :
page-agent(含 UI Panel)、@page-agent/core(纯核心逻辑) - 最新版本: v1.10.0
项目数据
- 📄 License: MIT
- 📦 npm:
page-agent - 💻 技术栈: TypeScript + npm workspaces + Vite
主要功能
核心作用
传统浏览器 GUI Agent(截图路线):
页面 → 截图 → 多模态 LLM 视觉理解 → 返回坐标/元素 → 执行点击
成本:多模态 API 费用高 + 服务器/无头浏览器基础设施
page-agent(文本 DOM 路线):
页面 → DOM 序列化为带索引文本 → 纯文本 LLM 推理 → 返回工具调用(click_element_by_index: 3)→ 直接在页面内执行
成本:纯文本 LLM(更便宜)+ 零后端(纯前端 JS)
使用场景
- SaaS 产品 AI Copilot:在你自己的产品里嵌入 AI 助手,用户说"帮我创建一个新项目,名字叫 X"------几行代码实现,不需要后端改动
- 智能表单填写:ERP、CRM、后台管理系统里的 20 步操作压缩成一句话
- 无障碍增强:为任意 Web 应用添加自然语言操作能力,语音命令、屏幕阅读器场景
- 跨标签页 Agent:配合 Chrome 扩展,Agent 可以跨页面工作(例如:从表格里读取数据,切换到另一个标签页填写)
- MCP 控制:通过 MCP Server(Beta)让外部 Agent 客户端控制浏览器
快速开始
一行接入(使用免费 Demo LLM,仅限技术评估):
html
<script src="https://cdn.jsdelivr.net/npm/page-agent@1.10.0/dist/iife/page-agent.demo.js" crossorigin="true"></script>
加载后页面右下角出现 Agent 面板,可以直接输入自然语言命令。
NPM 安装(生产用法):
bash
npm install page-agent
javascript
import { PageAgent } from 'page-agent'
const agent = new PageAgent({
model: 'qwen3.5-plus',
baseURL: 'https://dashscope.aliyuncs.com/compatible-mode/v1',
apiKey: 'YOUR_API_KEY',
language: 'zh-CN',
})
// 执行一个自然语言任务
const result = await agent.execute('点击登录按钮,然后填写用户名为 test@example.com')
console.log(result.success, result.message)
使用 @page-agent/core(无 UI 面板,嵌入式场景):
javascript
import { PageAgentCore } from '@page-agent/core'
import { PageController } from '@page-agent/page-controller'
const pageController = new PageController({ enableMask: true })
const agent = new PageAgentCore({
pageController,
model: 'gpt-4o',
apiKey: 'sk-...',
maxSteps: 20,
onAfterStep: async (agent, history) => {
console.log('步骤完成', history.at(-1))
}
})
const result = await agent.execute('查找产品列表中价格最低的商品并加入购物车')
支持的模型(OpenAI 兼容接口均可):
| 厂商 | 模型 |
|---|---|
| OpenAI | gpt-4o, gpt-4-turbo, gpt-5.2, gpt-5.4 |
| Anthropic | claude-opus-4.8, claude-sonnet-4, claude-haiku-3.5 |
| 阿里云 | qwen3.5-plus, qwen3.6-max, qwen3.6-flash |
| DeepSeek | deepseek-chat, deepseek-reasoner |
| gemini-2.0-flash(OpenAI 兼容端点) | |
| 本地 | Ollama(qwen3:14b,RTX 3090 24GB 测试通过) |
项目详细剖析
文本化 DOM:核心技术原理
page-agent 的 PageController 把 DOM 转换成一种带索引的简化文本结构:
ini
原始 DOM(简化):
<div class="form-container">
<input type="text" placeholder="用户名" />
<input type="password" placeholder="密码" />
<button class="btn-primary">登录</button>
<a href="/register">注册账号</a>
</div>
序列化为 LLM 输入(带索引的交互元素):
URL: https://example.com/login
Title: 登录 - My App
[0]<input placeholder="用户名" />
[1]<input type="password" placeholder="密码" />
[2]<button>登录</button>
[3]<a>注册账号</a>
每个可交互元素分配一个数字索引 [N],LLM 只需要返回"点击 2"或"在 0 输入 admin@example.com"。
DOM 处理流水线:
css
实时 DOM
↓ dom_tree/ 模块
FlatDomTree(扁平化的树结构,含 DomNode 映射)
↓ 脱水(Dehydration)
带索引的简化 HTML 文本
↓
传入 LLM 上下文
↓
LLM 返回工具调用(如:click_element_by_index: {index: 2})
↓
PageController.clickElement(2) → 找到对应 HTMLElement → 触发点击
只有满足以下条件的元素才会被标注索引(避免噪声):
isVisible: true(元素在视口内或虽不在视口但可滚动到)isInteractive: true(可点击/输入/选择的元素)isTopElement: true(不被其他元素遮挡)
ReAct 循环架构
bash
agent.execute("完成某个任务")
↓
┌─────────────────────────────────────────┐
│ 主循环(最多 maxSteps 次) │
│ │
│ ① Observe │
│ pageController.updateTree() │
│ → 刷新 DOM,获取当前页面文本 │
│ │
│ ② Think(LLM 调用) │
│ 输入:系统 prompt + 历史 + 当前 DOM │
│ LLM 输出(反思模型): │
│ { │
│ evaluation_previous_goal: "...", │← 上一步做得怎么样?
│ memory: "...", │← 需要记住什么?
│ next_goal: "...", │← 下一步要做什么?
│ action: { click_element_by_index: {index: 2} }
│ } │
│ │
│ ③ Act │
│ 执行 LLM 返回的工具调用 │
│ 记录到 history(持久化,下步可见) │
│ │
│ ④ 检查是否结束 │
│ LLM 调用 done 工具 → 返回结果 │
│ 或达到 maxSteps 上限 │
└─────────────────────────────────────────┘
↓
ExecutionResult { success, message, history }
反思模型(Reflection-Before-Action)
每一步 LLM 调用前,会把上一步的执行结果和历史传给模型,要求它先"反思"再行动:
json
{
"evaluation_previous_goal": "成功点击了登录按钮,页面跳转到首页",
"memory": "已完成登录,当前在首页,还需要找到设置页面",
"next_goal": "找到导航栏中的设置或账户选项并点击",
"action": {
"click_element_by_index": { "index": 5 }
}
}
evaluation_previous_goal 强制模型在执行下一步之前评估上一步效果,避免盲目继续(例如:点击了但页面没反应,应该换策略而不是再点一次)。
memory 字段是短期记忆机制------把关键进展压缩成 1-3 句话存入历史,让长任务里 LLM 不会"忘记"已经完成了什么。
内置工具系统
LLM 可以调用的工具(通过 Function Calling 机制):
| 工具 | 作用 |
|---|---|
click_element_by_index |
点击指定索引的元素 |
input_text |
在指定索引的输入框里输入文字 |
select_dropdown_option |
选择下拉框选项(按文字内容匹配) |
scroll |
垂直滚动(支持按页数或像素) |
scroll_horizontally |
水平滚动 |
execute_javascript |
执行任意 JS 代码(支持 AbortSignal 取消) |
wait |
等待 1-10 秒(等页面加载或动画完成) |
ask_user |
向用户提问(人机协作节点) |
done |
任务完成,返回结果 |
自定义工具(扩展默认行为):
javascript
const agent = new PageAgent({
model: 'gpt-4o',
apiKey: '...',
customTools: {
// 添加自定义工具
get_current_user: {
description: '获取当前登录用户信息',
inputSchema: z.object({}),
execute: async function() {
const user = await fetchCurrentUser()
return JSON.stringify(user)
}
},
// 设为 null 可以禁用内置工具
execute_javascript: null
}
})
Monorepo 结构
swift
packages/
├── page-agent/ → 主包(npm: page-agent),包含 UI 面板
├── core/ → 纯核心逻辑(npm: @page-agent/core),无 UI
├── llms/ → LLM 客户端,支持多厂商
├── page-controller/ → DOM 操作和可视化反馈
├── ui/ → 控制面板 + 国际化,与核心解耦
├── extension/ → Chrome 扩展(WXT + React)
└── website/ → 文档网站(React)
核心的模块边界设计:
page-controller不知道 LLM 的存在,只负责 DOM 操作llms不知道页面结构,只负责 LLM 通信core把两者组合成 ReAct 循环page-agent(主包)在core上层加 UI 面板,两者可独立使用
跨页面能力:Chrome 扩展 + MCP
Chrome 扩展(适合跨标签页任务):
- Agent 在一个扩展控制的上下文里运行,可以切换标签页、读取不同页面的 DOM
- 适合"从 A 页面抓数据,填到 B 页面"这类需要跨页面的工作流
MCP Server(Beta):
- 把 page-agent 暴露为 MCP 工具,让外部 Agent 客户端(如 Claude Desktop、Claude Code)远程控制浏览器
- 适合需要把浏览器控制能力接入更大 Agent 工作流的场景
性能和可靠性设计
- 步骤延迟:步骤间默认等待 400ms,给页面渲染和网络请求留出时间
- 点击后等待:点击操作后等待 200ms,确保 DOM 更新完成
- 并发保护 :防止
execute()并发调用,避免竞态条件 - AbortSignal 支持 :所有工具执行都接受取消信号,
execute_javascript也支持中断 - 自动重试:LLM 调用失败(429 限流、500 服务器错误)自动指数退避重试
- Token 用量追踪:每步记录 promptTokens、completionTokens,支持 Prompt Cache 命中统计
项目地址与资源
官方资源
- 🌟 GitHub : alibaba/page-agent
- 🚀 在线 Demo : alibaba.github.io/page-agent
- 📖 文档 : alibaba.github.io/page-agent/...
- 📦 npm : page-agent
总结
page-agent 的核心洞察是:浏览器自动化不一定需要"看图"。DOM 本身就是结构化的页面描述,把它序列化成带索引的文本,文本模型就能直接理解和操作------不需要多模态,不需要视觉理解,不需要坐标定位。
这个洞察把 GUI Agent 的门槛大幅降低:不需要服务器,不需要无头浏览器,不需要截图,一个 script 标签或一个 npm 包,在用户浏览器里直接运行。
反思模型(每步先评估上步 → 再规划下步)和持久化历史(关键信息压缩进 memory)是它处理多步骤任务的核心机制,保证 Agent 在长任务里不会迷失方向。
对于想给自己的 Web 产品加 AI Copilot 能力、或者想用 LLM 自动化内部工具操作的开发者,page-agent 是目前接入成本最低、对后端架构零侵入的选项之一。
探索 PrimeSkills ------ 精选 AI Agent 与技能的市场,每一个都经过真实企业工作流验证,去掉浮夸,留下真正有用的。
欢迎访问我的个人主页,发现更多有价值的见解和有趣的产品。