Fork me on GitHub

分类 Browser Use 下的文章

使用 Patchright 绕过浏览器机器人检测

我们知道,Browser Use 默认采用 Playwright 作为浏览器操作的基础工具。Playwright 原本是由微软开源推出的一款浏览器自动化测试工具,其设计初衷是方便开发人员进行跨浏览器测试。然而,由于其功能强大且使用简便,它逐渐被广泛应用于网络爬虫领域,这也引发了一些问题。许多网站为了保护自身信息安全,采取了封禁措施,试图阻止 Playwright 的访问。

浏览器自动化在运行过程中表现出的行为往往与普通用户操作有显著差异,这些差异使得具有反爬措施和网络应用防火墙(WAF)的网站能够有效识别和对抗 Playwright 驱动的爬虫行为。这也就导致了我们在使用 Browser Use 的时候,无法在一些网站上执行操作,大大限制了 Browser Use 的功能,如何绕过这些安全措施成为了 Browser Use 用户需要解决的问题。

道高一尺,魔高一丈,有反爬,就有反反爬。在使用 Playwright 的时候,也有一些方法绕过反爬机制,我们今天学习的 Patchright 就是基于 Playwright 改造的另一款浏览器自动化工具,它通过巧妙地模拟真实用户行为,使得浏览器操作可以绕过机器人检测。

Patchright 初体验

我们先通过一个真实场景体验下 Patchright 的效果。

Fingerprint.com 是一家专注于设备识别和反欺诈解决方案的企业,他们提供的浏览器机器人检测功能,通过 Smart Signal 技术实时检测自动化工具、搜索引擎爬虫等威胁,帮助网站保护自身免受机器人攻击。

fingerprint-home.png

可以点击下面的链接,进入产品页面体验该功能:

接下来我们使用 Playwright 和 Patchright 分别打开上面的产品页面,对比下两者有何不同。使用 Playwright 打开该页面:

import asyncio
from playwright.async_api import async_playwright

async def main():
  async with async_playwright() as p:
    browser = await p.chromium.launch(headless=False)
    page = await browser.new_page()
    await page.goto("https://fingerprint.com/products/bot-detection/")
    input()
    await browser.close()

asyncio.run(main())

可以看到 Fingerprint.com 成功检测出我们是机器人,并且标识出我们是通过自动化工具访问的:

fingerprint-bot-detection-playwright.png

接下来我们换成 Patchright 打开该页面:

import asyncio
from patchright.async_api import async_playwright

async def main():
  async with async_playwright() as p:
    browser = await p.chromium.launch(headless=False)
    page = await browser.new_page()
    await page.goto("https://fingerprint.com/products/bot-detection/")
    input()
    await browser.close()

asyncio.run(main())

Patchright 的使用和 Playwright 完全兼容,我们只需将上面的 playwright.async_api 换成了 patchright.async_api 即可。可以看到 Fingerprint.com 这次没有检测出来,我们成功绕过了它的机器人检测功能:

fingerprint-bot-detection-patchright.png

在 Browser Use 中使用 Patchright

我们昨天学习了 Browser Use 中关于浏览器的很多配置参数,包括会话相关参数和浏览器配置参数,通过 BrowserSession 传入,以调试模式启动并操作浏览器。在会话相关的参数中有一个 playwright 参数,表示 Playwright 或 Patchright API 客户端句柄,一般通过 await async_playwright().start() 得到,我们可以通过这个参数让 Browser Use 连接并复用已有的 Playwright 或 Patchright 浏览器实例:

from playwright.async_api import async_playwright

async def main():
  playwright = await async_playwright().start()
  browser_session = BrowserSession(
    playwright=playwright
  )
  agent = Agent(
    task="""
      打开 https://fingerprint.com/products/bot-detection/
      检查你是否被检测为机器人
    """,
    llm=llm,
    browser_session=browser_session,
    generate_gif=True
  )
  result = await agent.run()
  print(result)

asyncio.run(main())

将上面的 playwright.async_api 换成 patchright.async_api 即可在 Browser Use 中使用 Patchright ,最终的输出结果如下:

Result: Visited the bot detection page on Fingerprint.com. 
The page indicates clearly that 'You are not a bot' under the section 'Am I a bot?'. 
Both Automation Tool and Search Engine statuses are 'Not detected'. 
Task is complete.

下面是运行的动画过程:

agent_history.gif

此外,Browser Use 在最新版本中(browser-use >= 0.2.7)还提供了一种使用 Patchright 的简便方式,只需要在 BrowserSession 中传入 stealth=True 即可:

browser_session = BrowserSession(
    stealth=True
)
agent = Agent(
    task="your task here",
    llm=llm,
    browser_session=browser_session,
)

学习机器人检测技术

到这里,有些同学肯定会好奇,Fingerprint.com 到底是如何检测浏览器机器人的?而 Patchright 又是如何绕过它的检测的?知其然,知其所以然,我们接下来就来学习学习这里面的门道。

下面这个网站是 Rebrowser 整理的一些常见的机器人检测技术,并在其 项目首页 对这些技术一一做了详细介绍,是个不错的入门资料:

正常打开这个网站是这个样子:

rebrowser-bot-detector.png

而用 Playwright 打开就是一堆报红:

rebrowser-bot-detector-playwright.jpg

这里列出了一些非常基本的测试项,这些测试在任何网站上都很容易实现。可以确定的是,所有这些测试在主流的机器人检测产品中都有使用,尽管每个产品都有自己专有的算法和检测方式,但在 90% 的情况下,当你被阻止或看到任何验证码时,都是因为这些测试不通过导致的。所以,在进行任何类型的浏览器自动化之前,务必先通过所有这些测试。

我们重点来看下这些报红的测试项:

runtimeEnableLeak

默认情况下,Puppeteer、Playwright 和其他自动化工具依赖于 Runtime.enable 这个 CDP 方法来处理执行上下文。任何网站都可以通过几行代码检测到它:

const testRuntimeEnableLeak = async () => {
  const e = new Error()
  Object.defineProperty(e, 'stack', {
    configurable: false,
    enumerable: false,
    get() {
      // 检测到 DevTools 打开,或使用了 Runtime.enable CDP 方法
      return ''
    },
  })
  console.debug(e)

  setTimeout(testRuntimeEnableLeak, 100)
}

testRuntimeEnableLeak()

这段代码非常巧妙,是一个相当高级和隐蔽的检测手段。正常情况下,console.debug(e) 只会简单输出错误对象,不会触发 stack 属性的 getter 方法,而一旦 Chrome DevTools 打开或 CDP 的 Runtime.enable 被调用时,浏览器会自动获取 Error 对象的 stack 属性来在控制台中显示更详细的错误信息,从而能被我们检测到。这种技术不仅用于浏览器机器人检测,也常用于检测是否有人在调试网页,防止别人对网站做逆向工程。

知道了检测原理,反检测也就很简单了,有几种不同的方法:

  • 在页面载入之前注入一段 JS,重写 console.debug 方法,比如 console.debug = {},或者为 console 创建一个代理,window.console = new Proxy(origConsole, handler)
  • 因为检测是每隔一段时间检测一次,可以在调用 Runtime.enable 之后立即调用 Runtime.Disable,大概率也能逃过程序的检测;

navigatorWebdriver

这是一个老掉牙的检测手段,当 Chrome 浏览器正在由自动化软件驱动时,会将 navigator.webdriver 设置为 true,在正常的浏览器中该值应该为 false,检测代码如下:

if (navigator.webdriver === true ||
    typeof navigator.webdriver === 'undefined' ||
    Object.getOwnPropertyNames(navigator).length !== 0 ||
    Object.getOwnPropertyDescriptor(navigator, 'webdriver') !== undefined) {
    // 检测到浏览器自动化
}

绕过方法很简单,启动 Chrome 时加一个 --disable-blink-features=AutomationControlled 开关即可,注意这个开关 Browser Use 默认已经加过了。

网上还有一些其他绕过方法,比如在加载页面前注入下面的脚本:

await browser_context.add_init_script("""
  Object.defineProperty(navigator, 'webdriver', {
    get: () => undefined,  // or false
  });
""")

由于 navigator.webdriver 是只读的,所以直接设成 false 是不生效的,这里通过给 navigator 定义一个 webdriver 属性并重写其 getter 方法,使得 navigator.webdriver 的值变成 undefinedfalse

这种方法也能绕过一些网站的检测,但是绕不过上面的检测代码。在上面的代码里,除了对 navigator.webdriver 的值进行判断,而且还对其类型是否为 undefined 以及是否存在自定义属性等进行判断,导致这种方法很容易被检测出来。因此最保险的做法还是 --disable-blink-features=AutomationControlled 开关。

viewport

这也是一个简单的检测手段,当运行 Puppeteer 时,它默认使用 800x600 的视口大小,运行 Playwright 时,默认使用 1280x720 的视口大小。这个固定值非常明显,很容易被检测到,因为几乎没有哪个正常用户的浏览器会有这样的视口大小。

所以有些网站通过这两个固定值来检测 Puppeteer 和 Playwright 的使用。随便改一下视口大小就能绕过:

browser = await p.chromium.launch(headless=False)
browser_context = await browser.new_context(viewport={"height":721,"width":1281})
page = await browser_context.new_page()
await page.goto("https://bot-detector.rebrowser.net/")

pwInitScripts

这是一个 Playwright 特有的属性,默认会在每个页面的 window 对象上注入,还有一个 playwright__binding 方法也是一样的,检测代码如下:

if (window.__pwInitScripts !== undefined || 
    window.__playwright__binding__ !== undefined) {
    // 检测到 Playwright 使用
}

要绕过检测,可以在页面载入之前注入一段 JS,将该属性删除:

delete window.__pwInitScripts;
delete window.__playwright__binding__;

useragent

这个测试项通过检查 navigator.userAgentData 里是否同时包含 ChromiumGoogle Chrome 两项,正常用户使用的 Chrome 浏览器应该两项都包含。但是 Puppeteer 和 Playwright 通常使用 Chromium 浏览器,数据里只有 Chromium 一项,这是一个警告信号:

const fullVersionList = await navigator.userAgentData.getHighEntropyValues(['fullVersionList']);
const brands = fullVersionList.brands.map(item => item.brand);

if (fullVersionList.length &&
    brands.includes('Chromium') && 
    !brands.includes('Google Chrome')) {
    // 检测到你可能在用 Google Chrome 测试
}

可以使用 executablePath 启动用户自定义的浏览器绕过此检查,或者通过注入脚本,修改 navigator.userAgentData 的值。

小结

我们今天学习了如何使用 Patchright 在自动化浏览器操作中绕过机器人检测,通过在 Browser Use 中集成 Patchright,用户可以轻松地在复杂的反爬环境中执行浏览器自动化任务。我们还深入探讨了一些简单而有效的浏览器机器人检测技术,例如 runtimeEnableLeaknavigatorWebdriverviewportpwInitScriptsuseragent 等,通过学习相应的检测和反检测策略,可以帮我们更好的理解 Patchright 的使用。

浏览器机器人的检测和反检测,是一个复杂的话题,涉及浏览器行为模拟、网络安全、反爬虫技术等多个领域。同时,这也是一场永无尽头的猫鼠游戏,作为网站管理员,需要持续更新技术以适应最新的爬虫技巧,而作为爬虫开发者,也需要不断学习以应对不断演进的检测机制。


详解 Browser Use 的浏览器配置

昨天我们学习了 Browser Use 启动或连接浏览器的几种方法,包括 executable_path、Playwright 对象、Browser Server、CDP 和 PID 等,这些方法有一个共同点,都是通过 BrowserSession 类来设置参数,而这个类也是 Browser Use 配置和操作浏览器的核心。

BrowserSession 的配置参数非常繁杂,不过可以将其大致分为两类,一类是 会话相关参数(Session-Specific Parameters),包括浏览器连接参数和活动的 Playwright 对象实例,BrowserSession 通过这些参数连接浏览器并跟踪其活动;另一类是 浏览器配置模版(Browser Profile),包括 Browser Use 特有的参数和 Playwright 相关的参数,这类参数的特点是静态的,可以通过 BrowserProfile 存储,方便 BrowserSession 复用。

我们今天就对照官方文档,对这些参数做个盘点。

会话相关参数

会话相关参数,包括浏览器连接参数和 Playwright 对象实例,这些参数和活动的会话相关,无法存储在 BrowserProfile(...) 模板中。

浏览器连接参数

这些参数分别对应不同的连接到现有浏览器的方式,在昨天的文章中我们已经详细学过这些内容:

参数名默认值参数说明
wss_urlNone连接到 Playwright 协议的浏览器服务器地址
cdp_urlNone通过 CDP 协议连接到开启调试模式的 Chrome 浏览器
browser_pidNone根据 PID 连接到本地运行的浏览器进程

Playwright 对象实例

我们知道,BrowserBrowserContextPage 都是 Playwright 内置的对象,一般通过 (await async_playwright().start())(await async_patchright().start()) 以调试模式启动浏览器子进程,然后通过 CDP 将命令传递给浏览器。

我们可以将这些对象传递给 BrowserSession,让 Browser Use 连接并复用已有的浏览器实例:

参数名默认值参数说明
playwrightNone可选的 Playwright 或 Patchright API 客户端句柄
browserNone可选的 Playwright Browser 对象
browser_contextNone可选的 Playwright BrowserContext 对象
pageNone也可以写成 agent_current_page,表示智能体专注的前台页面
human_current_pageNone人类专注的前台页面,不必手动设置
initializedFalse将 BrowserSession 标记为已初始化,跳过启动和连接(不推荐)

浏览器配置模版

browser_profile: BrowserProfile = BrowserProfile()

BrowserSession 可以接受一个可选的 browser_profile 参数,它是一个配置模板,可以包含一些配置默认值,用法如下:

browser_profile = BrowserProfile(
  executable_path='/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
  user_data_dir='~/.config/browseruse/profiles/default',
)
browser_session = BrowserSession(    
  browser_profile=browser_profile
)

所有 BrowserProfile 中的参数都可以直接传给 BrowserSession,下面的写法和上面的代码没有区别:

browser_session = BrowserSession(    
  executable_path='/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
  user_data_dir='~/.config/browseruse/profiles/default',
)

如果某个参数同时传给 BrowserProfileBrowserSessionBrowserSession 中的参数值会覆盖 BrowserProfile 中的默认值。

尽管 BrowserProfile 是可选的,将参数传给 BrowserProfile 或者 BrowserSession 都可以,但使用 BrowserProfile 提供了一些额外的好处:

  • 直接传给 BrowserSession**kwargs 参数,是一个普通的 dict 字典,而传给 BrowserProfile 的参数具有明确的类型提示,在 IDE 中还能显示其 pydantic 字段描述;
  • 传给 BrowserProfile 的参数在启动浏览器之前就可以快速验证;
  • BrowserProfile 提供了一些辅助方法用来自动检测屏幕大小,设置本地路径,保存或加载配置为 json 等;

除此之外,BrowserProfile 更大的一个用处是将一些可重用的静态配置独立出来,保存到数据库中,用户对其查看和编辑,供用户的不同会话复用。一个用户常用的配置文件可能只有 2 到 3 个,但他创建的浏览器会话却可能多达成千上万个,将动态的浏览器连接信息和静态的可重用配置分开,可以极大的避免数据库空间的浪费。

BrowserProfile 的参数可以分为 Browser Use 参数Playwright 参数,下面将分别介绍。

Browser Use 参数

这些参数控制 Browser Use 的特定功能,区别于 Playwright 参数。它们可以传递给 BrowserSession(...) 或存储在 BrowserProfile 模板中。

keep_alive: bool | None = None

如果该参数设置为 True ,则将在 agent.run() 结束后不关闭浏览器,这对于使用同一浏览器实例运行多个任务非常有用。如果将其保留为 None ,则根据 Browser Use 运行时是否启动浏览器来决定是否关闭:如果 Browser Use 启动了自己的浏览器,则完成任务后关闭浏览器,如果 Browser Use 连接到现有浏览器,则将保持其打开状态。

stealth: bool = False

Browser Use 默认使用 Playwright 操作浏览器,但是很容易被当成机器人检测出来,Patchright 基于 Playwright 改造,使得浏览器操作可以绕过机器人检测。设置 stealth=True 表示使用 Patchright 操作浏览器。

allowed_domains: list[str] | None = None

允许浏览器访问的域名列表,如果为 None,则允许所有域名,支持 GLOB 匹配模式:

  • ['example.com'] - 仅会完全匹配 https://example.com/* ,不允许子域名。为确保安全性,请明确列出你希望授予访问权限的所有域名。
  • ['*.example.com'] - 这将匹配 https://example.com 及其所有子域名,包括 abc.example.com , def.example.com , admin.example.com 等,你需要确保所有子域名的安全性!
disable_security: bool = False

完全禁用所有基本的浏览器安全功能,比如允许和跨站点 iFrame 进行交互,但 此选项非常不安全,仅适用于小众用例,请慎用。

deterministic_rendering: bool = False

通过禁用特定于操作系统的字体提示、抗锯齿、GPU 加速渲染,标准化 DPI,并设置特定的 JS 随机种子以尝试避免非确定性 JS,实现更确定性的渲染,以便在不同的主机操作系统和硬件上获得一致的屏幕截图。这个参数较少使用,而且更容易被视为机器人并偶尔触发一些故障行为,除非你知道需要它,否则不推荐使用。

highlight_elements: bool = True

在屏幕上用彩色边框突出显示交互元素。

viewport_expansion: int = 500

以像素为单位的视口扩展。通过此功能,可以控制在 LLM 的上下文中包含页面的多少部分:

  • -1 : 页面上的所有元素都将被包含,无论其可见性如何(最高的令牌使用量,但最完整);
  • 0 : 只有当前在视口中可见的元素会被包含;
  • 500 (默认): 视口中的元素加上每个方向额外的 500 像素将被包含在内,从而在上下文和令牌使用之间提供平衡;
include_dynamic_attributes: bool = True

在选择器中包含动态属性以更好地定位元素。

minimum_wait_page_load_time: float = 0.25

捕获页面状态之前的最短等待时间。

wait_for_network_idle_page_load_time: float = 0.5

等待网络活动停止的时间。对于较慢的网站,可以增加到 3-5 秒。注意,这只跟踪基本内容的加载,而不是视频等动态元素。

maximum_wait_page_load_time: float = 5.0

最大等待页面加载的时间。

wait_between_actions: float = 0.5

执行动作之间的等待时间。

cookies_file: str | None = None

保存 cookies 的 JSON 文件路径,此选项已弃用,请改用 storage_state 参数。

profile_directory: str = 'Default'

你在 user_data_dir 中的 Chrome 配置文件子目录名称(例如 DefaultProfile 1Work 等)。除非你在单个 user_data_dir 中设置了多个配置文件并需要使用特定的配置文件,否则无需设置此项。

window_size: dict | None = None

有头模式的浏览器窗口大小,示例: {"width": 1920, "height": 1080}

window_position: dict | None = {"width": 0, "height": 0}

窗口位置,从左上角开始。

Playwright 参数

以下所有参数都是标准的 Playwright 参数,可以传递给 BrowserSessionBrowserProfile 来控制浏览器设置,由于数量众多,这里只作简单总结,不能一一详解。可以参考 Playwright 官方文档了解每个参数的详细用法。

启动选项

headless: bool | None = None

是否开启无头模式,无头模式指的是在没有用户界面的情况下运行浏览器;默认为 None,根据显示可用性自动检测。设置 headless=False 可以提供最大的隐蔽性,也是人机协作所必需的。

如果在没有连接显示器的服务器上设置 headless=False ,浏览器将无法启动,可以使用 xvfb + vnc 为无头浏览器提供一个可以远程控制的虚拟显示。

channel: BrowserChannel = 'chromium'

浏览器频道,可选值有:

  • 'chromium' - 当 stealth=False 时为默认
  • 'chrome' - 当 stealth=True 时为默认
  • 'chrome-beta'
  • 'chrome-dev'
  • 'chrome-canary'
  • 'msedge'
  • 'msedge-beta'
  • 'msedge-dev'
  • 'msedge-canary'

对于列表中未列出的其他基于 Chromium 的浏览器也是支持的,例如 Brave,只要提供自己的 executable_path 参数,并将 channel 设置为 chromium 即可。

executable_path: str | Path | None = None

用于启动用户自己安装的浏览器。

user_data_dir: str | Path | None = '~/.config/browseruse/profiles/default'

浏览器配置文件数据的目录,设置为 None 表示使用临时配置文件,即隐身模式。注意,多个运行中的浏览器不能同时共享一个 user_data_dir,如果你需要同时运行多个浏览器,必须将其设置为 None 或为每个会话提供一个唯一的 user_data_dir

args: list[str] = []

传递给浏览器的其他命令行参数,下面这个链接列出了 Chrome 所有可用的参数:

ignore_default_args: list[str] | bool = ['--enable-automation', '--disable-extensions']

Playwright 在启动 Chrome 时过滤掉列表中指定的参数,如果将其设置为 True 则禁用 Playwright 所有默认选项,只使用 args 中的参数(不推荐)。

env: dict[str, str] = {}

启动浏览器时要设置的额外环境变量,例如,{'DISPLAY': '1'} 用于使用特定的 X11 显示。

chromium_sandbox: bool = not IN_DOCKER

是否启用 Chromium 沙箱提高安全性,在 Docker 内部运行时,应该设为 False,因为 Docker 提供了自己的沙箱,可能与 Chrome 的沙箱冲突。

devtools: bool = False

是否为每个标签页自动打开开发者工具面板,如果此选项为 True,则无头选项 headless 将设置为 False

slow_mo: float = 0

通过指定的毫秒数减慢 Playwright 操作,方便用户看清发生了什么。

accept_downloads: bool = True

是否自动接受所有下载。

downloads_path: str | Path | None = '~/.config/browseruse/downloads'

本地文件系统目录,用于保存浏览器下载的文件,该参数还有两个别名 downloads_dirsave_downloads_path,但是推荐使用标准的 downloads_path

base_url: str | None = None

当设置 base_url 之后,调用 page.goto()page.route()page.wait_for_url() 等操作时可以使用相对 URL 路径。

proxy: dict | None = None

为浏览器设置代理,示例如下:

{
  "server": "http://proxy.com:8080",
  "username": "user",
  "password": "pass"
}
permissions: list[str] = ['clipboard-read', 'clipboard-write', 'notifications']

授予浏览器权限,点击下面的链接获取可用权限的完整列表:

storage_state: str | Path | dict | None = None

使用特定的浏览器存储状态,可以通过下面的命令生成存储状态文件:

$ playwright open https://example.com/ --save-storage=./storage_state.json

存储状态文件中包含 cookies 和 localStorage 等,然后通过下面的方式使用它(注意 user_data_dir 必须设置成 None):

session = BrowserSession(
  storage_state='./storage_state.json',
  user_data_dir=None
)

超时设置

参数名默认值参数说明
timeout30000连接远程浏览器的默认超时时间(毫秒)
default_timeoutNonePlaywright 操作的默认超时时间(毫秒)
default_navigation_timeoutNone页面导航的默认超时时间(毫秒)

视口选项

参数名默认值参数说明
viewportNone使用 width 和 height 的视口大小,示例: {"width": 1280, "height": 720}
no_viewportnot headless禁用固定视口,内容将随窗口调整大小,注意,不要使用此参数
device_scale_factorNone设备缩放因子(DPI),适用于高分辨率截图(设置为 2
screenNone可供浏览器使用的屏幕大小,如果未指定,则自动检测
color_scheme'light'首选颜色方案: 'light''dark''no-preference'
contrast'no-preference'对比度偏好: 'no-preference''more''null'
reduced_motion'no-preference'减少运动偏好: 'reduce''no-preference''null'
forced_colors'none'强制颜色模式: 'active''none''null'

设备模拟

参数名默认值参数说明
offlineFalse模拟网络离线
user_agentNone为浏览器设置特定的 User-Agent
is_mobileFalse是否考虑 meta viewport 标签并启用触摸事件
has_touchFalse指定视口是否支持触摸事件
geolocationNone地理位置坐标,示例: {"latitude": 59.95, "longitude": 30.31667}
localeNone指定用户区域设置,例如 en-GBde-DE 等,区域设置将影响 navigator.language 值、Accept-Language 请求头值以及数字和日期格式规则
timezone_idNone时区标识符(例如,‘America/New_York’

除此之外,还可以通过 playwright.devices 来模拟常见的设备:

BrowserProfile(
    ...
    **playwright.devices['iPhone 13'],
)

支持接受所有标准的 Playwright 参数,参考这里:

安全选项

参数名默认值参数说明
http_credentialsNoneHTTP 身份验证的凭据
extra_http_headers{}每个请求将发送的附加 HTTP 头
ignore_https_errorsFalse在发送网络请求时是否忽略 HTTPS 错误
bypass_cspFalse是否绕过内容安全策略(Content-Security-Policy)
java_script_enabledTrue是否启用 JavaScript
service_workers'allow'是否允许页面注册使用 Service Workers: 'allow''block'
strict_selectorsFalse如果为真,传递给 Playwright 方法的选择器将在匹配多个元素时抛出错误
client_certificates[]用于请求的客户端证书

录制跟踪

参数名默认值参数说明
record_video_dirNone别名 save_recording_path,保存 .webm 视频录制的目录
record_video_sizeNone视频大小,示例:{"width": 1280, "height": 720}
record_har_pathNone别名 save_har_path,保存 .har 网络跟踪文件的路径
record_har_content'embed'如何持久化 HAR 内容:'omit''embed''attach'
record_har_mode'full'HAR 录制模式:'full''minimal'
record_har_omit_contentFalse是否从 HAR 中省略请求内容
record_har_url_filterNoneHAR 录制的 URL 过滤器
traces_dirNone别名 trace_path,保存所有跟踪文件的目录,文件自动命名为 {traces_dir}/{context_id}.zip

信号处理

参数名默认值参数说明
handle_sighupTrue是否让 Playwright 捕获 SIGHUP 信号并终止浏览器
handle_sigintFalse是否让 Playwright 捕获 SIGINT 信号并终止浏览器,也就是 Ctrl+C 发送的信号
handle_sigtermFalse是否让 Playwright 捕获 SIGTERM 信号并终止浏览器,也就是 kill 默认发送的信号

小结

我们今天主要学习了 Browser Use 中关于浏览器的配置参数,鉴于浏览器的复杂性,相关参数也是非常的多,主要可以分为会话相关参数和浏览器配置模版;还学习了 BrowserProfileBrowserSession 的区别和用途,可以将静态的浏览器配置存储在 BrowserProfile 里,供 BrowserSession 复用。更多细节可以参考 Browser Use 和 Playwright 的官方文档:


使用 Browser Use 连接你的浏览器

浏览器是我们日常生活和工作中不可或缺的重要工具,Browser Use 的本质就是模拟人类对浏览器进行各种操作以完成用户任务。在之前的示例中,默认使用的是 Playwright 内置的 Chromium 浏览器,Browser Use 还支持多种启动或连接其他浏览器的方法,我们今天来学习这方面的内容。

通过 executable_path 启动本地浏览器

在之前介绍 Agent 配置时,我们曾学过 BrowserSession 参数的概念,它是 Browser Use 配置浏览器的核心类,它有一个 executable_path 参数,可以指定用户电脑上已安装的浏览器路径。当我们设置 executable_path 参数时,Browser Use 就会启动该浏览器,而不使用 Playwright 内置的浏览器:

from browser_use import Agent, BrowserSession

browser_session = BrowserSession(    
  executable_path='/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
  # executable_path='C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe',
  # executable_path='/usr/bin/google-chrome',
  user_data_dir='~/.config/browseruse/profiles/default',
)

agent = Agent(
  task="Your task here",
  llm=llm,
  browser_session=browser_session,
)

上面的代码使用的是用户已安装的 Chrome 浏览器,注意不同的操作系统下安装路径也不同。Browser Use 只支持基于 Chromium 内核的浏览器,比如 BravePatchrightRebrowserMicrosoft Edge 等,暂时不支持 Firefox 或 Safari 浏览器。

自定义 Chrome 启动参数

鉴于安全原因,从 v136 开始,Chrome 不再支持使用默认的配置文件来驱动浏览器,所以无法复用你已有的浏览器配置,如登录凭据,收藏夹,扩展程序等。Browser Use 为此创建了一个新的专用配置文件,位于 ~/.config/browseruse/profiles/default,可以在启动 Chrome 浏览器时,将配置文件设置成这个(注意使用全路径):

$ cd /Applications/Google\ Chrome.app/Contents/MacOS
$ ./Google\ Chrome --user-data-dir=/Users/aneasystone/.config/browseruse/profiles/default

在 macOS 上,还有一种更简单的方式启动应用:

# 以应用名称启动
$ open -a <应用名称> --args <参数>

# 以应用路径启动
$ open <应用路径> --args <参数>

比如:

$ open -a "Google Chrome" --args \
    --user-data-dir=/Users/aneasystone/.config/browseruse/profiles/default

或:

$ open "/Applications/Google Chrome.app" --args \
    --user-data-dir=/Users/aneasystone/.config/browseruse/profiles/default

需要注意的是,启动之前最好先退出所有的 Chrome 实例,不然参数可能无效

通过这种方式打开浏览器后,所有的用户数据会持久化到 Browser Use 的专用配置文件中,所以当我们再次用 Browser Use 启动浏览器时,就可以访问这些用户数据,包括且不限于下面这些:

  • 所有已登录的会话和 Cookies
  • 如果启用了自动填充,则可访问保存的密码
  • 浏览器历史记录和书签
  • 扩展程序及其数据

所以务必检查分配给智能体的任务,确保符合你的安全要求!对于任何敏感数据,请使用 Agent(sensitive_data=...),并使用 BrowserSession(allowed_domains=...) 限制浏览器访问。

使用现有的 Playwright 对象连接浏览器

我们之前学习过 BrowserBrowserContextPage 参数的概念,这三个都是 Playwright 内置的类:Browser 表示浏览器实例,代表整个浏览器进程,通常使用 playwright.chromium.launch() 等方法创建;BrowserContext 表示浏览器上下文,相当于一个隔离的浏览器会话,类似于浏览器的 “无痕模式”,不同 BrowserContext 之间完全隔离,互不影响;Page 表示一个页面,代表浏览器中的一个标签页,是实际进行页面操作的对象(点击、输入、导航等)。

它们之间的关系,类似下面这样的分层结构:

Browser
├── BrowserContext 1
│   ├── Page 1
│   └── Page 2
└── BrowserContext 2
    ├── Page 3
    └── Page 4

我们可以将现有的 BrowserBrowserContextPage 对象传递给 BrowserSession 类,让 Browser Use 连接并复用已有的浏览器实例:

async with async_playwright() as p:
    
  # 手动打开页面
  browser = await p.chromium.launch(headless=False)
  context = await browser.new_context()
  page = await context.new_page()
  await page.goto("https://playwright.dev")

  browser_session = BrowserSession(
    browser=browser,
    browser_context=context,
    page=page,
  )

  agent = Agent(
    task="这个页面讲的是什么?",
    llm=llm,

    # 复用浏览器会话
    browser_session=browser_session
  )
  result = await agent.run()

或者使用简写形式,直接将这三个对象传给 Agent 类:

agent = Agent(
  task="这个页面讲的是什么?",
  llm=llm,

  browser=browser,
  browser_context=browser_context,
  page=page,
)

这种连接方式可以将其他基于 Playwright 的项目非常容易地集成到 Browser Use 中,比如 PatchrightStagehand 等:

stagehand = Stagehand(
  config=config, 
  server_url=os.getenv("STAGEHAND_SERVER_URL"),
)
await stagehand.init()

agent = Agent(
  task='your task',
  page=stagehand.page
)

连接 Playwright 的 Browser Server

Playwright 的 Node.js 版本有一个 浏览器服务器(Browser Server) 的特性,我们可以通过 BrowserType.launchServer 来启动,服务器会暴露一个 WebSocket 接口供其他应用连接,实现远程操作浏览器。

使用下面的 Node.js 代码启动 Browser Server:

const { chromium } = require('playwright');  // Or 'webkit' or 'firefox'.

(async () => {
  // 启动服务器
  const browserServer = await chromium.launchServer();
  const wsEndpoint = browserServer.wsEndpoint();
  console.log(wsEndpoint);

  // 通过 ws 连接服务器
  const browser = await chromium.connect(wsEndpoint);
  browser.newPage("https://playwright.dev/");

  // 等待 600 秒
  await new Promise(resolve => setTimeout(resolve, 600 * 1000));

  // 关闭服务器
  await browserServer.close();
})();

其中 wsEndpoint 就是服务器的地址,可以通过 wss_url 参数传递给 BrowserSession 供 Browser Use 中使用:

browser_session = BrowserSession(    
  wss_url="ws://localhost:55660/4b762d7e1b8b9a66d8c3ece7a5dd3b81"
)
agent = Agent(
  task="访问 https://playwright.dev 总结页面内容",
  llm=llm,
  browser_session=browser_session
)

注意 Browser Server 的服务端和客户端版本需要一致。

通过 CDP 连接本地 Chrome 浏览器

CDP 协议,全称 Chrome DevTools Protocol,这是一个允许工具对 Chromium、Chrome 和其他基于 Blink 的浏览器进行调试、检查和性能分析的协议。目前有 很多项目 正在使用这个协议,包括 Chrome 的开发者工具

在启动 Chrome 浏览器时加上 --remote-debugging-port=9222 参数即可开启 CDP 协议:

$ open -a "Google Chrome" --args \
    --remote-debugging-port=9222 \
    --user-data-dir=/Users/aneasystone/.config/browseruse/profiles/default

注意,调试模式必须指定自定义的用户数据目录。

浏览器会监听 9222 端口,使用 curl 检查是否能连接:

$ curl http://localhost:9222/json/version

如果一切正常,将返回类似下面这样的浏览器信息:

{
  "Browser": "Chrome/137.0.7151.104",
  "Protocol-Version": "1.3",
  "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36",
  "V8-Version": "13.7.152.14",
  "WebKit-Version": "537.36 (@dd98cd04c723035396b6d301e1f196b29a14a0ff)",
  "webSocketDebuggerUrl": "ws://localhost:9222/devtools/browser/6dcde03c-cc5c-41b9-a7bf-64ebab62726d"
}

可以通过 cdp_url 参数传递给 BrowserSession 供 Browser Use 中使用:

browser_session = BrowserSession(    
  cdp_url="http://localhost:9222"
)
agent = Agent(
  task="访问 https://playwright.dev 总结页面内容",
  llm=llm,
  browser_session=browser_session
)

学习 CDP 协议

上面使用 curl 访问的 /json/version 接口,就是 CDP 协议中的一个接口,像这样的接口还有很多。比如访问 /json/list 接口:

$ curl http://localhost:9222/json/list

将返回浏览器当前打开的所有页面信息:

[ {
  "description": "",
  "devtoolsFrontendUrl": "...",
  "faviconUrl": "https://chromedevtools.github.io/devtools-protocol/images/logo.png",
  "id": "7C10F98B3BDCE163464F77ED2AC7948A",
  "title": "Chrome DevTools Protocol",
  "type": "page",
  "url": "https://chromedevtools.github.io/devtools-protocol/",
  "webSocketDebuggerUrl": "ws://localhost:9222/devtools/page/7C10F98B3BDCE163464F77ED2AC7948A"
}, {
  "description": "",
  "devtoolsFrontendUrl": "...",
  "faviconUrl": "https://mintlify.s3-us-west-1.amazonaws.com/browseruse-0aece648/_generated/favicon/favicon.ico?v=3",
  "id": "9624235768323EEEB7083FBD54748808",
  "title": "Connect to your Browser - Browser Use",
  "type": "page",
  "url": "https://docs.browser-use.com/customize/real-browser",
  "webSocketDebuggerUrl": "ws://localhost:9222/devtools/page/9624235768323EEEB7083FBD54748808"
} ]

访问 /json/new?{url} 接口:

$ curl -X PUT "http://localhost:9222/json/new?https://playwright.dev"

将打开一个新页面,并返回新页面的信息:

{
  "description": "",
  "devtoolsFrontendUrl": "...",
  "id": "BB33A59C140BB527452DB607BFCE3A03",
  "title": "",
  "type": "page",
  "url": "https://playwright.dev/",
  "webSocketDebuggerUrl": "ws://localhost:9222/devtools/page/BB33A59C140BB527452DB607BFCE3A03"
}

接口里返回的 webSocketDebuggerUrl 是该页面的调试地址,我们可以通过 WebSocket 连接它,并发送 CDP 命令去操作该页面:

cdp-ws.png

这里发送的 CDP 命令是 Page.navigate,将当前页面导航到一个新地址:

{"id": 1, "method": "Page.navigate", "params": {"url": "http://www.github.com"}}

还可以访问 /json/close/{targetId} 接口:

$ curl http://localhost:9222/json/close/BB33A59C140BB527452DB607BFCE3A03

将关闭对应的页面:

Target is closing

如果你对 CDP 协议感兴趣,可以参考其官方文档:

通过 PID 连接本地 Chrome 浏览器

Browser Use 还支持通过 PID 来连接 Chrome 浏览器:

browser_session = BrowserSession(
  browser_pid=46543
)
agent = Agent(
  task="访问 https://playwright.dev 总结页面内容",
  llm=llm,
  browser_session=browser_session
)

注意 Chrome 浏览器启动时必须加上 --remote-debugging-port=9222 参数,以调试模式启动。可以看下 Browser Use 底层的实现,通过 PID 找到进程的启动参数,然后从参数中找到 --remote-debugging-port 端口号,最终还是通过 CDP 协议来连接:

browser-pid.png

在线浏览器服务

目前有很多在线浏览器服务,它们都支持 WS 协议或 CDP 协议,感兴趣的可以试下:

小结

今天学习了 Browser Use 启动或连接浏览器的方法,包括通过 executable_path 启动本地浏览器、使用现有的 Playwright 对象连接浏览器、通过 Playwright 的 Browser Server 连接远程浏览器、通过 CDP 连接本地 Chrome 浏览器,以及通过 PID 连接本地 Chrome 浏览器等方法。这些技术使得 Browser Use 可以灵活地与不同的浏览器实例交互,从而更好地完成复杂的自动化任务。在使用这些功能时,需要注意安全性,确保不要无意中泄露敏感信息。


详解 Browser Use 的 Agent 用法(四)

经过这几天的学习,总算把 Browser Use Agent 所有的配置参数都搞清楚了,我们今天先对这些参数做一个总结回顾,然后继续来研究下 agent.run() 方法,学习它的基本用法和生命周期钩子等概念。

配置参数回顾

下面这张表格概括了 Agent 所有的配置参数,以及它的默认值和功能说明,可以帮助大家快速理解和回顾之前学习的内容:

参数名默认值参数说明
task必填表示智能体需要完成的任务,通常为自然语言描述。
llm必填使用的大语言模型实例。支持 DeepSeek、GPT-4 等。
pageNonePlaywright 的 Page 对象,用于让智能体直接使用预初始化的网页。
browserNonePlaywright 的 Browser 对象,用于指定浏览器实例。
browser_contextNonePlaywright 的 BrowserContext 对象,用于设置独立的浏览器上下文。
browser_profileNoneBrowser Use 内置的配置类,包括浏览器启动参数、连接参数和默认的视图信息。
browser_sessionNoneBrowser Use 内置的会话类,表示活跃的浏览器会话(实例化后的浏览器进程)。
controllerController()自定义工具管理类,可以通过装饰器 @controller.action 注册工具。升华智能体功能。
contextNone任意用户自定义的上下文对象,例如数据库连接、文件句柄或运行时配置,仅透传给自定义工具。
override_system_messageNone重写系统提示词的内容,用于替换默认的任务规划和工具调用提示词。
extend_system_messageNone在默认提示词后追加内容,用于增强提示词,而不是完全替换。建议优先使用。
max_actions_per_step10一次最多执行多少个大模型生成的动作,控制任务中模型生成动作的数目。
message_contextNone附加的任务上下文描述,用于让模型更好地理解任务或场景。
max_input_tokens128000最大支持输入 token 数,模型的上下文窗口大小(如 DeepSeek V3 为 64K,GPT-4 为 128K)。
tool_calling_method'auto'控制工具调用方式,可选 function_callingjson_moderawtoolsauto
use_visionTrue是否开启视觉能力,表示是否将网页截图作为消息的一部分。禁用能降低 token 消耗。
planner_llmNone用于规划任务的大语言模型实例,可以是一个比主模型更小或更便宜的模型。
use_vision_for_plannerFalse是否为规划模型开启视觉能力,如果开启,会传递网页截图给模型。
planner_interval1控制每隔多少步规划一次任务,默认是每步都规划。
is_planner_reasoningFalse是否通过 HumanMessage 而不是 SystemMessage 传递规划提示词,适配不支持 SystemMessage 的推理模型。
extend_planner_system_messageNone向默认规划提示词后附加额外内容,用于定制化任务分解的逻辑。
page_extraction_llmNone用于页面内容提取的大语言模型实例,如果未配置,默认使用主模型。
initial_actionsNone初始化时执行的固定动作(如打开某个页面),无需调用大模型,能节省 token。
save_conversation_pathNone保存会话记录的目录路径,包含每一步的 Prompt 和模型响应结果,便于调试和分析。
save_conversation_path_encoding'utf-8'会话记录文件的编码格式,默认为 UTF-8。
include_attributestitle, type, name, role保留关键网页元素的部分属性,用于节省 token(默认值包括 title, type 等带有语义信息的属性)。
validate_outputFalse完成任务时开启输出校验,确保任务的最终结果准确信息。
max_failures3遇到异常时的最大重试次数,可处理浏览器异常关闭、token 超限及接口限流等问题。
retry_delay10接口限流错误时延迟的时间(单位:秒),然后重试。
generate_gifFalse运行结束后生成操作过程的 GIF 动画,包含任务、每一步目标和截屏记录。
save_playwright_script_pathNone保存 Playwright 脚本的路径,用于重放操作过程(最新版本已移除,但派生了 Workflow Use 项目)。
register_new_step_callbackNone每次调用大模型获取下一步动作之后触发的回调函数,访问当前浏览器状态和大模型输出。
register_done_callbackNone整个任务完成后触发的回调函数,可访问智能体运行的所有历史数据。
register_external_agent_status_raise_error_callbackNone自定义是否在异常时触发 InterruptedError 的回调,如果返回 True,则触发。
injected_agent_stateNone用于注入已保存的智能体状态,支持从中断的地方继续任务。
enable_memoryTrue是否开启智能体的程序性记忆功能。
memory_configNone配置程序性记忆的相关参数,例如间隔步数、嵌入模型、向量数据库等。
available_file_pathsNone指定智能体可以访问和操作的文件路径列表,会在系统提示语中通知大模型。
sensitive_dataNone定义敏感数据及其对应的占位符,可防止敏感信息暴露给大模型。
sourcegit / pip / unknown告知 Browser Use 当前使用的来源场景,默认根据运行环境自动设置。

生命周期钩子

关于 Agent 的配置参数就此告一段落,接下来我们来看看 agent.run() 方法的定义:

async def run(
  self, 
  max_steps: int = 100, 
  on_step_start: AgentHookFunc | None = None, 
  on_step_end: AgentHookFunc | None = None
) -> AgentHistoryList:

它有三个参数:

  • max_steps - 限制循环的最大步数,防止智能体陷入死循环,默认是 100 步;
  • on_step_start - 在智能体处理当前状态并决定下一步行动之前执行;
  • on_step_end - 在智能体执行完当前步骤的所有操作后执行;

后两个参数被称为 Browser Use 的 生命周期钩子(Lifecycle Hooks),允许你在特定时刻执行自定义代码。我们可以在钩子函数中读取和修改智能体状态,实现自定义逻辑,改变配置,与外部应用程序集成等。

钩子函数类型如下:

AgentHookFunc = Callable[['Agent'], Awaitable[None]]

可以看出,每个钩子函数都应该是一个可调用的 async 函数,接受 agent 实例作为唯一参数:

async def on_step_start(agent: Agent):
  print(f"======== on_step_start {agent.state.n_steps} ========")

async def on_step_end(agent: Agent):
  print(f"======== on_step_end {agent.state.n_steps} ========")

我们知道,Agent 有一个配置参数 register_new_step_callback 用于注册回调,生命周期钩子和它的机制很相似。只不过注册的回调函数参数是 BrowserStateSummaryAgentOutput,可以在函数中访问当前的浏览器状态和大模型的输出,而钩子函数的参数为 Agent,可以在函数中访问整个智能体的状态和方法,使用起来更灵活。

钩子的访问点

下面是钩子函数中的一些有用的访问点:

  • agent.task - 让你看到主要任务是什么,可以使用 agent.add_new_task(...) 新增一个新的任务;
  • agent.controller - 用于访问 Controller 对象和 Registry 对象,包含可用的操作,比如在当前页面上执行某个动作:
agent.controller.registry.execute_action(
  'click_element_by_index',
  {
    'index': 123
  },
  browser_session=agent.browser_session
)
  • agent.context - 让你访问传递给 Agent(context=...) 的任何用户提供的上下文对象;
  • agent.sensitive_data - 包含敏感数据字典,可以就地更新以添加/删除/修改其中的内容;
  • agent.settings - 包含在初始化时传递给 Agent(...) 的所有配置选项;
  • agent.llm - 直接访问主 LLM 对象 (例如 ChatOpenAI );
  • agent.state - 提供对大量内部状态的访问,包括智能体当前的推理、输出、行动等:
# 模型的推理结果
model_thoughts = agent.state.history.model_thoughts()

# 模型的原始输出
model_outputs = agent.state.history.model_outputs()

# 智能体采取的行动
model_actions = agent.state.history.model_actions()

# 从网页提取的内容
extracted_content = agent.state.history.extracted_content()

# 智能体访问的 URL
urls = agent.state.history.urls()
  • agent.browser_session - 直接访问 BrowserSessionPlaywright 对象:
# 获取当前的 Page 对象
current_page = agent.browser_session.get_current_page()

# 获取当前的 BrowserContext 对象
browser_context = agent.browser_session.browser_context

# 获取当前上下文中所有打开的标签页
pages = agent.browser_session.browser_context.pages

# 当前页面 HTML
html = agent.browser_session.get_page_html()

# 当前页面的截图
screenshot = agent.browser_session.take_screenshot()

更多内容参考官网的 Lifecycle Hooks 文档:

历史列表和结构化输出

agent.run() 方法的返回值是一个 AgentHistoryList 对象,其中包含完整的执行历史。这个历史对于调试、分析和创建可重现的脚本是非常有用。下面是该对象常用的一些方法:

history.urls()              # List of visited URLs
history.screenshots()       # List of screenshot paths
history.model_thoughts()    # Get the agent’s reasoning process
history.model_actions()     # All actions with their parameters
history.action_names()      # Names of executed actions
history.action_results()    # Get results of all actions
history.extracted_content() # Content extracted during execution
history.is_done()           # Check if the agent completed successfully
history.has_errors()        # Check if any errors occurred
history.errors()            # Any errors that occurred
history.final_result()      # Get the final extracted content

其中 history.final_result() 是最常用的,用于输出文本格式的最终结果,不过我们也可以定义一个结构化的输出格式,以便后续处理。比如我们想从豆瓣读书上搜索关于 “大模型” 相关的书籍,并以结构化的形式展示出来,我们可以先定义书籍和书籍列表类型:

from typing import List
from pydantic import BaseModel

class Book(BaseModel):
  book_title: str          # 书名
  author: str              # 作者
  brief_introduction: str  # 简介
  score: float             # 评分

class Books(BaseModel):
  books: List[Book]

然后将其绑定在 Controller 上:

from browser_use import Controller

controller = Controller(output_model=Books)

将这个自定义的 Controller 传入 Agent,就可以通过 Books.model_validate_json(...)final_result 中得到结构化结果:

agent = Agent(
  task="进入豆瓣读书,搜索关于大模型相关的书籍,获取排名前三的书籍详情",
  llm=llm,
  controller=controller
)
history = await agent.run()
result = history.final_result()
parsed: Books = Books.model_validate_json(result)
for book in parsed.books:
  print('\n--------------------------------')
  print(f'书名:  {book.book_title}')
  print(f'作者:  {book.author}')
  print(f'简介:  {book.brief_introduction}')
  print(f'评分:  {book.score}')

输出结果如下:

--------------------------------
书名:  大模型算法:强化学习、微调与对齐
作者:  余昌叶
简介:  涉及强化学习、模型微调与对齐技术的前沿算法介绍。
评分:  9.6

--------------------------------
书名:  从零构建大模型
作者:  [美]塞巴斯蒂安·拉施卡 / 覃立波 / 冯骁骋
简介:  全面介绍大模型的构建方法和技术实践。
评分:  9.3

--------------------------------
书名:  解构大语言模型:从线性回归到通用人工智能
作者:  唐亘
简介:  探讨大语言模型从基础线性回归模型到通用人工智能的演变过程。
评分:  9.6

小结

到这里,Browser Use 的 Agent 用法基本上就讲解完了,不过之前在讲解浏览器相关的配置时还留了一个小尾巴。浏览器是一个很复杂的话题,比如如何自定义浏览器配置,如何使用 CDP 连接已有的浏览器实例,如何使用 Patchright 绕过浏览器自动化检测,等等等等,我们明天就来学习这些内容,完成 Browser Use 学习拼图中的最后一块。


详解 Browser Use 的 Agent 用法(三)

关于 Browser Use 的 Agent 配置参数还剩一些,我们今天继续学习。

注册回调

register_new_step_callback: (
    Callable[['BrowserStateSummary', 'AgentOutput', int], None]  # Sync callback
    | Callable[['BrowserStateSummary', 'AgentOutput', int], Awaitable[None]]  # Async callback
    | None
) = None,
register_done_callback: (
    Callable[['AgentHistoryList'], Awaitable[None]]  # Async Callback
    | Callable[['AgentHistoryList'], None]  # Sync Callback
    | None
) = None,
register_external_agent_status_raise_error_callback: Callable[[], Awaitable[bool]] | None = None,

在上一篇中,我们介绍了一个 Browser Use 的有趣功能,通过 generate_gif=True 参数可以在任务运行结束时生成一个 GIF 动画。但是想象一下,如果我们想基于 Browser Use 开发一款 Web 版的智能体应用,用户希望实时的看到每一步对浏览器的操作,就像 Manus 演示的那样,又该如何实现呢?

为此,Browser Use 提供了一些回调机制,允许我们在智能体执行的特定时刻访问其状态数据,并执行自定义代码:

  • register_new_step_callback - 该回调函数在每次调用大模型获取下一步动作之后触发,函数中可以访问当前的浏览器状态和大模型的输出;
  • register_done_callback - 该回调函数在整个任务完成之后触发,函数中可以访问智能体运行的所有历史数据;
  • register_external_agent_status_raise_error_callback - 这个回调函数有点意思,它并不是在出现异常时触发,而是让用户自定义是否触发异常,如果这个函数返回 True,就会触发 InterruptedError

其中最有用的应该是第一个回调函数,我们可以监听 Browser Use 的每一步执行,从浏览器状态中获取屏幕截图,实时地返回给 Web 前端:

from browser_use.agent.views import AgentOutput
from browser_use.browser.views import BrowserStateSummary
def register_new_step_callback(browser_state_summary: BrowserStateSummary, agent_output: AgentOutput, n_steps: int):
  print(f'========== STEP {n_steps} BEGIN ==========')
  print("BrowserStateSummary:")
  print(f'  Title: {browser_state_summary.title}')
  print(f'  Url: {browser_state_summary.url}')
  print(f'  Screenshot: {browser_state_summary.screenshot[:80]}...')
  print("AgentOutput:")
  print(f'  Previous Goal: {agent_output.current_state.evaluation_previous_goal}')
  print(f'  Next Goal: {agent_output.current_state.next_goal}')
  print(f'========== STEP {n_steps} END ==========')

from browser_use import Agent
async def main():
  agent = Agent(
    task="Compare the price of gpt-4.1-mini and DeepSeek-V3",
    llm=llm,
    
    # 监听每一步执行
    register_new_step_callback=register_new_step_callback,
  )
  result = await agent.run()

其中 browser_state_summary.screenshot 就是屏幕截图,注意它的格式是 BASE64 编码。

初始状态

injected_agent_state: AgentState | None = None,

在介绍这个参数之前,我们先学习 Browser Use 的一个新特性:单步运行,下面是示例代码:

agent = Agent(
  task="Compare the price of gpt-4.1-mini and DeepSeek-V3",
  llm=llm,
)

for i in range(100):
  done, valid = await agent.take_step()
  print(f'Step {i}: Done: {done}, Valid: {valid}')
  if done and valid:
    break

result = agent.state.last_result
print(result)

在这个例子中,我们通过 take_step() 方法,一次只运行一步,而不像 run() 方法那样一次性把任务跑完。这种运行方式让我们可以更灵活的控制和调试智能体,就像调试代码一样,每走一步就停下来,对智能体的状态做深入洞察。通过这种方式,我们不仅可以轻松实现上面那个实时返回网页截图的功能,而且可以做很多更有想象力的事,比如当用户观察到智能体的行为和自己预期有偏差时,可以通过 add_new_task() 方法来插入新的指令,及时纠正智能体的错误。

另一个用例是,在运行过程中我们可以将智能体的状态保存到持久化存储中,比如数据库或本地文件:

for i in range(100):
  done, valid = await agent.take_step()
  print(f'Step {i}: Done: {done}, Valid: {valid}')

  # 持久化状态
  async with await anyio.open_file('agent_state.json', 'w') as f:
    serialized = agent.state.model_dump_json(exclude={'history'})
    await f.write(serialized)

  if done and valid:
    break

这样做的好处是,就算智能体运行到一半崩掉了也不用担心,我们可以通过 injected_agent_state 参数将持久化的状态注入到一个新智能体里重新运行,Browser Use 会从之前断掉的地方恢复并继续后面的步骤:

# 加载状态
async with await anyio.open_file('agent_state.json', 'r') as f:
  loaded_json = await f.read()
  agent_state = AgentState.model_validate_json(loaded_json)

agent = Agent(
  task="Compare the price of gpt-4.1-mini and DeepSeek-V3",
  llm=llm,

  # 注入已有状态
  injected_agent_state=agent_state
)

开启程序性记忆

enable_memory: bool = True,
memory_config: MemoryConfig | None = None,

我们在 “记忆管理” 篇中介绍过,Browser Use 内置了一个基于 Mem0 的程序性记忆系统,该系统会每隔几步,对智能体的消息历史进行总结压缩,可以显著改善长任务的性能。这个功能默认是开启的,但是需要安装相关的依赖:

$ uv pip install "browser-use[memory]"

Browser Use 默认是每隔 10 步总结一次,可以通过 memory_config 参数进行修改:

agent = Agent(
  task="your task",
  llm=llm,

  # 开启程序性记忆
  enable_memory=True,
  # 记忆相关配置
  memory_config=MemoryConfig(
    llm_instance=llm,
    agent_id="my_custom_agent",
    memory_interval=15
  )
)

记忆相关的配置除了上面这三个,还有一些嵌入模型和向量数据库相关的配置,如下:

# 嵌入模型
embedder_provider: Literal['openai', 'gemini', 'ollama', 'huggingface'] = 'huggingface'
embedder_model: str = Field(min_length=2, default='all-MiniLM-L6-v2')
embedder_dims: int = Field(default=384, gt=10, lt=10000)

# 向量数据库
vector_store_provider: Literal[
  'faiss', 'chroma', 'qdrant', 'pinecone',
  'supabase', 'elasticsearch', 'redis',
  'weaviate', 'milvus', 'pgvector', 'upstash_vector',
  'vertex_ai_vector_search', 'azure_ai_search',
] = Field(default='faiss')
vector_store_collection_name: str | None = Field(default=None)
vector_store_base_path: str = Field(default='/tmp/mem0')
vector_store_config_override: dict[str, Any] | None = Field(default=None)

这些配置和 Mem0 的配置很像,但是略微有些区别,是 Mem0 的子集,具体解释参考官方文档:

文件处理

available_file_paths: list[str] | None = None,

这个参数用来告诉大模型它可以使用哪些文件,一旦设置,会在系统提示词中加一句话:

Here are file paths you can use: ['./test.md']

不过 Browser Use 并没有内置和文件处理相关的工具,需要我们自己实现。有两种比较常见的场景,一种是对用户文件进行读取、修改、处理等操作,下面是文件读取的示例:

@controller.action('根据文件路径读取文件内容')
async def read_file(path: str, available_file_paths: list[str]):

  async with await anyio.open_file(path, 'r') as f:
    content = await f.read()
  msg = f'文件内容:{content}'
  return ActionResult(extracted_content=msg, include_in_memory=True)

注意 available_file_paths 也是个框架参数,我们可以对 path 做个校验:

if path not in available_file_paths:
  return ActionResult(error=f'File path {path} is not available')
if not os.path.exists(path):
  return ActionResult(error=f'File {path} does not exist')

另一种场景是上传文件,可以通过另一个框架参数 browser_session 提供的几个辅助方法,在页面上定位到文件上传的按钮,然后上传即可,示例代码如下:

@controller.action('将指定路径的文件上传到某个可交互的元素',)
async def upload_file(index: int, path: str, browser_session: BrowserSession, available_file_paths: list[str]):

  file_upload_dom_el = await browser_session.find_file_upload_element_by_index(index)
  file_upload_el = await browser_session.get_locate_element(file_upload_dom_el)
  await file_upload_el.set_input_files(path)
  msg = f'成功上传文件到指定元素 {index}'
  return ActionResult(extracted_content=msg, include_in_memory=True)

然后在当前目录下创建一个测试文件 test.md,将自定义的 controller 和文件路径传给智能体:

agent = Agent(
  task="打开网页 https://kzmpmkh2zfk1ojnpxfn1.lite.vusercontent.net/ 并上传 test.md 文件",
  llm=llm,

  controller=controller,
  available_file_paths=["./test.md"]
)
result = await agent.run()

敏感数据

sensitive_data: dict[str, str | dict[str, str]] | None = None,

敏感数据处理是 Browser Use 的另一项特色功能,可以将敏感数据,如个人身份信息(PII)、银行卡号、密码或密钥等,使用占位符代替,防止敏感信息暴露给大模型。敏感数据通过 sensitive_data 参数来定义,支持两种格式,一种是 key value 形式,对所有站点生效(不推荐):

{
  "key": "value"
}

一种是带域名的 key value 形式,只对特定站点生效:

{
  "domain": {
    "key": "value"
  }
}

下面是官网的一个示例:

agent = Agent(
  task = """
    访问 https://travel.example.com
    并使用会员号 x_member_number 和访问码 x_passphrase 登录
  """
  llm=llm,

  # 传入敏感数据
  sensitive_data = {
    'https://*.example.com': {
      'x_member_number': '123235325',
      'x_passphrase': 'abcwe234',
    },
  },
  browser_session=BrowserSession(
    allowed_domains=['https://*.example.com']
  ),
  use_vision=False,
)

这个示例展示了在处理敏感数据时的几个要点:

  • 在用户任务里,使用占位符描述敏感数据;
  • 使用 sensitive_data 定义敏感数据,https://*.example.com 限定只在符合规则的网址下使用这些敏感数据;
  • 使用 browser_sessionallowed_domains 限定浏览器只能访问这些网址;
  • 关闭视觉能力,防止大模型在屏幕截图中看到敏感数据;

不过要注意的是,不建议使用 sensitive_data 作为登录凭据,官方的建议是提前登录好网站,可以人工登录,也可以用程序自动登录。比如通过下面的命令打开浏览器:

$ playwright open https://www.aneasystone.com/admin --save-storage auth.json

人工登录后,认证信息将保存到 auth.json 文件中,然后使用 storage_stateuser_data_dir 来复用 Cookie 数据:

agent = Agent(
  task="打开 https://www.aneasystone.com/admin",
  llm=llm,

  # 复用已有的 Cookies
  browser_session=BrowserSession(
    storage_state='./auth.json',
    user_data_dir=None
  )
)

数据收集来源

source: str | None = None,

这是一个几乎绝大多数用户都用不到的参数,用于告诉 Browser Use 我们是在什么场景下使用它的,如果不设置,默认值取决于你运行它的环境:

  • git - 通过源码运行
  • pip - 通过 pip 安装运行
  • unknown

我给大家介绍这个参数并不是想让大家使用它,而是想告诉大家一个事实,Browser Use 在运行的过程中默认会收集一些我们的使用数据,包括用户的任务、模型相关的配置、访问过的 URL 地址、执行过的动作以及报错信息等,如下:

telemetry-capture.png

Browser Use 使用 PostHog 收集这些数据,通过收集并分析这些数据,Browser Use 团队可以实时地了解库的使用情况,及时发现并修复错误,改善用户体验。

收集的数据是完全匿名的,并且不包含任何个人可识别信息。尽管如此,如果你对数据问题比较敏感,可以通过 ANONYMIZED_TELEMETRY 环境变量关闭该功能:

export ANONYMIZED_TELEMETRY=false

未完待续

至此,关于 Agent 的配置参数终于介绍完了,通过这几天的学习,相信大家对 Browser Use 的功能有了大概的了解,这些功能足够满足大多数用户的需求场景了。还记得我们之前从示例代码引出的三个问题吗?除了 Agent 的配置参数之外,还有 agent.run() 方法的入参和返回值,我们明天就来看看它们吧。


详解 Browser Use 的 Agent 用法(二)

话不多说,我们今天继续来学习 Browser Use 的 Agent 配置参数。

开启规划模型

planner_llm: BaseChatModel | None = None,
use_vision_for_planner: bool = False,
planner_interval: int = 1,
is_planner_reasoning: bool = False,
extend_planner_system_message: str | None = None,

为了更好地处理复杂的多步骤任务,Browser Use 支持配置单独的 规划模型(Planner model) 对任务进行高层规划,上面这些参数都是和规划模型相关的参数。

参数 planner_llm 是 LangChain 的 ChatModel,表示规划时使用的模型,可以是一个比主模型更小或更便宜的模型;参数 planner_interval 用于控制每隔多少步规划一次,默认是每步都规划;参数 use_vision_for_planner 表示是否开启视觉能力,如果模型不支持可以禁用,和 use_vision 一样,如果开启视觉能力,Browser Use 会将网页的截图也添加到消息中,可以让大模型更好地理解网页内容,但是同时也会提高成本。

agent = Agent(
  task="your task",
  llm=llm,

  # 开启规划模型
  planner_llm=planner_llm,
  # 规划时禁用视觉,默认是开启的
  use_vision_for_planner=False,
  # 每 4 步规划一次,默认是每步都规划
  planner_interval=4
)

当 Browser Use 开启规划模型时,会使用一段默认的规划提示词,将任务分解为更小的步骤(我们在 “记忆管理” 篇学习过),可以使用 extend_planner_system_message 在默认的提示词后面追加内容;另外,这段规划提示词默认是通过 SystemMessage 传给大模型的,但是有些推理模型不支持 SystemMessage,比如 o1、o1-mini 等,可以设置 is_planner_reasoning=True 参数,这时规划提示词是通过 HumanMessage 传给大模型。

内容提取模型

page_extraction_llm: BaseChatModel | None = None,

在 Browser Use 内置的 20 个工具里,有一个叫 extract_content,它的实现如下:

@self.registry.action(
  '提取页面内容以获取特定信息,并以结构化格式呈现',
)
async def extract_content(
  goal: str,
  page: Page,
  page_extraction_llm: BaseChatModel
):

  # 将页面内容转为 Markdown 文本
  content = markdownify.markdownify(await page.content(), strip=strip)

  # 内容提取提示词
  prompt = '''
  您的任务是提取页面的内容。
  您将获得一个页面和一个目标,您应该从页面中提取与此目标相关的所有信息。
  如果目标模糊,请总结页面。
  请以 JSON 格式响应。
  提取目标:{goal},页面:{page}'''
  template = PromptTemplate(input_variables=['goal', 'page'], template=prompt)

  # 调用内容提取大模型
  output = await page_extraction_llm.ainvoke(
    template.format(goal=goal, page=content))
  msg = f'📄  Extracted from page\n: {output.content}\n'
  return ActionResult(extracted_content=msg, include_in_memory=True)

它使用内容提取模型,从页面中提取特定的内容,并返回 JSON 格式。参数 page_extraction_llm 用于配置内容提取使用的模型,如果不配置,默认使用的是主模型。

初始动作

initial_actions: list[dict[str, dict[str, Any]]] | None = None,

昨天在学习浏览器设置时,我们介绍了一个有趣的例子,先通过 Playwright 做一些固定的动作,比如打开某个页面,然后再让 Browser Use 基于这个页面完成某个任务。其实,还有一种更简单的方法来完成这个过程:

initial_actions = [
  {'open_tab': {'url': 'https://playwright.dev'}},
]

agent = Agent(
  task="这个页面讲的是内容?",
  llm=llm,

  # 初始化动作
  initial_actions=initial_actions
)
result = await agent.run()

通过 initial_actions 参数,我们可以在没有大模型的情况下运行初始动作,对于一些初始动作相对固定的场景,这样做可以节省大量 token。参考 Controller 源码 找到所有可用的动作以及对应的参数。

保存会话记录

save_conversation_path: str | None = None,
save_conversation_path_encoding: str | None = 'utf-8',

这个参数一般用于调试和研究,我们知道 Browser Use 通过不断的调用大模型获取下一步动作,如果对这个过程感兴趣,可以在初始化 Agent 时加上这个参数:

agent = Agent(
  task="Compare the price of gpt-4.1-mini and DeepSeek-V3",
  llm=llm,

  # 保存会话记录
  save_conversation_path="logs/conversation",
  # 会话记录文件编码
  save_conversation_path_encoding="utf-8",
)

运行结束后会在 logs/conversation 目录下生成每一步使用的 Prompt 以及大模型输出的结果,方便定位哪一步出了问题。不过要注意的是,文件中只包含文本类型的消息,不包含 functions 参数或图片类型。

保留关键的元素属性

include_attributes: list[str] = [
  'title',
  'type',
  'name',
  'role',
  'aria-label',
  'placeholder',
  'value',
  'alt',
  'aria-expanded',
  'data-date-format',
  'checked',
  'data-state',
  'aria-checked',
],

这是一个高级参数,并不常用,大多数场景下使用默认值即可。但是我们可以通过该参数看看 Browser Use 的一些底层细节。

Browser Use 在调用大模型决定下一步动作时,会将当前网页的源码也作为上下文一起传过去。在这里 Browser Use 并没有使用完整的 HTML 源码,而是只保留了一些可点击的关键元素(并为每个元素标上序号,方便大模型点击或输入),而且只保留这些元素的部分属性,这也是一种巧妙的节省 token 的做法。

默认保留的属性如上所示,可以看到这些属性多少都带点语义信息。其中以 aria- 开头的属性是 Accessible Rich Internet Applications (ARIA) 的一部分,用于改善 Web 应用程序和网站的可访问性。这些属性提供了额外的信息,使得无障碍辅助技术(如屏幕阅读器)能够更好地理解和描述页面上的动态内容和用户界面元素的功能。

如果通过 save_conversation_path 参数开启会话记录,可以在会话记录文件中看到页面内容是这样的:

[Start of page]
[0]<a >Gmail />
[1]<a aria-label='搜索图片 '>图片 />
[2]<a aria-label='Google 应用' aria-expanded='false' role='button' />
[3]<a >登录 />
[4]<textarea title='Google 搜索' value='' aria-label='搜索' placeholder='' aria-expanded='false' name='q' role='combobox' />
[5]<div  />
[6]<div aria-label='按图搜索' role='button' />
[7]<input value='Google 搜索' aria-label='Google 搜索' name='btnK' role='button' type='submit' />
[8]<input value=' 手气不错 ' aria-label=' 手气不错 ' name='btnI' type='submit' />
Google 提供:
[9]<a >繁體中文 />
[10]<a >English />
香港
[11]<a >关于 Google />
[12]<a >广告 />
[13]<a >商务 />
[14]<a >Google 搜索的运作方式 />
[15]<a >隐私权 />
[16]<a >条款 />
[17]<div aria-expanded='false' role='button'>设置 />
[End of page]

如果调试时发现页面内容中丢失了部分关键信息,可以通过 include_attributes 参数来调整。

开启输出校验

validate_output: bool = False,

这个参数对最终的输出结果进行校验,确保输出的准确性,默认不开启。当开启后,Browser Use 会在完成任务之前再调用一次 _validate_output() 方法,该方法通过调用大模型判断用户任务是否真的完成:

if self.state.history.is_done():
  if self.settings.validate_output and step < max_steps - 1:
    if not await self._validate_output():
      continue

  await self.log_completion()
  break

校验的结果为 JSON 格式,包含 is_validreason 两个字段:

{
  "is_valid": false, 
  "reason": "The user wanted to search for 'cat photos', but the agent searched for 'dog photos' instead."
}

如果校验通过,则跳出循环;如果不通过,则将不通过的原因添加到消息中,继续下一轮。

失败重试

max_failures: int = 3,
retry_delay: int = 10,

Browser Use 在遇到异常情况时能自动重试,参数 max_failures 表示最大重试次数。它能处理下面这些异常情况:

  • 浏览器被异常关闭:直接返回 浏览器被关闭,无法处理 的错误,Browser Use 会重新打开浏览器重试;
  • 达到 token 上限:调用消息管理的 cut_messages() 方法,将消息裁剪成不超过 max_input_tokens 值;
  • 大模型响应无法解析:返回错误信息,并在后面增加一段提示,请返回一个包含所需字段的有效 JSON 对象
  • 接口限流错误:访问 OpenAI 或 Anthropic 接口过快时可能会遇到这个错误,Browser Use 会根据 retry_delay 参数等待一段时间,默认是 10 秒,然后重试;
  • 其他异常:直接返回错误信息;

生成 GIF 动画

generate_gif: bool | str = False,

这是一个很有 Browser Use 特色的功能,Browser Use 在每次操作浏览时都会对页面进行截屏,这个参数的作用是将这些截屏拼接成一个 GIF 动画,动画中还会加入用户的任务和每一步的目标,可以非常形象地演示 Browser Use 的运行过程。比如下面这个例子:

agent = Agent(
  task="查询《哪吒2魔童闹海》的豆瓣评分",
  llm=llm,

  # 生成 GIF 动画
  generate_gif=True
)

运行结束后,会在当前目录下创建一个 agent_history.gif 文件,如下所示:

agent_history.gif

注意,如果你生成的动画里中文显示乱码,大概率是没找到字体文件导致的。Browser Use 使用 PIL 库的 ImageDraw 在图片上绘制文本,按顺序检测下面这些字体是否存在(参考 browser_use/agent/gif.py 文件中的 font_options 数组):

font_options = [
  'Microsoft YaHei',  # 微软雅黑
  'SimHei',  # 黑体
  'SimSun',  # 宋体
  'Noto Sans CJK SC',  # 思源黑体
  'WenQuanYi Micro Hei',  # 文泉驿微米黑
  'Helvetica',
  'Arial',
  'DejaVuSans',
  'Verdana',
]

前几个是中文字体,不过在 macOS 上默认是没有的,加上 macOS 内置的中文字体 STHeiti LightSTHeiti Medium 即可。

生成 Playwright 脚本

save_playwright_script_path: str | None = None,

这也是一个非常有趣的功能,Browser Use 在运行结束后,可以将操作历史转存成一个 Playwright 脚本,这个脚本其实就是一个 Python 文件,里面的代码是调用 Playwright 的各个方法,重放 Browser Use 的整个操作过程。这个功能适用于下面这些应用场景:

  • 自动化前端测试:通过让智能体自动探索应用功能,快速生成 Playwright 测试脚本,大幅减少手动编写脚本的时间。这些标准化脚本可以在 CI/CD 中执行,用于回归测试;
  • 可靠的任务自动化:将智能体执行的任务(例如填写表单、导航复杂流程、下载文件等)转换为确定性的脚本,以进行重复执行,避免使用大模型带来的成本和结果波动;
  • 调试与回放:通过执行生成的脚本,轻松重现智能体的具体运行过程,用于调试目的;

不过值得注意的是,这个参数在最新版本中已经被移除了。我之所以介绍它,是因为这个参数直接派生了一个新的开源项目 Workflow Use,专门用来创建和执行确定性的工作流脚本,目前这个项目还处于很早期的阶段,感兴趣的朋友可以关注一波。

workflow-use.png

未完待续

今天的学习就到这了,又收获一堆 Browser Use 的小技巧,关于 Agent 的配置参数还剩一些,我们明天继续。


详解 Browser Use 的 Agent 用法(一)

经过这几天的学习,我们已经对 Browser Use 的 “任务规划”、“记忆管理” 和 “工具使用” 都有了更深入的理解,不过这些内容主要是围绕源码展开,偏理论,在实战中很难用上。所以,我们今天再来看下 Browser Use 的实际用法。

在入门篇里,我们已经介绍过 Browser Use 的基本用法,如下:

agent = Agent(
  task="Compare the price of gpt-4.1-mini and DeepSeek-V3",
  llm=llm,
)
result = await agent.run()

通过 Agent 初始化一个智能体,传入 LLM 实例和我们的任务,然后调用 agent.run() 启动智能体,得到结果。这里有三个点可以展开学习:

  1. Agent 类的初始化函数,除了 taskllm 这两个必填参数之外,还有哪些选填参数?
  2. agent.run() 方法还有哪些额外参数?
  3. agent.run() 的结果 result 是什么类型?

Agent 参数概览

Agent 类是 Browser Use 的核心组件,它除了 taskllm 两个必填参数之外,还有很多其他的配置参数:

agent-init.jpg

别看这里的参数列表这么长的一串,其实有不少参数在之前的学习中已经介绍过。今天就对所有参数做一个汇总介绍。

浏览器设置

page: Page | None = None,
browser: Browser | None = None,
browser_context: BrowserContext | None = None,
browser_profile: BrowserProfile | None = None,
browser_session: BrowserSession | None = None,

这 5 个参数都是和浏览器有关,我们知道 Browser Use 基于 Playwright 实现浏览器操作,这里的 PageBrowserBrowserContext 都是 Playwright 内置的类,是三个层次不同的概念:

  • Browser(浏览器实例)

    • 代表整个浏览器进程
    • 是最顶层的对象,包含所有的浏览器上下文
    • 通过 playwright.chromium.launch() 等方法创建
    • 一个 Browser 可以包含多个 BrowserContext
  • BrowserContext(浏览器上下文)

    • 相当于一个隔离的浏览器会话,类似于浏览器的 “无痕模式”
    • 每个 BrowserContext 都有独立的 cookieslocalStoragesessionStorage 等存储
    • 不同 BrowserContext 之间完全隔离,互不影响
    • 一个 BrowserContext 可以包含多个 Page
    • 可以设置独立的用户代理、视口大小、权限等
  • Page(页面/标签页)

    • 代表浏览器中的一个标签页
    • 是实际进行页面操作的对象(点击、输入、导航等)
    • 每个 Page 属于一个 BrowserContext
    • 包含页面的 DOM、JavaScript 执行环境等

Browser 和 Page 比较好理解,BrowserContext 作为他们之间的介质,形成类似下面这样的分层结构:

Browser
├── BrowserContext 1
│   ├── Page 1
│   └── Page 2
└── BrowserContext 2
    ├── Page 3
    └── Page 4

BrowserContext 主要用于隔离不同的测试场景,比如登录不同的用户或者使用不同的配置,这种分层设计既能高效复用浏览器资源,又能保证测试之间的隔离性。

BrowserProfileBrowserSession 是 Browser Use 内置的类,它们两的概念如下:

  • BrowserProfile(浏览器配置)

    • 静态的配置信息,包括浏览器的启动参数、连接参数、视图信息等
    • Browser Use 根据这些配置创建浏览器会话
  • BrowserSession(浏览器会话)

    • 表示一个活跃的浏览器会话,对应一个正在运行的浏览器进程
    • 这是 Browser Use 管理浏览器的核心,在构造记忆时,其中的浏览器状态就是通过它实现的
    • 上面的所有参数,最终都是转换为浏览器会话

浏览器设置是一个复杂的话题,后面我们单开一篇来介绍,目前先了解下这些概念。通过不同的浏览器设置,我们可以实现一些有趣的功能,比如我们先通过 Playwright 做一些固定的操作,比如打开某个页面,然后再让 Browser Use 基于这个页面完成某个任务(这样可以省去打开页面这一步消耗的 token):

async with async_playwright() as p:
    
  # 手动打开页面
  browser = await p.chromium.launch(headless=False)
  page = await browser.new_page()
  await page.goto("https://playwright.dev")

  agent = Agent(
    task="这个页面讲的是内容?",
    llm=llm,

    # 使用已有页面
    page=page
  )
  result = await agent.run()

自定义工具

controller: Controller[Context] = Controller(),
context: Context | None = None,

这两个参数在学习 Browser Use 的 “工具使用” 时已经学过,只要在函数上面加一个 @controller.action 装饰器就可以注册一个自定义工具:

from browser_use import Controller, ActionResult

controller = Controller()

@controller.action('查询某个城市的天气')
def weather(city: str) -> ActionResult:
  return ActionResult(
    extracted_content=f'{city}今天的天气晴,气温28摄氏度'
  )

然后将这个增强版的 controller 传入 Agent 即可:

agent = Agent(
  task="使用 weather 工具查下合肥的天气",
  llm=llm,
  
  # 自定义工具
  controller=controller
)

另外,自定义工具中还可以使用一些框架提供的参数,例如 pagebrowser_session 等,这样我们在工具里也可以运行 Playwright 代码与浏览器交互;还可以传入一个用户提供的上下文对象:

class MyContext:
  ...

这个对象可以包含任意内容,比如:数据库连接、文件句柄、队列、运行时配置等,然后通过 context 参数传递给 Agent

agent = Agent(
  task="使用 weather 工具查下合肥的天气",
  llm=llm,
  
  # 自定义工具
  controller=controller,
  context=MyContext()
)

Browser Use 并不会使用这个对象,它只是将其透传到用户自定义的工具里:

@controller.action('The description of action 1')
def custom_action_1(arg: str, context: MyContext) -> ActionResult:
  return ActionResult(...)

@controller.action('The description of action 2')
def custom_action_2(arg: str, context: MyContext) -> ActionResult:
  return ActionResult(...)

更多内容参考官方的 Custom Functions 文档:

自定义提示词

override_system_message: str | None = None,
extend_system_message: str | None = None,
max_actions_per_step: int = 10,
message_context: str | None = None,

参数 override_system_message 用于重写 Browser Use 默认的系统提示词,我们在之前学习 Browser Use 的 “任务规划” 时介绍过提示词的内容,重写的时候可以参考下。不过这种做法并不推荐,最好是通过 extend_system_message 往默认的提示词后面追加内容。

extend_system_message = """
REMEMBER the most important RULE:
ALWAYS open first a new tab and go first to url wikipedia.com no matter the task!!!
"""

默认的提示词中有一个参数 {max_actions} 用于控制大模型一次最多生成多少动作,默认是 10 个,可以通过 max_actions_per_step 参数修改:

agent = Agent(
  task="your task",
  llm = llm,

  # 一次最多执行多少个动作
  max_actions_per_step=3
)

我们还可以通过 message_context 参数对你的任务增加一些额外描述,以帮助大模型更好地理解任务:

agent = Agent(
    task="your task",
    message_context="Additional information about the task",
    llm = llm,
)

大模型相关配置

max_input_tokens: int = 128000,
tool_calling_method: ToolCallingMethod | None = 'auto',
use_vision: bool = True,

这几个是和大模型相关的一些配置。其中参数 max_input_tokens 表示大模型支持的最大输入 token 数,默认是 128K,根据你使用的模型来填即可,比如 DeepSeek V3 支持 64K,Gemini-2.5 支持 1024K 等。每次 Browser Use 往消息管理器中新增消息时,会计算消息的 token 数,当超出限制后,会调用 _message_managercut_messages() 方法,将消息裁剪成不超过 max_input_tokens

参数 tool_calling_method 表示大模型工具调用的方式,支持下面几种:

ToolCallingMethod = Literal[
  'function_calling', 
  'json_mode', 
  'raw', 
  'auto', 
  'tools'
]

当配置为 auto 时,Browser Use 会根据已知的模型名字返回工具调用方式,比如 gpt-4 会使用 function_callingclaude-3 会使用 toolsdeepseek-r1 会使用 raw 等;如果模型名字未知,会通过一个内置的方法来测试大模型支持哪种方式。

无论是 function_callingtools 还是 json_mode 方式,本质上都是大模型直接返回结构化响应,通过 LangChain 的 with_structured_output() 方法,可以自动解析,用来起非常方便;对于不支持结构化输出的模型,只能使用 raw 方式,Browser Use 会将所有的工具列表拼到 Prompt 里,让大模型返回 JSON 格式再解析。

参数 use_vision 表示是否开启视觉能力,如果模型不支持可以禁用,比如 DeepSeek 暂时不支持图片。当开启视觉能力时,Browser Use 会将网页的截图也添加到消息中,可以让大模型更好地理解网页内容和交互。但要注意的是,开启视觉能力对 token 的消耗非常大,对于 GPT-4o 来说,一张图片大约消耗 800-1000 个 token(约 0.002 美元),可以禁用降低成本。

未完待续

Agent 的配置参数实在是太多了,上面介绍的还不到一半,还有很多有意思的参数,比如敏感数据的处理,注册回调,失败重试,输出校验,生成 GIF 动画,程序性记忆,等等等等,我们明天继续学习。


再学 Browser Use 的工具使用

昨天我们学习了 Browser Use 是如何利用大模型的结构化输出能力实现工具调用的,通过 AgentOutput 模式定义与动态的 ActionModel 的结合,实现了智能体在执行网页任务时灵活选择和使用不同浏览器工具。

今天我们将继续探讨浏览器操作的具体实现以及如何自定义工具,以扩展 Browser Use 的功能。

工具调用流程

现在我们再次回顾下 Browser Use 智能体循环中的 step() 方法,调用大模型得到下一步动作后,接着通过 multi_act() 方法对每一个动作进行执行:

model_output = await self.get_next_action(input_messages)
result: list[ActionResult] = await self.multi_act(model_output.action)

接着通过 controller.act() -> registry.execute_action() 等方法,最后从 registry.actions 数组中找到对应的动作:

action = self.registry.actions[action_name]
return await action.function(params=validated_params, **special_context)

这里的 registry.actions 表示 Browser Use 支持的所有工具,那么这个数组是怎么来的呢?我们可以在 Controller 的初始化函数中找到答案:

browser-use-action-go-to-url.png

截图中是 go_to_url 的实现,注意函数上有一个 @self.registry.action 装饰器,这个装饰器的作用就是将这个函数实现添加到 registry.actions 数组中。

内置浏览器动作

Controller 是 Browser Use 工具调用的核心,这个类定义在 browser_use/controller/service.py 文件中,除了上面截图中的 go_to_url 动作,Browser Use 一共内置了 20 个操作浏览器的动作,如 open_tabscroll_downextract_content 等,我将其整理成表格如下:

Action 名称描述(中文)
done完成任务 - 返回文本,并指明任务是否完成(success=True 表示已完成,否则未完全完成)
search_google在 Google 中搜索查询,查询应像人类在 Google 搜索时一样具体,不要太模糊或太长
go_to_url在当前标签页导航到指定 URL
go_back后退
wait等待指定秒数,默认 3 秒
click_element_by_index通过索引点击元素
input_text向输入交互元素输入文本
save_pdf将当前页面保存为 PDF 文件
switch_tab切换标签页
open_tab在新标签页打开指定 URL
close_tab关闭已有标签页
extract_content提取页面内容以获取特定信息,例如所有公司名、特定描述、所有关于 xyc 的信息、带结构的公司链接等
get_ax_tree获取页面的可访问性树,格式为“role name”,返回指定数量的元素
scroll_down页面向下滚动指定像素数,未指定则滚动一页
scroll_up页面向上滚动指定像素数,未指定则滚动一页
send_keys发送特殊按键字符串,如 Escape、Backspace、Insert、PageDown、Delete、Enter,支持快捷键组合
scroll_to_text如果找不到想要交互的内容,则滚动到该文本位置
get_dropdown_options获取原生下拉框的所有选项
select_dropdown_option通过选项文本为交互元素选择下拉选项
drag_drop拖放页面上的元素或坐标,适用于画布绘图、可排序列表、滑块、文件上传和 UI 重排等

另外还内置了 6 个操作 Google Sheets 的动作,这些动作仅对 https://docs.google.com 域名生效:

Action 名称描述(中文)
read_sheet_contents获取整个工作表的内容
read_cell_contents获取一个单元格或单元格范围的内容
update_cell_contents更新一个单元格或单元格范围的内容
clear_cell_contents清除当前选定的任意单元格
select_cell_or_range选择特定的单元格或单元格范围
fallback_input_into_single_selected_cell备用方法在(仅一个)当前选定的单元格中输入文本

Playwright 介绍

分析上面内置的 20 个浏览器动作源码后你会发现,绝大多数动作都是通过 Playwright 库实现的,比如上面的 go_to_url 动作就是通过 Playwright 的页面对象完成的:

page = await browser_session.get_current_page()
if page:
  await page.goto(params.url)
  await page.wait_for_load_state()

Playwright 本身是一个现代网页应用的端到端测试库,支持多种浏览器和平台,并提供丰富的功能和工具,帮助开发者进行高效的 Web 测试。

playwright-home.png

它具备如下特性和亮点:

  • 跨平台、跨浏览器、跨语言:支持 Chromium、WebKit 和 Firefox 等现代渲染引擎,可在 Windows、Linux 和 macOS 等平台上进行测试,支持 Python、TypeScript、.Net、Java 等主流语言;
  • 可靠性、稳定性:它的 自动等待功能(Auto-wait) 无需开发者人为设置超时;专为网页设计的断言(Web-first assertions) 会自动重试直至条件满足;支持配置重试策略,捕获执行轨迹、视频和截图,确保测试的可靠性和稳定性;
  • 无需权衡、没有限制:和现代化浏览器架构保持一致,通过在进程外运行测试,摆脱了进程内测试运行器的限制。支持跨多个标签页、多个来源和多个用户的测试场景,并能够创建不同用户的不同上下文。此外,Playwright 产生真实用户无法区分的信任事件,并能够穿透 Shadow DOM 进行帧测试;
  • 完全隔离、高效测试:Playwright 为每个测试创建一个浏览器上下文,相当于一个新的浏览器配置文件,实现无开销的完全测试隔离。此外,它允许保存并重用上下文的认证状态,避免了每个测试重复登录,同时保持独立测试的完整隔离;
  • 强大的工具和功能:例如 代码生成器 通过记录操作来生成测试代码,以及 Playwright 检查器和追踪查看器等 调试工具 方便开发者诊断问题;

我们在前面安装 Browser Use 的时候,已经安装过 Playwright,其实就两步:

第一步,下载 Playwright 依赖包和命令行工具:

$ pip install playwright

第二步,安装 Chromium、Firefox 和 WebKit 的浏览器二进制文件

$ playwright install

注意,每个版本的 Playwright 需要特定版本的浏览器二进制文件才能运行,所以每次更新 Playwright 版本时,记得重新运行 playwright install 命令。

默认情况下,浏览器被下载到如下位置:

  • Windows: C:\Users\<YourUsername>\AppData\Local\ms-playwright
  • macOS: /Users/<YourUsername>/Library/Caches/ms-playwright
  • Linux: ~/.cache/ms-playwright

一切就绪后,就可以和 Browser Use 一样,通过 Playwright 的 API 来操作浏览器了:

import asyncio
from playwright.async_api import async_playwright

async def main():
  async with async_playwright() as p:
    browser = await p.chromium.launch()
    page = await browser.new_page()
    await page.goto("https://playwright.dev")
    print(await page.title())
    await browser.close()

asyncio.run(main())

具体用法参考 Playwright 的文档:

用户自定义工具

上面提到 Controller 是 Browser Use 工具调用的核心,所有的内置工具都注册在 controller.registry 里面。用户如果要自定义工具,也是通过它来注册的,下面是用户自定义工具的示例:

from browser_use import Controller, ActionResult

controller = Controller()

@controller.action('Ask human for help with a question')
def ask_human(question: str) -> ActionResult:
  answer = input(f'{question} > ')
  return ActionResult(
    extracted_content=f'The human responded with: {answer}',
    include_in_memory=True
  )

可以看到注册一个自定义工具非常简单,只需要在函数上面加一个 @controller.action 装饰器即可,函数入参任意,出参必须是 ActionResult 类型。通过自定义工具,用户可以实现额外的自定义行为、与其他应用的集成、或者像上面例子中那样,与人类进行交互。

然后将这个增强版的 controller 传入 Agent 即可:

agent = Agent(
  task='...',
  llm=llm,

  # 自定义工具
  controller=controller,
)

关于自定义工具的函数入参,还可以通过 Pydantic 来定义类型、默认值、校验规则、以及详细描述等:

class MyParams(BaseModel):
  field1: int
  field2: str = 'default value'
  field3: Annotated[str, AfterValidator(lambda s: s.lower())]  # example: enforce always lowercase
  field4: str = Field(default='abc', description='Detailed description for the LLM')

@controller.action('My action', param_model=MyParams)
def my_action(params: MyParams) -> ActionResult:
  ...

另外,自定义工具中还可以使用一些框架提供的参数,这些特殊的参数是由 Controller 注入的,例如 pagebrowser_session 等,这样我们的自定义工具也可以运行 Playwright 代码与浏览器进行交互:

@controller.action('Click element')
def click_element(css_selector: str, page: Page) -> ActionResult:
  await page.locator(css_selector).click()
  return ActionResult(extracted_content=f"Clicked element {css_selector}")

可用的框架提供参数如下:

  • page: 当前 Playwright 页面;
  • browser_session: 当前浏览器会话;
  • context: 用户自定义的上下文对象;
  • page_extraction_llm: 用于页面内容提取的语言模型实例;
  • available_file_paths: 可用于上传或处理的文件路径列表;
  • has_sensitive_data: 表示操作内容是否包含敏感数据标记,用于避免意外地将敏感数据日志记录到终端。

小结

至此,我们全面学习了 Browser Use 的三大核心功能:任务规划、记忆管理以及工具使用:

  1. 任务规划:大模型是如何组织提示词,对目标任务进行分析,确定下一步的动作;
  2. 记忆管理:如何存储和更新智能体的历史操作,以保持上下文的连贯性,提高任务执行的效率;
  3. 工具使用:通过大模型的格式化输出功能,让智能体灵活地选择工具,通过 Playwright 执行不同的浏览器操作,并支持用户自定义工具,以增强系统的扩展性和适应性。

通过详细剖析相关代码,相信大家对 Browser Use 的架构设计和运作机制有了更深入的理解,在后面的学习中,我将集中于 Browser Use 的实际用法,以及如何通过其强大的浏览器操作能力,去完成更多复杂的自动化任务。


学习 Browser Use 的工具使用

我们已经学习了 Browser Use 的 “任务规划” 和 “记忆管理”,通过剖析相关代码,相信大家对 Browser Use 的实现原理已经有了基本认识。今天我们将关注智能体三大核心组件中的 “工具使用”,学习 Browser Use 是如何使用工具的。

正如 Browser Use 的名字所示,它最重要的工具就是浏览器,通过各种浏览器操作,例如导航、滚动、点击、输入文本等,实现网页任务自动化和智能化;除此之外,Browser Use 也支持用户自定义工具,例如保存文件、访问数据库、发送通知、接收人类输入等,实现更丰富的智能体功能。

AgentOutput 模式

通过前面的学习,我们知道 Browser Use 通过 消息管理器 将系统提示词、用户任务、历史操作记录、当前浏览器状态以及上一步动作的执行结果等信息,组装成大模型的输入参数。调用大模型后,得到结构化的结果,类似于下面这个样子:

{
  "current_state": {
    "evaluation_previous_goal": "we did ok, team",
    "memory": "filled in xyz into page, still need to do xyz...",
    "next_goal": "click on the link at index 127, then open that new tab"
  },
  "action": [
    { "click_element_by_index": { "index": 127 } },
    { "switch_to_tab": { "page_id": 3 } }
  ]
}

这个结构化的结果就是 AgentOutput 模式,定义如下:

class AgentOutput(BaseModel):

  model_config = ConfigDict(arbitrary_types_allowed=True)

  current_state: AgentBrain
  action: list[ActionModel] = Field(
    ...,
    description='List of actions to execute',
    json_schema_extra={'min_items': 1},  # Ensure at least one action is provided
  )

很明显,这是一个 Pydantic 模式。

很多地方把 Pydantic 的 Model 翻译成 “模型”,但是为避免和大模型混淆,我统一翻译成 “模式”。

其中第一行的 model_config 是模式配置,arbitrary_types_allowed=True 表示模式中可以包含任意类型的字段,比如自定义类、第三方库的对象等。如果不设置为 True,Pydantic 默认只允许标准类型和已注册的类型。

下面的 current_stateactionAgentOutput 模式的两个字段,current_state 对应 AgentBrain 模式:

class AgentBrain(BaseModel):

  evaluation_previous_goal: str
  memory: str
  next_goal: str

表示当前的状态信息,以及前一个目标的回顾和下一个目标的展望。action 对应 ActionModel 模式:

class ActionModel(BaseModel):

  model_config = ConfigDict(arbitrary_types_allowed=True)

表示下一步要执行的动作,这是一个动态模式,因为每个动作的名称和参数都是不一样的。

值得一提的是,参数 action 后面的 Field(...)Pydantic 的特殊语法,这个不是省略的意思,而是表示该参数是必填的。不仅必填,后面的 {'min_items': 1} 还规定该列表至少包含一项。

Pydantic 模式和结构化输出

在大多数场景下,大模型都是直接用自然语言回应用户的,但是有一些特殊场景,我们需要模型以结构化格式输出,比如我们希望将模型输出存储在数据库中,并确保输出符合数据库模式。这就是 结构化输出(Structured outputs) 功能:

structured-output.png

结构化输出有两个要点:

  1. 模式定义:指示模型输出格式
  2. 结构化输出:模型返回符合该模式的输出

关于第一点,我们可以直接以 JSON 方式定义,或者使用 JSON Schema 方式定义,但是更常见的做法是使用 Pydantic 库,Pydantic 是一个现代的 Python 数据验证库,使用 Python 类型提示来定义数据模式并自动进行数据验证、序列化和文档生成。

Pydantic 特别适合定义结构化输出模式,下面是一个示例,通过继承 BaseModel 类定义了两个数据模式:

from pydantic import BaseModel, Field

class Student(BaseModel):
  name: str = Field(description="学生姓名")
  age: int = Field(description="学生年龄")
  gender: str = Field(description="学生性别")
  grade: int = Field(description="学生年级")
  school: str = Field(description="学生学校")

class StudentList(BaseModel):
  students: list[Student] = Field(description="学生列表")

关于第二点,目前有很多大模型都支持结构化输出的功能,不同的模型实现方式可能也不同,比如 Function calling、Tool calling、JSON 模式 等等。LangChain 提供了一个 with_structured_output() 方法,可以自动将模式绑定到模型并解析输出,用来起非常方便:

from langchain_openai import ChatOpenAI

llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)
llm_with_structure = llm.with_structured_output(StudentList)
structured_output = llm_with_structure.invoke("随机生成5条学生信息")

print(structured_output)

输出结果如下:

students=[
  Student(name='张伟', age=15, gender='男', grade=10, school='北京中学'), 
  Student(name='李娜', age=14, gender='女', grade=9, school='上海外国语学校'), 
  Student(name='王磊', age=16, gender='男', grade=11, school='广州实验中学'), 
  Student(name='刘婷', age=13, gender='女', grade=8, school='深圳科技中学'), 
  Student(name='陈强', age=17, gender='男', grade=12, school='杭州高级中学')
]

浏览器动作

通过上面的学习,我们明白了 Browser Use 是通过结合 AgentOutput 模式定义和大模型的结构化输出功能,从而得到下一步的动作。

但是,仔细想想就会发现一个问题,这里我们在调用大模型的时候,工具列表中始终只有一个 AgentOutput 工具,这和传统的工具调用很不一样,之前的做法是将所有的工具列出来,让大模型来判断下一步应该使用哪个。感兴趣的朋友可以将调用大模型的入参打印出来确认一下:

[
  {
    "type": "function",
    "function": {
      "name": "AgentOutput",
      "description": "AgentOutput model with custom actions",
      "parameters": {
        "properties": {
          "current_state": {
            "description": "Current internal working memory of the agent, we ask the LLM to decide new values for these on each output",
            "properties": {
              "evaluation_previous_goal": {
                "type": "string"
              },
              "memory": {
                "type": "string"
              },
              "next_goal": {
                "type": "string"
              }
            },
            "required": [
              "evaluation_previous_goal",
              "memory",
              "next_goal"
            ],
            "type": "object"
          },
          "action": {
            "description": "List of actions to execute",
            "items": {},
            "min_items": 1,
            "type": "array"
          }
        },
        "required": [
          "current_state",
          "action"
        ],
        "type": "object"
      }
    }
  }
]

可以看到,这里就一个 function。那么,大模型是如何选择下一步动作的呢?其实,这里的关键就在于前面 Browser Use 定义的 ActionModel 这个模式,这是一个动态模式,在 Agent 初始函数中有一个 _setup_action_models() 方法,它的作用是将所有可用的工具注入到这个模式中:

def _setup_action_models(self) -> None:
  """Setup dynamic action models from controller's registry"""
  
  # Initially only include actions with no filters
  self.ActionModel = self.controller.registry.create_action_model()
  # Create output model with the dynamic actions
  self.AgentOutput = AgentOutput.type_with_custom_actions(self.ActionModel)

这里的 controller.registry 是 Browser Use 工具调用的核心,所有内置的浏览器工具都注册在它的 registry.actions 数组里。这里通过它的 create_action_model() 方法动态地创建一个 ActionModel 模式,将所有可用的工具绑定到 ActionModel 模式中:

def create_action_model(self) -> type[ActionModel]:

  available_actions = {}
  for name, action in self.registry.actions.items():
    available_actions[name] = action

  fields = {
    name: (
      Optional[action.param_model],
      Field(default=None, description=action.description),
    )
    for name, action in available_actions.items()
  }

  return create_model('ActionModel', __base__=ActionModel, **fields)

这里我省略了一些代码:

  • 域名过滤:通过 domains 参数限定工具仅在某些域名下可用;
  • 页面过滤:通过 page_filter 函数判断工具是否在当前页面可用;

注入所有工具后,调用大模型的入参类似于下面这样:

[
  {
    "type": "function",
    "function": {
      "name": "AgentOutput",
      "description": "AgentOutput model with custom actions",
      "parameters": {
        "properties": {
          "current_state": {...},
          "action": {
            "description": "List of actions to execute",
            "items": {
              "properties": {
                "search_google": {
                  "anyOf": [
                    {
                      "properties": {
                        "query": {
                          "title": "Query",
                          "type": "string"
                        }
                      },
                      "required": [
                        "query"
                      ],
                      "title": "SearchGoogleAction",
                      "type": "object"
                    },
                    {
                      "type": "null"
                    }
                  ],
                  "default": null,
                  "description": "Search the query in Google, the query should be a search query like humans search in Google, concrete and not vague or super long."
                },
                "go_to_url": {
                  "anyOf": [
                    {
                      "properties": {
                        "url": {
                          "title": "Url",
                          "type": "string"
                        }
                      },
                      "required": [
                        "url"
                      ],
                      "title": "GoToUrlAction",
                      "type": "object"
                    },
                    {
                      "type": "null"
                    }
                  ],
                  "default": null,
                  "description": "Navigate to URL in the current tab"
                },
                ...
              },
              "type": "object"
            },
            "min_items": 1,
            "type": "array"
          }
        },
        "required": [
          "current_state",
          "action"
        ],
        "type": "object"
      }
    }
  }
]

这里的 action 参数的类型为 array 数组,数组中的每一项类型为 object 对象,对应不同的工具,每个工具的参数都是动态的。Browser Use 的工具调用虽然只有一个 AgentOutput,但是通过动态的 ActionModel 实现了不同工具的选择,让大模型输出下一步要执行的动作。

小结

今天我们探讨了 Browser Use 如何利用大模型的结构化输出能力简化了工具调用的过程,结合 AgentOutput 模式定义与动态的 ActionModel,实现智能体在执行网页任务时灵活选择和使用不同工具。

接下来的学习中,我们将深入探讨浏览器操作的具体实现以及如何自定义工具,以扩展 Browser Use 的功能。敬请期待下一篇的内容!


学习 Browser Use 的记忆管理

昨天我们学习了 Browser Use 的任务规划,对相关的代码和提示词进行了简单的剖析,正如前面所提到的,智能体 = 规划(Planning) + 记忆(Memory) + 工具使用(Tool use),所以按顺序,我们今天继续来看下 Browser Use 是如何管理记忆的。

消息管理器

回顾下 Browser Use 智能体循环中的 step() 方法,我们通过 _message_managerget_messages() 方法来组装大模型的输入:

@time_execution_async('--step (agent)')
async def step(self, step_info: AgentStepInfo | None = None) -> None:
  """Execute one step of the task"""

  input_messages = self._message_manager.get_messages()
  model_output = await self.get_next_action(input_messages)

  self.state.n_steps += 1

  result: list[ActionResult] = await self.multi_act(model_output.action)

  if len(result) > 0 and result[-1].is_done:
    logger.info(f'📄 Result: {result[-1].extracted_content}')

  self.state.last_result = result

_message_manager 被称为 消息管理器,其实也就是记忆管理器,里面存储了系统提示词、用户任务、运行过程中产生的历史消息,以及当前浏览器状态等等一系列的内容。Browser Use 就是通过它来存储和操作记忆的,是 Browser Use 记忆系统的核心。

那么这些内容是如何添加到记忆中的呢?我们可以在 Agent__init__ 方法中找到消息管理器的初始化代码:

# Initialize message manager with state
# Initial system prompt with all actions - will be updated during each step
self._message_manager = MessageManager(
  task=task,
  system_message=SystemPrompt(
    action_description=self.unfiltered_actions,
    max_actions_per_step=self.settings.max_actions_per_step,
    override_system_message=override_system_message,
    extend_system_message=extend_system_message,
  ).get_system_message(),
  settings=MessageManagerSettings(
    max_input_tokens=self.settings.max_input_tokens,
    include_attributes=self.settings.include_attributes,
    message_context=self.settings.message_context,
    sensitive_data=sensitive_data,
    available_file_paths=self.settings.available_file_paths,
  ),
  state=self.state.message_manager_state,
)

这里是消息管理器的四个参数:

  • task - 用户的任务;
  • system_message - 系统提示词,用户可以通过 override_system_message 参数重写,或者通过 extend_system_message 往默认的提示词后面追加内容;默认的提示词中有一个参数 {max_actions} 用于控制大模型一次最多生成多少动作,可以通过 max_actions_per_step 参数配置;
  • settings - 一些额外的配置参数,比如大模型的最大输入 token 数,消息上下文,敏感数据等;
  • state - 主要用于存储和操作运行过程中产生的历史记录,维护当前的 token 数等;

Browser Use 根据这四个参数构造初始记忆。看下 MessageManager_init_messages() 方法:

browser-use-init-message.png

这个方法展示了初始记忆是如何构造的,可以看到初始记忆包括:

  • 系统提示词
  • 任务的上下文信息,用户通过 message_context 参数传入,对任务加一些额外说明,帮助大模型更好的理解任务
  • 任务目标
  • 敏感数据说明,用户如果不希望将敏感信息暴露给大模型,可以使用 sensitive_data 字典,然后通过占位符来替代
  • 一条 AI 消息,包含一个简单的输出示例,也就是 AgentOutput
  • 一条工具消息 Browser started
  • 一条用户消息 [Your task history memory starts here]
  • 文件路径说明,用户可以通过 available_file_paths 参数告诉大模型可以访问哪些文件

记忆更新

初始记忆基本上是固定不变的,而运行日志和浏览器状态是动态的,在循环过程中,消息管理器会对记忆不断更新。我们把 step() 方法稍微充实下,加上 _message_manager 更新消息的逻辑,主要是加注释的三个地方:

@time_execution_async('--step (agent)')
async def step(self, step_info: AgentStepInfo | None = None) -> None:
  """Execute one step of the task"""
  
  ## 添加状态消息
  self._message_manager.add_state_message(...)

  input_messages = self._message_manager.get_messages()
  model_output = await self.get_next_action(input_messages)

  ## 添加模型输出消息
  self._message_manager.add_model_output(model_output)

  self.state.n_steps += 1

  ## 移除状态消息
  self._message_manager._remove_last_state_message()

  result: list[ActionResult] = await self.multi_act(model_output.action)

  if len(result) > 0 and result[-1].is_done:
    logger.info(f'📄 Result: {result[-1].extracted_content}')

  self.state.last_result = result

其实记忆更新的地方有很多,但是主流程里主要是这三个地方。首先是每一步的开头,通过 add_state_message() 将当前浏览器的状态和上一步工具调用的结果加到消息里:

browser_state_summary = await self.browser_session.get_state_summary(
  cache_clickable_elements_hashes=True
)
self._message_manager.add_state_message(
  browser_state_summary=browser_state_summary,
  result=self.state.last_result,
  step_info=step_info,
  use_vision=self.settings.use_vision,
)

浏览器的状态信息包括:当前网页地址、标题、标签页、页面内容等。如果开启视觉模型,还会将网页截图一起加到消息中:

browser-use-agent-message.png

根据这些信息调用大模型之后,得到 AgentOutput 结果,接着再通过 add_model_output() 将大模型输出添加到消息里:

model_output = await self.get_next_action(input_messages)
self._message_manager.add_model_output(model_output)

另外要注意的是,浏览器状态和工具调用结果大概率只针对当前循环是有用的,所以最后,通过 _remove_last_state_message() 将开头添加的状态信息移除:

self._message_manager._remove_last_state_message()

这样做的好处是聚焦每一步,防止上下文过多导致超出 token 限制或性能问题。

规划记忆

Browser Use 支持配置单独的 规划模型(Planner model) 对任务进行高层规划,如下:

agent = Agent(
  task="your task",
  llm=llm,

  # 规划模型,可以是一个比主模型更小或更便宜的模型
  planner_llm=planner_llm,
  # 规划时禁用视觉,默认是开启的
  use_vision_for_planner=False,
  # 每 4 步规划一次,默认是每步都规划
  planner_interval=4
)

使用规划模型可以改善任务分解和思考策略,从而更好地处理复杂的多步骤任务。当使用规划模型时,Browser Use 在调用大模型之前,首先会通过 _run_planner() 对任务进行规划,然后通过 add_plan() 将规划结果添加到记忆中:

# Run planner at specified intervals if planner is configured
if self.settings.planner_llm and self.state.n_steps % self.settings.planner_interval == 0:
  plan = await self._run_planner()
  self._message_manager.add_plan(plan, position=-1)

其中 _run_planner()get_next_action() 都是调用大模型,本质上都是基于当前状态评估下一步动作,他们两的逻辑很相似,只是系统提示词不一样,任务规划使用的提示词如下:

您是一个规划智能体,帮助将任务分解为更小的步骤并基于当前状态进行推理。
您的角色是:
1. 分析当前状态和历史
2. 评估朝着最终目标的进展
3. 识别潜在的挑战或障碍
4. 建议下一步的高层步骤

在您的消息中,将会有来自不同智能体的 AI 消息,格式各异。

您的输出格式应始终是一个 JSON 对象,包含以下字段:
{
  "state_analysis": "对当前状态和迄今为止所做工作的简要分析",
  "progress_evaluation": "对朝着最终目标的进展评估(以百分比和描述)",
  "challenges": "列出任何潜在的挑战或障碍",
  "next_steps": "列出2-3个具体的下一步",
  "reasoning": "解释您建议的下一步的理由"
}

忽略其他 AI 消息的输出结构。

保持您的回应简洁,专注于可操作的见解。

得到规划结果后,调用 add_plan() 将规划结果添加到记忆中,注意这里的 position=-1 参数,表示将其插入到上一步状态消息的前面,保证状态消息是最后一条,方便之后移除。

程序性记忆

在心理学领域,记忆被分为 外显记忆(explicit memory)内隐记忆(implicit memory)。外显记忆又被称为 陈述性记忆(declarative memory),是指关于事实或事件的记忆,可以被明确地表述出来;内隐记忆又被称为 程序性记忆(procedural memory),是指关于技能、过程或 “如何做” 的记忆,这种记忆不需要意识或有意回忆,而是自动地、无意识地参与行为,例如骑自行车、打字等。

Browser Use 内置了一个基于 Mem0 的程序性记忆系统,该系统会定期自动总结 Agent 的消息历史,对于较长的任务,随着消息历史的增长,可能导致上下文窗口溢出,开启程序性记忆可以显著改善长任务的性能。

Mem0 在其 官方博客 上宣称 Browser Use 集成 Mem0 之后,克服了内存和上下文的核心限制,实现了 98% 的任务完成率和 41% 的成本降低:

browser-use-with-mem0.png

可以通过 enable_memory 参数开启该功能:

agent = Agent(
  task="your task",
  llm=llm,

  # 开启程序性记忆
  enable_memory=True,
  # 记忆相关配置
  memory_config=MemoryConfig(
    llm_instance=llm,
    agent_id="my_custom_agent",
    memory_interval=15
  )
)

当开启程序性记忆后,循环的时候,每隔 memory_interval 步,调用一次 create_procedural_memory() 方法:

# generate procedural memory if needed
if self.enable_memory and self.memory and self.state.n_steps % self.memory.config.memory_interval == 0:
  self.memory.create_procedural_memory(self.state.n_steps)

该方法会回顾 Browser Use 最近的所有活动,然后使用 Mem0 创建一个程序性记忆摘要,最后用这个摘要替换掉原始消息,从而减少了令牌使用。这个过程有助于保持重要的上下文,同时为新信息释放上下文窗口。

深入源码的话,可以发现,程序性记忆摘要是调用 Mem0 的 add() 方法得到的:

results = self.mem0.add(
  messages=parsed_messages,
  agent_id=self.config.agent_id,
  memory_type='procedural_memory',
  metadata={'step': current_step},
)
return results.get('results', [])[0].get('memory')

注意将 memory_type 参数为 procedural_memory,这是 Mem0 的一种新的记忆类型,之前在学习 Mem0 的时候并没有特别关注它。其实它的实现也很简单,还是调用大模型,这里可以看看 Mem0 的源码:

mem0-procedural-memory.png

不过这里有一点疑惑,Mem0 在生成程序性记忆时,还会将记忆保存到向量数据库中,但是 Browser Use 是直接取 add() 方法返回的结果,并没有去查询记忆,这个保存到数据库里的长期记忆形同虚设,Browser Use 直接一个提示词就解决了,为啥还要绕道 Mem0 来实现,搞一堆嵌入模型和向量数据库相关的配置呢?

考虑到 Browser Use 项目目前非常活跃,更新比较频繁,估计后面会对记忆这块做一些优化和改造,后面我会持续关注这个项目。

小结

今天我们深入学习了 Browser Use 的记忆管理,通过剖析消息管理器相关的源码,我们了解了记忆是如何初始化、如何更新、以及如何获取的。另外,还学习了规划记忆和程序性记忆相关的知识,规划记忆通过任务分解和策略优化,更好地处理复杂的多步骤任务,程序性记忆通过不断总结和压缩历史消息,显著改善长任务的性能。

智能体的三大核心组件我们已经啃掉了两个,明天继续学习 Browser Use 是如何使用工具的。