如果你手头有一堆重复的网页操作——填表单、翻页、查数据、提交审批——那你大概率早试过脚本。脚本好用,但坏在一点:页面结构一改,选择器就废,维护成本远超写脚本时省下的时间。LaVague 这类把大语言模型接进浏览器的项目,正是在这个痛点上做文章。它不是我见过的第一个 LLM 网页自动化框架,但思路很干脆:让模型自己看网页、自己决定点什么、自己校验结果,而不是人肉去写死每一步。
这篇文章我会从项目设计动机、核心机制、本地跑通一个 Demo 的实操过程、以及踩坑排查四块来讲,适合已经做过一点自动化、又受够了脆弱脚本的开发者,也适合想在业务里引入 AI Agent 的应用工程师。我会尽量把原理讲透,也会把那些文档里不太会写的细节一并放出来。
1. 这个项目到底解决了什么问题
1.1 传统网页自动化的维护困境
传统网页自动化方案基本可以归成三类:基于选择器的脚本、录制回放、以及老牌的 RPA 工具。它们遇到的问题高度一致——页面前端一调整,路径就失效。做电商的人应该深有体会,商品详情页改一次 DOM 结构,之前跑的采集脚本就碎一片。前端是高度动态的,样式名、属性顺序、异步加载时机都在变,而脚本里写死的 CSS 选择器、XPath 却完全无法应对这种变化。
我见过不少团队专门养人维护自动化用例。改版一次,就得把所有用例过一遍,逐个排查失效路径,重新找定位点。这个工作的本质是“用人力去顶软件的脆弱性”,成本高但提升有限。另一个问题是业务流程通常不像脚本那样直来直去,中间有判断、有分支、有异常处理,一旦页面多出弹窗、验证码、推荐位,传统脚本的逻辑就变得很复杂,每个异常都要写一大堆条件判断,改起来更痛苦。
所以痛点可以概括成一句话:网页自动化的核心成本是识别和适应,而不是执行。传统工具把精力全放在了执行层,导致识别问题被无限放大。
1.2 大语言模型把“理解”带进了自动化循环
大语言模型出现之后,情况开始变化。模型能读懂自然语言指令,也能对截图、HTML、以及页面上的零散信息做推理。LaVague 这类项目的核心创新,是把原来由工程师手工完成的“页面分析—动作映射—结果校验”这条链路,交给模型去做了。
以前写自动化,你要告诉工具“点击按钮 A,然后在输入框 B 里输入文本 C,再按下回车”,每一步都被翻译成底层选择器。LLM 自动化则是反向的:你只需告诉 Agent “帮我把这个商品加入购物车并进入结算页”,模型自己去看页面上有哪些元素、有哪些操作可以做、哪个操作更符合你的意图,然后决定先点击哪里、输入什么、什么时候等待。
这等于把自动化从“固定序列执行”升级成了“目标驱动的可推理行为”。页面结构变了,只要语义上还看得出这是个购物车按钮,模型就能重新定位到它。这一下就把本该花在维护上的时间省回来了。当然代价也有:模型调用有延迟、有 token 成本,偶尔也会判断失误,但整体思路是完全可行的。
2. 核心架构与技术拆解
2.1 Agent 循环的基本单元
LaVague 这类实现的底层逻辑并不神秘,就是一个持续循环的 agent 闭环。大致可以分为四步:感知页面状态、生成候选动作、执行动作、观察反馈并继续判断。
第一步是感知。Agent 需要拿到当前页面的状态,常见方式有三类:整页截图、DOM 内容摘要、或混合模式。截图对视觉类模型很友好,模型能直接看到布局;DOM 摘要则适合纯文本模型,把可交互元素、链接、按钮、输入框提取出来,转成结构化的上下文。混合模式是两者都用,兼顾空间感和细节。
第二步是生成候选动作。模型把任务目标、当前页面状态、历史操作日志放在一起推理,输出类似“点击某某按钮”“在某某输入框中填写某某文本”这样的动作指令。这里的关键点是格式约束,通常会要求模型以 JSON 或代码块返回,方便程序解析。
第三步是执行动作。解析后的动作会交给浏览器自动化库,比如 Playwright 或 Selenium。不直接操作底层浏览器,而是通过这套库,一来复用成熟的等待策略和定位逻辑,二来方便做截图、追踪步数等诊断。
第四步是反馈。执行完之后,Agent 会再次获取页面状态,判断任务是否完成。没完成就回到第一步,直到达到终止条件或最大步数。这个循环看起来简单,但每个环节都有不少工程细节,后续章节会细说。
2.2 从自然语言到浏览器操作的关键映射
这一步是整个项目的技术核心:怎么让模型输出的抽象动作变成浏览器里真实、可执行的命令。你可以认为中间有一个“动作适配层”。
动作适配层要做的事,通常包括元素解析、动作翻译和恢复机制。元素解析负责把“那个蓝色按钮”“搜索框”这类描述变成页面上具体的元素 ID 或 XPath。实现上通常是先从 DOM 里收集所有可交互元素,给每个元素一个短 ID,再让模型在上下文里引用这个 ID。动作翻译则是把模型输出的“点击 A”“输入 B”翻译成浏览器自动化库的调用代码。这里要注意动作的组合,比如先点击再输入、输入前要不要清空已有文本、点击前需不需要滚动到可见区域,这些都要在适配层里做妥帖。恢复机制是页面报错时,适配层会尝试重新获取页面状态并询问模型“刚才那个动作是否成功”,从而自适应地纠正。
LaVague 里有个值得借鉴的设计思路,是把页面上的可交互元素预先“抽”出来,而不是把整段原始 HTML 一股脑丢给模型。原始 HTML 动不动几十 KB,token 消耗大,另外模型容易分不清哪些元素是可点击的、哪些是纯装饰。提取之后生成一份结构化的可交互元素清单,模型读起来轻松,输出的动作也更稳定。
这个映射层的质量,基本决定了整个 Agent 的上限。映射做得粗糙,模型就经常找错元素;映射做得细致,比如把按钮附近可见文本也一并提供,模型定位准确率会明显提高。
2.3 关键依赖与组件选型
我拆过这个类型的项目,发现成熟方案都会在几个关键依赖上做组合选择,而不是全部自己实现。
一是浏览器自动化内核。绝大多数实现选 Playwright,因为它跨浏览器、自动等待机制成熟,而且能很容易地进行页面截图、网络监听和快速加载页面。Selenium 也常见,但处理动态渲染和等待时不如 Playwright 顺手。
二是页面信息提取模块。有些用浏览器直接输出 DOM,有些先用 js 脚本筛选可交互元素,再给模型一个精简版。这一步做得好坏直接影响上下文长度和推理准确率。好的项目会自己写一套元素收集脚本,把 input、button、a、textarea、select 这些标签加上可见文本和坐标一起收集起来。
三是大模型接入。早期很多项目只用纯文本模型,从 HTML 摘要里做推断;现在更多则是视觉语言模型与文本模型配合,视觉模型负责看截图,文本模型负责管理多轮任务状态。根据不同工具的精度和成本灵活切换,是一个很常见的组合策略。
四是缓存与状态记忆。Agent 多步操作时,需要保留目标、已执行动作、当前页面 URL 等上下文。大部分实现会自己封装一个短期记忆组件,把最近几步的动作和结果保留在内存里,避免每轮都把所有历史重新塞给模型。
选型背后其实是一条原则:能复用成熟能力就绝不重复造轮子。浏览器内核、大模型调用、元素定位都有现成的方案,真正需要动手优化的是页面信息提取和动作约束。
3. 本地实操:从零搭建一个可用示例
3.1 环境准备与安装
先说结论:不需要特别强大的机器,普通笔记本电脑足够跑整个流程,只要你的大模型 API 可用。核心环境就两块:浏览器自动化的 Python 库,以及一个能调用大模型 API 的环境。
我这边按项目官方推荐的方式准备环境,先建一个干净的虚拟环境,避免和本机其他项目依赖冲突。然后安装浏览器自动化核心依赖,以 Playwright 为例,安装命令如下:
python -m venv .venv source .venv/bin/activate pip install playwright playwright install chromiumplaywright install chromium这一步会下载一个独立的 Chromium 浏览器,专门用于自动化,不动你日常使用的浏览器。如果你是刚接触,建议直接按这个来,省去系统浏览器兼容性的麻烦。
接下来是模型接入。大多数类似项目都需要你配置模型 API 的 Key 和端点。不同项目的变量名不太一样,但通常会涉及“模型名称”和“API Key”两个字段。这里我用自己的方式直接写了个简易封装,把大模型调用统一成函数,方便后面切换模型。核心目的不是夹带某个商家,而是告诉你接入点就两个:和模型提供方的鉴权、以及请求格式。
3.2 写一个最简单的自动化任务
环境就绪后,我们来写一个最小可用的 Agent。先不去跑真实项目,而是用一个简化实现说明核心循环。我选生活里最常见的场景:打开一个电子商城页面,自动搜索某类商品,并进入第一个搜索结果详情页。
代码大致长这样:
from playwright.sync_api import sync_playwright def get_page_state(page): # 提取可交互元素 elements = page.evaluate(""" () => { const tags = ['a', 'button', 'input', 'textarea', 'select']; const list = []; document.querySelectorAll(tags.join(',')).forEach((el, idx) => { const text = (el.innerText || el.value || el.placeholder || '').trim().slice(0, 50); if (text) { list.push({id: idx, tag: el.tagName, text: text}); } }); return list.slice(0, 30); } """) return {"url": page.url, "elements": elements} def llm_decide(task, state, history): # 这里调用你的大模型API,返回 {action: "click", element_id: 3} # 为了示例,我假设模型已经开始工作,返回结构化指令 return {"action": "click", "element_id": 3} def execute_action(page, action): elements = page.evaluate(""" () => { const tags = ['a', 'button', 'input', 'textarea', 'select']; return Array.from(document.querySelectorAll(tags.join(','))); } """) target = elements[action["element_id"]] if action["action"] == "click": target.click() elif action["action"] == "fill": target.fill(action["value"]) page.wait_for_load_state("domcontentloaded") with sync_playwright() as p: browser = p.chromium.launch(headless=True) page = browser.new_page() page.goto("https://example-store.com") task = "搜索蓝牙耳机,并进入第一个结果详情页" history = [] for step in range(5): state = get_page_state(page) decision = llm_decide(task, state, history) print("step", step, "decision", decision) execute_action(page, decision) history.append(decision) # 这里简单判活:如果页面上出现"详情"或"加入购物车"字样,就认为任务完成 if "详情" in page.content() or "加入购物车" in page.content(): print("任务完成,当前URL:", page.url) break browser.close()这段代码省略了大模型接口细节,但 Agent 循环的结构已经完整呈现。get_page_state把页面里的可交互元素提取成一个列表,每个元素有一个短 ID。llm_decide根据任务、当前页面状态和操作历史,返回结构化动作。execute_action把动作翻译成 Playwright 操作。关键的一点是,模型只看到“元素 ID + 文本”,而不是看到整个 HTML,这让输出格式非常容易解析。
3.3 让它跑起来并观察轨迹
写完后,我建议先别加太多复杂任务,跑一个只有两三个步骤的流程,观察 Agent 的每一步决策。比如让它在搜索框里输入关键词,点击搜索按钮,再点第一个结果。
我在实际尝试时,发现几个细节需要关注。第一是等待条件。页面上如果元素是异步渲染出来的,必须用wait_for_selector或者其他等待策略,否则模型决策完成、指令下发时,元素还没出现在 DOM 里,直接点击会报错。第二是动作失败后的重试。不能只在循环里机械地执行,最好拿到错误信息后把错误状态放回给模型,让模型自己换一个方式。比如点错了,模型从反馈里看到当前 URL 没变,就会尝试重新选择元素。
第三是记录每一步的截图。强烈建议在每步执行后给页面截个图。这样一是方便排查“模型到底看到了什么”,二是调试 prompt 时,你能直观看到模型理解偏差出在哪个环节。page.screenshot(path=f"step_{step}.png")一行代码就能做。
等你跑通了最小示例,再去接触完整的 LaVague 实现就会轻松很多。因为它们本质上是同一个循环,只是把模块拆分得更精致、提供了更丰富的动作类型和更强的错误恢复。
4. 踩坑记录与排查思路
4.1 常见报错与对应解法
我实际跑这类项目时,遇到最多的问题集中在定位、等待和模型输出格式三块。这里整理成一张速查表,最直接管用:
| 现象 | 可能原因 | 解决参考 |
|---|---|---|
| 点击元素报错“元素不可见” | 元素存在但被遮挡或未滚动到视野 | 执行动作前增加滚动到元素位置,或等待可见状态 |
| 页面已加载但元素找不到 | 数据是异步请求后渲染 | 用 Playwright 等待指定选择器出现再提取状态 |
| 模型输出的动作 JSON 无法解析 | 模型返回了多余格式 | 在 prompt 里强约束 JSON 格式,解析时用宽松正则提取 |
| 任务执行到一半就停 | 缺少任务完成判定,模型自认为完成 | 增加具体的终止条件,比如页面 URL 变化或目标元素消失 |
| token 消耗特别快 | 每步都把全部 HTML 塞给模型 | 改成只提取可交互元素摘要,通常能减少 70% 以上 token 消耗 |
| 多步骤上下文混淆 | 历史记录太长,模型跟丢目标 | 只保留最近 3 到 5 步操作,把任务目标固定在每轮 prompt 的最前面 |
这里最值得说的是元素不可见问题。很多开发新手会以为元素存在于 DOM 里就可以直接点击,但浏览器自动化里还有个“可见且稳定”的概念。元素可能渲染了,却被固定头部挡住了;也可能宽度为 0 在页面边角。解决办法也不复杂:执行点击前先用element.scroll_into_view_if_needed(),或者用 Playwright 的自动等待机制。如果用到自己的点击函数,最好把这个逻辑内置进去。
4.2 Prompt 与任务拆分的经验
跑过几次真实任务后,你会发现模型的表现很大程度上是 prompt 设计的产物。同样一个任务,prompt 里写一句话和写清楚目标、边界、输出格式,成功率完全是两个级别。
我习惯把 prompt 拆成四个部分。第一是角色设定,比如“你是一个网页自动化助手,只能根据页面元素信息做操作”。第二是任务描述,需要把用户意图写得很直白,“搜索蓝牙耳机”这种描述太泛,我会补充成“在搜索框中输入‘蓝牙耳机’并按下回车,等待搜索结果加载完成后点击第一条结果的标题”。第三是页面当前状态,把可交互元素列表塞进来。第四是输出格式约束,要求只输出 JSON,不允许解释。
任务拆分也是很重要的一环。LLM Agent 不适合一上来就处理十几个步骤的复杂任务,它的长期规划能力还没那么强。更好的做法是把任务切成多个短 Agent 单元,每个单元只做一件事,比如“填写登录表单”“进入搜索页面”“提取第一屏数据”。单元之间用工程逻辑衔接,而不是让模型在一轮里规划完所有步骤。我在业务落地时通常会用编排层来做任务切分,每个子任务单独调用一个新的 Agent 循环,这样单步的成功率会变得非常高。
5. 用这类方案改造自己业务的几个方向
5.1 适合做的场景
LLM 网页自动化最适合的场景,是那些页面结构变化频繁、业务逻辑又相对轻量的流程。典型的是数据采集类任务,比如定期抓取竞品价格、公告信息、招投标结果。这些页面改版频率高,传统脚本三天两头失效率,用 LLM Agent 之后,因为模型通过语义理解来定位元素,页面上按钮位置变了、样式改了,只要文字还差不多,就能继续跑。
另一个适合的场景是表单填报。很多内部系统是老的 B/S 架构,表单极其繁琐,字段又多,一个审批流程要填大量重复信息。用 LLM Agent 直接读取上一系统页面里的数据,再自动填入下一个系统,相当于给老系统加了个智能 RPA 层,而且不需要老系统开放任何 API。
还有一类场景是前端测试辅助。测试人员每天需要回归几十条用例,很多用例其实就是“走一遍主流程,看有没有报错”。用 LLM Agent 写一个描述性的测试步骤,模型自己去定位控件,比录制 UI 测试的维护成本低多了。当然,它没法替代完整的前端测试体系,但用于冒烟测试、快速反馈,效果很好。
5.2 不适合做的场景
我踩过几次坑后,也得承认它有不少边界。不适合的场景通常是这几类:需要极高稳定性的生产级核心链路、强依赖实时操作反馈的操作、以及涉及大量数据输入敏感信息的场景。
先说稳定性。LLM 输出天然有随机性,哪怕同样 prompt、同样页面,结果也可能有波动。如果你要做的是银行转账、订单支付这类不可逆操作,直接让模型自主跑,风险太大。可以的做法是让模型只做操作建议,最后一步关键确认仍然交给人工或传统规则。
其次是强实时反馈场景,比如需要读取验证码、处理复杂的滑动拼图。这类能力视觉模型有,但和专用方案比,速度慢、成功率不稳定,投入产出比很低。
敏感信息方面,不是模型能不能处理,而是你愿不愿意把账号密码、身份证号这类数据放进模型上下文中。如果你的 LLM 服务是外部 API,直接传敏感字段存在数据外泄风险。建议只传网页元素 ID 和脱敏文本,关键字段在本地脚本里填充,不给模型看到。
5.3 扩展建议
如果你准备把这个思路搬进自己的项目,我建议把注意力放在“编排层”和“评估层”上。编排层负责管理任务列表、处理分支逻辑、决定什么时候启动一个 Agent 循环,什么时候用传统脚本兜底。评估层则是轮询检查任务是否真的完成了,比如通过 URL 变化、页面关键文本出现、网络请求完成等信号判断。
我常建议团队先跑一个月“半自动模式”:Agent 做一个操作,就停在那边等人工确认。这样既能积累真实页面样本、调试 prompt,又不会在初期造成不可控的影响。等准确率稳定到可接受水平再切换成全自动,再搭配告警和人工介入机制。
另外一个容易被忽视的点是缓存页面状态。同一个页面结构,如果没有重大改版,其实可以缓存它的摘要结果。针对元素 ID 到真实 DOM 节点的映射做一次缓存,后续操作就不需要每步都重新分析整个页面,成本能降不少,速度也会快很多。
我在实际测试中最大的体会是:这类方案的价值不在“代替所有 RPA”,而在“把通用网页识别能力做成一个服务”。当你把它和浏览器事件、定时任务、消息队列接在一起,它就成了一个能应对页面变化的自动化底座。以后哪个页面改版,你不需要哭着改选择器,只需要看看模型是不是还能理解这个页面的语义。跑不通的时候,通常也不是模型能力不够,而是页面元素信息给得太粗糙。多花一点时间打磨元素提取和 prompt,回报非常直接。