深入剖析 Browser Use
Browser Use 介绍
Browser Use 是一个AI驱动浏览器自动化开源框架,让我们自然语言操作浏览器,目前在 GitHub 上已经获得了惊人的 49.9k star
下方演示展示了我启动 Browser Use 运行的一个小 demo,通过简单的自然语言指令"打开百度并搜索苹果",Browser Use 自主完成了网页导航、识别搜索框、输入关键词并执行搜索的全过程
Browser Use 工作原理
如图所示,Browser Use 首先捕获了浏览器实时状态,然后整合结构化信息交由 LLM 进行智能决策,随后执行确定的动作,完成后再次获取更新后的浏览器状态,循环往复直至任务完成。
这一自动化执行流程由三大核心模块组成,通过 browser_use/agent 的调度下协同工作形成了一个完整的自动化执行流程:
- 获取浏览器状态
- 浏览器管理模块(browser_use/browser)
- DOM 处理模块(browser_use/dom)
- 消息管理(browser_use/agent/message_manager)
- 动作执行(browser_use/controller)
获取浏览器状态
Browser Use 获取浏览器状态,主要函数如图所示,分为两个模块
-
browser_use/browser 基于 Playwright 实现浏览器的核心控制与管理,负责启动浏览器实例、处理浏览器上下文和页面
-
browser_use/dom 解析和提取页面元素信息、元素高亮定位
浏览器管理模块(browser_use/browser)
浏览器实例初始化策略
源码路径: browser_use/browser/browser.py
browser-use 提供了四种不同的浏览器初始化策略,以适应不同的使用场景:
- _setup_cdp:通过 Chrome DevTools Protocol 连接到正在运行的 Chrome 实例,便于调试
- _setup_wss:适用于连接到云端浏览器服务(如 anchorbrowser.io、browserless.io),实现远程操作
- _setup_strandard_browser:使用已安装 Chrome 的用户配置文件,保留登录状态和 Cookie
- _setup_browser:默认方法,创建新的浏览器实例,适用于基本场景
这里我分别测试了 1、3、4 三种初始化方式,对于方式 1 可以采用以下命令启动通过 --user-data-dir 来指定用户信息
方式 1、3 的底层逻辑实际相同,都是基于 Chrome DevTools Protocol (CDP) 进行连接,只不过方式 3 在代码中自动执行启动命令
ini
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" --remote-debugging-port=9222 --user-data-dir="存储用户和浏览器插件路径"
浏览器上下文创建与反检测技术
创建上下文的核心逻辑在 BrowserContext.create_context,整体代码比较简单,通过默认方式启动可以分为下四步
- context = browser.new_context(...) 创建新的上下文(无痕窗口)
- CDP 连接,且浏览器已有上下文,会使用当前浏览器的上下文
- context.tracing.start 记录浏览器操作的详细日志,用于调试和问题排查
- context.add_cookies 加载 cookie
- context.add_init_script 注入反检测脚本,用来避免网站检测到自动化行为的脚本
源码路径: browser_use/browser/context.py
重点是注入反检测脚本,毕竟我还是第一次学到😂
-
Webdriver 特征隐藏
- 自动化浏览器通常会将navigator.webdriver设置为true
-
浏览器语言伪装
- 覆盖浏览器的语言偏好设置,语言设置是浏览器指纹的重要组成部分
-
浏览器插件模拟
- 自动化浏览器通常没有安装插件
-
Chrome 运行时环境模拟
- 添加 Chrome 特有的浏览器对象,反爬系统会检查 window.chrome 对象结构
-
权限 API 修改
- Headless(无头)浏览器处理通知权限的方式与普通浏览器不同,对通知权限查询返回一致的结果
-
Shadow DOM
- 避免网站使用封闭模式 Shadow DOM 隐藏内容,无论网站指定什么模式,都返回开放模式的 Shadow DOM
- 还使用立即执行函数表达式(IIFE)封装,避免变量污染,细节
获取标签页
检查是否存在已打开的页面,如果存在,返回最后一个页面;如果不存在,创建新页面
DOM 处理模块(browser_use/dom)
DOM 处理模块是 browser-use 的核心,解析页面获取 DOM 节点的信息,提供精确的元素定位和交互能力,帮助 LLM 更准确的决策
以百度为例,介绍一些前端的概念,网页展示基于 HTML 文档,该文档由各种标签元素(如<div>, <a>, <img>等)组成层级结构。浏览器解析这些 HTML 元素后,将其转换为 DOM (文档对象模型) 树,其中每个 HTML 元素、文本和属性都变成了 DOM 树中的节点。JavaScript 可以通过这个 DOM 树动态操作和修改网页内容,实现交互功能。
我在百度页面执行了 browser_use/dom/buildDomTree.js 实现效果如图, 我们可以看到页面很多元素被高亮并打上索引,还返回了页面元素的结构化信息
这里我们可以想到两个问题,什么元素需要被高亮并打上了索引?需要返回元素的哪些关键信息来辅助AI决策?
让我们带着问题一起看看 buildDomTree 的实现逻辑吧
buildDomTree 函数深度优先递归遍历方式,将原生 DOM 树(n 叉树)转换为结构化对象模型,提取并组织每个节点的关键属性、层级关系和交互特性
-
DOM 节点处理:
- DOM 规范中定义了多种节点类型(DOCUMENT_NODE、COMMENT_NODE、ATTRIBUTE_NODE等),builDomTree 只会处理
元素节点(ELEMENT_NODE)
和文本节点(TEXT_NODE)
这两种最能代表页面内容和结构的两种核心节点类型 - 还会对这两种节点进行过滤,排除对LLM决策无价值的元素类型,例如空文本节点、script、svg 等对内容理解和交互决策无实质贡献的节点
- DOM 规范中定义了多种节点类型(DOCUMENT_NODE、COMMENT_NODE、ATTRIBUTE_NODE等),builDomTree 只会处理
-
xpath:为每个节点生成唯一的XPath标识,便于定位与追踪
-
isVisible:判断元素是否正常显示
-
isInViewport:确定元素是否在当前可视区域内
-
isTopElement:检测元素是否是其位置上的顶层元素
-
isInteractive:识别可交互元素,如链接、按钮等
-
highlightIndex:高亮元素并添加索引
xpath 每个节点生成唯一的XPath标识
javascript
function getXPathTree(element, stopAtBoundary = true) {
const segments = [];
let currentElement = element;
while (currentElement && currentElement.nodeType === Node.ELEMENT_NODE) {
// 遇到Shadow DOM或iframe边界时停止
if (
stopAtBoundary &&
(currentElement.parentNode instanceof ShadowRoot ||
currentElement.parentNode instanceof HTMLIFrameElement)
) {
break;
}
// 计算同名兄弟节点中的索引
let index = 0;
let sibling = currentElement.previousSibling;
while (sibling) {
if (
sibling.nodeType === Node.ELEMENT_NODE &&
sibling.nodeName === currentElement.nodeName
) {
index++;
}
sibling = sibling.previousSibling;
}
// 构建XPath片段
const tagName = currentElement.nodeName.toLowerCase();
const xpathIndex = index > 0 ? `[${index + 1}]` : "";
segments.unshift(`${tagName}${xpathIndex}`);
currentElement = currentElement.parentNode;
}
return segments.join("/");
}
getXPathTree 的逻辑还是比较简单,主要是对于有同级的重名节点,会获取对应索引进行拼接,剩下的就是不断的获取自己的父节点,判断是否有同级的重名节点,到根节点为止
vbnet
xpath: "html/body/div/div/div[3]/div/a"
isVisible 判断元素是否正常显示
isTopElement 检测元素是否是其位置上的顶层元素
在我省略的代码中有一个值得注意的点
- 如果元素在iframe中,默认认为它是顶层元素
- 我写个 demo 测试了下,在当前文档下调用 document.elementFromPoint 只能获取到 iframe,获取不到 iframe 的子节点,
- 通过iframe.contentDocument 继续调用会受到浏览器同源策略的限制
isInteractive 识别可交互元素
isInteractiveElement 函数采用多层次的判断策略,从明确的标签属性到隐含的行为特征,一层层的判断元素的交互性,代码看着倒是都很简单,但是判断非常的多...
- Cookie 横幅特殊处理,登录国外网站好像见的多一点,比如 stackOverflow
javascript
const isCookieBannerElement = (typeof element.closest === 'function') && (
element.closest('[id*="onetrust"]') ||
element.closest('[class*="onetrust"]') ||
element.closest('[data-nosnippet="true"]') ||
element.closest('[aria-label*="cookie"]')
);
函数首先检查元素是否位于 Cookie 横幅内,并进一步判断它是否为接受或拒绝按钮。这种特殊处理反映了现代网页浏览中常见的用户交互模式。
- 标准交互元素识别
函数检查元素是否属于公认的交互式 HTML 元素或具有交互角色:
javascript
const interactiveElements = new Set([
"a", "button", "details", "embed", "input", "menu", "menuitem",
"object", "select", "textarea", "canvas", "summary", "dialog",
"banner"
]);
const interactiveRoles = new Set(['button-icon', 'dialog', /* ... 其他角色 ... */]);
- 基于属性的交互性判断
javascript
const hasInteractiveRole =
hasAddressInputClass ||
interactiveElements.has(tagName) ||
interactiveRoles.has(role) ||
interactiveRoles.has(ariaRole) ||
(tabIndex !== null && tabIndex !== "-1" && element.parentElement?.tagName.toLowerCase() !== "body") ||
element.getAttribute("data-action") === "a-dropdown-select" ||
element.getAttribute("data-action") === "a-dropdown-button";
- cookie 横幅和同意界面
javascript
Apply to buildDomTree...
const isCookieBanner =
element.id?.toLowerCase().includes('cookie') ||
element.id?.toLowerCase().includes('consent') ||
/* ... 其他条件 ... */;
- 事件监听器和行为特征
最后,函数检查元素的行为特征,包括事件监听器和 ARIA 属性:
javascript
function getEventListeners(el) {
try {
return window.getEventListeners?.(el) || {};
} catch (e) {
// ...降级策略
}
}
// 检查点击相关事件
const listeners = getEventListeners(element);
const hasClickListeners = listeners && (/* ... 条件 ... */);
// 检查 ARIA 属性
const hasAriaProps = element.hasAttribute("aria-expanded") || /* ... 其他属性 ... */;
// 可编辑内容
const isContentEditable = element.getAttribute("contenteditable") === "true" ||
element.isContentEditable ||
element.id === "tinymce" ||
/* ... 其他条件 ... */;
// 元素是否可拖拽
const isDraggable =
element.draggable || element.getAttribute("draggable") === "true";
ARIA 我还是第一次听说,MDN 的解释是 无障碍富互联网应用(Accessible Rich Internet Applications,ARIA)是一组角色和属性,用于定义使残障人士更易于访问 web 内容和 web 应用程序(尤其是使用 JavaScript 开发的应用程序)的方法
highlightElement 高亮元素
doHighlightElements 默认值是 true, focusHighlightIndex 默认值是 -1, 默认情况下,经过上面的判断的元素才会进行高亮操作
通过控制台可以看到,highlightElement 创建一个高亮容器添加到 body 下,获取每个元素的位置,通过定位的覆盖在元素上,核心代码如下
javascript
// 创建或获取高亮容器
let container = document.getElementById(HIGHLIGHT_CONTAINER_ID);
if (!container) {
container = document.createElement("div");
container.id = HIGHLIGHT_CONTAINER_ID;
// 设置容器的基本样式
container.style.position = "fixed";
container.style.pointerEvents = "none"; // 确保不会干扰用户交互
container.style.top = "0";
container.style.left = "0";
container.style.width = "100%";
container.style.height = "100%";
container.style.zIndex = "2147483647"; // 最高层级
document.body.appendChild(container);
}
const rect = measureDomOperation(
() => element.getBoundingClientRect(),
"getBoundingClientRect"
);
// 计算最终位置
const top = rect.top + iframeOffset.y;
const left = rect.left + iframeOffset.x;
// 设置覆盖层位置和尺寸
overlay.style.top = `${top}px`;
overlay.style.left = `${left}px`;
overlay.style.width = `${rect.width}px`;
overlay.style.height = `${rect.height}px`;
// 将覆盖层和标签添加到容器中
container.appendChild(overlay);
源码路径:browser_use/dom/buildDomTree.js#L193
JavaScript 到 Python 的数据转换
JavaScript 获取 DOM 信息:buildDomTree.js 函数遍历浏览器DOM,构建一个包含节点关系和属性的扁平化 Map 结构:
javascript
{
map: {
21: {
type: "TEXT_NODE",
text: "hao123",
isVisible: true,
},
22: {
tagName: "a",
attributes: {},
xpath: "html/body/div/div/div[3]/a[2]",
children: ["21"],
isVisible: true,
isTopElement: true,
isInteractive: true,
isInViewport: true,
highlightIndex: 3,
},
468: {
tagName: "body",
attributes: {},
xpath: "/body",
children: ["4", "6", "466", "467"],
},
},
rootId: 468,
};
Python 解析转换:_construct_dom_tree 方法接收这个 Map 数据,针对不同的节点类型,系统创建相应的 Python 类实例,元素节点转换为 DOMElementNode 实例,文本节点转换为 DOMTextNode 实例,将 JavaScript 的简单数据结构转换为丰富的 Python 对象网络。
不仅如此 _construct_dom_tree, 返回两个关键数据结构
- html_to_dict:从根节点开始的完整DOM树,包含所有节点及其关系
- 在后续的消息管理的当前页面信息中的可交互元素就是通过 html_to_dict 获取的
- selector_map:高亮索引到节点的直接映射
- 在后续的动作执行中会用到
消息管理
系统提示词 (System Prompt)
定义了 AI 代理的角色、输入格式、响应规则, 我就不翻译一遍放在这了。
源码路径 browser_use/agent/system_prompt.md
定义任务
明确 AI 代理的最终任务目标
译:"你的最终任务是:"""打开百度,搜索苹果""". 如果你已经完成了最终任务,停止所有操作并在下一步使用 done 动作来完成任务。如果没有完成,则照常继续。"
javascript
[
{
"content": "Your ultimate task is: \"\"\"打开百度,搜索苹果\"\"\". If you achieved your ultimate task, stop everything and use the done action in the next step to complete the task. If not, continue as usual.",
"role": "user"
},
]
isInteractiveElement
输出示例
javascript
[
// ...省略
{ content: "Example output:", role: "user" },
{
content: null,
role: "assistant",
tool_calls: [
{
function: {
arguments: {
current_state: {
// 评估上一步操作的结果
evaluation_previous_goal: "Success - I opend the first page",
// 记录任务的累积进度和关键信息
memory: "Starting with the new task. I have completed 1/10 steps",
// 定义即将执行的具体目标
next_goal: "Click on company a",
},
// AI 代理计划执行的一个或多个操作
action: [
{
// 具体动作
click_element: {
// index 对应页面上高亮元素的序号
index: 0,
},
},
],
},
name: "AgentOutput",
},
id: "1",
type: "function",
},
],
},
//
{ content: "Browser started", role: "tool", tool_call_id: "1" },
// ...省略
];
在系统提示词 中已经定义了响应规则,为什么还要在对话消息中再给输出示例那,区别在哪?
-
系统提示词响应规则:相当于给 LLM 提供了详细的"理论教材
- 定义了数据结构和字段含义
- 说明了各个参数的使用规则
- 提供了全面的格式指南和约束
-
对话中的示例:相当于进行了生动的"实际演示
- 展示了完整的工具调用格式和流程
- 提供了具体场景中的应用方式
- 呈现了理想输出的真实样貌
到此理论结合实践,我有一种大师我悟了的感觉😲
决策执行记录
下面的提示词,是将 LLM 返回的决策和决策执行的结果,添加到上下文中,记录思考过程和反馈执行结果,帮助LLM避免重复操作、根据历史经验调整策略、理解当前状态与任务起点的关系,从而保持行动的连贯性和目标一致性。
"[Your task history memory starts here]"提供了明确的历史记录起点,避免与前面的示例输出混淆,确保LLM能够清晰区分示范内容和实际执行历史。
javascript
[
// ...省略
// 任务历史开始标记
{ content: "[Your task history memory starts here]", role: "user" },
// LLM 返回的决策动作
{
content: null,
role: "assistant",
tool_calls: [
{
function: {
arguments: {
current_state: {
// 评估上一步操作的结果
evaluation_previous_goal:
"Unknown - The page is currently blank, so no previous actions can be evaluated.",
// 记录任务的累积进度和关键信息
memory:
"The task is to open Baidu and search for '苹果'. Currently on a blank page.",
// 定义即将执行的具体目标
next_goal: "Open Baidu's homepage.",
},
action: [
{
go_to_url: {
url: "https://www.baidu.com",
},
},
],
},
name: "AgentOutput",
},
id: "2",
type: "function",
},
],
},
{ content: "", role: "tool", tool_call_id: "2" },
{
content: "Action result: 🔗 Navigated to https://www.baidu.com",
role: "user",
},
// ...省略
];
当前页面信息
看到 Browser Use 它将复杂的网页结构转化为 LLM 能理解的简明描述,让 LLM 通过截图获得视觉布局,结构化文本理解元素关系,使用简单的数字索引(如[12]、[13])精确定位元素
javascript
[
// ...省略
{
content: [
{
text: `
[Task history memory ends] - 表示历史记录已结束
[Current state starts here] - 表示当前状态信息开始
// 译:以下是一次性信息-如果你需要记住它,请将其写入内存
The following is one-time information - if you need to remember it write it to memory:
Current url: https://www.baidu.com/
Available tabs:
[TabInfo(page_id=0, url='https://www.baidu.com/', title='百度一下,你就知道')]
// 译:视口内当前页面顶层的交互元素:
Interactive elements from top layer of the current page inside the viewport:
[Start of page]
[0]<a 新闻/>
[1]<a hao123/>
[2]<a 地图/>
...省略
[31]<a 京公网安备11000002000001号/>
[32]<a 京ICP证030173号/>
互联网新闻信息服务许可证11220180008
[33]<img />
[End of page]
Current step: 2/100
Current date and time: 2025-03-20 17:24
`,
type: "text",
},
{
// 页面截图
image_url: {
"url": "data:image/png;base64,"
},
type: "image_url",
},
],
role: "user",
},
];
注 ,提示词中特意强调"The following is one-time information"(这是一次性信息),这是因为 browser-use 对上下文做了优化,在每次请求结束后,都会删除历史中页面信息,避免base64图像和大量页面元素描述占用过多令牌空间
Tools / Function Calling
Tools/Function Calling是现代AI模型与外部系统交互的关键机制,它通过结构化接口让AI能够调用特定功能并执行具体操作。现在爆火的 MCP(Multimodal Conversational Platform)正是这种机制的一种实现形式。
Tools 的组成
Tools 机制主要由两个关键部分组成:
- tool_choice:指定默认或强制使用的工具,每个工具通常包含以下核心元素
- description:工具的功能描述,如"具有自定义动作的AgentOutput模型"
- name:工具的唯一标识符,如"AgentOutput"
- parameters:工具接受的参数结构定义
- tools数组:定义可用工具及其参数规范
参数结构
在 Browser Use 框架中,AgentOutput工具需要两个主要参数:
- current_state 参数
这部分定义AI代理的状态管理系统,包含三个关键字段:
-
evaluation_previous_goal:对上一步操作结果的评估
-
memory:任务进度和关键信息的存储
-
next_goal:即将执行的下一步目标
这种状态设计使AI能够保持上下文感知,实现连续性决策。
- action参数
action参数定义了AI可以执行的具体操作,它是一个操作数组,每个操作项只能包含一种操作类型,例如:
- click_element:点击页面元素
- done:标记任务完成
JSON Schema实现参数验证和结构约束
- properties:定义对象的各个属性
- anyOf:支持多种可能的类型或结构
- required:指定必须提供的字段
- type:约束值的数据类型
- default:提供默认值
- min_items:控制数组的最小长度
DeepSeek 的限制
在将上下文传递给语言模型之前,browser-use 框架针对 DeepSeek 系列模型(deepseek-reasoner和deepseek-r1)做了专门的处理,
-
DeepSeek 模型(包括 deepseek-reasoner 和 deepseek-r1)不支持函数调用(- Tools / Function Calling)功能,需要将某些特殊消息类型(如 ToolMessage)转换为基本的 HumanMessage 格式,将 AI 消息中的 tool_calls 转换为普通的 JSON 字符串,使模型能够正确处理
-
DeepSeek 模型不允许连续出现多个相同角色(人类或 AI)的消息,必须将连续的人类消息或 AI 消息合并成单个消息
动作执行
动作执行流程
以"打开百度,搜索苹果"任务为例,执行流程如下:
- LLM分析当前状态并返回结构化决策(如导航到百度)
- 决策被解析为具体动作(如go_to_url)
- Agent将动作传递给Controller
- Controller通过Registry查找并执行对应的处理函数
动作注册机制
Browser Use 通过 @registry.action 装饰器实现动作注册系统,除了内置的核心动作外,又提供了灵活的扩展机制,支持添加自定义动作
总结
Browser Use 原理表面看似简单,但实际包含复杂精妙的设计,有着大量操作细节和技术深度