跳到主要内容

02 · 对 browser-use 的薄包装:三个子类到底改了什么

本项目最容易被误解的一点:它不是一个浏览器 agent 的实现,而是上游 browser-use外挂。本章用“改了什么 / 没改什么”的视角,把四个扩展点逐一点清,让你知道哪些行为要去上游读、哪些是本仓库自己加的。

2.0 全局视角:四个扩展点

子类父类(上游)本项目到底加了什么文件
BrowserUseAgentbrowser_use Agent重写工具调用方式选择;重写 run(加信号处理/GIF/脚本导出)src/agent/browser_use/browser_use_agent.py:30
CustomControllerbrowser_use Controller注册“求助/上传文件”动作;改写 act 以分流 MCP;挂 MCP 工具src/controller/custom_controller.py:39
CustomBrowserbrowser_use Browser自定义 Chromium 启动参数、窗口、端口冲突处理src/browser/custom_browser.py:34
CustomBrowserContextbrowser_use BrowserContext什么都没加(只转发构造)src/browser/custom_context.py:15

最后一行很说明问题:CustomBrowserContext 整个类体只有一句 super().__init__(...)(src/browser/custom_context.py:22)。它存在只是为了占住一个未来可能要改的扩展位——目前零改动,诚实地放着。

2.1 BrowserUseAgent:工具调用方式的“自动挡”

要解决的小问题: 不同模型“调用工具”的姿势不一样——有的支持原生 function calling,有的根本没有工具支持,得让模型直接吐原始 JSON。选错姿势就报错或乱答。

思路: 当用户在 UI 选 auto 时,按模型与库名自动挑一种姿势(src/agent/browser_use/browser_use_agent.py:31-45):

# 真实逻辑(节选),browser_use_agent.py:33-43
if tool_calling_method == 'auto':
if is_model_without_tool_support(self.model_name):
return 'raw' # 模型没工具能力 → 让它直接出原始文本
elif self.chat_model_library == 'ChatOpenAI':
return 'function_calling' # OpenAI 系 → 用原生函数调用
elif self.chat_model_library == 'AzureChatOpenAI':
return 'function_calling'
else:
return None # 其余 → 交给上游默认

判断“模型有没有工具支持”用的是上游的 is_model_without_tool_support(browser_use_agent.py:20 导入)。这一处是本项目为“多 LLM 接入”补的兼容垫片——因为它接了很多冷门模型(见 04 章)。

2.2 BrowserUseAgent.run:把上游循环重抄一遍,只为插几个钩子

run 重写了整个主循环(browser_use_agent.py:47-169)。逻辑骨架其实和上游一致——for step in range(max_steps): await self.step(...)——但本项目在前后塞了几样东西:

run(max_steps):
注册 Ctrl+C 信号处理(暂停/恢复/二次中断退出) ← browser_use_agent.py:59-66
执行 initial_actions(若有)
for step in range(max_steps):
若 paused → 等待恢复
若连续失败≥阈值 → break ← :83
若 stopped → break ← :88
on_step_start 回调(UI 截图就靠它)
await self.step(step_info) ← 真正一步,逻辑在上游
on_step_end 回调
若 history.is_done() → 可选校验输出 → break
else: 追加“未在最大步数内完成”的失败记录 ← :113-131
finally: 存 Playwright 脚本 / 关闭 / 生成 GIF ← :144-169

关键: 真正“看一眼网页 → 问 LLM → 执行动作”的 self.step() 没被重写,仍在上游。本项目重写 run 纯粹是为了:

  • SignalHandler 实现 Ctrl+C 暂停/恢复(browser_use_agent.py:59-66);
  • self.state.stopped / paused 这两个标志位被 UI 的停止/暂停按钮驱动(browser_use_agent.py:88-95)——03 章会看到 UI 怎么设这俩标志;
  • 收尾时导出可复现的 Playwright 脚本(browser_use_agent.py:144-157)与 GIF(:164-169)。

2.3 CustomController:两个自定义动作 + MCP 分流

控制器(Controller)在 browser-use 里是“动作注册表 + 执行器”。本项目子类做三件事。

(a) 注册“求助”动作 ask_for_assistant 它的描述本身就是一段给 LLM 的指令——“尽量自主完成,但遇到要凭据/要人判断/复杂验证码这类硬阻塞,就调我求助”(src/controller/custom_controller.py:54-59)。被调用时它转发给 ask_assistant_callback(由 UI 注入),从而把控制权交回给人(03 章详述):

# 真实逻辑(节选),custom_controller.py:60-68
async def ask_for_assistant(query: str, browser: BrowserContext):
if self.ask_assistant_callback:
user_response = await self.ask_assistant_callback(query, browser) # 等人回
msg = f"AI ask: {query}. User response: {user_response['response']}"
return ActionResult(extracted_content=msg, include_in_memory=True)

(b) 注册 upload_file 动作:按元素 index 找到文件上传控件并 set_input_files,且做了白名单(available_file_paths)与存在性校验(custom_controller.py:73-107)。

(c) 重写 act 以分流 MCP 工具。 这是巧妙处:执行动作时,若动作名以 mcp 开头,就绕过上游注册表,直接拿出 MCP 工具对象 ainvoke;否则走上游 execute_action(custom_controller.py:124-140):

# 真实逻辑(节选),custom_controller.py:126-140
if action_name.startswith("mcp"):
mcp_tool = self.registry.registry.actions.get(action_name).function
result = await mcp_tool.ainvoke(params) # MCP 工具单独路径
else:
result = await self.registry.execute_action(action_name, params, ...)

为什么要分流?因为 MCP 工具是运行时动态注册进来的 LangChain 工具,签名/注入参数和上游内置动作不一样,走同一条路会出错(04 章讲注册细节)。

2.4 CustomBrowser:亲手拼 Chromium 启动参数

要解决的小问题: 默认启动的浏览器太“干净”,容易被网站识别为机器人;而且要支持无头/有头、Docker、自定义窗口、远程调试端口。

_setup_builtin_browser 手工拼一组 chrome_args(src/browser/custom_browser.py:43-109):把上游预置的若干参数集合按条件并进来——

# 真实逻辑(节选),custom_browser.py:66-76
chrome_args = {
f'--remote-debugging-port={...}',
*CHROME_ARGS, # 上游基础反检测参数
*(CHROME_DOCKER_ARGS if IN_DOCKER else []), # 在 Docker 里才加
*(CHROME_HEADLESS_ARGS if self.config.headless else []),
*(CHROME_DISABLE_SECURITY_ARGS if self.config.disable_security else []),
f'--window-size={screen_size["width"]},{screen_size["height"]}',
*self.config.extra_browser_args, # 用户额外参数(如 user-data-dir)
}

两个值得记的细节:

  • 端口冲突自愈:启动前用一个 socket connect_ex 探测远程调试端口是否被占,占了就把 --remote-debugging-port 这个参数移除,避免冲突(custom_browser.py:80-82)。
  • new_context 合并配置:把 browser 级与 context 级配置字典浅合并后新建 CustomBrowserContext(custom_browser.py:36-41)——context 级覆盖 browser 级。

巧妙之处

  • 空壳子类占位:CustomBrowserContext 零改动但保留(custom_context.py:15-22),为未来扩展留扩展点,不偷偷塞逻辑。
  • MCP 动作旁路:act 用动作名前缀 mcp 把外部工具与内置动作分两条执行路径(custom_controller.py:126)。
  • 求助动作=给 LLM 的策略提示:把“何时该求助”写进动作描述里,让模型自己判断边界(custom_controller.py:54-59)。
  • 端口探测自愈:启动前主动探测、动态删参,避免重复实例抢同一调试端口(custom_browser.py:80-82)。

边界与坑

  • 子类直接依赖上游内部结构:self.chat_model_libraryself.state.*self.registry.registry.actions 等都是上游字段(browser_use_agent.py:36custom_controller.py:129)。上游一改命名,这里就裂——这是钉死 ==0.1.48 的原因。
  • run 把上游主循环整段重抄而非用钩子复用(browser_use_agent.py:76-133),上游循环逻辑升级时,这份副本不会自动跟上。

代码地图

主题文件符号
工具调用方式自动选择src/agent/browser_use/browser_use_agent.py_set_tool_calling_method
重写主循环 + 信号/GIF/脚本src/agent/browser_use/browser_use_agent.pyrun
自定义动作src/controller/custom_controller.pyask_for_assistant, upload_file
MCP 分流执行src/controller/custom_controller.pyact
Chromium 启动参数src/browser/custom_browser.py_setup_builtin_browser, new_context
空壳 contextsrc/browser/custom_context.pyCustomBrowserContext