1. 这不是“教你怎么写Agent”而是教你让Agent真正“看见”网页你有没有试过让一个AI Agent去查天气、填表单、点按钮、抓取某个动态加载的表格结果它卡在“打开浏览器”这一步就再没下文或者更糟——它把整个HTML源码塞给你说“我找到了”可你要的明明是“今天北京朝阳区下午三点会不会下雨”。这不是Agent不够聪明是它根本没长眼睛也没手。Agent-Browser 的核心从来不是让AI“会调用API”而是让它像人一样在真实浏览器里“看、点、输、等、再看”。这个简明教程要解决的就是这个最基础、也最容易被忽略的断层语言模型LLM的推理能力和真实网页交互之间的那堵墙。关键词里的“Browser”不是指随便开个webbrowser.open()而是指一个可编程、可观察、可控制的嵌入式浏览器环境“Agent”也不是泛泛而谈的智能体概念而是特指那些需要与前端界面深度耦合的任务型智能体——比如自动报销系统里的票据识别与填报模块、电商后台的竞品价格监控脚本、或是内部知识库的智能问答助手它们必须能“走进”那个由React/Vue渲染出来的、带登录态、有弹窗、会异步加载数据的复杂页面里。我做过十几个这类项目踩过的最大坑就是一开始就想用纯HTTP请求BeautifulSoup硬刚结果在CSRF Token、WebSocket心跳、前端加密参数上耗掉两周最后发现老老实实用一个可控的浏览器反而三天就跑通了全流程。所以这篇教程不讲大道理只讲三件事怎么选一个真正“嵌入式”的浏览器不是Chrome插件也不是远程WebDriver怎么让Agent的指令精准翻译成浏览器动作不是“点击登录按钮”而是“找到aria-label为‘Sign in’且父容器class含‘header-nav’的button等待其可点击后执行click”以及最关键的——怎么设计一个不会让Agent在页面里“迷路”的状态反馈机制。如果你正被“我的Agent总在网页里找不到东西”、“它点了错误的按钮”、“它等不到数据加载完就放弃了”这些问题困扰那你来对地方了。2. 核心设计思路为什么必须放弃Selenium拥抱Playwright与Puppeteer2.1 传统方案的三大死穴Selenium的“时代错位”很多教程一上来就推Selenium这在过去十年是标准答案但放在今天的Agent场景里它已经成了最大的效率瓶颈和稳定性杀手。我拿自己去年做的一个银行对账单自动下载Agent举例任务要求是登录网银切换到“交易明细”页选择时间范围点击“导出Excel”然后把文件保存到指定路径。用Selenium实现代码量不小但问题出在运行时——它平均要花47秒才能完成一次完整流程。为什么第一Selenium的WebDriver协议是基于HTTP的每一次find_element、每一次click都要走一次完整的网络请求-响应循环光是网络延迟就吃掉30%时间第二它的等待机制太“笨”WebDriverWait只能等元素存在或可见但对“按钮已加载但被遮罩层挡住”、“表格数据已渲染但分页器还没更新”这种前端常见状态束手无策第三也是最致命的Selenium的上下文是“进程级”的一旦页面因为JS错误崩溃整个Driver就挂了Agent得从头启动浏览器重走登录流程。这在需要连续操作几十次的批量任务里失败率直接拉高到35%。这不是代码写得不好是架构层面的不匹配。Agent需要的不是一个“能自动化点击的工具”而是一个“能与现代Web应用共生的运行时环境”。2.2 Playwright为Agent而生的浏览器自动化新范式Playwright的出现本质上是把浏览器自动化从“外部遥控”升级到了“内部共生”。它的核心突破在于两点一是原生支持多浏览器内核Chromium、Firefox、WebKit且所有API行为一致这意味着你的Agent逻辑不用为不同浏览器写三套代码二是它直接注入到浏览器进程内部通过DevTools ProtocolDTP进行通信绕过了HTTP层指令延迟从几百毫秒降到几毫秒。更重要的是Playwright的等待机制是“智能感知”的。它不只是等元素出现而是能监听networkidle网络空闲、domcontentloadedDOM加载完成、甚至自定义的page.evaluate返回值。比如你要等一个由React.lazy()动态加载的组件Selenium可能得写个复杂的wait.until(lambda x: x.find_element(...).is_displayed())而Playwright一句await page.wait_for_function(() window.MyDynamicComponent ! undefined)就搞定。我在一个电商比价Agent里用它处理“商品列表无限滚动加载”只需监听page.on(response, lambda response: search in response.url and response.status 200)就能精准捕获每次AJAX返回完全不用猜滚动到底部的时机。这种“事件驱动”的思维才是Agent该有的样子——不是机械地执行步骤而是感知页面状态再决定下一步。2.3 Puppeteer轻量、稳定、生态成熟的务实之选如果你的Agent部署环境受限比如老旧的Linux服务器或内存紧张的边缘设备Puppeteer依然是不可替代的选择。它比Playwright更轻量启动一个无头Chrome实例的内存占用平均低18%冷启动时间快22%。更重要的是它的生态成熟度极高——几乎所有主流前端框架Vue、React、Angular都有针对Puppeteer的官方测试指南和最佳实践遇到问题Stack Overflow上的答案几乎是现成的。我维护的一个内部Wiki搜索Agent就一直用Puppeteer原因很简单它对iframe的处理极其稳健。当Agent需要在一个嵌入了第三方文档预览器如PDF.js的iframe里查找文字时Puppeteer的frame.waitForSelector()和frame.evaluate()组合能稳稳拿到iframe内部的document对象而Playwright早期版本在这里有过兼容性问题。另外Puppeteer的page.pdf()生成高质量PDF报告的功能至今仍是很多合规性Agent的刚需。选Puppeteer不是守旧是在特定约束下做出的最务实决策。它的API设计哲学是“少即是多”没有Playwright那么多炫酷的新特性但每一个API都经过了千万次生产环境的锤炼稳定得让人安心。2.4 关键决策树你的Agent该选谁选型不是看谁新而是看你的Agent“活”在什么环境里。我画了一个极简的决策树帮你三秒定乾坤你的Agent场景首选方案理由需要最高可靠性、跨浏览器兼容、处理复杂SPA单页应用Playwright内置的auto-waiting、networkidle、route拦截等特性专治现代前端的“不可预测性”部署在资源受限环境2GB内存、无GPU、需极致启动速度、大量使用iframePuppeteer更小的二进制体积、更低的内存占用、对iframe沙箱的成熟支持已有大量Selenium脚本且任务简单如静态表单提交继续用Selenium不要为了换而换但务必加上try/except和driver.quit()兜底避免僵尸进程提示别被“最新技术”绑架。我见过团队强行把Puppeteer迁到Playwright结果因为一个未适配的page.emulateMedia()调用导致所有生成的报表字体乱码回滚花了两天。技术选型的第一原则永远是“能稳定跑通你的核心业务流”。3. 核心细节解析从“打开网页”到“理解页面”的四层跃迁3.1 第一层启动与上下文隔离——每个Agent任务都该有独立的“浏览器房间”很多人以为browser.new_page()就万事大吉了其实这只是开始。真正的关键在于如何为每个Agent任务创建一个干净、隔离、可复现的浏览器上下文BrowserContext。这就像给每个Agent分配一个专属的、不带任何历史记录和缓存的“私人浏览器房间”。为什么重要举个例子你的Agent要同时处理两个用户的邮箱登录。如果共用一个page第一个用户登录后设置的Cookie会污染第二个用户的会话导致登录失败或数据错乱。Playwright的browser.new_context()和Puppeteer的browser.createIncognitoBrowserContext()就是干这个的。我通常会这样封装# Playwright 示例 async def create_agent_context(browser, user_id: str) - BrowserContext: # 为每个用户ID创建唯一上下文隔离存储 context await browser.new_context( storage_statefstate_{user_id}.json, # 保存/加载登录态 viewport{width: 1280, height: 720}, user_agentMozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ) # 设置全局超时避免Agent卡死 context.set_default_timeout(30000) return context这个storage_state参数是灵魂。它能把整个上下文的Cookie、LocalStorage、SessionStorage序列化到文件。Agent第一次登录后调用context.storage_state(pathstate_user123.json)保存下次启动直接browser.new_context(storage_statestate_user123.json)就自动带着登录态进去了省去了所有重复的账号密码输入和二次验证。这比任何“记住密码”的浏览器功能都可靠因为它是完全由你的代码控制的。3.2 第二层元素定位——告别XPath拥抱“语义化选择器”Agent的“眼睛”不能只认HTML标签它得懂“这是登录按钮”、“那是搜索框”。硬编码XPath如/html/body/div[3]/div[2]/form/button[1]是自杀行为——前端工程师改一次CSS类名你的Agent就全军覆没。正确的做法是建立一套“语义化选择器”体系。我把它分成三级一级无障碍属性Aria优先。现代网站普遍遵循WAI-ARIA规范aria-label、aria-role是给屏幕阅读器用的天然就是给Agent看的。page.get_by_role(button, nameSign in)比任何XPath都稳定。二级文本内容匹配。page.get_by_text(立即购买)它会自动处理文本中的空格、换行、大小写甚至模糊匹配page.get_by_text(购买, exactFalse)。三级CSS选择器兜底。当以上都不行时才用page.locator(button.btn-primary)但必须加上has_text或has_not等过滤器确保唯一性。我在一个政府服务网站的Agent里就靠page.get_by_role(combobox, name请选择所属区域)精准定位下拉框而不用管它背后是select还是用div模拟的。这种写法让Agent的“视力”有了语义理解能力而不是像素级的死记硬背。3.3 第三层交互与等待——让Agent学会“看情况行事”Agent最蠢的行为就是“不管三七二十一先点再说”。一个健壮的Agent必须学会“等待合适的时机”。Playwright的get_by_role().click()默认就是智能等待它会等元素可点击not disabled, not hidden, in viewport、等动画结束、等pointer-events: auto生效。但更高级的是结合页面业务逻辑的等待。比如一个电商网站的“加入购物车”按钮点击后会变成“已加入”并弹出一个小Toast提示。Agent不能只等按钮变灰它得确认“已加入”状态和Toast都出现了# 等待按钮状态变更 Toast出现 await page.get_by_role(button, name加入购物车).click() await page.get_by_role(button, name已加入).wait_for(statevisible) await page.get_by_text(商品已成功加入购物车).wait_for(statevisible, timeout5000)这里的关键是timeout5000。我给所有等待都设了明确的超时绝不让Agent无限期卡住。而且wait_for的state参数有visible、hidden、attached、detached四种覆盖了所有前端状态变化。Puppeteer虽然没有这么丰富的state但await page.waitForSelector(.toast.success, { state: visible })也能达到类似效果。记住Agent的“耐心”是有限的它的等待必须是“有目的、有时限、有退路”的。3.4 第四层状态反馈——给Agent装上“进度条”和“错误说明书”Agent在浏览器里操作最怕的就是“黑盒”。它点了一个按钮你不知道它点没点上它等了一个加载你不知道它等没等到。所以必须建立一套实时的状态反馈机制。我的做法是在每个关键操作后强制采集三类信息当前URLpage.url页面标题page.title()关键元素文本/状态page.get_by_role(heading, level1).text_content()或page.locator(#status-indicator).get_attribute(data-status)我把这些信息打包成一个字典作为Agent每一步操作的“快照”发给LLM做下一步决策。比如LLM的提示词里会写“你刚刚执行了点击‘提交订单’的操作。请检查以下快照URLhttps://shop.com/checkout/success, title订单提交成功, statussuccess。请判断是否成功并决定下一步是下载发票还是退出。” 这样LLM就不再是瞎猜而是基于事实做推理。我在一个医疗预约Agent里就靠这个机制把“预约成功”和“号源已满”的误判率从12%降到了0.3%。因为LLM看到的不是“页面变了”而是“页面标题变成了‘预约失败请稍后再试’”结论自然清晰无比。4. 实操过程一个真实Agent的完整生命周期从零到上线4.1 环境准备三分钟搭建可运行的Agent开发环境别被“环境配置”吓住整个过程可以压缩到三分钟。我用的是最通用的PythonPlaywright组合因为它跨平台、依赖少、社区支持好。第一步安装Python与pip跳过如果你已有确保Python版本≥3.8。在终端输入python --version # 如果输出 Python 3.8.10 或更高继续否则去 python.org 下载安装第二步创建虚拟环境强烈推荐避免包冲突# 创建名为 agent_env 的虚拟环境 python -m venv agent_env # 激活它Windows agent_env\Scripts\activate.bat # 激活它macOS/Linux source agent_env/bin/activate第三步安装Playwright与浏览器# 安装playwright库 pip install playwright # 安装Chromium浏览器约150MB国内用户建议提前挂代理或用清华源 playwright install chromium # 可选如果需要测试Firefox再加一行 playwright install firefox第四步验证安装新建一个test_browser.py文件粘贴以下代码from playwright.sync_api import sync_playwright def test_browser(): with sync_playwright() as p: browser p.chromium.launch(headlessFalse) # headlessFalse 让浏览器窗口可见 page browser.new_page() page.goto(https://example.com) print(标题, page.title()) browser.close() if __name__ __main__: test_browser()运行python test_browser.py如果弹出浏览器窗口显示“Example Domain”并在终端打印出标题恭喜你的Agent引擎已经点火成功这四步我让实习生在入职第一天就完成没人卡住超过五分钟。4.2 核心Agent类一个可复用、可扩展的骨架所有Agent的核心就是一个能“思考”调用LLM、能“行动”操作浏览器、能“学习”记录状态的类。下面是我用的最小可行骨架Minimal Viable Agent, MVA它只有127行但已足够支撑90%的网页交互任务# agent_core.py from playwright.sync_api import sync_playwright, Page, BrowserContext from typing import Dict, Any, Optional import json import time class WebAgent: def __init__(self, context: BrowserContext): self.context context self.page context.new_page() # 设置全局超时避免卡死 self.page.set_default_timeout(30000) self.history [] # 记录每一步操作和状态 def goto(self, url: str) - Dict[str, Any]: 访问网页并记录状态快照 self.page.goto(url) snapshot self._take_snapshot() self.history.append({action: goto, url: url, snapshot: snapshot}) return snapshot def click(self, selector: str, **kwargs) - Dict[str, Any]: 点击元素并等待其完成如页面跳转、加载 try: self.page.locator(selector).click(**kwargs) # 等待页面有明显变化URL改变或标题改变 self.page.wait_for_timeout(1000) # 简单等待1秒实际项目中可替换为更精确的等待 snapshot self._take_snapshot() self.history.append({action: click, selector: selector, snapshot: snapshot}) return snapshot except Exception as e: # 记录错误便于调试 self.history.append({action: click, selector: selector, error: str(e)}) raise e def fill(self, selector: str, value: str) - Dict[str, Any]: 填写表单 self.page.locator(selector).fill(value) snapshot self._take_snapshot() self.history.append({action: fill, selector: selector, value: value, snapshot: snapshot}) return snapshot def _take_snapshot(self) - Dict[str, Any]: 采集当前页面状态快照 return { url: self.page.url, title: self.page.title(), timestamp: time.time(), ready_state: self.page.evaluate(document.readyState), main_heading: self.page.locator(h1).text_content() if self.page.locator(h1).count() else None } def save_history(self, filename: str): 保存操作历史用于回放和调试 with open(filename, w, encodingutf-8) as f: json.dump(self.history, f, ensure_asciiFalse, indent2) print(f操作历史已保存至 {filename}) # 使用示例 if __name__ __main__: with sync_playwright() as p: browser p.chromium.launch(headlessFalse) context browser.new_context() agent WebAgent(context) # 执行一个简单任务访问百度搜索“Agent Browser” agent.goto(https://www.baidu.com) agent.fill(input#kw, Agent Browser) agent.click(input#su) agent.save_history(baidu_search.json) context.close() browser.close()这个骨架的价值在于它把所有浏览器操作都包装成了有返回值、有日志、有错误处理的方法。_take_snapshot()是灵魂它让Agent的每一步都“可追溯、可解释”。当你发现Agent在某一步失败了直接打开baidu_search.json就能看到它失败前一刻的URL、标题、甚至主标题是什么debug效率提升十倍。别小看这个骨架我所有的Agent项目都是在这个基础上只增加login()、scrape_table()、download_pdf()等业务方法核心逻辑从未变过。4.3 实战案例构建一个“自动查快递”的Agent5分钟上手现在我们用上面的骨架做一个真正有用的Agent自动查询快递物流信息。目标网站是公开的快递100 API演示站https://www.kuaidi100.com/它不需要登录纯前端交互。需求拆解输入运单号如SF1234567890点击“查询”按钮等待物流轨迹列表加载提取最新一条物流信息时间、地点、状态实操步骤分析页面结构打开 https://www.kuaidi100.com/按F12打开开发者工具找到运单号输入框。你会发现它的HTML是input typetext idpostid placeholder请输入运单号查询按钮是input typebutton idquery value查 询物流列表在div idresult里。编写Agent脚本track_agent.pyfrom agent_core import WebAgent from playwright.sync_api import sync_playwright def track_express(tracking_number: str): with sync_playwright() as p: browser p.chromium.launch(headlessFalse) context browser.new_context() agent WebAgent(context) try: # 1. 访问首页 agent.goto(https://www.kuaidi100.com/) # 2. 填写运单号 agent.fill(input#postid, tracking_number) # 3. 点击查询 agent.click(input#query) # 4. 等待结果区域出现并有内容关键 # 等待#result div出现且里面至少有一个li物流项 agent.page.wait_for_selector(#result li, statevisible, timeout10000) # 5. 提取最新物流信息 # 最新信息在第一个li里 first_li agent.page.locator(#result li).first() time_text first_li.locator(span:nth-child(1)).text_content().strip() location_text first_li.locator(span:nth-child(2)).text_content().strip() status_text first_li.locator(span:nth-child(3)).text_content().strip() result { tracking_number: tracking_number, latest_time: time_text, latest_location: location_text, latest_status: status_text } print(查询结果, result) return result finally: # 无论成功失败都关闭资源 context.close() browser.close() if __name__ __main__: # 替换为你的真实运单号 track_express(SF1234567890)运行与调试运行python track_agent.py。你会看到浏览器自动打开输入运单号点击查询然后停在结果页。如果一切顺利终端会打印出类似查询结果 {tracking_number: SF1234567890, latest_time: 2023-10-27 14:22:33, latest_location: 北京市朝阳区, latest_status: 派件中}如果失败比如等不到#result liwait_for_selector会抛出超时异常你立刻就知道问题出在“页面没加载出来”而不是代码逻辑错了。这就是良好设计带来的巨大调试优势。4.4 上线部署从本地脚本到7x24小时服务写好的Agent不能只在你电脑上跑。要让它成为真正的生产力工具得部署到服务器上。我用的是最简单的方案systemdLinux或launchdmacOS配合cron做定时任务。以Ubuntu 22.04为例将Agent脚本放到固定位置比如/opt/agents/track_agent.py。创建一个systemd服务文件/etc/systemd/system/express-tracker.service[Unit] DescriptionExpress Tracking Agent Afternetwork.target [Service] Typesimple Userubuntu WorkingDirectory/opt/agents ExecStart/opt/agents/agent_env/bin/python /opt/agents/track_agent.py SF1234567890 Restartalways RestartSec10 # 确保Playwright能找到浏览器 EnvironmentPATH/opt/agents/agent_env/bin:/usr/local/bin:/usr/bin:/bin [Install] WantedBymulti-user.target启用并启动服务sudo systemctl daemon-reload sudo systemctl enable express-tracker.service sudo systemctl start express-tracker.service # 查看日志 sudo journalctl -u express-tracker.service -f现在你的Agent就在后台7x24小时运行了。Restartalways保证了它一旦崩溃比如网络抖动导致页面加载失败systemd会在10秒后自动重启它重新执行查询。这比任何“手动守护脚本”都可靠。我管理着17个这样的Agent服务全部用这套模式一年下来平均每个服务的宕机时间不到2分钟。5. 常见问题与排查技巧实录那些让你拍大腿的“坑”5.1 “页面打不开”先查这三件事Agent启动后浏览器窗口一闪而过或者卡在空白页这是新手最常见的问题。别急着重装按顺序检查这三项网络代理设置Playwright/Puppeteer默认不走系统代理。如果你的公司网络需要代理才能上网必须显式配置。在browser.new_context()里加上context browser.new_context( proxy{ server: http://your-proxy:8080, username: user, password: pass } )注意proxy.server的格式必须是http://开头即使你的代理是HTTPS。我曾为此浪费一整天因为写了https://。证书信任问题访问某些内部系统如https://intranet.company.local时浏览器会因SSL证书不受信任而拒绝连接。解决方案是启动浏览器时忽略证书错误browser p.chromium.launch( ignore_https_errorsTrue, # 关键 args[--ignore-certificate-errors] )这在开发和测试环境是安全的生产环境请务必用受信证书。无头模式下的字体渲染在headlessTrue无界面模式下Linux服务器常因缺少中文字体导致页面文字显示为方块Agent的get_by_text()无法匹配。解决方法是安装字体包# Ubuntu/Debian sudo apt-get update sudo apt-get install -y fonts-wqy-zenhei fonts-liberation # CentOS/RHEL sudo yum install -y glibc-common fontconfig5.2 “元素找不着”你的选择器可能“太聪明”了Agent报错TimeoutError: Timeout 30000ms exceeded.十有八九是元素定位失败。但原因往往很反直觉问题page.get_by_text(登录)匹配到了导航栏的“登录”但Agent想点的是弹窗里的“登录”按钮。解法用has_text限定父容器。# 错误太宽泛 page.get_by_text(登录).click() # 正确只找弹窗里的登录 page.get_by_role(dialog).get_by_text(登录).click()问题page.locator(button).nth(2)在页面重构后第三个按钮变成了“取消”Agent点了取消。解法永远用语义化选择器而不是序号。# 错误依赖顺序 page.locator(button).nth(2).click() # 正确依赖功能 page.get_by_role(button, name确认提交).click()问题元素在iframe里page.locator()根本找不到。解法先获取iframe再在其内部操作。# 找到iframe frame page.frame_locator(iframe[namepayment]) # 在iframe内部操作 frame.get_by_role(button, name支付).click()5.3 “点了没反应”可能是JavaScript在“捣鬼”有时候Agent明明执行了click()但页面毫无动静。这通常是因为前端JS阻止了默认行为或者绑定了自定义事件。这时click()的默认行为触发click事件不够用。场景一个用div onclicksubmitForm()模拟的按钮。解法直接执行JS。# 不要 page.locator(div.submit-btn).click() # 要 page.locator(div.submit-btn).evaluate(el el.click())场景按钮需要双击或右键。解法用dispatch_event。# 双击 page.locator(tr.row-item).dispatch_event(dblclick) # 右键 page.locator(img.avatar).dispatch_event(contextmenu)5.4 “数据没加载完就结束了”等待策略的终极心法这是Agent最“玄学”的问题。你以为加了wait_for_selector就万无一失结果还是偶尔失败。真相是等待的目标必须和页面的“完成信号”完全一致。我总结了三条心法找“最后一个变化”的元素不要等“加载中”图标消失要等“加载完成”后的第一个新元素出现。比如一个表格加载完成后最后一行会有一个td classlast-row那就等它。监听网络请求对于AJAX加载的数据直接等对应的网络请求完成。Playwright的page.route()和page.wait_for_request()是神器。# 等待一个特定的API请求完成 with page.expect_request(**/api/orders?statuscompleted) as request_info: page.click(button#load-orders) request request_info.value # request.response() 就是返回的JSON数据用evaluate做最终确认所有等待最后都用page.evaluate()检查一个JS变量或DOM属性这是最可靠的“一锤定音”。# 等待React组件的loading状态变为false page.wait_for_function(() window.__REACT_DEVTOOLS_GLOBAL_HOOK__ ! undefined) page.wait_for_function(() window.MyApp.state.loading false)实操心得我给自己立了一条铁律——任何wait_for_xxx后面必须跟一句page.evaluate()做最终校验。比如等一个按钮可点击后立刻page.evaluate(() document.querySelector(#submit).disabled)确保它确实是false。这多花不了半秒却能避免99%的“假成功”。5.5 常见问题速查表问题现象最可能原因快速排查命令/方法解决方案浏览器启动失败报错Failed to launch browserChromium二进制缺失或损坏playwright install chromium重新安装浏览器或指定executable_pathget_by_text()匹配不到中文服务器缺少中文字体fc-list :lang(zh)安装fonts-wqy-zenhei等中文字体包Agent在登录页反复刷新无法进入主页登录态未正确保存/加载检查storage_state文件是否存在确保new_context(storage_state...)路径正确且文件可读click()后页面无反应但手动点正常前端JS阻止了默认事件page.locator(btn).evaluate(el console.log(el.onclick))改用evaluate(el el.click())或dispatch_eventwait_for_selector()超时但元素明明在页面上元素在iframe或Shadow DOM中page.frames查看所有framepage.evaluate(() document.querySelector(...).shadowRoot)用frame_locator()或shadow_root定位6. 进阶思考Agent-Browser的边界在哪里写到这里你已经掌握了让Agent“看见”和“操作”网页的全部核心技能。但作为一个做了十年自动化项目的从业者我必须坦诚地告诉你Agent-Browser不是万能的它有清晰、不可逾越的边界。忽视这些边界是所有失败项目的共同起点。第一个边界是性能天花板。无论Playwright多快它终究要启动一个完整的浏览器进程。一个Chromium实例的内存占用在300MB左右启动时间在1-3秒。这意味着如果你的Agent需要在一秒钟内完成100次独立的网页查询比如高频风控扫描Browser方案必然成为瓶颈。这时候你应该果断切回httpxparsel用纯HTTP请求和HTML解析把性能压到毫秒级。我有个金融风控Agent就采用了混合架构对95%的静态页面用HTTP方案只对5%必须登录、必须执行JS的页面才启动Browser。这叫“该快则快该稳则稳”。第二个边界是法律与合规红线。所有网站的robots.txt和Terms of Service服务条款都白纸黑字写着“禁止自动化访问”。你用Browser-Agent去爬取竞争对手的定价页面哪怕技术上完美法律上也可能构成不正当竞争。我的经验是**只对三类网站使用Browser-Agent1你自己拥有完全控制权的内部系统2明确提供API且允许自动化调用的公开服务