ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Agent开发实战:无头浏览器如何让大模型真正操作网页

Agent开发实战:无头浏览器如何让大模型真正操作网页 做Agent开发的朋友十有八九会卡在同一个问题上Agent的“大脑”已经有了可它怎么真正去操作网页我最近给几个Agent项目做技术方案时发现大家最后都会落到同一个答案上——无头浏览器。这玩意儿说白了就是把浏览器内核跑到后台没有窗口但能正常渲染页面、执行JavaScript、模拟点击输入。对Agent来说它就是那双“手”帮大模型把规划好的步骤变成真实世界的网页操作。这篇内容我把主流方案捋了一遍重点讲Playwright、Puppeteer、Selenium这几款怎么选、怎么配、实际跑起来有哪些坑适合正在做Agent开发、网页自动化、自动化测试或者想给爬虫程序升级的朋友参考。1. 为什么Agent开发绕不开无头浏览器很多刚接触Agent的人会问大模型不是能直接生成答案吗为什么非要让Agent去操作浏览器这个问题的答案恰恰是理解Agent落地的关键。1.1 Agent的“眼睛”“手”和普通接口调用的区别咱们先拆解一下Agent的运行逻辑。一个完整的Agent通常由规划Planning、记忆Memory、工具调用Tool Use和编排Harness几个部分构成。大模型负责“想”但真正“做”得靠工具去执行而浏览器就是工具层里非常重要的一类。为什么很多任务必须上浏览器而不是调API最直接的原因就是大量网站根本没有公开API或者说就算有API也跟你用户在网页上能做的操作不对等。比如你要做一个自动比价的Agent去电商平台查商品价格、库存、优惠信息这些数据大部分藏在动态网页里得登录、得点按钮、得等数据异步加载出来纯靠requests去拉HTML根本拿不到完整内容。无头浏览器解决的就是这个“信息获取与操作执行”的断层。它可以在没有界面的情况下完整加载一个网页执行里面的JavaScript等动态数据渲染完成然后再去读取DOM、截图、或者模拟用户操作。对Agent来讲浏览器相当于提供了一双随时可用的“眼睛”和“手”。同时无头浏览器还解决了Agent任务执行中的“可观察性”问题。Agent任务执行到哪一步了、页面变成什么样了、元素有没有出现这些都可以通过截图、抓DOM、录视频等方式记录下来供上层模型和开发者判断。这一点是普通HTTP请求完全做不到的。我在实际项目里最常用的场景有三类一是动态数据抓取比如后台管理系统里的报表数据二是多步骤操作比如自动填表单、点击翻页、处理弹窗三是执行结果验证任务说“下单成功”那就去页面上找成功标识或者截图确认。这三类需求没有无头浏览器Agent基本寸步难行。1.2 无头浏览器是个“容器”不只是浏览器这里要纠正一个误区无头浏览器不只是“没有窗口的浏览器”它更像是一个可编程的“页面操作容器”。以Playwright为例它底层是完整的Chromium或Firefox内核但通过WebDriver或CDP协议暴露出来让开发者可以精确控制每一个页面、每个标签页、每一帧渲染结果。更重要的是无头浏览器给Agent开发提供了一个天然的“工具化”边界。你在设计Agent架构时不需要让大模型直接去拼浏览器底层指令而是把浏览器的能力封装成一个一个工具函数比如“打开网页”“点击元素”“输入文字”“截图返回”等。Agent只需要决定“下一步调哪个工具”浏览器这边负责“怎么执行”。这种分工一旦清晰Agent的稳定性和可维护性都会大幅提高。所以给Agent选无头浏览器本质上是在选“工具底座”。底座选得好后面封装工具、写流程、排查问题都会顺畅很多选得不好等Agent跑复杂任务的时候你会被各种莫名其妙的元素定位失败和内存崩溃折磨到怀疑人生。2. 目前主流的无头浏览器横向对比选型先想清楚这个章节我给几个主流方案做个比较。先说结论如果今天有人让我推荐一个给Agent用的无头浏览器我首选Playwright。但如果你已经在Node项目里Puppeteer也不错如果公司有大量的Selenium历史脚本那也可以继续用。其他更高层封装后面单独说。2.1 PlaywrightAgent开发的事实首选Playwright是这几年浏览器自动化领域绕不开的名字。它由一家大型软件公司的开源团队维护最大的特点是把“开发者体验”做到了极致。它支持Chromium、Firefox、WebKit三种内核这意味着你可以用同一套API同时测Chrome、Edge、Safari背后的渲染引擎。对Agent开发来说这种多内核支持很有价值。我在做一个需要兼容多个浏览器环境的Agent时直接用Playwright切内核一行参数就搞定省掉了很多单独适配工作。Playwright另一个杀手锏是“自动等待”。传统工具里你要写time.sleep(3)等页面加载Playwright则是在执行点击、输入、截图这些操作前会自动等待元素处于可操作状态这个机制在Agent场景里极其重要。因为Agent每一步操作都依赖上一步的页面状态如果页面没加载完就去点击十次有九次会失败。有了自动等待至少少踩一半的坑。此外Playwright的调试工具很完善。录制脚本、生成trace日志、录视频、截图都是开箱即用。我在给Agent排错时最常用的操作就是把trace文件拉出来回放看看Agent到底在页面上做了什么操作这一步比看任何日志都直观。2.2 PuppeteerNode生态里的轻量选择如果你本身是Node.js技术栈Puppeteer可能是更自然的选择。它是Chrome团队出品的Node库基于Chrome DevTools Protocol工作。Puppeteer的API设计直观上手快和Playwright在“无头浏览器”这个定位上功能很接近但最大的差异在于生态绑定Puppeteer主要支持Chromium系浏览器对Firefox和WebKit的支持是后来才慢慢补的成熟度和细腻程度不如Playwright。Puppeteer也有它自己的优势。如果你的Agent项目本身是一个Node.js服务比如用LangChain.js或者其他TypeScript框架写的直接在同一个进程里用Puppeteer依赖管理最简单不用引入一套新的运行时。它的社区生态非常庞大各种网页抓取库、自动化框架都是基于Puppeteer做的遇到问题基本都能搜到答案。不过我在实际项目里比较少单独给Agent用Puppeteer主要原因是自动等待机制和调试体验稍逊色一些。新写的代码里我基本都用Playwright只有当项目里已经跑着大量Puppeteer脚本时才考虑保留。2.3 Selenium老牌选手依然能打但略显笨重Selenium是这个领域资历最老的方案它通过WebDriver协议控制真实浏览器支持的语言非常多Java、Python、C#、JavaScript、Ruby基本你用什么语言都能接。如果你维护的是遗留的自动化测试框架那Selenium是绕不开的存量技术债。但Agent开发场景下Selenium的问题也很明显一是没有内置的自动等待代码里到处是显式等待和轮询逻辑写起来啰嗦二是调试工具不如新一代方案方便录trace、回放这种功能基本没有三是启动一个Selenium的远程节点需要额外配置Selenium Grid对快速搭建Agent原型来说偏重。我给一个客户的Agent项目做技术选型时他们原本用了Selenium跑了一个月发现两个问题页面只要稍微改版脚本就崩因为等待逻辑全是硬编码的并发跑多个任务时远程节点资源管理也麻烦。后来换到Playwright同样一批场景代码量少了三分之一稳定性还提高了不少。如果你是Agent新手不建议从Selenium入门当然如果你们团队有成熟的Selenium基建也不必为换而换够用就好。2.4 面向Agent的封装层Browser-Use与MCP除了直接用上面这些库现在还有一个更“Agent原生”的方向就是在Playwright等库之上再做一层面向大模型的封装。比如browser-use这个开源项目它把浏览器操作封装成了Agent可以直接调用的工具给大模型传“打开页面”“获取内容”“点击元素”这类语义化APIAgent只需要自然语言就能操作浏览器。这类封装对做原型验证特别方便你不用自己写一堆浏览器工具的封装代码跑通流程再说。另一个大趋势是MCPModel Context Protocol它把“Agent能力”和“工具实现”做了一个标准接口让不同的Agent框架可以轻松接入各种外部工具。浏览器这个场景自然也被纳入MCP生态里。如果你在做可扩展的Agent平台用MCP方式暴露浏览器能力能让Agent生态里的其他组件更容易对接。这几层之间的关系简单来说就是无头浏览器是底盘Playwright/Puppeteer是驱动层browser-use是面向模型的封装层MCP是做工具互通的标准协议。做Agent的时候不用一上来就全上可以先从底盘加驱动开始跑通再决定要不要上封装层。下面是几个方案的快速对比表供选型参考方案语言生态浏览器支持自动等待调试/追踪Agent友好度适合场景PlaywrightPython/Node/Java/.NETChromium/Firefox/WebKit内置强trace、视频、录制极高新建Agent项目、跨浏览器场景PuppeteerNode主要是Chromium基础支持中等较高Node技术栈、快速原型Selenium多语言多浏览器需自行实现较弱一般遗留测试框架、存量项目Browser-UsePython基于Playwright依赖底层依赖底层极高Agent原型、面向LLM的操作封装MCP方案多语言依赖实现依赖实现依赖实现极高可扩展Agent平台、工具互通3. Agent场景下的无头浏览器实操配置与调用选型定了接下来就是动手。这里我以Playwright Python版为主把Agent场景下最常碰到的一套配置和调用流程完整走一遍从环境准备到工具封装再到调试追踪每一步都有代码和心得。这几个步骤跑通了你就能给Agent接上一双真正能干活的手。3.1 环境准备与最小示例先装Playwright和对应浏览器内核。Python环境里直接用pip或uv安装pip install playwright playwright install chromium第二行命令不要省它负责下载Chromium内核的二进制文件不执行的话运行时会报浏览器找不到的错误。如果你的项目只需要Chromium就只装chromium如果还要测WebKit或Firefox把对应内核名字加上就行。不过给Agent日常使用只装Chromium是完全够用的还省磁盘空间。装好后最小示例大概长这样import asyncio from playwright.async_api import async_playwright async def main(): async with async_playwright() as p: browser await p.chromium.launch(headlessTrue) page await browser.new_page() await page.goto(https://example.com) print(await page.title()) await page.screenshot(pathexample.png, full_pageTrue) await browser.close() asyncio.run(main())这段代码里headlessTrue就是无头模式后台运行不弹窗口headlessFalse则是有头模式调试的时候可以直观看到浏览器在做什么。这里有一个非常重要的实操建议调试Agent逻辑时一定要先用headlessFalse跑一遍等确认步骤稳定了再切回无头模式。我踩过太多次“无头模式跑出错但有头模式一切正常”的坑原因是有些页面会根据浏览器窗口可见性做不同渲染策略。另外异步API在Agent场景里几乎必备因为Agent往往会并行跑多个任务同步API一旦碰上一个慢页面整个进程就卡住了。所以建议直接用async_playwright写异步代码。3.2 三个必须掌握的配置点等待策略、上下文隔离、视角参数无头浏览器配置里等多久、怎么等是门大学问。Playwright内置了自动等待但你还得学会怎么用显式等待补充它。我的习惯是打开页面后先等一个核心元素出现再继续操作。比如要实现“登录后台并抓取数据”这个Agent任务最怕的就是页面一直转圈。写法类似于await page.goto(url, wait_untildomcontentloaded, timeout30000) await page.wait_for_selector(#app, timeout10000)wait_until控制页面加载到哪个阶段才认为成功domcontentloaded比load更快适合数据是异步加载的页面如果页面必须等图片和脚本全部加载完才用load或者networkidle。注意networkidle虽然最“稳”但很多站点一直有网络请求容易等超时慎用。第二个关键点是上下文隔离。在Playwright里browser.new_context()会创建一个干净的上下文环境包括独立的Cookie、缓存和本地存储。Agent每执行一个任务最好都新建一个上下文任务结束后关闭避免上一个任务留下的登录态、表单数据污染下一个任务。我用一段代码说明context await browser.new_context( viewport{width: 1280, height: 720}, localezh-CN, user_agentMozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 ) page await context.new_page() # 执行Agent任务 await context.close()这里把viewport、locale、user_agent都设置成比较常见的真实浏览器参数。很多站点会读取这些信息来判断访问者类型设置成常规桌面浏览器的值可以有效减少被识别为自动化工具的概率。如果你需要反复使用同一个登录状态比如很多内部系统的验证码登录很麻烦也可以把上下文保存到磁盘下次直接加载这个功能对Agent批量任务比较实用。第三个要提的是headless新老模式的差异。新版Playwright支持headlessTrue和headlessFalse而在老版本里还有一个headlessold的旧模式不过现在已经基本废弃了。日常开发直接写布尔值就行。有头模式和无头模式在截图、字体渲染上都可能有微小差异最后上线前一定要用无头模式完整回归一遍所有Agent步骤。3.3 把浏览器能力封装成Agent可调用的工具无头浏览器装好、配置跑通后接下来的关键步骤是把浏览器能力封装成Agent可调用的工具函数。这个封装是整个Agent项目里最核心的工程之一。封装的好坏直接决定上层大模型能不能顺利完成任务。我从一个真实项目里抽象出一个简单例子。假设我们要做一个“查询商品价格”的Agent底层需要两步打开商品页面、读取价格文本。对应工具函数大概长这样async def open_page(page, url): try: await page.goto(url, wait_untildomcontentloaded, timeout30000) return {status: ok, title: await page.title()} except Exception as e: return {status: error, message: str(e)} async def extract_price(page, selector): try: element await page.wait_for_selector(selector, timeout5000) text await element.inner_text() return {status: ok, price: text.strip()} except Exception as e: return {status: error, message: str(e)}这里有两个细节值得特别注意。第一每个工具函数都必须有完整的异常处理因为Agent调工具时模型本身不知道工具会报什么错而一个健壮的工具接口应该永远返回一个结构化状态让上层知道“成功了”还是“失败了”。第二每个工具函数尽量不要让Agent去传复杂参数。比如extract_price里的selector如果让大模型自己写CSS选择器经常会出现选择器写错导致元素定位失败。更可靠的做法是让Agent传语义化参数比如“价格区域”然后在代码里提前把选择器和语义化名字做一张映射表。这样大模型不需要懂CSS也能准确调用工具。封装完成后就可以串成一个简单的Agent执行流程模型决定“先打开商品页再提取价格”程序这边顺序调用两个工具把结果回传给模型模型自然语言总结输出。整个链路其实不复杂难的是工具的边界设计——什么该让模型决定什么该在代码里写死这个度需要你在实际项目里根据任务复杂度慢慢调整。3.4 调试与追踪让Agent执行过程“看得见”Agent跑起来之后最让人头疼的问题就是它明明调用了工具页面也操作了但结果不对。这时候如果没有可视化的追踪手段排查成本会非常高。Playwright在这方面下了很大功夫我很依赖三个功能录视频、trace回溯、请求失败监听。录视频很简单在创建上下文时设置record_video_dir参数这个上下文里每个页面操作都会被自动录下来任务结束后视频会存成webm格式context await browser.new_context(record_video_dirvideos/)任务结束后我直接把视频拉出来看一遍基本上就能判断Agent是在哪一步“走偏”的。比如Agent本来该点击“确认支付”结果点到了旁边“取消订单”看视频一眼就能发现。trace回溯更强。它不仅能记录页面操作还能记录每一个步骤的DOM快照、网络请求、控制台日志。开启方法是在上下文创建后调用tracing.start()任务结束时tracing.stop(pathtrace.zip)然后把文件拖进Playwright的Trace Viewer页面就能像看录像回放一样逐帧检查。我在做Agent并发任务排查时这个功能帮了大忙好几个诡异问题都是靠回放定位到的。请求失败监听也建议默认开启。很多页面加载失败不是白屏而是某些CDN资源挂了页面照样渲染但关键数据缺失。通过page.on(requestfailed)把失败请求记下来可以快速判断是不是外部资源问题page.on(requestfailed, lambda req: print(请求失败:, req.url, req.failure))把这些追踪手段配置好之后Agent的开发体验会完全不同。调试不再是“盲人摸象”而是像看实时探案记录一样每一步都有据可查。4. Agent实战中的常见问题与排查记录无头浏览器和Agent结合之后实际跑起来的问题非常现实。我把这段时间在项目里遇到的高频问题整理成了一份排查手册每一条都是我真实踩过的坑。大家做Agent遇到类似情况时可以直接照着排查。4.1 页面加载超时与白屏表现是Agent任务卡在打开网页那一步控制台报TimeoutError或者页面打开后一直是白屏/旋转加载状态。这种情况通常有三类原因目标站点响应慢、页面依赖的静态资源加载不出来、页面本身是SPA单页应用需要等JS执行完才渲染内容。我的排查顺序是先提高goto的timeout看是不是单纯速度问题再用page.on(requestfailed)监听资源加载情况最后看页面HTML结构确认是不是JS渲染问题。如果是SPA渲染就把wait_until调成load或者直接wait_for_selector等待核心DOM节点出现。这里有个细节Playwright的自动等待默认是等元素“可操作”但有些页面元素虽然出现了内容却还在异步加载这时候需要额外等一个网络请求完成或数据节点文本变化不要一看到元素就继续执行。4.2 被目标站点识别为自动化工具表现是返回异常数据、弹出验证码、页面直接拒绝访问。这种现象确实存在尤其是一些有安全策略的站点会对无头浏览器做特征检测主要检查浏览器的指纹参数、是否开启自动化控制标志、UA字符串等。针对这个问题最基础的做法是设置更真实的UA和viewport参数这在前面的代码示例里已经写过。另外不要一打开页面就立刻做操作模拟一点人类行为比如轻微滚动、停顿几十毫秒会降低被识别概率。但说实话这种方式并不能保证100%绕过检测而且需要在合规范围内使用不能用于恶意抓取或攻击。如果你的Agent是拿自己业务系统的数据或者授权过的站点做自动化一般不会遇到这个问题。如果非要做外部站点高对抗场景那就不是换一个无头浏览器能解决的了需要考虑更复杂的浏览器指纹方案那个层面的内容已经超出今天这篇文章的范畴。4.3 并发任务一多进程就崩内存与资源管理Agent同时跑多个浏览器实例时内存很容易被打满进程直接OOM退出。这是新手最容易忽略的问题因为本地一两个任务看不出来一上并发就崩。核心原因是没有做好生命周期管理上下文创建了不关闭、浏览器实例没复用、任务失败后没有清理现场。我的建议是每个Agent任务使用独立的context任务结束用finally确保context.close()执行浏览器实例尽量复用不要每个任务都重新启动一个浏览器进程启动浏览器的开销远比启动一个上下文大得多在任务调度层加信号量或队列限制并发数量。举个例子用一个asyncio.Semaphore(3)就能把并发浏览器上下文限制在3个以内避免系统资源被瞬间占满。另外给容器或进程设置合适的内存上限也很重要至少预留1GB给一个浏览器实例会比较稳妥。4.4 元素定位失败与页面结构变化表现是Agent执行到某个步骤时报ElementNotFound或者明明代码里写了选择器偶尔还是找不到元素。这类问题在动态页面上尤其多原因可能是页面结构改版、元素在iframe里、或者元素是对话框/悬浮层里的内容。我自己的排查方法是先在录制模式跑一遍用Playwright的codegen工具生成正确的选择器优先用get_by_role、get_by_text这类语义化定位器比单纯写CSS类名更抗页面改版。如果元素在iframe里要先切换到对应iframe再去定位。如果页面经常做A/B测试某类元素会随逻辑不同而改变那就多写几个候选选择器按顺序尝试找不到第一个就试下一个。这类兜底逻辑虽然笨但在Agent场景里挺好用因为Agent每多失败一步整个任务的成功率就往下掉一截。关于元素定位的具体选择器规则我列了个小表方便参考定位方式示例适用场景get_by_rolepage.get_by_role(button, name登录)按钮、链接等交互元素get_by_textpage.get_by_text(确认支付)文本内容定位get_by_placeholderpage.get_by_placeholder(请输入手机号)表单输入框CSS选择器page.locator(.price-tag)结构稳定的元素XPathpage.locator(xpath//div[classprice])复杂结构兜底4.5 高频问题速查表下面这张表是我记在项目文档里的排障速查表覆盖Agent场景下无头浏览器最常见的问题、可能原因和对应解决方案遇到问题可以直接对应查。问题现象常见原因排查手段解决方案执行到中途报TimeoutError等待策略太严/页面加载慢打开trace看卡在哪一步调整wait_until换成等待核心元素无头模式正常、有头模式报错或反过来页面渲染策略与可见性相关对比两种模式截图统一用无头模式回归调试时用有头模式被识别自动化、弹验证码指纹特征/自动化标志被检测检查UA、viewport、webdriver标记设置真实UA和设备参数减慢操作节奏并发任务多时进程崩溃上下文未关闭/内存超限free -h观察内存记录进程数复用浏览器实例、关闭context、加并发信号量元素找不到页面改版/iframe/动态渲染codegen生成正确选择器用语义化定位多候选选择器兜底页面加载后内容为空SPA异步渲染未完成查看HTML结构、请求日志等待DOM节点或网络请求完成日志显示agent execution terminated due to error.Agent执行异常被上层中断拉取trace和视频回放定位具体工具步骤给工具加大粒度错误捕获最后再分享一个小经验。我试过把Playwright的上下文保存到硬盘在Agent需要重复登录的场景下复用登录态这样能省掉大量验证码和登录流程的时间。具体做法是登录成功后调用context.storage_state(pathstate.json)下次创建上下文时直接加载storage_statestate.jsonCookie和本地存储都会自动恢复。这个方法在我做内部系统的数据采集Agent时特别管用直接把整个登录环节从任务流程里删掉了。另外如果你在做一个Agent工具平台建议从一开始就把浏览器的trace和视频能力做成默认开启、可视化回放这对后续的Agent排错和功能迭代价值会越来越大。无头浏览器只是工具真正决定Agent可靠性的是你对任务的拆解、工具的边界设计和故障排查能力。走通一遍之后你会发现这条路虽然坑不少但每一步都值得。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进