☰
浏览器智能体框架源码解析:从DOM到动作执行的完整链路
2026/10/12 13:24:03 网站建设 项目流程

最近把一套开源的浏览器智能体框架从头到尾读了一遍,起因是我想搞清楚一个大模型到底是怎么做到“看着网页、点着网页、把活干完”的。市面上讲 Agent 概念的文章很多,但真正落到底层逻辑的太少。很多人以为 AI 操控网页是什么黑魔法,其实拆开之后你会发现,核心就两件事:第一,把网页结构压成模型能读的文本;第二,把模型输出的意图翻译成浏览器能执行的动作。这篇东西我打算从源码层面把这套机制完整捋一遍,读完你不仅能看懂这类框架的原理,还能自己动手改出适合业务场景的浏览器智能体。

1. 两个绕不开的问题:网页怎么被“看懂”,操作怎么被“执行”

先说一个很多人都忽略的事实:大模型本身既不认识网页,也不会点击按钮。它只能接收文本、输出文本。所以任何浏览器智能体框架,本质上都在做同一件事——在这两层之间搭一座桥。

第一层叫信息压缩。一个普通网页的 DOM 节点动辄几百上千个,大型后台管理系统一个页面几万个节点也不稀奇。如果把这些原始 DOM 全部塞进大模型的上下文,先不提 token 烧钱的问题,模型根本抓不住重点。它需要的是“哪些按钮可以点、哪些输入框可以填、哪些文字描述了这个页面的状态”,而不是那些埋在 style 里的颜色、字体、布局信息。

第二层叫动作投影。即便模型看懂了页面,它也没法自己移动鼠标。它只能输出一段文本,说“我想点击搜索按钮,我想在输入框里填入关键词”。框架要做的,是把这段自然语言或者结构化指令变成浏览器协议层面的真实调用,比如page.click(selector)、page.fill(selector, text)。

这两层需求决定了框架的基本架构:一个负责把 DOM 转成紧凑文本的格式化模块,一个负责定义动作集合和执行动作的执行器,还有一个把两者串起来的 Agent 主循环。理解了这三个模块,整个开源项目的骨架就在你脑子里了。

很多人直接上手读代码会被各种抽象类绕晕,我的建议是先抓住一个核心数据流:网页 -> 可交互元素列表 -> 文本快照 -> 模型 -> 动作 JSON -> 浏览器执行 -> 新的页面状态。这个循环跑通了,剩下的一切都是在这个主线上做工程优化。

2. 网页的压缩与重建:DOM 树如何变成 LLM 能读的文本

这一章讲框架中最基础的模块——页面表示层。它解决的是“怎么让模型看见网页”。

2.1 可交互元素的提取:不是所有节点都值得进上下文

我在读源码时发现,这个框架的第一步并不是把整个 DOM 序列化成文本,而是先做一次“可交互元素提取”。它遍历整个 DOM 树,把所有符合交互条件的节点挑出来,生成一个元素清单。

这个清单大概长这样:

class InteractiveElement: index: int # 元素编号,模型通过这个编号引用元素 tag: str # 标签名,如 button、input、a、textarea text: str # 可见文本内容 attributes: dict # 关键属性,如 id、class、name、type xpath: str # 元素定位路径

判断一个元素是否“可交互”,框架里有几层过滤逻辑。第一层是标签过滤:button、input、a、textarea、select必然保留,因为它们是天然的用户交互入口。第二层是可见性过滤:元素不能是display: none、visibility: hidden、opacity: 0,尺寸不能为 0,不能脱离视口。第三层是状态过滤:disabled的按钮、只读的输入框通常会被剔除,因为它们点了也没用。

这里有个工程上的小技巧值得注意:判断元素可见性的顺序是有讲究的。先判断标签类型(成本极低),再判断 CSS 属性(需要计算样式,成本中等),最后判断是否在视口内(需要几何计算,成本最高)。这个顺序能在大页面上省下大量计算时间。

2.2 从元素清单到模型文本:编号和元信息的组织方式

拿到可交互元素清单后,下一步是把它们翻译成模型能读的文本。这个框架的做法非常直接——它在每个元素前面加一个索引标记,把整个页面变成一张“编号地图”。

格式化后的页面快照大致长这样:

Page Title: 商品管理后台 URL: https://example.com/admin/products [1] button "新增商品" [2] input placeholder="搜索商品名称" [3] button "搜索" [4] a "下一页" [5] div "共 23 条记录"

这种格式对模型极其友好。编号给了模型一个“指代方式”,标签和文本给了模型“判断依据”,placeholder 和 aria-label 这些属性进一步补充了元素的语义信息。

我在调试这类框架时发现一个很实用的技巧:直接把格式化后的文本打印出来看。如果模型行为异常,十有八九是这一步出了问题——要么该保留的文本被裁掉了,要么无关的导航按钮占用了大量 token。你需要在调试器里导出这份文本,站在模型的角度去审视:如果我只看到这些信息,我能正确操作这个页面吗?这一步能快速定位很多“看起来是模型笨、其实是喂料有问题”的案例。

2.3 容易被忽略的数据源:iframe、Shadow DOM 与图片的文本替代

如果你只解析主文档的 DOM,一定会踩坑。现在很多中后台系统都在用 iframe 内嵌表单,流量大的电商页面也喜欢用 Shadow DOM 封装组件,这些节点在主文档树里就是一个空壳,真正的可交互元素藏在子文档里。

这个框架的处理方式是递归遍历所有 iframe 的 contentDocument,把内部的可交互元素并入主清单,再在文本里标注这些元素来自哪个 iframe。标注很重要,因为执行动作时需要切到对应的 frame 上下文。

另外一个是图片的处理。很多导航栏是图标加文字,文字可以提取,但纯图标按钮就需要靠aria-label或者alt属性。如果这两个都没有,框架会做一次视觉兜底——把截图里对应的区域标记出来,配合视觉模型识别。这个能力不是默认开启的,因为调用视觉模型的成本比纯文本高不少,通常只在文本信息不足时才启用。

这里的取舍逻辑很值得学习:文本能搞定的就绝不麻烦视觉模型,一个模块的复杂度应该匹配它解决的实际问题,而不是越强大越好。

3. 动作不是自由发挥:交互协议与可执行动作空间

模型看懂了网页,接下来要“动手”。这一章讲框架怎么定义和管理动作。

3.1 动作用 JSON 定义,而不是自由文本

这个框架在系统提示词里明确要求模型以 JSON 格式输出下一步动作。原因不难理解:JSON 结构稳定、容易解析、便于做参数校验。如果让模型自由输出文本,比如“点击一下页面底部的确认按钮”,解析层还得做语义理解,准确率和鲁棒性都会下降。

动作的 schema 大致如下:

{ "actions": [ { "action": "click", "index": 3, "description": "点击搜索按钮" }, { "action": "input_text", "index": 2, "text": "无线耳机", "description": "在搜索框输入关键词" } ] }

注意这里是一个动作数组,允许模型一次输出多个连续动作。框架内部会按顺序逐个执行,每执行一个就重新抓取一次页面状态,避免多个动作基于同一个过期快照导致定位失败。

3.2 动作白名单:每个动作都要对应一个真实可执行的方法

我在源码里看到框架预定义了一套动作集合,每一种都对应执行器模块中的一个具体函数。常见的动作包括:

动作名参数执行语义
clickindex点击指定编号的元素
input_textindex, text在指定元素输入文本
scrolldirection页面滚动
select_optionindex, option下拉框选值
navigateurl地址栏跳转
switch_tabtab_id切换标签页
go_back无浏览器后退
extract_contentindex提取元素内容文本
wait无等待网络空闲

动作白名单是框架可控性的基石。模型再聪明,也只能在这套固定动作里组合,不能自由发挥。这种“有限动作空间”的设计哲学在 Agent 系统里非常关键——你不可能让一个语言模型直接操作正则表达式去修改网页,但你可以让它选择“等待 0.5 秒”或者“点击重试按钮”。

3.3 参数校验与失败的反馈回路

动作执行不是拿到 JSON 就直接跑,中间还有一层校验。比如click动作的index必须是整数且在索引范围内;input_text的text长度不能超过限制;navigate的 URL 必须通过合法性和协议检查。

如果校验失败,框架不会直接崩溃,而是把错误信息追加到对话历史中,让模型看到“我刚才提交的 action 非法,原因是索引越界”,然后由模型自行修正。这个设计非常优雅——它不是用 if-else 去覆盖每一种错误,而是把错误当成上下文喂回给模型,利用模型自身的推理能力完成自我修复。

这也解释了为什么这类框架通常需要多轮调用模型:一次推理往往不够,模型需要基于上一次动作的执行结果逐步调整。整个系统的智商上限取决于模型,但工程下限取决于错误反馈链路是否通畅。

4. 一个“想一步、做一步、看一步”的循环:主控逻辑的拆解

前面讲的是“眼睛”和“手”,这一章看“大脑”怎么调度它们。Agent 主控循环是整个框架的中枢神经系统。

4.1 状态机的流转:从页面快照到动作执行

主循环可以用一个简化的状态机来描述:

while step < max_steps: snapshot = get_page_snapshot(browser_state) messages.append({"role": "user", "content": format_messages(snapshot)}) response = llm.chat(messages) actions = parse_actions(response) for action in actions: if action.name == "done": return summarize(result) result = execute_action(action) if result.is_error(): messages.append({"role": "system", "content": f"执行失败: {result.error}"}) break else: messages.append({"role": "system", "content": f"成功: {action.to_string()}"}) step += 1

这个循环里最微妙的一点是:每次模型回复之后,框架不会立刻判定失败,而是先把动作结果写进消息历史,然后开始下一轮。换句话说,整个 Agent 运行过程是一个连续对话——“我看到了什么、我做了什么、结果怎么样、我下一步做什么”。

4.2 上下文的增长与裁剪

无限对话是不可能的。每一步动作加上页面快照,少说也要一两千 token,跑 20 步之后上下文就非常拥挤了。这个框架的处理方式是:在窗口溢出时,保留系统提示词和最初的目标描述,把中间的执行过程摘要化。

摘要化有两种实现。一种是直接截断,只保留最近 N 轮;另一种是用一次额外的模型调用,把中间过程压缩成一段摘要。前者省 token,后者信息保留度好。源码里默认用截断,但预留了摘要策略的扩展接口。

这里我建议你在二次开发时认真考虑上下文策略和任务类型的匹配。如果任务是单页表单填写,最近几轮的信息就够用;如果是多页面长链路操作,历史中的关键中间状态(比如之前拿到的订单号、跳转前的页面 URL)必须保留,否则模型会“失忆”,重复执行已经完成的前置操作。

4.3 断点续跑与序列化:长任务的救命稻草

这个框架支持把 Agent 的完整运行状态序列化到本地文件。状态包括:对话消息列表、当前页面 URL、已执行动作的记录、剩余步数。如果中间进程崩溃,可以从断点恢复继续跑。

这个能力看起来不起眼,但对真实业务非常重要。长任务的 LLM 调用链跑一次动辄几十步,中途任何网络抖动或者进程被杀都可能前功尽弃。断点续跑相当于给了整个系统一个“存档”能力,让长时间运行的 Agent 任务具备可恢复性。

5. 比点击更难的环节:动态页面里的元素定位策略

前面讲过,模型操作动作里携带的是索引编号。但真实浏览器执行点击时,不能用索引直接点击——索引只是快照里的排序,页面经过任何一次渲染变化就可能失效。这一章专门拆解框架的定位策略,也是最容易出现自研问题的地方。

5.1 四种定位策略的优先级与适用场景

框架内部对元素的定位分为四个层级,按优先级从高到低排列:

优先级定位方式原理适用场景
1索引直接定位从当前快照列表取节点引用页面未改版,快照仍有效
2选择器回退用节点的 id、稳定 class、data 属性生成 CSS 选择器页面局部刷新,结构未变
3XPath 定位根据元素层级路径定位结构稳定,无 id/class
4语义重定位触发重新抓取 DOM,生成新快照后交给模型重找页面大范围重建

第一层级其实很快,因为元素清单本身还保留着节点对象引用,点击就是直接调element.click()。但如果点完一次之后页面发生了 React 或 Vue 的重新渲染,旧引用就失效了,这时候必须降级到第二层级。

5.2 为什么不能用保存的 JavaScript 对象引用一直点

很多第一次做浏览器自动化的人会踩一个坑:我明明已经拿到了某个按钮的元素对象,为什么下一次点击就报element is detached from the DOM?

原因是现代前端框架几乎都是“数据驱动视图”:状态一变,整个组件子树都会重新渲染,旧的 DOM 节点被销毁、新节点被创建。你手里拿的那个引用指向的是一栋已经被推平的老房子,地址虽然一样,但住户已经换了。所以任何只缓存节点引用的方案,在 SPA 应用面前都会失效慢。

这个框架的做法是:动作执行立刻重新获取页面状态,每一轮都基于最新快照做决策。上一轮的索引只在那一轮有效,进入下一轮就要重新生成。这个“每步一快照”的设计牺牲了一点 token,但换来了定位的可靠性。

5.3 定位失败之后的“重新理解”机制

定位策略全部失败后,框架不会把动作标记为 fatal,而是把“元素未找到”的信息连同当前页面的新快照一起交给模型,让模型再想别的办法。比如点击“确认付款”按钮失败,可能是因为真正该点的是弹窗里的“确认”,或者按钮文字被改成了“立即支付”。模型看到新快照之后,会自动调整策略。

这种“失败即反馈”的机制,实质上是把元素定位从“程序逻辑问题”转化成了“模型理解问题”。后者显然更容易解决,因为模型天然擅长文字的灵活匹配。

6. 状态管理与安全兜底:能让 Agent 放心裸奔的前提

Agent 跑得再好,如果安全措施跟不上,就是定时炸弹。这一章聊框架在状态管理和安全兜底上的工程细节。

6.1 Token 预算与步数上限:双重保险

框架默认设置了最大执行步数,比如 50 步。超过步数上限,主循环强制终止,返回当前状态供人工接管。这个设置防止了模型在错误方向上死循环烧钱。

token 预算方面,框架在每次调用模型前会估算消息列表的 token 数,超限就触发裁剪策略。裁剪不是简单砍掉最早的消息,而是优先保留目标描述和最近一步的快照——这两个是最重要的决策依据。

6.2 浏览器上下文的隔离与复用

对于多任务并发场景,框架为每个任务创建单独的浏览器上下文。上下文之间完全隔离,cookie、localStorage、sessionStorage 互不干扰。任务结束后可以关闭并清理,避免内存泄漏。

这里值得注意的是“用户数据目录”的概念。如果任务需要保持登录态,可以挂载一个持久化的用户数据目录,让每次运行都复用同一套浏览器配置。这在跑需要登录的业务系统时非常实用,省去了每次都要走登录流程的麻烦。

6.3 动作执行的错误分类与重试策略

源码里对执行错误做了分类,不同的错误走不同的处理路径:

错误类型典型场景处理策略
元素未找到页面状态变了、选择器失效降级定位,未果则反馈模型重决策
元素不可交互被遮挡、处于 loading 状态等待后重试
网络错误请求超时、连接中断指数退避重试
浏览器崩溃标签页异常关闭重启浏览器恢复会话

错误分类的价值在于,不是所有错误都值得让模型“思考”一遍。很多错误是瞬时性的,等一下、重试一次就好了。如果每次都调用大模型,费钱费时还容易把上下文带偏。工程里的“少打扰模型”有时候比“多给模型反馈”更明智。

7. 读源码时最值得动手改的三处地方

最后分享一点个人经验。如果你准备把这个框架接入自己的业务,有几个点是最值得动手改造的,也是最能体现框架灵活性的地方。

7.1 自定义动作的注册方式

框架允许注册自定义动作。比如你的业务里有个“发送告警邮件”的按钮,你可以给框架新增一个动作send_alert_email,在系统提示词里告诉模型这个动作的存在和适用场景。模型会在遇到相关需求时自动选择调用它。

这个扩展机制非常强大——它相当于把任意工具函数变成模型的能力。我建议你优先把自己业务流程中固定、重复的页面操作封装成动作,而不是让模型一步步去点。每封装一个动作,Agent 的可靠性和速度都会上一个台阶。

7.2 页面快照格式的定制

框架默认的快照格式是通用的,但你的业务页面可能更适合其他格式。比如你可以把表格数据压缩成 CSV 喂给模型,让它直接做数据分析;或者把产品列表渲染成更紧凑的摘要形式,减少视觉复杂度。

快照格式直接决定了模型“读到”的信息密度。我见过一个案例,某个项目把原始文本快照改成表格形式后,模型的正确率提升了大概十几个百分点。这一步的收益往往被低估。

7.3 定位策略的按需替换

某些企业内部系统会大量使用特殊组件(比如自研的表格、树形控件),标准选择器可能定位不到。这时候可以在定位模块里注入针对这些组件的定制逻辑,比如“如果元素带>

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询