
作为一个在自动化测试和爬虫开发这行摸爬滚土多年的老兵我最近把市面上主流的浏览器自动化工具挨个试了一遍突然有种豁然开朗的感觉。大家都在聊LLM Agent怎么用自然语言驱动浏览器干活但实测下来传统的浏览器自动化工具配合合理的封装反而在很多实际场景里更靠谱、更落地。这篇文章我不会去空谈概念而是把我实测Playwright、Selenium、Puppeteer这些工具过程中的踩坑、对比和思考分享出来特别是当我尝试把大模型能力和这些传统工具结合时看到了另一条比纯LLM Agent更稳健的技术路径。如果你正在纠结项目选型或者好奇LLM Agent之外还有什么可能性这篇文章应该值得你花几分钟看看。1. 内容整体设计与思路拆解1.1 为什么我会去测这些东西其实最早接触浏览器自动化是十年前用Selenium做Web UI回归测试那时候真的是又爱又恨。爱的是它确实能模拟用户在浏览器里的完整操作流程恨的是它的脆弱和慢。后来出现Puppeteer和Playwright确实好了不少但传统的自动化工具天然有一个绕不开的短板——只能执行我们写死的、明确的命令一旦页面结构稍微变一下脚本就崩了。近两年LLM Agent特别火大家开始试着让大模型直接“看”网页、“理解”任务然后自己操作浏览器。听起来很美好一个Agent能根据用户随口说的一句“帮我把这个网站上的商品信息爬下来”来自己决定怎么点击、怎么输入、怎么翻页本质上已经超越了传统自动化工具“写死指令”的模式。但我的实测结论是纯LLM Agent在真实业务中的稳定性还远远不够。大模型会犯各种低级错误比如点击了错误的位置、在表单里填入了不合理的值、无法处理登录后的动态权限等。这些问题的根源倒不是大模型笨而是它的“感知”和“动作”之间缺少了一层可靠的系统控制自然语言本身的不确定性让整个过程变得不可预测。1.2 我的整体方案选型思路我这次实测的排序很简单先去对比了Selenium 4、Puppeteer、Playwright这三个主流浏览器自动化框架重点看了它们的API设计、并发能力、对现代Web特性的支持度。然后又选了几个开源项目比如Browser-use、UI-TARS对比它们在真实网页上的任务完成率。最终我自己的选型思路是不再把浏览器自动化工具和LLM Agent当成两个对立的方案而是把传统工具当成LLM的“手和眼睛”把大模型当成“决策层”。具体来说用LLM去理解用户需求和解析网页里的结构化信息用Playwright这类工具去执行实际的动作这样既保留了大模型的灵活性又用传统框架的稳定性把“下限”兜住。这个思路听起来不算惊艳但真在工程里落地稳定性比纯Agent路线高出一大截成本也低得多。2. 核心细节解析与实操要点2.1 三大传统工具的核心能力拆解我先说一下对三款工具的理解因为很多朋友在选择的时候会纠结其实它们各有侧重。Selenium是自动化测试界的老前辈生态最完善支持的语言最多Java、Python、C#、Ruby都有还能通过WebDriver驱动市面上几乎所有主流浏览器。但它的问题也明显——架构相对老旧API设计得比较繁琐执行慢在并发场景下资源占用很离谱。如果你需要在多个浏览器上跑兼容性测试Selenium依然是安全的选择。Puppeteer是由Chrome团队开发的默认直接操作Chrome或Chromium性能和API设计都更现代化。但它有个很尴尬的限制就是只支持Chrome系浏览器Firefox的支持是今年才逐渐完善的而且也没有官方支持IE这样老旧的浏览器。它的李子很薄通常是做爬虫和网页截图的首选。Playwright是我个人最喜欢的一个它同样是微软出品设计理念直接对标Puppeteer但做得更彻底。它可以同时驱动Chromium、Firefox和WebKit也就是Safari内核支持多标签页、多浏览器上下文的并发测试而且它有自动等待机制——默认情况下元素要等稳定了才去点击操作这直接干掉了让无数人头疼的“元素找不到”“点击无效”的问题。2.2 LLM Agent的核心工作流与我实测中看到的瓶颈接着说说LLM Agent。它的核心工作流通常是这样的大模型先把网页的可见文本、DOM结构、截图等信息变成多模态输入然后根据自己的推理规划下一步动作比如“点击登录按钮”“填写邮箱字段”再把这些规划转换成自动化框架能执行的指令。整个过程就是一个“感知-思考-行动”的循环。我特意拿几个网站在Browser-use上做了测试最直观的感受是它在处理简单的、结构清晰的页面时表现得挺惊艳比如搜索关键词、点击第一篇文章、提取正文这类操作的成功率还能看。但一旦页面复杂起来比如有多层弹窗、懒加载内容、动态阴影地区DOM节点模型就会频繁陷入循环或者直接帮用户跳到一个不可逆的错误操作。这里面的深层原因是LLM Agent对“步骤执行”的容错机制太弱。在传统测试里点击一个按钮失败了我们会有明确的重试逻辑但LLM Agent经常会自作聪明地换一个方式重新尝试结果越走越偏。2.3 两条路线的核心差异对比如果要把这两条路线的差异讲清楚直接用一张表格来对照最直观维度传统浏览器自动化LLM Agent执行确定性高只要脚本正确就能复现低同一任务多次执行结果可能不同开发效率需要写大量定位和等待逻辑只需描述目标模型自动规划路径环境变化适应性弱页面一变脚本往往就废了相对强能理解页面语义变化调试体验非常成熟日志、截图、录像、DOM追踪黒盒感强出了问题很难复现资源成本低纯脚本执行不消耗接口费用高每次操作都要消耗大量Token稳定性保障强有明确定位器和重试机制弱依赖模型能力随机性强适用场景高频稳定操作、大规模爬虫、自动化测试一次性、探索性、长尾任务从表格能明显看出这两条路线根本不是谁取代谁的关系而是各自都有难以替代的生态位。对我来说纯LLM Agent更多是“锦上添花”的角色传统工具依然是真正的“雪中送炭”。3. 实操过程与核心环节实现3.1 环境准备与基础配置这次实测我全部基于Python来操作因为Python在对大模型的调用、数据处理、生态集成上都比较顺手。环境准备其实不复杂关键确定好版本避免后面哭。我的环境是Python 3.10、Playwright 1.40以上版本浏览器内核直接用playwright install命令下载。如果你想跟我的环境保持一致在项目根目录执行python -m venv .venv source .venv/bin/activate # Windows下用 .venv\Scripts\activate pip install playwright playwright install chromium关于playwright install有个坑要提前说明因为它默认会下载Chromium、Firefox、WebKit三个内核体积大概有几百MB。如果只做Chrome系页面的自动化可以直接指定只装chromium能节省一半时间playwright install chromium3.2 用Playwright搭一个带LLM能力的通用页面操作工具接下来我分享一下我是怎么把Playwright和大模型结合起来做一个“长眼睛的自动化工具”的。思路说简单也简单就是用Playwright把页面截图和数据提取出来把这堆信息丢给大模型让它输出结构化的操作指令再由Playwright去执行并返回结果。第一步是先封装一个浏览器管理器。注释我都写得很清楚方便你直接改from playwright.sync_api import sync_playwright class BrowserSession: def __init__(self, headlessFalse): self._playwright sync_playwright().start() self.browser self._playwright.chromium.launch(headlessheadless) self.context self.browser.new_context( viewport{width: 1280, height: 720}, user_agentMozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0 Safari/537.36 ) self.page self.context.new_page() def navigate(self, url: str): self.page.goto(url, wait_untilnetworkidle, timeout30000) # 这里用networkidle是确保页面资源基本加载完毕 # 如果网络环境差可以考虑换成load或domcontentloaded def snapshot(self): 返回页面的截图base64和可访问性文本用于喂给大模型 img self.page.screenshot(typejpeg, quality70) text self.page.evaluate(() document.body.innerText) return img, text def execute_action(self, action: dict): # action示例{action: click, selector: text登录} kind action[action] if kind click: self.page.click(action[selector], timeout5000) elif kind fill: self.page.fill(action[selector], action[value]) elif kind wait: self.page.wait_for_timeout(action[ms]) elif kind scroll: self.page.mouse.wheel(0, action[y]) elif kind go_back: self.page.go_back() else: raise ValueError(f不支持的action类型: {kind}) return {ok: True} def close(self): self.browser.close() self._playwright.stop()这层封装并不复杂但它把关键点都照顾到了截图、文本提取、action执行都是后面和LLM配合的基石。第二步是设计大模型的操作规划提示词。这里要注意一点如果你让大模型自由发挥那整个流程一定会失控。我的经验是提供一个极约束力的输出格式让模型只能输出结构化JSON。SYSTEM_PROMPT 你是一个浏览器自动化操作员。你会收到页面截图和页面文本。 根据用户任务输出下一步唯一动作必须使用下面的JSON格式禁止输出任何其他内容。 可选动作: 1. click: {action: click, selector: textxxx} selector请优先使用文本选择器如果文本不唯一再使用CSS选择器。 2. fill: {action: fill, selector: #xxx, value: 输入内容} 3. wait: {action: wait, ms: 2000} 4. scroll: {action: scroll, y: 800} 5. done: {action: done, result: 任务完成这里放最终结果} 规则 - 每次只输出一个动作。 - 如果页面加载较慢就输出wait动作。 - 只有当用户任务真正完成或无法继续时才输出done。 - 提取数据时把数据放在done动作的result字段里。 在大模型选择上这一步反而是最自由的。我用过GPT-4系列也试过国产的GLM和Qwen在同样的结构化提示词下都能跑通。差异主要在速度和价格如果考虑成本和响应速度轻量模型在“parse文本、点击按钮”这类简单动作上完全够用。我自己平时调试用GLM正式跑批量任务反而会更倾向于DeepSeek-V3或Qwen系列性价比更香。3.3 自动完成任务的小例子抓取搜索结果为了验证这个“Playwright LLM”组合的可行性我设计了一个真实的抓取任务打开某知识平台搜索“浏览器自动化”把第一页搜索结果的文章标题、链接、简介全部提取出来。跑完这套流程后我看到LLM成功理解了任务语义自动完成了“输入关键词-点击搜索-提取列表”三步操作而且提取出的数据结构化程度很高是纯脚本很难直接做到的。核心调度代码大致是这样import json import base64 from openai import OpenAI client OpenAI( base_urlhttps://your-llm-api-endpoint, api_keyyour-api-key ) def run_autonomous_task(start_url, task_desc, max_steps10): session BrowserSession(headlessTrue) session.navigate(start_url) results [] for step in range(max_steps): img, page_text session.snapshot() img_b64 base64.b64encode(img).decode() response client.chat.completions.create( modelgpt-4o-mini, temperature0, messages[ {role: system, content: SYSTEM_PROMPT}, {role: user, content: f当前页面截图(见data:image/jpeg;base64,{img_b64})\n\n页面文本:{page_text}\n\n目标任务:{task_desc}} ], ) raw response.choices[0].message.content.strip() raw raw.replace(json, ).replace(, ) action json.loads(raw) if action[action] done: results action[result] break print(f[step {step 1}] 执行动作: {action}) session.execute_action(action) session.close() return results if __name__ __main__: data run_autonomous_task( start_urlhttps://example-platform.com/search, task_desc在搜索框输入‘浏览器自动化’点击搜索提取第一页结果中的标题、链接、摘要。 ) print(json.dumps(data, ensure_asciiFalse, indent2))这里有个小细节我在循环里用了一个max_steps限制最大步数这是实战中特别重要的保底机制。不加这个限制如果模型陷入死循环脚本会无休止地执行click和scroll直到把浏览器资源耗光。3.4 为什么这个组合比纯LLM Agent更稳我理解很多人会问这看起来跟LLM Agent差不多为什么我说是另一条路关键差别在于分工方式。纯LLM Agent是把“感知、规划、执行”全塞给模型相当于让一个只会纸上谈兵的人自己去拧螺丝。而我的方案是把执行层完全交给Playwright这类确定性工具模型只负责“看和想”然后输出简单的动作指令。举个例子登录操作。纯LLM Agent要自己去处理“用户名输入框在页面哪个位置”“密码框要不要先清空”“登录按钮在账号密码填完后是否可点”这一系列问题每一步都可能因为页面微小的布局变化而失败。但在我的方案里定位逻辑依然靠传统的选择器体系模型只需要输出“点击登录按钮”这个决策至于按钮在哪、用什么定位方式找到它、点击前要不要等待全是Playwright的自动等待和智能定位在兜底。这样的设计让整个系统具备两个特别宝贵的性质一是“中间层可插拔”觉得某个模型不好用了就直接换API业务代码完全不用动二是“过程可观测”每一步的大模型输出、动作执行结果、页面截图都能记录下来调试时能快速定位问题。4. 常见问题与排查技巧实录4.1 定位不到元素优先排查iframe和Shadow DOM这事几乎每个写过自动化的人都遇到过。我这次实测中也踩了一次恰好是在某个用Vue写的后台系统上点击一个“确认弹窗”按钮时反复超时。排查了半天最后用page.frames才发现那个按钮在iframe里Playwright默认的page.click根本访问不到。正确做法是先切换上下文再操作frame session.page.frames[1] # 或者用frame_locator精确定位 frame.click(text确认)还有一个同样隐蔽的坑是Shadow DOM。现在很多组件库都会把内部结构裹在#shadow-root里直接用page.click(text确认)是点不到的。解决办法是用穿透选择器比如page.click(my-component::shadow#confirm-btn)。如果你排查定位问题时发现控制台一直报“strict mode violation”说明匹配到了多个元素把selector收紧一下就好比如加上可见性过滤或者用nth匹配。4.2 动态页面加载慢从networkidle降到domcontentloaded最开始我封装navigate时用的是wait_untilnetworkidle在网速好的时候一切正常但后来跑到一个图片很多的电商页面整整等了40秒都没进入加载完成状态。原因是networkidle要求网络连接空闲时间超过500毫秒而电商页面的轮询请求、埋点上报、广告脚本基本不会停。后来我把wait_until改成了domcontentloaded配合sleep或者显式等待某个核心元素出现页面加载效率高了很多。这类动态页面还有个关键技巧就是优先使用带自动等待的点击操作比如page.click内部会等待元素可见、可点。千万别动不动就加time.sleep去“硬等”那样不仅慢还特别容易造成脚本不稳定。4.3 LLM输出不是合法JSON加入重试和规则校验用大模型做决策时最头疼的并不是模型“不会”操作而是它的输出格式偶尔会不符合要求。哪怕我在提示词里写得很清楚必须输出JSON但有时候模型还是会夹杂一句“好的我来帮你...”之类的话。我的对策是在调度层加一个输出清洗和重试机制。先尝试去掉代码块标记直接解析解析失败就丢回给模型要求重新输出并附带错误信息。如果连续三次解析失败就放弃这个动作让系统终止避免把错误传导到后续步骤。def robust_parse(raw_text, max_retry3): for attempt in range(max_retry): try: cleaned raw_text.strip().replace(json, ).replace(, ) return json.loads(cleaned) except Exception as e: last_error str(e) # 把错误信息回传给模型 raise RuntimeError(fLLM JSON解析失败最后错误: {last_error})4.4 浏览器并发占用内存过高用上下文隔离替代多开浏览器在做批量任务时我一开始的直觉是开多个浏览器实例并行结果跑了几十个标签页后电脑直接卡死。后来查了文档才发现Playwright支持一个浏览器实例里创建多个context每个context就相当于一个独立的浏览器会话隔离性和多开浏览器几乎一样资源占用却小很多。所以现在写并发任务时我都是复用Browser实例按任务数创建context然后设定一个合理的并发上限比如8个避免资源过载。4.5 登录态失效问题用storage_state持久化session很多需要登录的站点每次重新启动浏览器都要重新走登录流程非常费时。Playwright提供了一个特别好用的storage_state机制可以把cookies和localStorage保存到本地文件下次启动时直接复用。做法很简单第一次手动或自动登录后调用context.storage_state(pathstate.json)之后每次新建context时传入storage_state参数。我实测发现这个方式比直接保存cookie列表靠谱得多因为部分现代框架的认证信息存在IndexedDB或者内存里光靠cookies往往不够。4.6 常见问题速查表症状可能原因解决方案元素点击报timeout元素在iframe/Shadow DOM/遮罩层下切换frame、用穿透选择器、检查遮罩层页面加载永远不结束站点有轮询或埋点请求改用domcontentloaded配合显式等待LLM输出解析失败模型返回了多余文字或格式错误清洗文本、加入重试和自动纠错并发任务内存爆炸开了过多浏览器实例复用Browser用context隔离并发登录态反复失效认证信息没持久化用storage_state保存cookies和localStorage按钮总是点不中页面有懒加载或元素被刷新替换用auto-wait避免硬编码sleep5. 后续还可以怎么扩展这次实测做完我最深的体会是“工具没有新旧只有合适不合适”。LLM Agent这条路确实代表着未来但现阶段它更适合作为增强传统自动化的“大脑”而不是彻底替代整个执行链路。如果后续想继续扩展我认为还有几个特别值得做的方向。一是把动作结果用视觉回归的方式校验比如每次点击后截图对比差异能有效发现模型走错路的问题二是引入“多模型路由”简单动作用轻量模型复杂推理用强模型成本可以压得非常低三是把中间层的动作抽象成领域技能比如登录、翻页、提取列表这样不同业务任务可以直接复用技能库不需要每次重新启发式探索。另外如果你准备投入生产环境最好在项目开始就设计好日志采集和监控链路。LLM输出、动作执行耗时、页面截图、成功失败状态这些数据越早收集越好后面做优化和问题复盘都离不开它们。我这次因为前期只做了简单的日志导致后面想复盘某次误操作时找半天才把现场信息拼齐这个亏大家可以不用重复踩。说到底最后能给普通用户带来价值的永远是稳定、低成本、可复现的方案。能在LLM这波浪潮里稳住自己的选型判断不盲目追新也不固步自封这才是真正实用的技术路线。