browser-use 怎么让 LLM 看懂网页——86k⭐ 浏览器 Agent 的 DOM 处理管线拆解

browser-use 怎么让 LLM "看懂"网页------86k⭐ 浏览器 Agent 的 DOM 处理管线拆解

一个 LLM 看到的网页和你看到的完全不同。你看到按钮、输入框、链接,LLM 看到的是一大坨 HTML。browser-use 在中间做了一件事:把复杂的 DOM 树压缩成 LLM 能理解的精简文本,同时保留足够的交互信息让 Agent 知道该点哪里。

这个项目现在 86k⭐,是 GitHub 上最热的浏览器 Agent 框架。我花了两天读它的源码,发现最有意思的不是 Agent 循环本身,而是 DOM 处理管线------它决定了 LLM 到底能"看到"多少有效信息。

DOM 序列化:从几万个节点到几千字的文本

打开一个普通的电商页面,DOM 节点数量轻松过万。把整棵 DOM 树塞给 LLM?Token 爆炸,而且 99% 的节点对交互来说毫无意义。browser-use 的做法是四步流水线:

css 复制代码
原始 DOM → 简化树 → Paint Order 过滤 → 树优化 → 标注可交互元素

核心代码在 browser_use/dom/serializer/serializer.pyserialize_accessible_elements 方法里:

python 复制代码
def serialize_accessible_elements(self) -> tuple[SerializedDOMState, dict[str, float]]:
    # 第一步:创建简化树(包含可交互元素检测)
    simplified_tree = self._create_simplified_tree(self.root_node)
    
    # 第二步:按绘制顺序过滤被遮挡的元素
    if self.paint_order_filtering and simplified_tree:
        PaintOrderRemover(simplified_tree).calculate_paint_order()
    
    # 第三步:优化树结构(删除多余的父节点)
    optimized_tree = self._optimize_tree(simplified_tree)
    
    # 第四步:给可交互元素分配索引号
    self._assign_interactive_indices_and_mark_new_nodes(filtered_tree)
    
    return SerializedDOMState(_root=filtered_tree, selector_map=self._selector_map)

每一步都在做减法。最终输出的文本可能只有原始 DOM 的 5%-10%,但保留了所有对 Agent 有用的信息。

可交互元素检测:不止是 button 和 input

这是我觉得 browser-use 做得最细致的地方。判断一个元素是否"可交互"不只是看标签名。ClickableElementDetector.is_interactive() 方法用了一套层层递进的判断逻辑:

python 复制代码
class ClickableElementDetector:
    @staticmethod
    def is_interactive(node: EnhancedDOMTreeNode) -> bool:
        # 1. 跳过非元素节点、html 和 body
        if node.tag_name in {'html', 'body'}:
            return False
        
        # 2. 检查 JavaScript 点击事件监听器(通过 CDP 检测)
        # 这个能覆盖 Vue @click、React onClick、Angular (click)
        if node.has_js_click_listener:
            return True
        
        # 3. iframe 大小检查:只标记大于 100x100px 的 iframe
        if node.tag_name.upper() == 'IFRAME':
            if width > 100 and height > 100:
                return True
        
        # 4. label 元素的特殊处理
        # 有 for 属性的 label 跳过(避免双重激活)
        # 包裹了 input/select/textarea 的 label 标记为可交互
        if node.tag_name == 'label':
            if node.attributes and node.attributes.get('for'):
                return False
            if has_form_control_descendant(node, max_depth=2):
                return True
        
        # 5. 搜索相关元素检测
        search_indicators = {
            'search', 'magnify', 'glass', 'lookup', 'find',
            'query', 'search-icon', 'search-btn'
        }
        # 检查 class、id、data-* 属性
        
        # 6. 无障碍属性检查
        # focusable、editable、settable → 可交互
        # checked、expanded、pressed、selected → 可交互
        # disabled、hidden → 不可交互
        
        # 7. 原生交互标签
        interactive_tags = {
            'button', 'input', 'select', 'textarea',
            'a', 'details', 'summary', 'option'
        }

这里有个容易忽略的细节:has_js_click_listener 不是通过解析 HTML 拿到的,是通过 Chrome DevTools Protocol (CDP) 的事件监听器检测获取的。也就是说,哪怕一个 <div> 没有任何 HTML 属性表明它可点击,只要 JavaScript 给它绑了 click 事件,browser-use 也能检测到。

这比传统的"按标签名判断"靠谱很多。真实世界的网页里,大量的可点击元素就是普通的 div 和 span,只靠标签名判断会漏掉一大半。

视口阈值和可见性判断:不发送 LLM 看不到的东西

browser-use 不会把整个页面的所有元素都发给 LLM。它有一个 viewport_threshold 参数,默认 1000 像素------只有在视口范围上下 1000px 内的元素才被标记为"可见":

python 复制代码
class DomService:
    def __init__(self, browser_session, viewport_threshold=1000):
        self.viewport_threshold = viewport_threshold

可见性判断不只看位置,还检查 CSS 样式:

python 复制代码
@classmethod
def is_element_visible_according_to_all_parents(
    cls, node, html_frames, viewport_threshold=1000
):
    computed_styles = node.snapshot_node.computed_styles or {}
    
    display = computed_styles.get('display', '').lower()
    visibility = computed_styles.get('visibility', '').lower()
    opacity = computed_styles.get('opacity', '1')
    
    # display:none、visibility:hidden、opacity:0 都算不可见
    css_hidden = display == 'none' or visibility == 'hidden'
    css_hidden = css_hidden or float(opacity) <= 0

对于 iframe 中超出视口的隐藏元素,browser-use 还会收集它们的信息作为"提示"发给 LLM,告诉 Agent:"往下滚 3 页有个搜索框"。这个设计让 Agent 知道该滚动页面去找目标元素:

python 复制代码
def _count_hidden_elements_in_iframes(self, node):
    # 收集隐藏的可交互元素信息
    hidden.append({
        'tag': subtree_root.tag_name or '?',
        'text': text or '(no label)',
        'pages': pages_down,  # 距离当前视口多少页
    })
    # 限制最多 10 个,避免上下文膨胀
    current_node.hidden_elements_info = hidden[:10]

Paint Order 过滤:被遮挡的按钮不要发给 LLM

这个功能解决了一个实际问题:网页上经常有元素互相覆盖。一个对话框弹出来,后面的按钮被挡住了,但 DOM 里它还是存在的。如果把被遮挡的元素也发给 LLM,Agent 可能会尝试点击一个实际上点不到的按钮。

PaintOrderRemover 会根据元素的绘制顺序(z-index、position 等)计算哪些元素被覆盖了,然后从序列化结果中移除:

python 复制代码
# 序列化时启用 paint order 过滤
if self.paint_order_filtering and simplified_tree:
    PaintOrderRemover(simplified_tree).calculate_paint_order()

这个过滤在实际使用中很有价值。我测试过几个场景:登录弹窗、Cookie 同意横幅、聊天窗口------这些浮层下面的元素都被正确过滤掉了。没有这个,Agent 经常会尝试点击被遮挡的元素然后陷入重试循环。

Agent 循环:think → evaluate → act

DOM 处理管线解决了"让 LLM 看到什么"的问题,Agent 循环解决了"让 LLM 做什么"的问题。browser-use 的 Agent 每一步都走这个流程:

python 复制代码
class Agent:
    async def step(self):
        # 1. 获取浏览器当前状态(DOM + 截图)
        browser_state = await self.browser_session.get_state()
        
        # 2. 构建消息(系统提示 + 当前状态 + 历史记录)
        messages = self.message_manager.build_messages(browser_state)
        
        # 3. 调用 LLM 获取决策
        response = await self.llm.invoke(messages)
        # 返回结构化输出:
        # - current_state.thinking: 思考过程
        # - current_state.evaluation_previous_goal: 上一步评估
        # - current_state.next_goal: 下一步目标
        # - actions: 要执行的动作列表
        
        # 4. 执行动作
        for action in response.actions:
            result = await self.tools.execute(action)

有几个值得注意的设计决策。

每步最多执行 5 个动作。 max_actions_per_step=5 控制 LLM 一次最多输出 5 个动作。太少效率低,太多容易出错------因为后面的动作依赖前面的执行结果,LLM 在预测第 6 个动作时很可能已经偏了。

循环检测。 loop_detection_window=20 会检查最近 20 步是否出现重复操作。实际用下来,Agent 最常见的死循环是反复点击同一个按钮------这个检测能在第 3-4 次重复时就介入。

自动重规划。 planning_replan_on_stall=3 表示连续 3 步没有进展就重新规划。不是重启整个任务,而是重新评估当前状态,选择不同的行动路径。

Vision 模式:截图 + DOM 双通道

browser-use 支持三种 vision 模式:True(每步都截图)、False(纯 DOM)、"auto"(按需截图)。

实际效果差异很大。我用同一个任务(在 Amazon 上搜索商品并加入购物车)测试了纯 DOM 和 Vision 模式:

  • 纯 DOM 模式:8 步完成,有一次点错了下拉菜单的选项
  • Vision 模式:6 步完成,视觉信息帮助 LLM 更准确地理解页面布局

代码里有个有意思的细节------针对 Claude Sonnet 系列模型自动调整截图分辨率:

python 复制代码
# Auto-configure llm_screenshot_size for Claude Sonnet models
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)

Claude Sonnet 在 1400x850 这个分辨率下效果最好,分辨率太高反而会因为 token 消耗增加而降低效率。

实际上手:20 行代码跑一个浏览器 Agent

说了这么多原理,跑起来其实很简单:

python 复制代码
from browser_use import Agent, Browser, ChatBrowserUse
import asyncio

async def main():
    browser = Browser()
    agent = Agent(
        task="去掘金搜索 browser-use,找到星数最高的相关文章,告诉我标题和作者",
        llm=ChatBrowserUse(),  # 也可以用 ChatGoogle 或 ChatAnthropic
        browser=browser,
        use_vision="auto",
        max_actions_per_step=3,
    )
    result = await agent.run()
    print(result)

asyncio.run(main())

安装也就两条命令:

bash 复制代码
uv init && uv add browser-use && uv sync
uvx browser-use install  # 安装 Chromium(如果没有的话)

三个踩坑记录

1. 元素定位偏移。 高 DPI 屏幕上,browser-use 早期版本的坐标计算有 bug------用的是设备像素而不是 CSS 像素。现在的代码专门处理了这个:

python 复制代码
# IMPORTANT: Use CSS viewport instead of device pixel viewport
# This fixes the coordinate mismatch on high-DPI displays
css_visual_viewport = metrics.get('cssVisualViewport', {})

如果你在 Retina 屏的 Mac 上跑,确保用最新版本。

2. iframe 里的元素交互。 browser-use 默认限制 iframe 深度为 5 层、最多 100 个 iframe。如果目标元素在深层嵌套的 iframe 里(比如某些广告平台的后台),可能需要调大 max_iframe_depth

3. LLM 超时设置。 不同模型的响应速度差异很大。browser-use 针对 Gemini Pro 设了 90 秒超时,其他模型默认更短。如果用较慢的自部署模型,记得手动设置 llm_timeout,否则复杂页面的分析会频繁超时。

和 Playwright 直接写脚本比,值不值得用?

如果你的自动化任务是固定流程(每天登录某个后台导出报表),Playwright 脚本更快更稳。browser-use 的价值在于处理模糊任务和动态页面------"帮我在这个网站上找到最便宜的机票"这种,你没法提前写死每一步的 selector。

从源码来看,browser-use 在 DOM 处理上做了大量工作来减少 LLM 的认知负担。可交互元素检测覆盖了 JS 事件监听器、ARIA 属性、搜索相关 class 名等 7 种判断维度;paint order 过滤解决了元素遮挡的误点击问题;viewport threshold 控制了上下文窗口大小。这些细节不读源码很难感受到。

项目地址:github.com/browser-use...

相关推荐
_张一凡2 小时前
【大语言模型学习】2026年最适合新手的小型LLM训练项目全指南:从26M到1B,3块钱就能从头训练
llm·aigc·大语言模型·大语言模型微调
猫头虎2 小时前
一个插件,国内直接用Claude Opus 4.7
人工智能·langchain·开源·prompt·aigc·ai编程·agi
KC2703 小时前
老板主动给我涨薪!揭秘制造业数字化转型省300万的3招
人工智能·aigc
向量引擎4 小时前
向量引擎中转站偷走我半条命后终于把API密钥这件事整明白了
人工智能·aigc·api·ai编程·ai写作·key·api调用
程序员ys4 小时前
Function Calling 解锁Agent与外部系统交互
aigc·openai·agent
AI专业测评5 小时前
2026网文圈大地震:顶配AI写作神器实测,这几款让“代练”彻底失业
人工智能·算法·aigc·ai写作
_张一凡5 小时前
【大语言模型学习】2026年十大LLM训练数据集汇总
人工智能·学习·语言模型·aigc·大模型训练·llm数据集
MY_TEUCK6 小时前
从零开始:使用Sealos Devbox快速搭建云原生开发环境
人工智能·spring boot·ai·云原生·aigc
Polaris_T6 小时前
2026最新字节大模型岗面经汇总(多平台整理)
人工智能·经验分享·算法·aigc·求职招聘