01

AI 怎么操控浏览器?

从一个最简示例出发,看懂 browser-use 如何让 AI 像人一样"看见"网页、"点击"按钮。

5 行代码,AI 就能帮你上网干活

想象一下:你跟 AI 说"去 GitHub 查一下 browser-use 这个仓库有多少星",AI 就真的打开浏览器、搜索、找到结果、告诉你答案。这就是 browser-use 做的事情。

最简的用法只需要这几行:

代码

from browser_use import Agent, ChatBrowserUse

agent = Agent(
    task='Find the number of stars of the following repos: browser-use, playwright, stagehand, react, nextjs',
    llm=ChatBrowserUse(model='bu-2-0'),
)
agent.run_sync()
            
大白话

引入两个主角:Agent 是"干活的机器人",ChatBrowserUse 是它的大脑(AI 模型)。

创建一个 Agent,告诉它任务是什么——这里是要查 5 个 GitHub 仓库的星数。

给它装上 browser-use 官方微调过的模型,专门擅长操控浏览器。

一行启动!run_sync() 会阻塞运行直到任务完成。

💡
关键认知

browser-use 的设计哲学是"一行代码开跑"。你不需要手动管理浏览器进程、写 DOM 选择器、处理弹窗——Agent 把这些全部包了。你的工作就是:定义任务 + 选模型。

完整一点的写法:手动配浏览器

上面的例子用了"默认一切"的写法。如果你想要更多控制——比如用自己的模型、指定浏览器配置——可以这样做:

代码

from browser_use import Agent, Browser, ChatBrowserUse
import asyncio

async def main():
    browser = Browser()

    agent = Agent(
        task="Find the number of stars of the browser-use repo",
        llm=ChatBrowserUse(),
        browser=browser,
    )
    await agent.run()

if __name__ == "__main__":
    asyncio.run(main())
            
大白话

引入 Agent(机器人)、Browser(浏览器实例)、ChatBrowserUse(AI 大脑)。

browser-use 是异步架构,所以主函数要用 async def。

Browser() 创建一个浏览器实例,可以传各种配置参数。

创建 Agent,把任务、大脑、浏览器三者绑定在一起。

agent.run() 启动异步循环,Agent 开始一步步执行任务。

asyncio.run() 是 Python 标准的"启动异步程序"写法。

它到底在做什么?一眼看懂循环

Agent.run() 启动后,内部是一个"观察→思考→行动"的循环,每一步都是:

🤖
Agent
🧠
LLM
🔄
循环判断
点击"下一步"开始演示

这个循环在源码中就是 Agent.run() 里的 while self.state.n_steps <= max_steps 循环,每次循环调用一次 step()

测测你理解了多少

browser-use 的 Agent 执行任务时,内部是什么模式?

02

认识核心角色

Agent、Browser、DOM Service、Tools 四大角色各司其职——搞清谁干什么,源码就通了。

四个角色,一张图看明白

browser-use 的架构像一家小公司:老板下达任务,四个人各干各的活。

🤖

Agent

总指挥。接收用户任务,循环"观察→思考→行动",协调其他三个角色完成目标。源码在 agent/service.py

🌐

Browser / BrowserSession

浏览器管家。通过 CDP 协议 控制真实的 Chromium 浏览器:开页面、截图、管理 Tab。源码在 browser/session.py

👁️

DomService

AI 的眼睛。把网页 DOM 树解析成 LLM 能理解的文本格式——哪些元素可以点击、输入框在哪、按钮叫什么。源码在 dom/service.py

🔧

Tools

AI 的手。把 LLM 输出的"动作意图"翻译成真实浏览器操作——点击、输入、滚动、导航。源码在 tools/service.py

角色对话:一次完整执行长什么样?

下面模拟一次"查 GitHub 星数"任务中四个角色之间的对话,帮你感受数据在它们之间怎么流动的:

Agent 的 __init__:像搭积木一样组装

Agent 的构造函数接收大量参数,但核心就是"把四个角色绑在一起"。看看源码中最关键的几行:

代码

class Agent(Generic[Context, AgentStructuredOutput]):
    def __init__(
        self,
        task: str,
        llm: BaseChatModel | None = None,
        browser_session: BrowserSession | None = None,
        browser: Browser | None = None,
        tools: Tools[Context] | None = None,
        controller: Tools[Context] | None = None,
        ...
    ):
            
大白话

Agent 是一个泛型类,支持自定义上下文和结构化输出类型。

task 是必填参数——你要 AI 干什么。

llm 是 AI 大脑,不传就用默认的 ChatBrowserUse。

browser_session 和 browser 是同一个东西(别名),传一个就行。

tools 和 controller 也是别名——是"手",不传就用默认的 Tools()。

省略号代表还有几十个可选参数,比如超时、截图、敏感数据等。

📌
设计模式

browser-use 大量使用"别名参数"模式:browserbrowser_session 是同一个东西,toolscontroller 也是。这让 API 对新手更友好——不管你叫哪个名字都能用。

测测你理解了多少

browser-use 中,哪个角色负责把网页 HTML 翻译成 LLM 能理解的文本?

03

AI 的眼睛和手

DomService 怎么把网页变成 AI 能读的文本?Tools 怎么把 AI 的想法变成真实操作?

DomService:给 AI 一双"读网页"的眼睛

LLM 不能直接看 HTML——它需要一段结构化的文本描述,告诉它"页面上有什么、在哪里、能干什么"。DomService 就是干这个的:

代码

class DomService:
    def __init__(
        self,
        browser_session: 'BrowserSession',
        cross_origin_iframes: bool = False,
        paint_order_filtering: bool = True,
        max_iframes: int = 100,
        max_iframe_depth: int = 5,
        viewport_threshold: int | None = 1000,
    ):
            
大白话

DomService 绑定一个浏览器会话——它要解析哪个页面的 DOM,就看这个会话。

cross_origin_iframes:要不要解析跨域的 iframe(嵌套网页)。默认不解析,安全。

paint_order_filtering:按绘制顺序过滤元素——被遮挡的元素不传给 AI,减少噪音。

max_iframes / max_iframe_depth:防止嵌套太多 iframe 导致性能爆炸。

viewport_threshold:视口阈值——超出视口太远的元素会被标记为"不可见"。

DomService 最终调用 DOMTreeSerializer 把 DOM 树变成一段文本。这段文本长这样:

代码

<page_stats>
12 links, 5 interactive, 1 iframes, 48 total elements
</page_stats>

[Start of page]
<nav>
  [<a href="/">] Home
  [<input type="search" placeholder="Search..."*>]
</nav>
[End of page]
            
大白话

先是页面统计:有多少链接、多少可交互元素、多少 iframe——给 AI 一个全局概览。

然后是页面的结构化表示,用类似 HTML 的格式,但只保留 AI 需要关心的元素。

方括号标记可交互元素(链接、输入框、按钮),AI 看到这些就知道"这里可以点击/输入"。

[Start/End of page] 告诉 AI 当前视口是否覆盖了整个页面——没覆盖就要滚动。

Tools:把 AI 的想法变成真实操作

LLM 返回的不是 Python 代码,而是结构化的 JSON,像这样:{"click": {"index": 5}}。Tools 类负责把这个 JSON 翻译成真实的浏览器操作。

代码

class ClickElementAction(BaseModel):
    index: int | None = Field(
        default=None, ge=1,
        description='Element index from browser_state'
    )
    coordinate_x: int | None = Field(
        default=None,
        description='Horizontal coordinate relative to viewport left edge'
    )
    coordinate_y: int | None = Field(
        default=None,
        description='Vertical coordinate relative to viewport top edge'
    )
            
大白话

ClickElementAction 是一个 Pydantic 模型——定义了"点击"动作的参数格式。

index:通过元素编号点击。DomService 给每个可交互元素编了号,AI 引用编号就行。

coordinate_x / y:通过坐标点击。有些模型(如 Claude Sonnet 4)支持坐标点击,更精准。

两种方式二选一——有 index 用 index,没有就用坐标。

数据流动:从网页到 AI 再到手

一次完整的"点击"操作,数据经历了这些环节:

🌐
Browser
👁️
DomService
🧠
LLM
🔧
Tools
点击"下一步"开始演示

不只是点击:Tools 支持的所有动作

browser-use 内置了大量动作类型,每种都对应一个 Pydantic 模型:

NavigateAction 打开一个 URL——就像你在地址栏输入网址按回车
ClickElementAction 点击元素——通过编号或坐标,就像鼠标左键单击
InputTextAction 在输入框里打字——先点击,再输入文字
ScrollAction 滚动页面——上下滚、滚到指定元素位置
SearchAction 用搜索引擎搜索——默认用 DuckDuckGo(验证码少)
ExtractAction 从当前页面提取信息——用 LLM 从页面内容中抽取结构化数据
SendKeysAction 发送键盘按键——回车、Tab、Escape 等特殊键
DoneAction 任务完成!告诉 Agent "我搞定了",并附上最终答案
💡
关键设计

所有动作都用 Pydantic 模型定义——这意味着 LLM 的输出会被自动校验。如果 AI 说了"点击编号 -1 的元素",Pydantic 会拒绝(因为 ge=1 限制最小值为 1)。这种"在数据层面保证安全"的思路很值得学习。

测测你理解了多少

LLM 怎么知道要点击哪个元素?

04

多模型大脑

browser-use 怎么同时支持 OpenAI、Anthropic、Google、Ollama 等十几种模型?秘密在 BaseChatModel 协议。

十一个 Chat 类,一个统一协议

__init__.py 的导出列表里,你能看到 browser-use 支持的所有模型:

代码

__all__ = [
    'Agent', 'BrowserSession', 'Browser',
    'Controller', 'DomService', 'SystemPrompt',
    # Chat models
    'ChatOpenAI',
    'ChatGoogle',
    'ChatAnthropic',
    'ChatBrowserUse',
    'ChatGroq',
    'ChatLiteLLM',
    'ChatMistral',
    'ChatAzureOpenAI',
    'ChatOCIRaw',
    'ChatOllama',
    'ChatVercel',
]
            
大白话

核心导出就那几个:Agent、Browser、Controller、DomService。

然后是整整 11 个 Chat 类!每个对应一家 AI 模型提供商。

ChatBrowserUse 是官方微调模型,专门为浏览器操控优化。

ChatOllama 支持本地模型——不想把数据发到云端就用它。

ChatLiteLLM 是个万能适配器,能连接 100+ 模型提供商。

这 11 个类之所以能互换使用,是因为它们都遵循同一个协议:BaseChatModel

BaseChatModel:一份"大脑入职合同"

Protocol 是 Python 的接口机制。BaseChatModel 定义了"作为一个合格的 AI 大脑,你必须会什么":

代码

@runtime_checkable
class BaseChatModel(Protocol):
    model: str

    @property
    def provider(self) -> str: ...

    @property
    def name(self) -> str: ...

    async def ainvoke(
        self,
        messages: list[BaseMessage],
        output_format: type[T] | None = None,
        **kwargs: Any
    ) -> ChatInvokeCompletion[T] | ChatInvokeCompletion[str]: ...
            
大白话

@runtime_checkable 让你能在运行时检查一个对象是否符合协议。

model: str —— 你得告诉别人你是什么模型(比如 "gpt-4o"、"claude-sonnet-4-6")。

provider 属性 —— 你是哪家的?("openai"、"anthropic"、"google"...)

name 属性 —— 你的显示名称。

ainvoke 方法 —— 这是核心!接收消息列表,返回 AI 的回复。支持结构化输出(output_format)。

💡
设计洞察

browser-use 用 Protocol 而非抽象基类,意味着你甚至可以传入自己写的类,只要它有 model 属性和 ainvoke 方法,Agent 就能用——零侵入式扩展。

以 ChatOpenAI 为例:怎么跟具体模型通信

看看 ChatOpenAI 是怎么实现 BaseChatModel 协议的:

代码

@dataclass
class ChatOpenAI(BaseChatModel):
    model: ChatModel | str
    temperature: float | None = 0.2
    frequency_penalty: float | None = 0.3
    api_key: str | None = None
    base_url: str | httpx.URL | None = None
    max_retries: int = 5

    @property
    def provider(self) -> str:
        return 'openai'
            
大白话

用 @dataclass 自动生成 __init__,省去手写样板代码。

model 是必填的——"gpt-4o"、"gpt-4.1-mini" 等等。

temperature 默认 0.2——偏向确定性输出,适合自动化场景。

frequency_penalty 0.3 防止模型无限重复(比如死循环输出制表符)。

max_retries=5:浏览器自动化网络不稳定,多试几次更可靠。

provider 属性返回 "openai"——Agent 用它来判断模型类型,自动调整提示词。

Agent 怎么根据模型自动调参?

Agent 初始化时会检查模型类型,自动调整行为:

代码

# set flashmode = True if llm is ChatBrowserUse
if llm.provider == 'browser-use':
    flash_mode = True

# Auto-configure llm_screenshot_size for Claude Sonnet
if llm_screenshot_size is None:
    model_name = getattr(llm, 'model', '')
    if isinstance(model_name, str) and model_name.startswith('claude-sonnet'):
        llm_screenshot_size = (1400, 850)

# DeepSeek models do not support vision
if 'deepseek' in self.llm.model.lower():
    self.settings.use_vision = False
            
大白话

如果用的是 browser-use 官方微调模型,自动开启 flash_mode(精简提示词,更快)。

如果是 Claude Sonnet,自动把截图缩放到 1400x850——这个模型对图片大小有偏好。

如果是 DeepSeek 模型,关闭视觉功能——它还不支持图片理解。

这就是 provider 属性的价值:Agent 能根据模型自动做适配。

⚠️
踩坑提醒

如果你用了 DeepSeek 或 Grok-3 模型但忘了关闭 use_vision,Agent 会自动帮你关并打印警告。但最好一开始就设置好,避免浪费 API 调用。

测测你理解了多少

如果你想用自己的 AI 模型接入 browser-use,需要怎么做?

05

从看懂到会改

自定义 Action、System Prompt,以及实际调试技巧——学完这章你就能魔改 browser-use 了。

SystemPrompt:AI 的"工作手册"

Agent 给 LLM 的系统提示词不只是一段文字,而是一套精心设计的"工作手册"。看看它怎么加载的:

代码

class SystemPrompt:
    def __init__(
        self,
        max_actions_per_step: int = 3,
        override_system_message: str | None = None,
        extend_system_message: str | None = None,
        use_thinking: bool = True,
        flash_mode: bool = False,
        is_anthropic: bool = False,
        is_browser_use_model: bool = False,
        model_name: str | None = None,
    ):
            
大白话

max_actions_per_step:每步最多执行几个动作(默认 3)。限制数量让 AI 更谨慎。

override_system_message:完全替换系统提示词——你说了算。

extend_system_message:在默认提示词后面追加内容——更安全的方式。

use_thinking:让 AI 在行动前先"想一下",输出思考过程。

flash_mode:精简模式——去掉思考和评估,追求速度。

is_anthropic / is_browser_use_model:根据模型类型选择不同的提示词模板。

它内部会根据模型类型选择不同的提示词模板文件:

代码

if self.is_browser_use_model:
    if self.flash_mode:
        template_filename = 'system_prompt_browser_use_flash.md'
    elif self.use_thinking:
        template_filename = 'system_prompt_browser_use.md'
elif self.flash_mode and self.is_anthropic:
    template_filename = 'system_prompt_flash_anthropic.md'
elif self.flash_mode:
    template_filename = 'system_prompt_flash.md'
elif self.use_thinking:
    template_filename = 'system_prompt.md'
else:
    template_filename = 'system_prompt_no_thinking.md'
            
大白话

官方微调模型用专属模板(browser_use 后缀),更精简高效。

每种组合(flash/thinking/anthropic)都有对应的 .md 模板文件。

这意味着你可以在不改源码的情况下,替换这些 .md 文件来定制 AI 行为。

最终,模板里的 {max_actions} 占位符会被替换成实际数字。

自定义 System Prompt:让 AI 更懂你的场景

browser-use 提供两种方式定制系统提示词。最常用的是 extend_system_message——在默认提示词后面加内容:

代码

agent = Agent(
    task='Search for AI news and summarize',
    llm=ChatOpenAI(model='gpt-4o'),
    extend_system_message="""
Additional rules:
1. Always click "Accept cookies" if a cookie banner appears
2. Never sign up or log in to any website
3. If a CAPTCHA appears, skip that site and move on
""",
)
            
大白话

extend_system_message 是追加的——默认提示词还在,你只是在后面加了规则。

规则 1:遇到 Cookie 弹窗自动点"接受"——省得 AI 卡在这里。

规则 2:永远不要注册或登录——防止 AI 乱填表单。

规则 3:遇到验证码就跳过——不浪费时间。

📌
实用技巧

override_system_message 是完全替换,适合深度定制。但新手建议用 extend_system_message,因为默认提示词经过大量测试调优,里面有处理各种边界情况的指令。

AgentMessagePrompt:每次发给 LLM 的完整上下文

每一步,Agent 都会构造一个超长的"用户消息"发给 LLM。这个消息包含了 AI 做决策需要的所有信息:

代码

state_description = (
    '<agent_history>\n'
    + (self.agent_history_description.strip('\n')
       if self.agent_history_description else '')
    + '\n</agent_history>\n\n'
)
state_description += '<agent_state>\n'
    + self._get_agent_state_description().strip('\n')
    + '\n</agent_state>\n'
state_description += '<browser_state>\n'
    + self._get_browser_state_description().strip('\n')
    + '\n</browser_state>\n'
            
大白话

agent_history:之前做了什么——AI 需要知道历史才能避免重复。

agent_state:当前任务描述、文件系统状态、todo 列表、敏感数据等。

browser_state:当前页面的 DOM 元素列表、截图、Tab 信息、滚动位置。

三个 XML 标签把不同类型的信息分块,方便 LLM 区分理解。

循环检测:防止 AI 在原地打转

AI 有时会陷入死循环——反复点击同一个按钮,或者在两个页面之间来回跳。browser-use 内置了 ActionLoopDetector 来应对:

代码

def get_nudge_message(self) -> str | None:
    if self.max_repetition_count >= 12:
        messages.append(
            f'Heads up: you have repeated a similar action'
            f' {self.max_repetition_count} times in the last'
            f' {len(self.recent_action_hashes)} actions.'
        )
    elif self.max_repetition_count >= 5:
        messages.append(
            f'Heads up: you have repeated a similar action'
            f' {self.max_repetition_count} times.'
        )
            
大白话

循环检测器持续统计最近 20 步中每个动作的哈希值出现次数。

重复 5 次:温和提醒"你重复了,还在进步吗?"

重复 12 次:严肃警告"你可能卡住了,换个方法吧。"

注意:这只是"提醒",不是"禁止"。AI 可以忽略提醒继续执行——毕竟有时候重复是合理的(比如翻页)。

💡
工程哲学

browser-use 的循环检测是"软约束"而非"硬约束"——它只给 LLM 提示,不强制阻止动作。这种设计尊重了 AI 的判断力:有时候重复是有意的(比如逐页翻页),硬阻断反而会打断正确行为。

step() 方法:一次完整执行的四阶段

Agent 的核心方法是 step(),每一步都严格按照四个阶段执行:

1
Phase 0: 等待 CAPTCHA(可选)

如果之前在解验证码,先等它解完再继续。

2
Phase 1: 准备上下文(_prepare_context)

截图 + 获取浏览器状态 + 解析 DOM + 更新动作模型。

3
Phase 2: 获取 AI 决策并执行(_get_next_action + _execute_actions)

把上下文发给 LLM,拿到 JSON 动作,Tools 翻译成浏览器操作并执行。

4
Phase 3: 后处理(_post_process)

更新历史、检查下载、判断是否完成、记录遥测数据。

调试技巧:看日志、存对话、设超时

📝
save_conversation_path

设置这个参数,Agent 会把每一步发给 LLM 的完整对话保存到文件——调试时的第一选择。

⏱️
llm_timeout / step_timeout

llm_timeout 控制 LLM 调用的超时(默认 60s),step_timeout 控制整步超时(默认 180s)。网络慢时可以调大。

🪞
max_failures

连续失败多少步后停止(默认 5)。调试时建议设成 2,快速发现问题。

代码

agent = Agent(
    task='Debug example task',
    llm=ChatOpenAI(model='gpt-4o'),
    save_conversation_path='./debug_conversation',
    max_failures=2,
    llm_timeout=90,
    step_timeout=120,
    max_actions_per_step=1,
)
            
大白话

save_conversation_path:把每步对话存到 ./debug_conversation 目录。

max_failures=2:连续失败 2 次就停——快速暴露问题。

llm_timeout=90:给 LLM 90 秒响应时间(默认 60 可能不够)。

step_timeout=120:每步最多执行 120 秒。

max_actions_per_step=1:每步只做一个动作——方便逐步调试。

测测你理解了多少

想在默认提示词基础上加一条规则"遇到 Cookie 弹窗自动点接受",该用哪个参数?