这系列写到第七篇了。前面几篇都在聊怎么把 AI Skills 拆成可落地的实战技能有数据处理、文档生成、代码审查今天聊一个我很早就想写、但一直觉得时机没到的方向——让 AI 自己动手操作浏览器。说白了就是让 AI Agent 不光能“想”还能“做”。你给它一个任务目标比如“去某个后台把这三张报表下载下来按日期重命名后归档”它自己打开浏览器、输入网址、登录、点击菜单、筛选数据、逐个下载最后把文件整理好。全程不需要你手动点一下。这个场景我过去两年在好几个项目里反复用到从最初的 Selenium 脚本到后来的 Playwright 接管再到近期把浏览器操作封装成 AI Skills 给 Agent 调用踩过的坑不少但沉淀出来的东西也确实能打。这篇文章把整个技术路径和实操细节完整铺开适合已经玩过 AI Agent、想进一步拓展其动手能力的开发者也适合做 RPA 替代方案、网页自动化测试、批量数据采集的朋友。1. 为什么是 AI 操作浏览器而不是传统自动化脚本1.1 传统浏览器自动化的天花板先说一个很多人容易混淆的概念浏览器自动化和 AI 操作浏览器看起来都是在控制浏览器但本质上差了一层。传统自动化脚本Selenium、Playwright、Puppeteer 这一类核心逻辑是你用代码精确描述每一步操作打开 URL、等待元素出现、点击某个 XPath 定位的按钮、输入文本、按回车。每一步都是确定的脚本本身就是完整的行为序列。问题在于这类脚本极其脆弱——前端改一个 class 名、调整一下 DOM 结构、加一个弹窗提示脚本十有八九当场崩掉。维护脚本的时间和写脚本的时间几乎一样多。我接触过一个真实项目某运营后台每个月改版一次自动化脚本平均存活周期不到两周。每次改版负责脚本的同事都要花小半天去排查哪里定位失效、哪里流程变了。这个成本算下来还不如人手工点。传统自动化的另一个硬伤是“没有判断力”。脚本只会按照既定路线走遇到预期之外的弹窗、网络延迟导致的页面状态差异、数据加载顺序变化脚本自己是不会想办法的。异常处理逻辑你得全部预埋等于把各种意外情况都提前枚举一遍——这恰恰是它不擅长的事。1.2 AI Agent 给浏览器操作带来了什么变化AI 操作浏览器的核心差异在于任务目标由 AI 理解操作路径由 AI 动态决策异常情况由 AI 现场处理。它不再需要你预先写死“点击第几个按钮、输入什么内容”而是由 AI 根据当前页面的实际状态DOM 结构、可见文本、按钮位置实时判断下一步动作。页面改版了AI 会重新理解新界面找到对应的操作入口继续执行任务。遇到弹窗AI 会识别这是一个需要关闭的干扰项先处理掉再继续。这不是未来的设想而是已经能落地的方案。目前主流的技术路径有三类第一类是基于浏览器扩展的 Agent 产品算是最接近用户的操作方式第二类是基于 Playwright/Puppeteer 之类的库自己搭建控制层第三类是把浏览器操作能力封装成“工具”让 AI Agent 以调用工具的方式去操作。这三条路各有各的适用场景。我自己用得最多的是第三类——把浏览器操作封装成 Skill因为它的可控性最强既能享受 AI 的灵活性又不至于完全失控。这也是这篇文章要展开的重点。2. 核心原理拆解AI 到底是怎么“看”网页和操作网页的2.1 从 DOM 到操作指令AI 理解网页的两条路径要让 AI 操作浏览器第一步是让它“理解”当前网页是什么状态。这个环节有两条主流技术路径理解它们各自的原理你就知道该怎么取舍了。路径一是“读 DOM 结构”。浏览器里的每个页面本质上是一棵文档对象模型树AI 可以直接拿这棵树的文本化表示比如 HTML 源码或简化后的元素列表来理解页面。这条路的好处是精确——元素的属性、层级、可交互状态都能拿到适合需要严谨操作的场景。缺点是对 AI 的上下文窗口消耗很大一个复杂的页面可能有几千个节点全部塞给 AI 既不现实也没必要。路径二是“截图给 AI 看”。直接把页面渲染结果截图让多模态模型看图理解。这条路更接近人眼操作页面上有什么就看见什么对布局变化的容忍度非常高。缺点是模型对图片中元素的定位精度有限很难做到像素级操作偶尔会点错地方。实战中最稳的方案是两条路结合先用 DOM 提取出页面的关键交互元素按钮、输入框、链接生成一份精简的页面摘要再配合截图供 AI 校验。决策时优先依据 DOM 摘要做操作判断信息不足时用截图补位。这就是目前主流 AI Agent 操作浏览器的底层逻辑。2.2 操作指令的回传机制AI 说了不算代码执行才算AI 理解完页面之后面临第二个关键问题AI 决策出来的“下一步操作”怎么变成浏览器里真实发生的行为这中间必须有一个“执行层”。AI 本身不会碰浏览器它是通过输出结构化的操作指令——比如“点击 id 为 submit 的按钮”——交给执行层去调用浏览器自动化接口落地。Playwright 和 Puppeteer 在这个环节扮演的就是执行层的角色。我用的方案是把 Playwright 的常用动作封装成几个原子函数打开页面、点击元素、输入文本、滚动、等待、提取内容、切换标签页。每个函数都暴露给 AI 作为可调用工具。AI 的任务就变成了决定“用什么参数调用哪个函数”而不是自己写底层指令。这层抽象非常关键——它把“AI 理解用户意图”和“代码控制浏览器”两件事解耦了。AI 负责聪明代码负责稳定。2.3 Skill 的本质把操作经验固化下来在这个架构里AI Skills 又扮演什么角色它是一层比“工具函数”更上层的封装——不只是给 AI 提供操作能力还要告诉 AI 在什么场景下用、按什么顺序用、遇到什么情况怎么处理。比如定义一个“下载月度报表”的 Skill里面不仅包括浏览器操作函数还包括这个任务特有的 complete 流程先登录哪个系统、进入哪个菜单、筛选什么时间范围、点击哪个导出按钮、下载后如何处理文件。这些流程如果每次都靠 AI 现场摸索既有不确定性也浪费时间。固化成 Skill 之后AI 调用它就像师傅带徒弟——徒弟知道大方向师傅提供了具体操作清单两者配合才是最高效的。这也是为什么我强烈建议把“AI 操作浏览器”这件事做成 Skills 而不是临时脚本。Skills 的可复用性、可维护性、可分享性都是散装脚本给不了的。3. 实操过程从零搭建一个让 AI 控制浏览器的 Skill3.1 技术选型与环境准备动手之前先把技术栈定下来。我这里的方案是 Python Playwright 自定义 Agent 调度框架Skill 用 Markdown 格式的说明文件来承载。之所以选 Playwright 而不是 Selenium实话说有三个硬理由第一Playwright 的自动等待机制比 Selenium 聪明得多它会在元素可交互之前自动等待少写一堆 sleep第二Playwright 天然支持多标签页、多浏览器上下文对 AI Agent 这种可能同时跑多个任务的场景非常友好第三Playwright 的追踪功能可以直接录制操作轨迹排查问题的时候能回放现场这个对 AI 操作场景太重要了——AI 操作出错了你得有办法复盘它到底做了什么。环境安装很简单Python 3.10 环境下两条命令pip install playwright playwright install chromium第二条命令会下载 Chromium 内核。如果你的运行环境网络受限可以手动指定浏览器路径Playwright 也支持使用系统已安装的 Chrome。3.2 定义 Skill 配置文件告诉 AI 它能做什么、怎么做Skill 的核心是一个说明性文件我习惯取名SKILL.md。它不写代码逻辑而是用自然语言告诉 AI 这个 Skill 能干什么、调什么工具、按什么步骤来。下面是一个我在实际项目中用过的精简模板# 浏览器自动操作技能 ## 能力说明 本技能允许 AI 通过浏览器执行网页操作包括打开页面、登录、点击、填入内容、提取数据、文件下载等。适用于数据采集、表单提交、定期巡检、批量处理等场景。 ## 可用工具 1. open_page(url) - 打开指定网址 2. click(selector, description) - 点击指定元素 3. fill(selector, text) - 在输入框中填入文本 4. extract_content(selector) - 提取页面内容 5. wait_for(selector, timeout) - 等待元素出现 6. scroll_page(direction) - 滚动页面 7. download_files() - 触发并管理下载任务 ## 操作规范 - 打开页面前先检查当前是否已有页面避免重复开启 - 登录操作优先尝试公共登录入口避免触发验证 - 点击按钮前先确认元素是否可见可交互 - 每次操作后留出页面渲染时间不要连续快速操作 - 遇到弹窗先识别类型属于干扰性的直接关闭属于必要确认的再处理 ## 执行步骤参考 1. 明确任务目标拆解为页面操作子任务 2. 按子任务顺序逐个执行每完成一步做一次状态校验 3. 最终结果以结构化文本汇总输出这个文件写好之后Agent 调度框架会把它和对应的工具函数注册表喂给模型——可以看作给 AI 发了一份“岗位说明书”它知道自己能调什么工具、按什么规范来。这是整个方案里最重要的一步Skill 写得好不好直接决定 AI 的实操规范性。3.3 工具函数层的实现细节Skill 说明书是给 AI 看的工具函数是给代码跑的两者通过函数签名对接。这一层看起来简单但细节决定成败。先看核心代码from playwright.sync_api import sync_playwright class BrowserAgent: AI 驱动浏览器操作的执行层 def __init__(self): self._pw sync_playwright().start() self._browser self._pw.chromium.launch(headlessFalse) self._context self._browser.new_context( accept_downloadsTrue, # 允许下载 viewport{width: 1280, height: 800} ) self._page self._context.new_page() def open_page(self, url: str) - str: 打开指定网址 self._page.goto(url, wait_untildomcontentloaded, timeout30000) return f页面已打开: {self._page.title()} def click(self, selector: str, description: str ) - str: 点击指定元素 self._page.locator(selector).click(timeout10000) self._page.wait_for_load_state(networkidle, timeout10000) return f已点击: {description or selector} def fill(self, selector: str, text: str) - str: 在输入框中填入文本 self._page.locator(selector).fill(text) return f已填入: {text} def extract_content(self, selector: str) - str: 提取指定区域的内容 elements self._page.locator(selector) count elements.count() results [] for i in range(min(count, 50)): try: results.append(elements.nth(i).inner_text(timeout3000)) except Exception: continue return \n.join(results) if results else 未提取到内容 def wait_for(self, selector: str, timeout: int 15000) - str: 等待元素出现 self._page.locator(selector).wait_for(statevisible, timeouttimeout) return f元素已出现: {selector} def close(self): self._browser.close() self._pw.stop()这里有几个细节值得展开说第一wait_untildomcontentloaded而不是load。如果一个页面有大量异步资源图片、广告、统计脚本等load事件可能要几十秒AI 操作讲究响应速度DOM 就绪即可开始处理。第二每次click之后要做networkidle等待。这是为了避免 AI 连续发出操作指令时上一个操作触发的异步请求还没完成下一个操作就开始了导致拿到的是残缺页面状态。这个等待是 Playwright 里最实用的武器之一代价是稍微慢一点换来的是极高的稳定性。第三extract_content里加了数量上限50个元素和异常兜底。原因很实际AI 提取内容时经常用一个宽泛的选择器限流能防止输出量失控异常兜底能防止单个元素渲染异常导致整个提取失败。3.4 一个完整的任务闭环让 AI 完成“采集归档”工作光有骨架不行用一个真实任务把整个链路串起来演示一遍。就以我前面提到的“下载报表并归档”为例完整看一下 AI 从接收指令到完成任务的运行过程。任务描述用户发给 Agent 的话打开公司的运营数据后台用管理员账号登录进入“日报下载”页面下载最近 7 天的日报 Excel 文件统一保存到本地 data/daily_reports 目录下文件名按日期命名。Agent 拿到这个任务后会调用我的SKILL.md里的操作规范和工具函数经历下面几个阶段阶段一分析任务拆解子步骤AI 首先在内部把任务拆解为打开登录页、填写账号密码、提交登录、导航到日报下载页、设置日期范围最近7天、逐个下载文件、检查下载结果。这个拆解不需要展示给用户但它决定了后续工具调用的顺序。阶段二执行登录操作agent.open_page(https://data.example.com/login) agent.fill(#username, admin_user) agent.fill(#password, xxxxxx) agent.click(#login-btn, 登录按钮) agent.wait_for(#dashboard, 等待进入管理后台)这一段操作里AI 选择元素的方式可能不是写死选择器而是根据页面文本内容定位——比如登录按钮的description部分会作为上下文信息给模型参考。如果登录后出现了预期之外的验证码AI 会根据SKILL.md里的“操作规范”判断如果验证码是外部安全机制它会停下等待人工介入如果只是内部系统的滑动拼图它可以尝试用辅助函数处理。阶段三进入下载页面并筛选日期agent.click(text日报下载, 导航到日报下载页) agent.click(#date-range-picker, 打开日期选择器) # AI 根据“最近7天”这个语义决定调用脚本填入起止日期 agent.fill(#start-date, 2025-03-01) agent.fill(#end-date, 2025-03-07) agent.click(#query-btn, 执行筛选)这个环节最能体现 AI 和传统脚本的差异。传统脚本是预设好填哪两个日期AI 是理解“最近7天”这个自然语言指令然后自己算出日期范围填进去。哪个方案灵活一目了然。阶段四下载文件并核验download_btn agent.page.locator(button:has-text(下载)) for i in range(download_btn.count()): with agent.page.expect_download() as download_info: download_btn.nth(i).click() download download_info.value download.save_as(fdata/daily_reports/{download.suggested_filename})这里会有一个常见的坑服务器返回的文件名不一定符合要求AI 会结合下载时间推断日期并重命名文件。我在实际场景中是让 AI 在下载后做一次文件列表检查——如果没有匹配预期的文件它会把异常信息记录下来并尝试重新下载。阶段五汇总输出结果任务执行完成后AI 会输出一份简短的执行报告包含下载成功的文件清单、耗时、有无异常及处理方式。用户可以通过这份报告判断是否还需要人工介入。这个完整的闭环流程我实测下来稳定可靠的核心原因在于每一阶段之间都有状态校验而不是一口气跑完所有动作。你可以把每个工具函数调用后返回的状态信息当作“路标”AI 每走一步都确认自己在正确的路上再走下一步。4. 常见问题与排查技巧实录这个方案跑起来之后真正消耗时间的地方往往不是写代码而是排查各种边界情况。列几个我在项目里实际撞过、也给后来同事解决过的典型问题每一个都是真金白银换来的经验。4.1 元素定位失败AI 说找不到按钮但人眼明明看到了这是最高频的问题。排查思路先说结论九成情况不是 AI 的问题是你给的选择器不够稳定或者元素本身处于不可交互状态。先看元素是否在 iframe 里。很多后台系统用了大量 iframe 嵌套Playwright 默认的locator只能访问主文档你必须显式切换 frameframe agent.page.frame_locator(iframe#content) frame.locator(button.submit).click()这个坑排查起来很隐蔽因为打开浏览器人眼看着一切正常代码一跑就找不到元素。处理方法是凡是涉及复杂后台页面先检查 DOM 树里有没有 iframe 包裹目标区域。再看元素是否被遮挡。有些页面的按钮上覆盖着半透明的遮罩层、弹窗背景或者其它元素Playwright 点击时会因为“元素不可见/不可交互”而超时。这不是定位失败是命中不了。解决方案是用forceTrue强制点击或者先把遮挡元素处理掉——后者更稳健因为强制点击可能触发的是错误区域。4.2 AI 操作节奏太快页面还没加载完就执行下一步AI 的决策速度比人快这是优点也是隐患。人在浏览器里操作大脑会无意识地等待页面渲染完成再动手AI 不会——它只要判断“当前状态可以执行下一步了”就会立刻发出指令根本不管你底层的网络请求是否还在进行。解决办法是在工具函数层面强制加“安全等待”而不是把等待交给 AI 自觉。前面代码里已经体现了这个思路click之后的wait_for_load_state(networkidle)open_page之后的 DOM 就绪等待。这套机制加上去之前我的 Agent 偶尔会出现在一个页面上连续点错两三个按钮的情况加上之后这个问题的出现频率降到趋近于零。如果你脚本里大量手动写time.sleep()我建议尽快替换成 Playwright 的自动等待机制。前者是固定时间盲等网络慢了等不够网络快了又浪费时间后者是根据实际页面状态智能等待又快又稳。4.3 下载文件失效或目录受限浏览器自动化里的下载文件是一个高发问题区失败原因五花八门。最常见的是运行环境的下载目录没有权限尤其是把 Agent 部署在容器或服务器上时。别笑这个坑我在生产环境踩过AI 已经把下载触发了但系统提示“保存失败”任务卡在那一步回不来了。解决办法是在创建浏览器上下文时显式指定可写的下载目录self._context self._browser.new_context( accept_downloadsTrue, downloads_path/data/downloads )另外有一个隐藏问题某些网站下载文件时是弹窗确认机制或者是“先请求后返回文件”的流式下载expect_download可能捕获不到。这种场景我用的是替代方案——获取文件的真实下载 URL由代码直接请求并保存import requests # 从页面上提取下载链接 file_url agent.page.locator(a.download-link).get_attribute(href) response requests.get(file_url, cookiesagent.context.cookies(), timeout60) with open(fdata/daily_reports/{date}.xlsx, wb) as f: f.write(response.content)这个方案绕开了浏览器下载流程稳定性和可控性都更好。4.4 页面频繁改版Skill 里的步骤失效了怎么办要给 AI 操作浏览器这件事设置一个合理的预期Skill 里写好的操作步骤遇到页面改版时一定会失效。这不是 Bug这是 Web 生态的常态。我的应对策略是三层第一Skill 说明书里不要写死“精确路径”尽可能写“任务目标语义化步骤”。比如别写“点击左侧菜单第三个按钮”要写“进入日报下载页面”。AI 会根据当前页面的实际情况寻找入口而不是机械执行坐标位置。第二工具函数层保持原子化这样单个步骤失效时AI 可以用其他函数补位。比如页面默认用新标签页打开报表AI 发现当前页没有反应它会主动检查是不是开了新标签页然后切换过去处理。第三给 Agent 建立“失败上报”意识。Skill 的说明里写了连续两次尝试同一操作都失败时停止操作并输出错误报告等待人工介入。这个约定非常关键——它防止 AI 在页面改版后陷入死循环式的乱点同时给维护者留下排查线索。4.5 浏览器资源没释放Agent 跑久了系统响应变慢AI 操作浏览器是长会话场景如果 Agent 长时间挂着浏览器进程会积累大量标签页、缓存、网络连接最终拖垮系统。排查时看任务管理器和浏览器自身的chrome://memory-internals你会发现内存占用高得惊人。解决方案有两个层面。应用层面定期执行清理动作def cleanup(self): 释放多余标签页并清理缓存 pages self._context.pages if len(pages) 1: for page in pages[1:]: page.close() self._context.clear_cookies()调度层面给 Agent 设置任务生命周期——每个大任务完成后重建浏览器上下文而不是复用一个永久的会话。虽然多花了一点初始化时间但换来的是稳定性和可控性非常划算。4.6 问题速查表把常见问题整理成一张表方便你随时对照排查现象大概率原因处理办法AI 找不到元素元素在 iframe 内 / 被遮挡 / 未渲染完成切换 frame / 处理遮挡 / 加自动等待操作过快导致状态错误缺少 networkidle 等待工具函数层统一加等待机制点击无效但人不觉得有问题遮罩层拦截点击事件用forceTrue或先移除遮罩下载毫无反应下载目录不可写 / 弹窗确认设置 downloads_path / 改用 requests 直链Agent 越来越卡标签页泄漏 / 缓存堆积任务后重建上下文 / 清理标签页页面改版后步骤全废Skill 步骤写得太机械步骤语义化 / AI 自适应查找替代入口登录后仍停留登录页登录状态未正确保存检查 cookie 与 localStorage 持久化5. 一些值得说的实战心得这个方案跑了大半年有些东西是在一次次踩坑中才真正想明白的。5.1 让 AI 操作浏览器本质是“人机协同时代的基础设施”不要把 AI 操作浏览器单纯理解为“自动化工具的升级版”它是一个更基础的能力底座。当 AI 能安全、稳定地操作浏览器时很多以前需要专门开发能力的场景一夜之间变成了“给 AI 写个 Skill 就行”——比如竞品价格巡检、网站可用性监控、批量内容发布、复杂表单申报、数据定时归档。这些任务以前都需要专门的爬虫工程师或者 RPA 开发现在一个会写 Skill 的开发者就能搞定。这种变化的意义不只是省人力更在于降低“软件自动化”的准入门槛。传统 RPA 需要理解业务流、页面结构、异常处理成本极高现在 AI 天然懂业务语义、能实时理解页面、能处理不确定性反而把自动化这件事从“专门项目”变成了“日常工具”。5.2 信任边界很重要给 AI 操作加上安全护栏AI 操作浏览器和传统脚本最大的不同是 AI 的行为不是完全可预测的。这既是优势也是风险。我的做法是加几道硬性的安全护栏第一操作白名单。Skill 能调用的工具函数是固定集合AI 不能自己发明新操作只能在给定的动作里组合。这限制了它的能力边界也就限制了风险边界。第二敏感操作二次确认。涉及提交表单、删除数据、发送信息、付款之类的操作工具函数里强制加一个confirm参数AI 需要先输出操作预览由人工确认后才执行。这套机制看起来麻烦但确实能避免一些不可挽回的后果。第三全程操作日志。Agent 每次操作都写入结构化的操作日志时间、操作类型、目标元素、页面 URL、执行结果配合 Playwright 的视频录制或追踪功能出问题时可以完整复盘。这一点在调试阶段帮了我太多忙。5.3 从“给 AI 写脚本”到“教 AI 怎么工作”最后说一个理念层面的转变。大多数人刚开始接触 AI 操作浏览器时会不自觉地把它当成“AI 版的 Selenium”——写更智能的自动化脚本。这个视角不能说全错但格局小了。真正高效的用法是把你对业务的理解、对异常情况的处理经验、对任务完成质量的标准写进 Skill 文件里。你起草过完整的 SKILL.md 就会明白这个文件本质上是一本“微型 SOP 手册”AI 拿着它干活等于一个经验丰富的助理在严格执行你定下的操作规程。这也是为什么我强调 Skill 文件不能用一句话敷衍了事也不能只列工具列表。你要在里面写清楚“遇到什么情况优先做什么”“什么场景下绝对不能怎么做”“完成任务的验收标准是什么”。这些内容越详细AI 的实操表现就越像“一个有经验的同事”而不是“一个听话但呆板的实习生”。写到这里我想你已经对“让 AI 自动操作浏览器”这个方向有了全貌式的理解。从环境搭建到工具封装从 Skill 编写到异常排查这套方案已经在多个真实项目里跑通并持续产生价值。下一次我再更新这个系列时打算聊聊怎么把多个 Skill 组合编排成一套完整的业务流程——让 AI 从“会干一件事”进化到“会管一条线”。在那之前你可以先把你手头最重复、最耗时的浏览器操作挑出来试着写成你的第一个 Skill这个起点绝对值得你花时间投入。