Browser Use 源码揭秘:49.9K+ Star 的 AI 驱动浏览器自动化框架

8,692 阅读13分钟

深入剖析 Browser Use

Browser Use 介绍

Browser Use 是一个AI驱动浏览器自动化开源框架,让我们可以通过自然语言操作浏览器,目前在 GitHub 上已经获得了惊人的 49.9k star

下方演示展示了我启动 Browser Use 运行的一个小 demo,通过简单的自然语言指令"打开百度并搜索苹果",Browser Use 自主完成了网页导航、识别搜索框、输入关键词并执行搜索的全过程

agent_history.gif

Browser Use 工作原理

image.png

如图所示,Browser Use 首先捕获了浏览器实时状态,然后整合结构化信息交由 LLM 进行智能决策,随后执行确定的动作,完成后再次获取更新后的浏览器状态,循环往复直至任务完成。

这一自动化执行流程由三大核心模块组成,通过 browser_use/agent 的调度下协同工作形成了一个完整的自动化执行流程:

  1. 获取浏览器状态
    • 浏览器管理模块(browser_use/browser)
    • DOM 处理模块(browser_use/dom)
  2. 消息管理(browser_use/agent/message_manager)
  3. 动作执行(browser_use/controller)

获取浏览器状态

image.png

Browser Use 获取浏览器状态,主要函数如图所示,分为两个模块

  1. browser_use/browser 基于 Playwright 实现浏览器的核心控制与管理,负责启动浏览器实例、处理浏览器上下文和页面

  2. browser_use/dom 解析和提取页面元素信息、元素高亮定位

浏览器管理模块(browser_use/browser)

浏览器实例初始化策略

image.png 源码路径: browser_use/browser/browser.py

browser-use 提供了四种不同的浏览器初始化策略,以适应不同的使用场景:

  1. _setup_cdp:通过 Chrome DevTools Protocol 连接到正在运行的 Chrome 实例,便于调试
  2. _setup_wss:适用于连接到云端浏览器服务(如 anchorbrowser.io、browserless.io),实现远程操作
  3. _setup_strandard_browser:使用已安装 Chrome 的用户配置文件,保留登录状态和 Cookie
  4. _setup_browser:默认方法,创建新的浏览器实例,适用于基本场景

browser use 官方文档

这里我分别测试了 1、3、4 三种初始化方式,对于方式 1 可以采用以下命令启动通过 --user-data-dir 来指定用户信息

方式 1、3 的底层逻辑实际相同,都是基于 Chrome DevTools Protocol (CDP) 进行连接,只不过方式 3 在代码中自动执行启动命令

"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"   --remote-debugging-port=9222 --user-data-dir="存储用户和浏览器插件路径"
浏览器上下文创建与反检测技术

image.png

创建上下文的核心逻辑在 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
    • image.png
  • 浏览器语言伪装

    • 覆盖浏览器的语言偏好设置,语言设置是浏览器指纹的重要组成部分
    • image.png
  • 浏览器插件模拟

    • 自动化浏览器通常没有安装插件
    • image.png
  • Chrome 运行时环境模拟

    • 添加 Chrome 特有的浏览器对象,反爬系统会检查 window.chrome 对象结构
    • image.png
  • 权限 API 修改

    • Headless(无头)浏览器处理通知权限的方式与普通浏览器不同,对通知权限查询返回一致的结果
    • image.png
  • Shadow DOM

    • 避免网站使用封闭模式 Shadow DOM 隐藏内容,无论网站指定什么模式,都返回开放模式的 Shadow DOM
    • 还使用立即执行函数表达式(IIFE)封装,避免变量污染,细节
    • image.png
获取标签页

image.png

检查是否存在已打开的页面,如果存在,返回最后一个页面;如果不存在,创建新页面

DOM 处理模块(browser_use/dom)

DOM 处理模块是 browser-use 的核心,解析页面获取 DOM 节点的信息,提供精确的元素定位和交互能力,帮助 LLM 更准确的决策

image.png

以百度为例,介绍一些前端的概念,网页展示基于 HTML 文档,该文档由各种标签元素(如<div>, <a>, <img>等)组成层级结构。浏览器解析这些 HTML 元素后,将其转换为 DOM (文档对象模型) 树,其中每个 HTML 元素、文本和属性都变成了 DOM 树中的节点。JavaScript 可以通过这个 DOM 树动态操作和修改网页内容,实现交互功能。

image.png

我在百度页面执行了 browser_use/dom/buildDomTree.js 实现效果如图, 我们可以看到页面很多元素被高亮并打上索引,还返回了页面元素的结构化信息

这里我们可以想到两个问题,什么元素需要被高亮并打上了索引?需要返回元素的哪些关键信息来辅助AI决策?

让我们带着问题一起看看 buildDomTree 的实现逻辑吧

image.png

buildDomTree 函数深度优先递归遍历方式,将原生 DOM 树(n 叉树)转换为结构化对象模型,提取并组织每个节点的关键属性、层级关系和交互特性

  • DOM 节点处理:

    • DOM 规范中定义了多种节点类型(DOCUMENT_NODE、COMMENT_NODE、ATTRIBUTE_NODE等),builDomTree 只会处理元素节点(ELEMENT_NODE)文本节点(TEXT_NODE)这两种最能代表页面内容和结构的两种核心节点类型
    • 还会对这两种节点进行过滤,排除对LLM决策无价值的元素类型,例如空文本节点、script、svg 等对内容理解和交互决策无实质贡献的节点
  • xpath:为每个节点生成唯一的XPath标识,便于定位与追踪

  • isVisible:判断元素是否正常显示

  • isInViewport:确定元素是否在当前可视区域内

  • isTopElement:检测元素是否是其位置上的顶层元素

  • isInteractive:识别可交互元素,如链接、按钮等

  • highlightIndex:高亮元素并添加索引

xpath 每个节点生成唯一的XPath标识

image.png

  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 的逻辑还是比较简单,主要是对于有同级的重名节点,会获取对应索引进行拼接,剩下的就是不断的获取自己的父节点,判断是否有同级的重名节点,到根节点为止

xpath: "html/body/div/div/div[3]/div/a"
isVisible 判断元素是否正常显示

image.png

isTopElement 检测元素是否是其位置上的顶层元素

image.png

在我省略的代码中有一个值得注意的点

  • 如果元素在iframe中,默认认为它是顶层元素
    • 我写个 demo 测试了下,在当前文档下调用 document.elementFromPoint 只能获取到 iframe,获取不到 iframe 的子节点,
    • 通过iframe.contentDocument 继续调用会受到浏览器同源策略的限制
isInteractive 识别可交互元素

isInteractiveElement 函数采用多层次的判断策略,从明确的标签属性到隐含的行为特征,一层层的判断元素的交互性,代码看着倒是都很简单,但是判断非常的多...

  1. Cookie 横幅特殊处理,登录国外网站好像见的多一点,比如 stackOverflow
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 横幅内,并进一步判断它是否为接受或拒绝按钮。这种特殊处理反映了现代网页浏览中常见的用户交互模式。

  1. 标准交互元素识别

函数检查元素是否属于公认的交互式 HTML 元素或具有交互角色:

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', /* ... 其他角色 ... */]);
  1. 基于属性的交互性判断
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";
  1. cookie 横幅和同意界面

Apply to buildDomTree...

const isCookieBanner =

  element.id?.toLowerCase().includes('cookie') ||

  element.id?.toLowerCase().includes('consent') ||

  /* ... 其他条件 ... */;
  1. 事件监听器和行为特征

最后,函数检查元素的行为特征,包括事件监听器和 ARIA 属性:

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 高亮元素

image.png

doHighlightElements 默认值是 true, focusHighlightIndex 默认值是 -1, 默认情况下,经过上面的判断的元素才会进行高亮操作

image.png

通过控制台可以看到,highlightElement 创建一个高亮容器添加到 body 下,获取每个元素的位置,通过定位的覆盖在元素上,核心代码如下


// 创建或获取高亮容器
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 结构:

{
  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 对象网络。

image.png

不仅如此 _construct_dom_tree, 返回两个关键数据结构

  • html_to_dict:从根节点开始的完整DOM树,包含所有节点及其关系
    • 在后续的消息管理的当前页面信息中的可交互元素就是通过 html_to_dict 获取的
  • selector_map:高亮索引到节点的直接映射
    • 在后续的动作执行中会用到

消息管理

系统提示词 (System Prompt)

image.png

定义了 AI 代理的角色、输入格式、响应规则, 我就不翻译一遍放在这了。

源码路径 browser_use/agent/system_prompt.md

定义任务

明确 AI 代理的最终任务目标

译:"你的最终任务是:"""打开百度,搜索苹果""". 如果你已经完成了最终任务,停止所有操作并在下一步使用 done 动作来完成任务。如果没有完成,则照常继续。"

[
    {
        "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

输出示例

[
  // ...省略
  { 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" },
  // ...省略
];

系统提示词中已经定义了响应规则,为什么还要在对话消息中再给输出示例那,区别在哪?

image.png

  • 系统提示词响应规则:相当于给 LLM 提供了详细的"理论教材

    • 定义了数据结构和字段含义
    • 说明了各个参数的使用规则
    • 提供了全面的格式指南和约束
  • 对话中的示例:相当于进行了生动的"实际演示

    • 展示了完整的工具调用格式和流程
    • 提供了具体场景中的应用方式
    • 呈现了理想输出的真实样貌

到此理论结合实践,我有一种大师我悟了的感觉😲

决策执行记录

下面的提示词,是将 LLM 返回的决策和决策执行的结果,添加到上下文中,记录思考过程和反馈执行结果,帮助LLM避免重复操作、根据历史经验调整策略、理解当前状态与任务起点的关系,从而保持行动的连贯性和目标一致性。

"[Your task history memory starts here]"提供了明确的历史记录起点,避免与前面的示例输出混淆,确保LLM能够清晰区分示范内容和实际执行历史。

[
  // ...省略
  // 任务历史开始标记
  { 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])精确定位元素

[
  // ...省略
  
  {
    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图像和大量页面元素描述占用过多令牌空间

image.png

Tools / Function Calling

Tools/Function Calling是现代AI模型与外部系统交互的关键机制,它通过结构化接口让AI能够调用特定功能并执行具体操作。现在爆火的 MCP(Multimodal Conversational Platform)正是这种机制的一种实现形式。

Tools 的组成

image.png

Tools 机制主要由两个关键部分组成:

  • tool_choice:指定默认或强制使用的工具,每个工具通常包含以下核心元素
    • description:工具的功能描述,如"具有自定义动作的AgentOutput模型"
    • name:工具的唯一标识符,如"AgentOutput"
    • parameters:工具接受的参数结构定义
  • tools数组:定义可用工具及其参数规范
参数结构

image.png

在 Browser Use 框架中,AgentOutput工具需要两个主要参数:

  1. current_state 参数

这部分定义AI代理的状态管理系统,包含三个关键字段:

  • evaluation_previous_goal:对上一步操作结果的评估

  • memory:任务进度和关键信息的存储

  • next_goal:即将执行的下一步目标

这种状态设计使AI能够保持上下文感知,实现连续性决策。

  1. action参数

action参数定义了AI可以执行的具体操作,它是一个操作数组,每个操作项只能包含一种操作类型,例如:

  • click_element:点击页面元素
  • done:标记任务完成
JSON Schema实现参数验证和结构约束

image.png

  • properties:定义对象的各个属性
  • anyOf:支持多种可能的类型或结构
  • required:指定必须提供的字段
  • type:约束值的数据类型
  • default:提供默认值
  • min_items:控制数组的最小长度

DeepSeek 的限制

image.png

在将上下文传递给语言模型之前,browser-use 框架针对 DeepSeek 系列模型(deepseek-reasoner和deepseek-r1)做了专门的处理,

image.png

  • DeepSeek 模型(包括 deepseek-reasoner 和 deepseek-r1)不支持函数调用(- Tools / Function Calling)功能,需要将某些特殊消息类型(如 ToolMessage)转换为基本的 HumanMessage 格式,将 AI 消息中的 tool_calls 转换为普通的 JSON 字符串,使模型能够正确处理

  • DeepSeek 模型不允许连续出现多个相同角色(人类或 AI)的消息,必须将连续的人类消息或 AI 消息合并成单个消息

动作执行

动作执行流程

以"打开百度,搜索苹果"任务为例,执行流程如下:

  1. LLM分析当前状态并返回结构化决策(如导航到百度)
  2. 决策被解析为具体动作(如go_to_url)
  3. Agent将动作传递给Controller
  4. Controller通过Registry查找并执行对应的处理函数

image.png

动作注册机制

image.png

Browser Use 通过 @registry.action 装饰器实现动作注册系统,除了内置的核心动作外,又提供了灵活的扩展机制,支持添加自定义动作

image.png

总结

Browser Use 原理表面看似简单,但实际包含复杂精妙的设计,有着大量操作细节和技术深度