
1. 项目概述当LLM需要“看见”浏览器想象一下你让一个语言模型去帮你完成一些网页操作比如“帮我在电商网站上找到最便宜的无线耳机并加入购物车”。对于人类来说这很简单打开网页眼睛一扫找到搜索框、商品列表、价格标签、购物车按钮然后点击。但对于一个纯粹处理文本的LLM来说它面对的是一个由成千上万行HTML、CSS和JavaScript代码构成的、结构复杂的“黑箱”。它无法“看见”网页的视觉布局无法理解一个div在屏幕上呈现为一个按钮还是一个广告横幅。这就是browser-use这类浏览器Agent所要解决的核心问题为LLM构建一双能“看懂”网页的“眼睛”和一双能“操作”网页的“手”。browser-use在GitHub上获得了超过86k颗星这个惊人的数字背后反映的是社区对构建实用、可靠的AI Agent的强烈需求。它不是一个简单的“浏览器自动化工具”而是一套精心设计的处理管线Processing Pipeline专门负责将网页的原始文档对象模型DOM转换、过滤、压缩成LLM能够理解和处理的“语言”。这个过程我们称之为DOM处理管线。它的目标是在信息保真度和处理效率之间找到最佳平衡点既要让LLM获得足够决策的信息又要避免因上下文过长Context Length而导致的成本飙升或性能下降。简单来说browser-use的工作流可以概括为获取原始DOM - 理解与抽象 - 压缩与表征 - 交付给LLM决策 - 执行动作并观察结果。本文将深入拆解这个管线中的每一个核心环节看看一个顶流的开源项目是如何巧妙地解决“让LLM看懂网页”这个复杂问题的。无论你是AI应用开发者、对Agent技术感兴趣的研究者还是希望了解前沿工程实践的工程师这篇拆解都能为你提供扎实的、可直接借鉴的洞见。2. DOM处理管线的核心架构与设计哲学2.1 为什么原始DOM对LLM是“天书”要理解browser-use的设计首先得明白原始DOM为什么不适合直接喂给LLM。一个现代网页的DOM树可能包含数万个节点对应着HTML中的每一个标签、属性、文本节点。直接将其以文本形式例如通过document.documentElement.outerHTML dump出来可能会产生一个超过10万token的庞然大物。这不仅会瞬间耗尽大多数LLM的上下文窗口例如GPT-4 Turbo的128K token也很容易被占满更重要的是这些信息中充斥着大量对任务无关的“噪声”。这些噪声包括渲染无关的节点大量的div、span仅用于布局和样式控制没有直接的语义或交互意义。脚本与样式内容内联的script和style标签内容对理解页面功能和可操作元素帮助有限但token消耗巨大。隐藏元素通过CSS设置为display: none或visibility: hidden的元素用户不可见通常也不应被操作。重复的样板代码页头、页脚、导航栏等通用结构在每个页面都可能重复出现。如果让LLM直接阅读这份“天书”它就像被扔进了一个堆满杂乱零件的仓库并被要求“找到那个红色的螺丝刀”。效率低下且容易出错。因此DOM处理管线的首要任务就是“降噪”和“结构化”。2.2 browser-use管线的整体设计思路browser-use的管线设计遵循一个清晰的层次化策略可以类比为一个信息过滤与提炼的漏斗。第一层可访问性过滤与基础清理。这一步基于一个关键假设用户只能与可见且可交互的页面元素进行交互。因此管线首先会利用浏览器提供的API如Chrome DevTools Protocol或Playwright等自动化库来筛选出当前在视口中可见、且未被禁用disabled属性为false的元素。同时会剥离掉script、style、注释等对交互决策无用的节点。这一步大幅削减了初始数据量。第二层语义增强与属性提取。仅仅有标签名和基础属性是不够的。一个button它的文本内容是“提交”还是“取消”一个input它是用来输入邮箱的还是搜索的这一步会为每个候选元素提取丰富的语义属性这些属性是LLM决策的关键依据通常包括innerText元素的可见文本这是理解其功能最重要的信号。aria-label/aria-labelledby专门为可访问性设计的标签通常包含精确的描述。placeholder对于输入框提示文本至关重要。type对于输入框和按钮类型text,submit,checkbox等定义了其行为。roleARIA角色明确说明了元素的用途button,link,textbox等。以及id,name,class等可用于唯一或分类标识的属性。第三层空间结构与层次关系编码。LLM需要理解元素之间的相对位置关系。一个在“登录表单”内的输入框和一个在“页头搜索栏”内的输入框意义完全不同。browser-use会采用一种紧凑的方式编码元素的层级路径例如使用CSS选择器片段或XPath索引以及在视口中的近似坐标或使用基于DOM顺序的索引。这有助于LLM建立页面的“心理地图”。第四层压缩与表征格式化。这是将处理后的信息“翻译”成LLM友好格式的最后一步。browser-use不会将整个处理后的DOM树以XML或HTML格式发送。相反它会将每个候选元素及其关键属性格式化成一个结构化的文本描述。一种常见的格式是线性列表或简化的树形描述例如[1] button: idsubmit-btn, text登录, rolebutton, located inside form#loginForm [2] input: typetext, nameusername, placeholder请输入邮箱, roletextbox, follows [1] [3] link: text忘记密码, href/forgot-password, rolelink, near [2]同时项目可能会引入更高级的压缩策略比如将视觉上相邻的、功能相似的元素如一个表单内的所有输入项进行分组用一个更高层级的描述来代表从而进一步节省token。第五层与LLM的交互协议。处理后的DOM表征会与用户的指令、历史操作记录一起构成发送给LLM的提示词Prompt。LLM的输出被严格约束为一种可解析的动作指令比如CLICK [idsubmit-btn]或TYPE [nameusername] “userexample.com”。browser-use再解析这个指令通过浏览器自动化驱动执行。这个分层管线的设计哲学是逐步提炼保留精华。每一层都丢弃一些信息但目标是丢弃对当前任务最不重要的信息最终得到一个高信噪比、LLM可消化、且足以支撑准确决策的页面摘要。3. 核心环节一DOM的获取、过滤与语义增强3.1 高效获取“活性”DOMbrowser-use通常不直接解析原始的HTML字符串因为那代表的是初始页面源码而非经过JavaScript动态修改后的当前状态。它依赖于成熟的浏览器自动化工具如Playwright或Puppeteer。这些工具提供了访问“实时DOM”的能力。一个关键的操作是执行JavaScript来获取和过滤DOM。例如通过page.evaluate()注入一段脚本在浏览器上下文内部执行。这段脚本的核心任务是遍历DOM树从document.body或document.documentElement开始。应用可见性过滤器使用element.checkVisibility()API现代浏览器或计算样式getComputedStyle(element).display ! ‘none’且visibility ! ‘hidden’来判断元素是否可见。应用交互性过滤器检查元素是否disabled以及元素类型是否可交互如button,input,a,select等或具有role”button”等ARIA角色。视口裁剪通过element.getBoundingClientRect()判断元素是否在当前视口内或至少部分可见。这对于长页面尤其重要可以忽略屏幕外的内容。实操心得element.checkVisibility()是一个更强大的API它考虑了CSS样式、祖先元素可见性、内容可见性content-visibility等多种因素比手动计算样式更准确。但在较旧的浏览器环境中可能需要回退方案。3.2 关键语义属性的提取策略对于过滤后留下的每个元素需要提取一组标准化的属性集。这个属性集的设计直接影响LLM的判断能力。browser-use通常会定义一个优先级逻辑来获取元素的“最佳描述文本”// 伪代码获取元素描述文本的优先级 function getElementDescription(element) { // 1. 首选 aria-label (明确的无障碍标签) if (element.ariaLabel?.trim()) return element.ariaLabel; // 2. 其次 innerText (可见文本) const text element.innerText?.trim(); if (text text.length 100) return text; // 避免过长文本块 // 3. 对于输入框placeholder是关键 if (element.tagName ‘INPUT’ element.placeholder?.trim()) return 输入框: ${element.placeholder}; // 4. 使用 alt 属性图片 if (element.tagName ‘IMG’ element.alt?.trim()) return 图片: ${element.alt}; // 5. 回退到 title 属性或生成一个基于标签和id的通用描述 if (element.title?.trim()) return element.title; return ${element.tagName.toLowerCase()}${element.id ? ‘#’ element.id : element.className ? ‘.’ element.className.split(‘ ‘)[0] : ‘’}; }除了文本描述以下属性几乎总是被收集标识符id,name。它们是唯一选择器的最佳来源。交互类型tagName,type(button,submit,text,checkbox),role。状态checked(复选框/单选框),value(输入框当前值)。位置线索通过element.getBoundingClientRect()得到的x, y, width, height或计算其在DOM树中的索引路径。注意事项innerText的提取需要谨慎。一个包含大量子元素的div可能会返回巨量的、拼接在一起的文本这可能是无意义的。通常需要设置一个长度阈值或者只提取直接文本子节点textContentof direct children。对于列表、表格等结构化数据可能需要特殊的处理逻辑来保持其结构性。3.3 处理动态内容与iframe的挑战现代网页大量使用动态加载和iframe。browser-use的管线必须应对这些挑战。动态内容页面加载后通过Ajax或WebSocket更新的内容必须能被捕获。一种常见策略是定期重新运行DOM提取流程或者在检测到页面主要区域发生变化通过MutationObserver时触发。但这需要平衡实时性和性能开销。iframeiframe是一个独立的文档上下文。browser-use需要能够识别并切换到iframe内部去获取其DOM。这要求自动化工具支持page.frame()之类的API。在表征时需要明确标注元素来自哪个iframe例如[in iframe#payment] button: text”确认支付”。这一阶段的输出是一个由“富语义元素对象”组成的列表每个对象都包含了足够LLM理解其“是什么”和“能做什么”的信息。但这还不是最终形态数据量可能仍然很大。4. 核心环节二DOM的结构化压缩与表征4.1 从树形结构到线性化表征原始的DOM是树形结构但LLM处理的是线性序列的token。如何将树形关系有效地编码进线性序列是一个关键问题。browser-use通常采用以下几种策略或其组合缩进列表法用缩进来表示层级关系。这是最直观的方法。- body - div#header - a.logo: text首页 - form#search - input[typetext]: placeholder搜索... - button: text搜索 - div#main - h1: text商品列表 - div.product: text商品A 价格100 - div.product: text商品B 价格200这种方法保留了清晰的父子关系但当层级很深时会占用较多横向空间token。路径前缀法为每个元素赋予一个基于其在树中位置的唯一路径标识。[0] body [0-0] div#header [0-0-0] a.logo: text首页 [0-0-1] form#search [0-0-1-0] input: placeholder搜索... [0-0-1-1] button: text搜索 [0-1] div#main [0-1-0] h1: text商品列表在后续LLM输出动作指令时可以引用这个路径ID如CLICK [0-0-1-1]。这种方式非常紧凑但丢失了元素类型的直接信息LLM需要额外记忆“路径ID到元素描述”的映射。扁平列表关系描述法browser-use更倾向于使用的方法。它将所有元素放在一个扁平列表中每个元素有唯一索引如[1],[2]然后在元素的描述中通过自然语言说明其位置关系。[1] link: text首页, idlogo, located at top-left corner. [2] form: idsearch, contains input and button for search. [3] input: typetext, placeholder搜索..., inside [2]. [4] button: text搜索, inside [2], right after [3]. [5] heading: text商品列表, level1, below the header. [6] product item: text商品A 价格100, below [5]. [7] product item: text商品B 价格200, below [6].这种方法在token使用和可读性之间取得了很好的平衡。LLM可以轻松地理解“inside [2]”和“below [5]”所表达的空间和逻辑关系。4.2 智能分组与信息聚合对于高度重复或相似的元素分组是极佳的压缩手段。例如一个商品列表页可能有20个结构完全相同的div class”product”每个里面包含图片、标题、价格、按钮。与其枚举20次不如进行抽象[8-27] product list (20 items): - Each item has: image, title, price, Add to Cart button. - Example item [8]: titleWireless Headphone A, price$99.99. - Example item [9]: titleBluetooth Speaker B, price$59.99. ... - You can refer to specific item by its index (8 to 27).这样我们用一小段描述概括了20个元素的核心特征和模式节省了数百个token。LLM在需要操作特定商品时仍然可以通过索引来指定如CLICK [8] button。实现分组需要算法识别具有相似HTML结构、CSS类和视觉布局的元素。4.3 视觉与布局线索的融合纯文本描述有时无法区分紧密相邻的元素。一些更先进的Agent会尝试融入简单的视觉线索。例如在元素描述中加入其屏幕坐标的简化表示[3] input: placeholderEmail, (area: top:200-220px, left:300-500px) [4] input: placeholderPassword, (area: top:230-250px, left:300-500px) [5] button: textSign In, (area: top:280-310px, left:400-450px)或者使用方向词进行强化“位于页面中央的登录框”、“右上角的用户头像”。这些信息对于LLM理解“哪个输入框在上面哪个在下面”非常有帮助能减少歧义。这一阶段的输出是一个高度精炼、结构化、富含语义和关系信息的页面摘要文本。它可能只有原始DOM token数量的5%-20%但却包含了完成大多数交互任务所需的90%以上的关键信息。5. 核心环节三与LLM的协同工作流与动作执行5.1 提示词工程构建有效的上下文处理后的DOM摘要不会单独发送给LLM。它被嵌入到一个结构化的提示词模板中这个模板定义了Agent的角色、任务、操作规范和上下文。一个典型的提示词结构如下你是一个网页浏览助手。你的目标是通过操作网页元素来完成用户指令。 当前页面摘要如下 [此处插入DOM摘要] 你可以执行以下操作 - CLICK [元素索引或描述]点击一个元素。 - TYPE [元素索引或描述] [文本]向输入框输入文本。 - SCROLL [方向] [可选像素数]滚动页面。 - WAIT [秒数]等待一段时间。 - NAVIGATE [URL]导航到新页面。 - EXTRACT [信息描述]从页面中提取并返回信息。 历史操作 [此前步骤的记录用于维持会话一致性] 当前用户指令”[用户的具体指令例如登录到example.com用户名为test密码为123456]” 请根据页面摘要规划并输出下一步要执行的操作。只输出操作指令不要有其他解释。这个提示词做了几件关键事明确角色和约束让LLM进入“浏览器Agent”的角色并限定其输出格式。提供决策依据DOM摘要是LLM的“视觉输入”。定义动作空间告诉LLM它能做什么Click, Type等避免其天马行空。提供短期记忆历史操作帮助LLM理解当前任务进展到了哪一步。清晰的任务目标用户指令是最终导向。5.2 LLM的决策与输出解析LLM接收到提示词后会输出类似TYPE [3] “testexample.com”的指令。这里有一个关键点LLM如何引用元素通过索引如[3]。这是最精确、无歧义的方式要求DOM摘要中的元素索引是稳定且唯一的。通过描述如[input placeholder”Email”]。这更接近人类语言但需要解析器能稳健地匹配描述。通常系统会结合两者优先使用索引当索引不明确或需要泛化操作时如“点击所有同意条款的复选框”使用描述性匹配。browser-use需要一个稳健的解析器来将LLM的自然语言或结构化指令输出转换为具体的、可执行的浏览器自动化命令。这个解析器要能处理一定的模糊性和LLM可能犯的格式错误。5.3 动作执行与状态更新解析出指令后Agent通过Playwright等库执行操作例如# 伪代码 if action ‘CLICK’: selector convert_index_to_css_selector(element_index) # 例如将索引[3]转换为‘#email-input’ await page.click(selector) elif action ‘TYPE’: selector convert_index_to_css_selector(element_index) await page.fill(selector, text)执行后页面状态发生变化。Agent需要重新感知页面即重新启动DOM处理管线获取新的页面摘要。这个“感知-决策-执行-再感知”的循环构成了Agent与环境的交互回路。新的DOM摘要会和之前的操作历史一起构成下一个循环的提示词输入。实操心得在动作执行后加入一个短暂的固定等待如0.5-1秒或智能等待等待某个特定元素出现或消失是至关重要的。这给了页面足够的时间完成JavaScript渲染或网络请求避免在页面未稳定时就进行下一次感知导致决策基于过时信息。这是实践中Agent稳定性的一个关键技巧。6. 常见问题、挑战与优化策略实录6.1 元素定位失败最头疼的问题即使经过精心处理LLM指示点击[button: text”Submit”]但执行时依然可能失败。原因和解决方案如下问题1页面动态变化导致索引失效。上一次感知到的元素[3]在执行动作后页面刷新或DOM更新[3]可能指向了完全不同的元素。策略避免过度依赖绝对索引。在每次动作执行后、下一次感知前重新建立完整的元素索引映射。或者在指令中更多地使用基于稳定属性的描述如id、name或独特的text并在解析时进行实时匹配。问题2选择器不够健壮。将[button: text”Submit”]简单转换为button:has-text(“Submit”)可能匹配到多个按钮或因为文本大小写、空格差异而匹配失败。策略使用更健壮的匹配逻辑。例如文本匹配使用模糊匹配包含关系或计算相似度并结合其他属性如role、邻近元素进行精确定位。优先使用id选择器它是唯一性最高的。问题3元素被遮挡或不在视口。Playwright的点击操作默认会滚动到元素并确保其可操作但极端情况下可能失败。策略在执行操作前可以显式调用page.waitForSelector(selector, state’visible’, timeout5000)来等待元素。对于复杂场景可以引入重试机制。6.2 LLM的“幻觉”与指令遵循LLM可能会输出不符合规范的指令或者对页面摘要产生误解“幻觉”。问题1输出非指定格式。LLM可能输出“我应该先点击用户名输入框”而不是CLICK [3]。策略在提示词中强烈强调输出格式并使用结构化输出技术如要求LLM输出JSON或在提示词末尾提供更严格的格式示例。也可以在解析层增加一个轻量级的LLM或规则引擎对不规范输出进行纠正。问题2误解页面能力。LLM可能试图执行一个页面上不存在的操作例如要求“拖拽滑块”但你的动作空间里根本没有DRAG操作。策略在提示词中清晰界定动作空间边界。如果LLM反复请求边界外操作可以在系统层面回复“该操作暂不支持请尝试其他方式”。6.3 性能与成本的权衡DOM处理管线的每一步都有开销。过于复杂的过滤和压缩算法会增加延迟发送过长的上下文给LLM会增加成本和响应时间。优化策略1分级压缩。不是所有任务都需要完整的页面摘要。对于“点击登录按钮”这样的简单任务也许只需要提取所有按钮和链接就够了。可以根据用户指令的复杂度动态调整DOM处理的“粒度”。优化策略2缓存与差分更新。如果两次感知之间页面只有局部变化如弹窗出现可以只处理变化的部分而不是重新处理整个DOM树。这需要比较DOM快照实现起来复杂但对性能提升显著。优化策略3LLM调用优化。使用更小、更快的模型来处理简单的、模式化的决策例如从三个明显的按钮中选一个而只在需要复杂推理时调用大模型如GPT-4。这种“大小模型协同”的架构是降低成本的有效途径。6.4 处理复杂交互与多步任务对于“找到最便宜的耳机加入购物车”这样的任务它包含多个子步骤导航、搜索、排序、筛选、比较、点击。Agent需要具备任务分解和状态管理能力。策略browser-use这类框架通常会与一个高层任务规划器结合。这个规划器可能是一个更强大的LLM负责将用户指令分解为一系列原子操作子目标然后由本文描述的“感知-执行”循环来逐一完成。规划器还需要处理子任务失败的情况并制定备用计划如“如果按价格排序失败则尝试提取所有价格后自行计算”。构建一个真正鲁棒的浏览器AgentDOM处理管线是基石但它只是整个系统的一部分。如何让LLM更好地理解这个“提炼过的世界”如何设计更有效的交互协议如何处理异常和边缘情况这些都是工程上持续挑战。browser-use的86k星正是因为它为这个复杂问题提供了一个相对完整、可扩展且效果出色的开源解决方案为整个社区搭建了一个极高的起点。在实际项目中你可以直接使用它也可以深入其源码借鉴它的管线设计思想来构建更适合自己特定场景的网页“眼睛”和“手”。