Fork me on GitHub

2025年6月

详解 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 是如何使用工具的。


学习 Browser Use 的任务规划

昨天我们学习了 Browser Use 的核心特性和基础用法,通过一个简单的示例带大家体验了 Browser Use 解决问题的大致流程,今天我们继续学习 Browser Use 相关的知识。

Browser Use 本质上是一个智能体,Lilian Weng 在 LLM Powered Autonomous Agents 这篇博客中总结到,智能体至少要包含三个核心组件:规划(Planning)记忆(Memory)工具使用(Tool use)

agent-overview.png

其中,规划和记忆好比人的大脑,规划是对问题进行分析思考,记忆可以储存历史知识,工具使用好比人的五官和手脚,可以感知世界,与外部源(例如知识库或环境)进行交互,以获取额外信息,并执行动作。

Browser Use 通过大模型来实现任务规划,根据网页操作的结果决定下一步该做什么;使用内置的消息管理器和 Mem0 来存储操作历史,方便后续步骤回溯所需的关键信息;Browser Use 的工具就是浏览器,它可以对浏览器进行各种操作,如浏览、滚动、输入、点击、等等,除此之外,Browser Use 也支持用户自定义其他的工具。

今天,我们就来看看 Browser Use 是如何实现任务规划的。

思考-行动-观察 循环

智能体的核心是 思考-行动-观察 循环,这三个组件在一个持续的循环中协同工作,从而实现智能体的自主性、交互性和决策能力。这个循环如下图所示:

agentic-rag.png

我们看 Browser Use 代码,同样使用了这样的逻辑:

@time_execution_async('--run (agent)')
async def run(
  self, max_steps: int = 100
) -> AgentHistoryList:
  """Execute the task with maximum number of steps"""

  for step in range(max_steps):

    step_info = AgentStepInfo(step_number=step, max_steps=max_steps)
    await self.step(step_info)

    if self.state.history.is_done():
      break
  else:
    agent_run_error = 'Failed to complete task in maximum steps'
    logger.info(f'❌ {agent_run_error}')

  return self.state.history

上面是 agent.run() 方法的实现,这个方法看上去很复杂,但是把日志、跟踪、异常处理等细节代码去掉,剩下的骨架就是这样一个 for 循环。在这个循环中,Browser Use 不断的调用 step() 方法,直到 is_done() 任务完成为止。很明显,这是 ReAct 的思路,为了防止陷入死循环,Browser Use 通过 max_steps 控制最大循环次数,默认是 100 次。

循环中不断调用的 step() 方法实现如下:

@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

这里我同样将一些细节代码去掉了,只保留核心骨架。可以看到 Browser Use 首先通过 _message_manager 组装消息,然后调用 get_next_action() 获取下一步的动作,接着调用 multi_act() 执行动作,也就是操作浏览器或调用其他工具,这里暂时跳过,后面学习 “工具使用” 时再来看它。

规划下一步动作

从上面的代码中我们了解到,get_next_action() 方法用于获取下一步的动作,很显然,这一步就是调用大模型,也是 Browser Use 任务规划的核心,代码如下:

@time_execution_async('--get_next_action (agent)')
async def get_next_action(self, input_messages: list[BaseMessage]) -> AgentOutput:
  """Get next action from LLM based on current state"""

  input_messages = self._convert_input_messages(input_messages)
    
  output = self.llm.invoke(input_messages)

  parsed_json = extract_json_from_model_output(output.content)
  parsed = self.AgentOutput(**parsed_json)
  
  return parsed

这块的原始代码也比较多,这里省略了。Browser Use 的处理非常精细,对不同的 LLM 采用不同的调用方式,返回的结果也有不同的解析方式,常用的方式有下面这些:

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

可以看两张图感受下,下面这张图是调用大模型部分:

browser-use-get-next-action.png

如果是 raw 调用方式,则通过 LangChain 的 llm.invoke() 方法直接调用大模型;如果是其他调用方式,则通过 结构化输出(Structured outputs) 的方式来调用,首先通过 llm.with_structured_output() 指定模型输出结构得到 structured_llm,实际上就是在入参里加上 function_callingtools 参数,然后再通过 structured_llm.ainvoke() 调用大模型。参考下面的 LangChain 文档学习结构化输出的具体用法:

下面这张图是解析模型结果部分:

browser-use-get-next-action-2.png

如果结构化输出正常直接返回解析后的结果,否则先尝试解析 tool_calls 参数,最后再尝试直接 JSON 解析。

无论是用哪种调用方式,最终都需要将其解析成固定的 AgentOutput 格式,得到下一步要执行的动作,便于后续的 multi_act() 方法执行。

规划提示词

step() 方法中我们看到,Browser Use 通过 MessageManager 来组装大模型的提示词,关于消息管理器的内容,我们放到后面的 “记忆管理” 再深入学习,今天只关注从消息管理器得到的提示词是什么样的。

消息管理器组装的消息包括:系统提示词(System Prompt)用户消息(HumanMessage)助手消息(AIMessage)工具消息(ToolMessage) 等。如果将他们平铺开来,大概的布局如下所示:

<系统提示词>

<用户追加提示词>

Your ultimate task is: <这里是任务目标>

[Your task history memory starts here]
这里是每一步产生的历史记忆
[Task history memory ends]

[Current state starts here]
<这里是浏览器的当前状态,包括当前网页地址,标签页,页面内容等>

Current step: 9/100
Current date and time: 2025-06-04 06:36

其中系统提示词和任务目标是固定的,而历史记忆和当前状态每一步会动态更新,历史记忆是持久的,会不断追加,当前状态是临时的,会随着浏览器的变化而变化。

Browser Use 默认的系统提示词位于 browser_use/agent/system_prompt.md 文件中,翻译成中文如下:

我是一个专为自动化浏览器任务而设计的AI代理。我的目标是按照规则完成最终任务。

# 输入格式

任务
之前的步骤
当前URL
打开的标签页
交互元素
[索引]<类型>文本</类型>

- 索引:用于交互的数字标识符
- 类型:HTML元素类型(按钮、输入框等)
- 文本:元素描述
  示例:
  [33]<div>用户表单</div>
  \t*[35]*<button aria-label='提交表单'>提交</button>

- 只有在[]中带有数字索引的元素才可交互
- (堆叠的)缩进(用\t表示)很重要,意味着该元素是上方元素(索引较低)的(html)子元素
- 带有\*的元素是在上一步之后添加的新元素(如果url没有改变)

# 响应规则

1. 响应格式:您必须始终使用此确切格式的有效JSON进行响应:
   {"current_state": {"evaluation_previous_goal": "Success|Failed|Unknown - 分析当前元素和图像,检查之前的目标/操作是否按任务意图成功完成。提及是否发生了意外情况。简要说明原因",
   "memory": "已完成工作的描述和需要记住的内容。要非常具体。在这里始终计算您做了某事的次数和剩余次数。例如:已分析0个网站,共10个。继续执行abc和xyz",
   "next_goal": "下次立即行动需要完成的任务"},
   "action":[{"一个动作名称": {// 动作特定参数}}, // ... 序列中的更多动作]}

2. 动作:您可以在列表中指定多个动作以按顺序执行。但每个项目始终只指定一个动作名称。每个序列最多使用{max_actions}个动作。
常见动作序列:

- 表单填充:[{"input_text": {"index": 1, "text": "用户名"}}, {"input_text": {"index": 2, "text": "密码"}}, {"click_element": {"index": 3}}]
- 导航和提取:[{"go_to_url": {"url": "https://example.com"}}, {"extract_content": {"goal": "提取姓名"}}]
- 动作按给定顺序执行
- 如果动作后页面发生变化,序列会被中断,您将获得新状态
- 只提供动作序列直到会显著改变页面状态的动作
- 尽量高效,例如一次性填写表单,或在页面无变化时链接动作
- 只有在有意义时才使用多个动作

3. 元素交互:

- 只使用交互元素的索引

4. 导航和错误处理:

- 如果不存在合适的元素,使用其他功能完成任务
- 如果卡住了,尝试替代方法 - 如返回上一页、新搜索、新标签页等
- 通过接受或关闭来处理弹窗/cookies
- 使用滚动来寻找您要找的元素
- 如果您想研究某些内容,打开新标签页而不是使用当前标签页
- 如果出现验证码,尝试解决 - 否则尝试不同方法
- 如果页面未完全加载,使用等待动作

5. 任务完成:

- 一旦最终任务完成,立即使用done动作作为最后动作
- 除非达到max_steps的最后一步,否则不要在完成用户要求的所有内容之前使用"done"
- 如果达到最后一步,即使任务未完全完成也要使用done动作。提供到目前为止收集的所有信息。如果最终任务完全完成,将success设为true。如果用户要求的内容未全部完成,将done中的success设为false!
- 如果您需要重复执行某事,例如任务说"每个"、"对于所有"或"x次",在"memory"中始终计算您执行的次数和剩余次数。不要停止,直到按任务要求完成。只有在最后一步后才调用done。
- 不要臆造动作
- 确保在done文本参数中包含为最终任务发现的所有内容。不要只说您完成了,而要包含任务要求的信息。

6. 视觉上下文:

- 当提供图像时,使用它来理解页面布局
- 右上角带有标签的边界框对应元素索引

7. 表单填充:

- 如果您填写输入字段且动作序列被中断,通常是因为某些内容发生了变化,例如字段下方出现了建议。

8. 长任务:

- 在memory中跟踪状态和子结果。
- 您将获得浓缩先前任务历史的程序性记忆摘要(每N步)。使用这些摘要来维护关于已完成动作、当前进度和下一步的上下文。摘要按时间顺序出现,包含关于导航历史、发现、遇到的错误和当前状态的关键信息。参考这些摘要以避免重复动作并确保朝着任务目标持续进展。

9. 提取:

- 如果您的任务是查找信息 - 在特定页面上调用extract_content来获取和存储信息。
  您的响应必须始终是指定格式的JSON。

下面是用户消息的一个示例:

 HumanMessage --- 这里是用户消息,描述任务目标
Your ultimate task is: """Compare the price of gpt-4.1-mini and DeepSeek-V3""". 
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.

 ToolMessage 
Browser started

[Your task history memory starts here] --- 这里是每一步的历史记录

 HumanMessage 
Action result: 🔗  Navigated to https://www.google.com

 HumanMessage 
Action result: ⌨️  Input gpt-4.1-mini price into index 4

 HumanMessage 
[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.google.com/search?q=gpt-4.1-mini+price
Available tabs:
[
    TabInfo(page_id=0, url='https://www.google.com/search?q=gpt-4.1-mini+price', title='gpt-4.1-mini price - Google 搜索', parent_page_id=None), 
    TabInfo(page_id=1, url='https://www.google.com/search?q=deepseek-v3+price', title='deepseek-v3 price - Google 搜索', parent_page_id=None)
]
Interactive elements from top layer of the current page inside the viewport:
[Start of page]
[0]<a title='Google 首页' />
[1]<textarea value='gpt-4.1-mini price' aria-label='搜索' placeholder='' aria-expanded='false' name='q' role='combobox'>gpt-4.1-mini price />
[2]<div  />
  [3]<div aria-label='清除' role='button' />
[4]<div aria-label='按图搜索' role='button' />
[5]<button aria-label='搜索' type='submit' />

... 1217 pixels below - scroll or extract content to see more ...
Current step: 9/100
Current date and time: 2025-06-04 06:36

调用大模型后,得到下一步要执行的动作 AgentOutput,它大概长这个样子:

{
    "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 } }
    ]
}

如果对这个中间过程感兴趣,可以在初始化 Agent 时加一个 save_conversation_path 参数:

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

运行结束后会在 logs/conversation 目录下生成每一步使用的 Prompt 以及对应的模型输出结果,方便调试和研究。

小结

今天我们学习了 Browser Use 的任务规划,从智能体的 思考-行动-观察 循环入手,对 Browser Use 的代码进行了简单剖析,主要关注的是规划相关的代码和提示词部分,对于记忆管理和工具使用部分,我们明天继续学习。


Browser Use 快速入门

2025 年 3 月 6 号,中国 AI 创业公司 Monica 发布 Manus,号称 “全球首款通用 AI 代理”,其应用场景覆盖旅行规划、股票分析、教育内容生成等 40 余个领域;据称,Manus 在 GAIA 基准测试中刷新了 SOTA 记录,性能远超同类产品,凭借 KOL 助力,一时间刷屏全网,内测邀请码一码难求,甚至被炒到 5 万块钱。

在 Manus 的演示中,有一项功能让人印象深刻:在处理用户问题时,它能够像人类一样操作浏览器,自主浏览网页,滚动页面,检索信息。根据后来网友的爆料,其核心功能使用的是开源的 Browser Use 项目。

Manus 的本质仍然是 Agent 调用工具那一套,它的亮点主要是产品体验,但本身并没有多大的技术门槛;在 Monica 发布 Manus 之后,MetaGPT 团队仅花费 3 小时就开发了 OpenManus 项目,也能够自主浏览网页,查询和总结信息,实现了和 Manus 类似的功能,得到社区的广泛关注;而它同样也使用了 Browser Use 项目。

Browser Use 介绍

Browser Use 是一款基于 Python 的开源 AI 自动化工具,旨在通过集成大模型与浏览器操作,实现网页任务自动化和智能化。它可以作为 Agent 操作浏览器的工具进行使用,它提供了一个简单的接口,使得 Agent 能够执行各种浏览器操作,如导航、点击、输入文本等。

browser-use-home.png

它的核心特性如下:

  • 视觉 + HTML 提取:结合视觉理解与 HTML 结构提取,实现全面的网页交互;
  • 多标签管理:自动处理多个浏览器标签,以应对复杂的工作流程和并行处理;
  • 元素跟踪:提取点击元素的 XPath 并重复执行精确的 LLM 操作以实现一致的自动化;
  • 自定义操作:添加用户自定义操作,例如保存文件、访问数据库、通知或人类输入处理;
  • 自我纠正:智能错误处理和自动恢复以实现强大的自动化工作流程;
  • 任意 LLM 支持:与所有 LangChain LLM 兼容,包括 GPT-4、Claude 3 和 Llama 2 等;

browser-use-features.png

Browser Use 在 WebVoyager 基准测试中达到了 SOTA 性能,使用 GPT-4o 在 586 个多样化的 Web 任务中取得了令人印象深刻的 89.1% 成功率:

browser-use-sota.png

感兴趣的话,可以看下他们的技术报告:

下面我们通过一个简单的示例来体验下 Browser Use 的基本功能。

环境准备

Browser Use 需要 Python 3.11 或更高版本,官方建议使用 uv 来创建 Python 虚拟环境:

$ uv venv --python 3.11

激活该虚拟环境:

$ source .venv/bin/activate

安装 Browser Use 及其相关的依赖项:

$ uv pip install browser-use

Browser Use 依赖于 Playwright 来操作浏览器,所以还需要下载并安装 Playwright 所需的浏览器(如 Chromium、Firefox 和 WebKit)及其相关的依赖项:

$ uv run playwright install

简单示例

下面是 Browser Use 的简单示例:

from dotenv import load_dotenv
load_dotenv()

from langchain_openai import ChatOpenAI
llm = ChatOpenAI(model="gpt-4.1-mini")

from browser_use import Agent
async def main():
    agent = Agent(
        task="Compare the price of gpt-4.1-mini and DeepSeek-V3",
        llm=llm,
    )
    result = await agent.run()
    print(result)

import asyncio
asyncio.run(main())

首先,我们需要设置大模型的 API KEY,可以将其保存在 .env 文件中,在程序入口处,通过 load_dotenv() 从中读取环境变量,然后初始化一个 LLM 实例。Browser Use 统一使用 LangChain 的 Chat models 接口 来调用大模型,不同的模型对应的环境变量也不同,比如 OpenAI 的环境变量如下:

OPENAI_API_KEY=sk-xxx
OPENAI_API_BASE=https://api.bianxie.ai/v1

要注意的是,并不是所有的模型都可以用,只有支持工具调用的模型才可以,比如 OpenAI、Anthropic、Gemini、DeepSeek-V3 等。对于这些比较热门的模型,官方也给出了相应的示例供参考:

紧接着,初始化一个 Agent 智能体,传入 LLM 实例和我们的任务,最后调用 agent.run() 启动智能体。

运行结果

在上面的示例中,我们的任务是 “对比 GPT-4.1 mini 和 DeepSeek-V3 的价格”,可以看到 Browser Use 首先打开浏览器:

INFO     [telemetry] Anonymized telemetry enabled. See https://docs.browser-use.com/development/telemetry for more information.
INFO     [agent] 🧠 Starting a browser-use agent v0.2.5 with base_model=gpt-4.1-mini +tools +vision +memory extraction_model=gpt-4.1-mini 
INFO     [agent] 🚀 Starting task: Compare the price of gpt-4.1-mini and DeepSeek-V3
INFO     [browser] 🌎 Launching local browser driver=playwright channel=chromium user_data_dir=~/.config/browseruse/profiles/default
INFO     [agent] 📍 Step 1: Evaluating page with 0 interactive elements on: about:blank

然后直接导航到 https://www.google.com

INFO     [agent] 🧠 LLM call => ChatOpenAI [✉️ 7 msg, ~3153 tk, 31265 char, 📷 img] => JSON out + 🔨 20 tools (function_calling)
INFO     [agent] 👍 Eval: Success - The browser is initialized at a blank page with no errors.
INFO     [agent] 🧠 Memory: Started the task to compare prices of gpt-4.1-mini and DeepSeek-V3. At step 1 of 100.
INFO     [agent] 🎯 Next goal: Navigate to a search engine to find pricing information for gpt-4.1-mini and DeepSeek-V3.
INFO     [controller] 🔗  Navigated to https://www.google.com
INFO     [agent] ☑️ Executed action 1/1: go_to_url
INFO     [agent] 📍 Step 2: Ran 1 actions in 8.03s: ✅ 1
INFO     [agent] 📍 Step 2: Evaluating page with 18 interactive elements on: https://www.google.com/

在页面上找到输入框,并输入 gpt-4.1-mini price

INFO     [agent] 🧠 LLM call => ChatOpenAI [✉️ 10 msg, ~3558 tk, 138362 char, 📷 img] => JSON out + 🔨 20 tools (function_calling)
INFO     [agent] 👍 Eval: Success - The Google search homepage loaded correctly with the search input textarea and search button available.
INFO     [agent] 🧠 Memory: Navigated to Google homepage to search for pricing information on gpt-4.1-mini and DeepSeek-V3. At step 2 of 100.
INFO     [agent] 🎯 Next goal: Input the search query 'gpt-4.1-mini price' into the search box and submit the search.
INFO     [controller] ⌨️  Input gpt-4.1-mini price into index 4
INFO     [agent] ☑️ Executed action 1/2: input_text
INFO     [agent] Element index changed after action 1 / 2, because page changed.
INFO     [agent] 📍 Step 3: Ran 2 actions in 7.46s: ✅ 2
INFO     [agent] 📍 Step 3: Evaluating page with 24 interactive elements on: https://www.google.com/

2.png

然后找到 “搜索” 按钮,并点击提交:

INFO     [agent] 🧠 LLM call => ChatOpenAI [✉️ 14 msg, ~3891 tk, 189291 char, 📷 img] => JSON out + 🔨 20 tools (function_calling)
INFO     [agent] 👍 Eval: Success - The search query 'gpt-4.1-mini price' is entered and Google search options are presented with the 'Google 搜索' button available.
INFO     [agent] 🧠 Memory: Entered search query 'gpt-4.1-mini price' on Google. At step 3 of 100.
INFO     [agent] 🎯 Next goal: Click the 'Google 搜索' button to perform the search for 'gpt-4.1-mini price'.
INFO     [controller] 🖱️  Clicked button with index 14: 
INFO     [agent] ☑️ Executed action 1/1: click_element_by_index
INFO     [agent] 📍 Step 4: Ran 1 actions in 7.17s: ✅ 1
INFO     [agent] 📍 Step 4: Evaluating page with 43 interactive elements on: https://www.google.com/search?q=gpt-4.1-mini+price...

进入 Google 搜索页面:

3.png

接着打开一个新的 Tab 页,搜索 “DeepSeek-V3 price”:

INFO     [agent] 🧠 LLM call => ChatOpenAI [✉️ 17 msg, ~4927 tk, 550034 char, 📷 img] => JSON out + 🔨 20 tools (function_calling)
INFO     [agent] 👍 Eval: Success - Search results loaded with multiple sources indicating pricing for gpt-4.1-mini.
INFO     [agent] 🧠 Memory: Searched for 'gpt-4.1-mini price' on Google and fetched the top search results page with prices visible from multiple sources like OpenAI, Microsoft Azure, LiveChatAI, and LangDB. Step 4 of 100.
INFO     [agent] 🎯 Next goal: Scroll down to find any information or search for 'DeepSeek-V3 price' in a new tab to compare prices of the two models.
INFO     [controller] 🔍 Scrolled down the page by one page
INFO     [agent] ☑️ Executed action 1/2: scroll_down
INFO     [browser] 👁️ Foregound tab changed by human from [0]google.com/search?q=gp… ➡️ [1]google.com/search?q=De… (agent will stay on [1]google.com/search?q=De…)
INFO     [controller] 🔗  Opened new tab with https://www.google.com/search?q=DeepSeek-V3+price
INFO     [agent] ☑️ Executed action 2/2: open_tab
INFO     [agent] 📍 Step 5: Ran 2 actions in 11.81s: ✅ 2
INFO     [agent] 📍 Step 5: Evaluating page with 57 interactive elements on: https://www.google.com/search?q=DeepSeek-V3+price

4.png

从搜索页面中提取 DeepSeek-V3 的价格:

INFO     [agent] 🧠 LLM call => ChatOpenAI [✉️ 21 msg, ~5107 tk, 539567 char, 📷 img] => JSON out + 🔨 20 tools (function_calling)
INFO     [agent] 👍 Eval: Success - DeepSeek-V3 pricing information is visible on the page from several sources, including DeepSeek API Docs and LiveChatAI, showing token input/output costs and fees.
INFO     [agent] 🧠 Memory: Navigated to DeepSeek-V3 price search results on Google. Observed available price data for DeepSeek-V3 including standard prices and fees. At step 5 of 100.
INFO     [agent] 🎯 Next goal: Extract detailed pricing information for DeepSeek-V3 from visible snippets and then switch back to the GPT-4.1-mini tab to extract its pricing for comparison.
INFO     [controller] 📄  Extracted from page
: 
{
  "DeepSeek-V3 Pricing Details": {
    "Token Pricing": {
      "Input Tokens": {
        "Cache Hit": "$0.07 per 1 million tokens",
        "Cache Miss": "$0.27 per 1 million tokens",
        "Alternative Source": "$0.000140 per 1,000 input tokens (equivalent to $0.14 per 1 million tokens)"
      },
      "Output Tokens": {
        "Standard": "$1.10 per 1 million tokens",
        "Alternative Source": "$0.000280 per 1,000 output tokens (equivalent to $0.28 per 1 million tokens)",
        "Microsoft Source": "$0.00456 per 1,000 output tokens (equivalent to $4.56 per 1 million tokens)"
      }
    },
    "Additional Fees": {
      "Fee": "$0.0004 (currency/unit not specifically defined, possibly per 1,000 tokens or per request)"
    },
    "Pricing Notes": [
      "Price varies depending on whether input tokens are cache hits or misses, with cache hits being significantly cheaper.",
      "There was a 45-day promotional trial period ending with price adjustments (e.g., RMB0.5 for cache hit and RMB2 for cache miss per million tokens).",
      "Different pricing may appear across platforms—some mention prices in RMB or USD with slight variations.",
      "Microsoft Azure AI Foundry lists DeepSeek-V3 input at $0.00114 per 1,000 tokens and output at $0.00456 per 1,000 tokens, which is higher than other reported prices.",
      "Use via OpenRouter is indicated as free, but presumably with limits or different terms."
    ]
  }
}

INFO     [agent] ☑️ Executed action 1/1: extract_content
INFO     [agent] 📍 Step 6: Ran 1 actions in 15.61s: ✅ 1
INFO     [agent] 📍 Step 6: Evaluating page with 57 interactive elements on: https://www.google.com/search?q=DeepSeek-V3+price

然后又切回到第一个 Tab 页面,从页面结果中提取出 GPT-4.1-mini 的价格:

INFO     [agent] 🧠 LLM call => ChatOpenAI [✉️ 24 msg, ~5877 tk, 541068 char, 📷 img] => JSON out + 🔨 20 tools (function_calling)
INFO     [agent] 👍 Eval: Success - Extracted detailed pricing information for DeepSeek-V3 from the search snippets. Pricing includes token costs for input and output, additional fees, and varied pricing notes.
INFO     [agent] 🧠 Memory: Extracted DeepSeek-V3 pricing details: Input tokens cost between $0.07 and $0.27 per million tokens depending on cache hit/miss, output tokens cost $1.10 per million tokens standard, with some alternative prices noted. Additional fees of $0.0004 apply. Now need to extract pricing information for gpt-4.1-mini for comparison. Step 6 of 100.
INFO     [agent] 🎯 Next goal: Switch to the GPT-4.1-mini tab and extract the pricing information from the page content.
INFO     [controller] 🔄  Switched to tab 0
INFO     [agent] ☑️ Executed action 1/2: switch_tab
INFO     [controller] 📄  Extracted from page
: 
{
  "gpt-4.1-mini_pricing": {
    "source_dates": ["2025-04-14"],
    "usage_pricing_per_1M_tokens": {
      "input_tokens": "$0.40",
      "output_tokens": "$1.60"
    },
    "fine_tuning_pricing_per_1M_tokens": {
      "input": "$0.80"
    },
    "alternative_pricing_reports": [
      {
        "source": "Microsoft Azure",
        "regional_input_price_per_1M_tokens": "$0.44",
        "cached_input_price_per_1M_tokens": "$0.11"
      },
      {
        "source": "Artificial Analysis",
        "blended_price_per_1M_tokens": "$0.26",
        "input_token_price": "$0.15",
        "output_token_price": "$0.60"
      },
      {
        "source": "LiveChatAI",
        "input_token_price_per_1M": "$1.10 (o3-mini model compared to GPT-4.1's $2.00)"
      }
    ],
    "summary": "The main consistent pricing for GPT-4.1-mini from multiple sources lists $0.40 per 1M input tokens and $1.60 per 1M output tokens. Fine-tuning input price is $0.80 per 1M tokens. Some regional or blended prices vary slightly."
  }
}

INFO     [agent] ☑️ Executed action 2/2: extract_content
INFO     [agent] 📍 Step 7: Ran 2 actions in 15.10s: ✅ 2
INFO     [agent] 📍 Step 7: Evaluating page with 100 interactive elements on: https://www.google.com/search?q=gpt-4.1-mini+price...

最后,综合两次搜索结果,得到最终结论:

INFO     [agent] 🧠 LLM call => ChatOpenAI [✉️ 28 msg, ~6941 tk, 555953 char, 📷 img] => JSON out + 🔨 20 tools (function_calling)
INFO     [agent] 👍 Eval: Success - Extracted pricing information for gpt-4.1-mini from current search results. Pricing shows $0.40 per 1 million input tokens and $1.60 per 1 million output tokens as main consistent pricing from multiple sources. Various alternative prices also noted.
INFO     [agent] 🧠 Memory: Extracted pricing details for DeepSeek-V3 and gpt-4.1-mini. DeepSeek-V3 input token prices range from $0.07 to $0.27 per million tokens, and output token price around $1.10 per million tokens. gpt-4.1-mini input tokens cost $0.40 per million tokens and output tokens cost $1.60 per million tokens. Step 7 of 100.
INFO     [agent] 🎯 Next goal: Complete the task by summarizing and comparing the prices of gpt-4.1-mini and DeepSeek-V3.
INFO     [agent] ☑️ Executed action 1/1: done
INFO     [agent] 📄 Result: Comparison of prices for gpt-4.1-mini and DeepSeek-V3: 

- gpt-4.1-mini pricing: Approximately $0.40 per 1 million input tokens and $1.60 per 1 million output tokens. Some alternative regional prices and blended prices vary but are generally close.

- DeepSeek-V3 pricing: Input token costs vary from around $0.07 (cache hit) to $0.27 (cache miss) per 1 million tokens, with output tokens costing about $1.10 per 1 million tokens standard. There are also alternative prices reported with some discrepancies across platforms.

Summary: DeepSeek-V3 generally offers cheaper input token pricing especially when cache hits apply, but output token pricing is somewhat lower for DeepSeek-V3 compared to gpt-4.1-mini. Overall, gpt-4.1-mini is priced higher on both input and output tokens relative to the cache-hit price of DeepSeek-V3 but can be comparable or slightly lower than cache-miss prices or some alternative sources for DeepSeek-V3.
INFO     [agent] 📍 Step 8: Ran 1 actions in 16.25s: ✅ 1
INFO     [agent] ✅ Task completed successfully
INFO     [browser] 🛑 Stopped the chromium browser keep_alive=False user_data_dir=~/.config/browseruse/profiles/default cdp_url=None pid=27541

官方案例

官方文档中还列出了一些使用 Browser Use 的案例,比如:

  • 文档编写:在 Google 文档中写一封信给我的爸爸,感谢他所做的一切,并将文档保存为 PDF。
  • 简历投递:阅读我的简历并寻找机器学习职位,将其保存到文件中,然后在新标签页中开始申请。
  • 航班查询:在 kayak.com 上查找从苏黎世到北京的航班。
  • 数据收集:在 Hugging Face 上查找具有 cc-by-sa-4.0 许可证的模型,并按点赞数排序,保存前 5 个到文件中。

感兴趣的同学可以直接查看官方文档中的视频:


使用 OpenMemory MCP 跨客户端共享记忆

在人工智能飞速发展的今天,大型语言模型已经深刻改变了我们与技术交互的方式。然而,这些强大的 AI 助手却有一个显著的局限性 —— 它们无法在不同对话之间保持记忆,每次交互都如同初次见面。不仅如此,随着现在的 AI 助手越来越多,比如 Cursor、Claude Desktop、Windsurf、Cline 等等,记忆的碎片化只会越来越严重,我们要不断的告诉这些 AI 助手关于你的偏好、工作习惯和历史对话。

今天,给大家介绍一个由 Mem0 提出的解决方案 —— OpenMemory MCP,基于 Mem0 技术构建的私有化、本地优先的 AI 记忆层,以及 MCP 标准协议,在自己的设备上打造一个专属 AI 的记忆系统,能够让所有支持 MCP 协议的 AI 助手获得持久的、上下文感知的记忆能力。

open-memory-mcp.png

核心特性

OpenMemory 的核心特性如下:

  • 个性化互动(Personalise Interaction) - 让你的 AI 助手记住你的风格、你的偏好、过去的问题以及首选解决方案;
  • 跨客户端支持(Supported Clients) - 兼容所有支持 MCP 协议的客户端,如 Cursor、Claude Desktop、Windsurf、Cline 等,在不同的客户端之间切换时不会丢失你的记忆;
  • 私有的持久存储(Private, Persistent Storage) - 隐私优先设计,所有的记忆内容都安全地存储在你的本地设备;
  • 完全控制(Full Memory Control) - 你对你的记忆拥有完全的控制权,你可以决定保存什么,何时过期,以及哪些 MCP 客户端可以访问它。

环境搭建

接下来,我们在本地安装 OpenMemory MCP Server。首先,克隆代码并进入 openmemory 目录:

$ git clone https://github.com/mem0ai/mem0.git
$ cd mem0/openmemory

创建环境变量文件:

$ make env

这个命令会在 api 目录和 ui 目录下各创建一个 .env 文件。我们需要在 api/.env 文件中配上 OPEN_API_KEY 参数,如果使用的是兼容 OpenAI 的三方接口,还需要配上 OPENAI_BASE_URL 参数:

OPENAI_API_KEY=sk-xxx
OPENAI_BASE_URL=https://api.bianxie.ai/v1

接下来,构建镜像:

$ make build

这个命令会基于当前目录下的 docker-compose.yml 文件创建两个镜像:

  • mem0/openmemory-mcp - 包含后端 API 和 MCP 服务器;
  • mem0/openmemory-ui - 前端 React 应用程序,OpenMemory 控制台页面;

如果镜像构建没问题,就可以启动服务了:

$ make up

在 Docker Desktop 中查看运行的三个容器(除了前后端,还有一个 Qdrant 向量存储):

open-memory-containers.png

OpenMemory MCP Server 地址为 http://localhost:8765,可以通过 http://localhost:8765/docs 查看 OpenMemory API 文档:

open-memory-api.png

另外,访问 http://localhost:3000/ 进入 OpenMemory 控制台:

open-memory-dashboard.png

在 Cherry Studio 中使用 OpenMemory MCP

接下来,让我们体验下 OpenMemory MCP 是如何工作的。这里我使用了 Cherry Studio 客户端,这是一款国产开源的 AI 客户端,支持绝大多数大模型服务商,也支持知识库、绘图、搜索、翻译、MCP 等特色功能:

cherry-studio-home.png

下载并安装客户端后,进入 “设置” -> “MCP 服务器” 页面,直接配置 SSE 地址:

http://localhost:8765/mcp/openmemory/sse/aneasystone

Cherry Studio 和 MCP Server 建立连接后,会列出所有可用工具:

cherry-studio-mcp-setting.png

可以看到 OpenMemory MCP 提供了四个工具:

  • add_memories : 存储新的记忆对象
  • search_memory : 检索相关记忆
  • list_memories : 查看所有存储的记忆
  • delete_all_memories : 完全清除记忆

然后打开新会话,将 MCP Server 启用:

cherry-studio-chat-enable-mcp.png

将自己的信息告诉它,让他记住:

cherry-studio-chat.png

可以看到它成功调用 add_memories 工具,将我的信息保存到记忆中。接下来,再开一个新会话,测试下它是否真的记住了我:

cherry-studio-chat-2.png

在 Claude Desktop 中使用 OpenMemory MCP

此时我们可以在 OpenMemory 控制台的 Apps 页面查看由 Cherry Studio 创建的记忆:

open-memory-apps.png

这些记忆可以共享给其他客户端使用,接下来,我们就在 Claude Desktop 中验证。下载并安装 Claude Desktop 后,注册并登录账号,确保能正常对话。

然后,从 OpenMemory 控制台上找到 Claude 的安装命令:

$ npx install-mcp i http://localhost:8765/mcp/claude/sse/aneasystone --client claude

运行该命令,输入 MCP Server 名称:

claude-mcp-install.png

这个命令会自动修改 Claude 的配置文件 claude_desktop_config.json

{
  "mcpServers": {
    "openmemory": {
      "command": "npx",
      "args": [
        "-y",
        "supergateway",
        "--sse",
        "http://localhost:8765/mcp/claude/sse/aneasystone"
      ]
    }
  }
}

由于 Claude 只支持 STDIO 传输协议的 MCP Server,这里使用 supergateway 将 SSE 转换为 STDIO 协议。不过最新的 supergateway 貌似有问题,每次 Claude 连接 MCP Server 时就会自动断掉,我这里将其换成了 mcp-remote 才可以:

{
  "mcpServers": {
    "openmemory": {
      "command": "npx",
      "args": [
        "mcp-remote",
        "http://localhost:8765/mcp/claude/sse/aneasystone"
      ]
    }
  }
}

配置之后,重启 Claude Desktop 应用,Claude Desktop 在启动时会自动加载所有的 MCP Server(其实就是为每个 Server 启动一个独立的进程,运行配置文件中的命令)。加载成功后,在对话框下方会看到一个工具图标,点开后可以看到加载成功的 OpenMemory MCP Server 和工具列表:

claude-mcp-setting.png

这时我们就可以在 Claude 里验证下它知不知道我是谁:

claude-chat.png


学习 Mem0 的 API 接口

Mem0 提供了 Python 和 TypeScript 两种 SDK 供开发者选用,并且支持将其接口暴露成 REST API 以方便其他语言和工具集成。我们今天系统地学习下 Mem0 提供的这些接口。

记忆管理

我们以 Python SDK 为例,介绍下 Mem0 记忆管理相关的接口。首先初始化 Memory 实例:

from mem0 import Memory

m = Memory()

如果要对 Memory 进行自定义配置,则通过 Memory.from_config() 初始化:

from mem0 import Memory

config = {
    // ...
}

m = Memory.from_config(config)

关于自定义配置选项,前几天已经详细地学习过,可以参考前几天的内容。

添加记忆

add() 方法用于添加新的记忆,其中 messages 参数可以是一个字符串:

messages = "你好,我叫张三"
result = m.add(messages, user_id="zhangsan")

也可以是一个对象:

messages = {
    "role": "user",
    "content": "你好,我叫张三"
}
result = m.add(messages, user_id="zhangsan")

还可以是一个数组:

messages = [{
    "role": "user",
    "content": "你好,我叫张三"
}, {
    "role": "assistant",
    "content": "您好,张三,请问有什么需要我帮助?"
}]
result = m.add(messages, user_id="zhangsan")

多模态支持

Mem0 支持多模态,可以在对话中添加图片:

messages = [{
    "role": "user",
    "content": "你好,我叫张三"
}, {
    "role": "assistant",
    "content": "您好,张三,请问有什么需要我帮助?"
}, {
    "role": "user",
    "content": "帮我翻译下这张图上的文字"
}, {
    "role": "user",
    "content": {
        "type": "image_url",
        "image_url": {
            "url": "<image-url>"
        }
    }
}]
result = m.add(messages, user_id="zhangsan")

如果使用 Mem0 平台,除了支持图片内容,还支持 在对话中添加文档,比如 TXT、MDX、PDF 等。

助手记忆

我们除了为用户添加记忆之外,也可以为助手添加记忆,比如你正在开发一个文本写作智能体,使用助手记忆维持长上下文的一致性:

result = m.add(messages, agent_id="writer-agent")

关闭推导

默认情况下,我们在添加记忆时,会经过提取和更新两个阶段,Mem0 提供了一个参数可以 关闭这个推导过程,直接存储原始消息:

result = m.add(messages, user_id="zhangsan", infer=False)

查询记忆

在数据库中搜索与用户问题最相关的记忆:

related_memories = m.search("我是谁?", user_id="zhangsan")

Mem0 平台的搜索接口还有几个 高级参数,比如 keyword_search=True 开启关键词搜索,增强搜索召回率,rerank=True 开启重排序,确保最相关的记忆优先出现,filter_memories=True 开启过滤,去除无关的记忆,提高搜索精度。

另外,我们还可以根据 user_id 获取指定用户的所有记忆:

all_memories = m.get_all(user_id="zhangsan")

根据 memory_id 获取指定记忆:

specific_memory = m.get("<id>")

获取指定记忆的变更历史:

history = m.history(memory_id="<id>")

更新记忆

如果发现大模型自动维护的记忆不对,我们可以手动对记忆进行修改:

result = m.update(memory_id="<id>", data="我叫张三丰")

删除记忆

删除指定记忆:

m.delete(memory_id="<id>")

删除用户的所有记忆:

m.delete_all(user_id="zhangsan")

清空所有记忆:

m.reset()

异步记忆

Mem0 提供了一个异步操作记忆的 AsyncMemory 类,它的所有操作都是非阻塞的,这在开发高并发应用程序时非常有用:

from mem0 import AsyncMemory

m = AsyncMemory()

AsyncMemory 中的方法和 Memory 类完全相同,且具有一样的参数,但需要和 async/await 一起使用:

result = await m.add(messages, user_id="zhangsan")

Mem0 REST API

Mem0 提供一个 REST API 服务器,它使用 FastAPI 编写,用户可以通过 HTTP 端点执行所有操作。

首先克隆代码并进入 server 目录:

$ git clone https://github.com/mem0ai/mem0.git
$ cd server

然后在当前目录中创建一个 .env 文件并设置环境变量,运行所需的唯一环境变量是 OPENAI_API_KEY

OPENAI_API_KEY=your-openai-api-key

它默认使用 PGVector 作为向量数据库,Neo4j 作为图数据库,这些按需配置:

POSTGRES_HOST=localhost
POSTGRES_PORT=6333
POSTGRES_DB=test
POSTGRES_USER=
POSTGRES_PASSWORD=
POSTGRES_COLLECTION_NAME=test

NEO4J_URI=neo4j://localhost:7687
NEO4J_USERNAME=neo4j
NEO4J_PASSWORD=password

接着安装所需依赖:

$ pip install -r requirements.txt

最后,启动 REST API 服务器:

$ uvicorn main:app --reload

启动成功后,可以通过 http://localhost:8888 访问,默认会进入 /docs 页面,这是 Mem0 REST API 的 OpenAPI 文档:

rest-api-server.png

这时就可以使用 HTTP 接口访问 Mem0 API 了,也可以使用 MemoryClient 在代码中调用它:

from mem0 import MemoryClient

client = MemoryClient(
    host="http://localhost:8888",
    api_key="xxx",
)

result = client.add("你好,我叫张三", user_id="zhangsan")
print(result)

related_memories = client.search("我是谁?", user_id="zhangsan")
print(related_memories)

如果不设置 host 参数,默认使用的 https://api.mem0.ai,也就是 Mem0 平台的 API 接口。

OpenAI 兼容接口

除了 HTTP 接口,Mem0 还有一个特色功能,它提供了一个 OpenAI 兼容的对话接口,可以轻松地将 Mem0 的长期记忆功能集成到我们的聊天应用程序中。

import os
from mem0.proxy.main import Mem0

client = Mem0(api_key=os.environ.get("MEM0_API_KEY"))

messages = [{
    "role": "user",
    "content": "我喜欢四川美食,但是我不能吃辣"
}]
chat_completion = client.chat.completions.create(
    messages=messages, model="gpt-4o-mini", user_id="zhangsan"
)
print(chat_completion.choices[0].message.content)

messages = [{
    "role": "user",
    "content": "给我推荐一些四川美食",
}]
chat_completion = client.chat.completions.create(
    messages=messages, model="gpt-4o-mini", user_id="zhangsan"
)
print(chat_completion.choices[0].message.content)

Mem0 为此做了一个在线演示页面,实现了类似 ChatGPT 的聊天功能,但是带长期记忆,感兴趣的朋友可以尝试下: