你有没有遇到过这种情况想把某个公开网站上的信息整理下来先翻十几页列表再逐个点开详情最后还得一页页截图存档手动操作一小时起步。上个月我就在做这类重复劳动做到第两百个页面的时候心里只有一个念头这活儿要是能让AI替我干了该多好。于是就有了这次实践的起点——SkillsBot技能库。它的思路很简单给大模型配一套可以操作浏览器的“技能”让AI替我做点击、输入、滚动、翻页、截图这些动作。我只需要用自然语言说“打开某个页面翻到第五页把前三篇文章的标题和链接抓下来顺便截个图”剩下的交给调度进程去执行。这篇文章不是概念介绍而是我完整搭过一遍、跑通过一遍的实战记录。里面会拆解浏览器自动化调度的核心机制、技能库的注册与调用逻辑、截图功能的两种实现路径还会把我在环境配置、多步调度、并发处理上踩过的坑一并列出来。想用AI解放双手的朋友尤其是做自动化测试、数据采集、办公流程提效的人可以直接照着搭一套。1. 从“会聊天的AI”到“会动手的AI”浏览器自动化的技术跃迁1.1 大模型Agent的基本运行逻辑过去我们使用大模型基本停留在对话层面输入一段问题模型返回一段文字。这是推理能力但它没有“行动能力”。而Agent智能体这个概念给大模型装上了手和脚——模型不再是只输出文本而是可以输出一个结构化的“调用指令”由外部程序去执行再把执行结果返回给模型做下一步判断。这整个过程的核心叫函数调用Function Calling也叫工具调用。大模型本身不知道浏览器里发生了什么但它可以根据用户请求生成类似“调用截屏工具参数为网址”这样的指令。我的SkillsBot本质上就是这套机制的落地一个负责理解请求的大脑LLM一个负责执行动作的躯干浏览器自动化框架还有一本告诉大脑“你会哪些动作”的技能说明书。1.2 SkillsBot在AI与浏览器之间扮演的角色直接让大模型写一段Selenium脚本然后跑起来这种思路不新鲜但问题是让模型每次从头写代码不稳定、效率低、还容易出错。更合理的做法是把常用的浏览器操作预先封装成一个个带参数接口的“技能”注册进技能库模型只需要做选择题而不是做作文题。SkillsBot的核心角色就是一个技能注册中心调度执行器。注册中心维护着所有可用技能的清单包括技能名称、功能描述、参数格式。调度执行器负责接收模型的选择结果调用真实浏览器完成操作把结果以文本或图像形式交回给模型继续推理。这样设计的好处非常直观稳定可控每个技能是经过测试的代码块不会像模型临时生成的脚本那样跑一半崩掉接口标准技能之间可以组合比如“打开网页”“等待加载”“截取元素”可以串联成新的复合技能可扩展每加一个新技能模型就多一项能力不用改核心逻辑1.3 浏览器自动化调度技术的现实应用场景这套能力能用在哪儿决定了它值不值得折腾。我实测下来这几个方向受益最大批量信息采集与留证。比如需要保存几十个产品页面的快照人工操作累且容易漏交给AI调度后它自己会循环打开地址、滚动加载、截图归档还能按你的要求对图片重命名。自动化测试回归。让AI根据测试用例描述去操作页面检查跳转逻辑是否正确、表单校验是否生效、按钮状态是否变化失败时自动截图标记。这对个人项目或小团队来说比维护一堆测试脚本省心得多。办公流程串联。把网页端的查询、填写、导出动作封装成技能模型按你的指令编排顺序执行相当于给普通软件加了一个“智能宏”。2. 核心机制拆解SkillsBot技能库如何编排浏览器操作2.1 技能注册与参数声明技能库里的每一项技能不能只是函数名加一行注释因为大模型无法可靠地靠“猜”来调用函数。必须给它一份结构化的参数清单让模型明白这个技能接受哪几个参数、每个参数的类型是什么、哪些必填、可选参数默认值是多少。我在SkillBot里采用了一套轻量级的技能注册方案核心是一个装饰器加一个参数Schema说明。下面是一个“打开页面并截图”的技能定义from skillsbot import register_skill register_skill( nameopen_page_and_capture, description打开指定URL等待页面加载完成后截图并返回截图内容, parameters{ type: object, properties: { url: {type: string, description: 要打开的完整网址必须包含协议头}, full_page: {type: boolean, description: 是否截取整页长图默认false只截视口}, wait_seconds: {type: number, description: 页面加载后的额外等待时间单位秒} }, required: [url] } ) def open_page_and_capture(url: str, full_page: bool False, wait_seconds: float 2.0): # 具体执行浏览器操作 ...这段代码里的description字段不是摆设。模型是靠这些描述来理解参数的语义的比如我写上“必须包含协议头”它就不会在用户输入“www.example.com”时直接传给浏览器而是会自动补全成“https://www.example.com”。这是文本生成模型的一个有趣特性你给它的说明书越准确、越完整它在生成调用指令时就越规范。2.2 调度引擎如何“看懂”并调用技能技能注册好之后真正负责“理解并调用”的是一个调度循环。我把这个循环类比成“老板交代任务-秘书分解执行-汇报结果-老板继续决策”的过程。调度第一步把用户的自然语言请求和当前技能库的清单一起发给模型Prompt的大致结构是你是一个浏览器自动化助手。你可以调用以下技能完成用户请求 技能列表 - open_page_and_capture打开URL并截图。参数url(string,必填), full_page(boolean,可选), wait_seconds(number,可选) - click_element点击页面中的元素。参数selector(string,必填), index(number,可选) - fill_input向输入框填充文本。参数selector(string,必填), text(string,必填) 当前浏览器状态已打开 https://example.com 用户请求在搜索框输入“天气”点击搜索按钮等待3秒截图结果页。模型收到这些信息后会输出一连串结构化指令比如先调用fill_input再调用click_element最后调用open_page_and_capture。调度器按顺序执行每一步把结果回填到运行状态中再把这个状态作为下一轮对话的上下文。关键点在于每一步执行的结果都会攒在上下文中。模型不需要“记住”刚才点到了哪里因为调度器会把当前页面URL、可用元素数量、上次操作的成功与否都写进消息。这相当于给模型装了一个外部记忆也是多步操作能稳定跑下来的原因。2.3 截图模块的两种实现路径截图是标题里特别提到的一个功能也是这套系统里最有实用价值的一块。我在实现时发现截图可以走两条完全不同的路线按需选即可。第一种是页面级截图面向“我想看一眼这个页面长什么样”的场景。用浏览器自动化框架提供的页面截图接口即可实现默认截取当前视口设置full_pageTrue时可以截取整页长图。这类截图适合存档、比对、汇报。第二种是元素级截图面向“我只关心某个区域”的场景。先通过选择器定位到目标元素再基于该元素的边界框做裁剪。举例来说只要页面里某个表格的截图就不需要整页打包裁出表格区域即可生成的图片更小、更清晰也方便后续做图像识别。两种路径在代码里可以统一封装async def capture(target, full_pageFalse): page await context.new_page() await page.goto(target[url], wait_untilnetworkidle) if target.get(element): locator page.locator(target[element]) await locator.screenshot(pathtarget.get(path)) else: await page.screenshot(pathtarget.get(path), full_pagefull_page) await page.close()截图生成之后是直接存文件还是返回给模型取决于你要做什么。如果只是给用户看存文件即可如果要让模型自己分析截图内容就得把图片转成Base64编码塞进对话消息里。这个我会在第3章详细讲。3. 实操手记从零搭建一套可用的浏览器调度技能3.1 环境准备与依赖安装先说环境。我的实践环境是Python 3.10操作系统是某Linux发行版浏览器自动化框架用的是某开源的无头浏览器自动化库就是常用来做爬虫和自动化测试的那个。这套方案在Windows和macOS上同样能跑只是浏览器驱动安装方式略有差异。安装依赖很简单pip install skillsbot playwright pillow playwright install chromium第二条命令用来安装浏览器内核。这一步经常有人卡住因为网络原因导致浏览器二进制下载缓慢。如果下载失败可以换用系统自带浏览器的通道模式也就是不下载库内置的Chromium而是让自动化库直接驱动系统里已安装的Chrome或Edge。配置方式如下from playwright.sync_api import sync_playwright with sync_playwright() as p: browser p.chromium.launch(channelchrome, headlessTrue)用channelchrome之后库会去找系统安装的Chrome或Edge不再依赖内置的Chromium包。这种方式对网络环境更宽容而且浏览器版本跟随系统更新兼容性反而更好。3.2 编写“打开页面并截图”技能环境就绪之后我先把第2章里定义的open_page_and_capture技能真正实现出来。这里要注意一个问题技能函数本身的执行逻辑要尽量健壮因为模型生成的参数有时候是不完整的甚至会传错类型。我的实现里加了几层保护from urllib.parse import urlparse import base64 from io import BytesIO from PIL import Image register_skill( nameopen_page_and_capture, description打开指定URL等待页面加载完成后截图并返回截图内容, parameters{ type: object, properties: { url: {type: string, description: 要打开的完整网址必须包含协议头}, full_page: {type: boolean, description: 是否截取整页长图默认false只截视口}, wait_seconds: {type: number, description: 页面加载后的额外等待时间单位秒} }, required: [url] } ) def open_page_and_capture(url: str, full_page: bool False, wait_seconds: float 2.0): # 参数校验如果缺少协议头自动补 https if not url.startswith((http://, https://)): url https:// url # 域名合法性检查 parsed urlparse(url) if not parsed.netloc or . not in parsed.netloc: return {error: URL格式不合法} with sync_playwright() as p: browser p.chromium.launch(channelchrome, headlessTrue) page browser.new_page(viewport{width: 1280, height: 800}) page.goto(url, wait_untildomcontentloaded, timeout15000) if wait_seconds 0: page.wait_for_timeout(int(wait_seconds * 1000)) screenshot_bytes page.screenshot(full_pagefull_page) browser.close() # 压缩截图为JPEG降低体积 img Image.open(BytesIO(screenshot_bytes)).convert(RGB) buffer BytesIO() img.save(buffer, formatJPEG, quality85) encoded base64.b64encode(buffer.getvalue()).decode(utf-8) return {screenshot_base64: encoded, url: url, width: img.width, height: img.height}执行结果返回的是Base64编码的图片数据而不是直接存文件。这样做的好处是调度器拿到图片数据后既可以写文件也可以直接塞给多模态模型做视觉分析灵活性更强。等待策略我选的是domcontentloaded加额外等待时间而不是networkidle。原因很简单很多页面会有持续轮询的接口networkidle可能永远等不到空闲状态白白超时。domcontentloaded可以快速返回再根据页面特性手动加一个等待时间这个“固定延时手动补偿”的方式在实际使用中比哪种智能等待都稳定。3.3 实现连续多步的有状态调度单步截图能做之后真正提效率的是“连续多步调度”。比如“打开某站点登录进入个人中心把当前页面的第一张图片截下来”这种任务需要浏览器状态在多个步骤之间保持连贯。实现有状态调度的关键是让浏览器实例在调度循环中保持存活而不是每步都重新启动浏览器。启动一个全新的浏览器再访问一次URL和连续操作之间的体验天差地别——登录状态、Cookie、滚动位置都会丢。我设计了一个会话对象来承载浏览器状态class BrowserSession: def __init__(self): self._playwright sync_playwright().start() self.browser self._playwright.chromium.launch(channelchrome, headlessTrue) self.context self.browser.new_context() self.page self.context.new_page() self.history [] # 记录操作历史供模型参考 def execute(self, skill_name: str, params: dict): # 根据skill_name分发到对应的执行函数 ... self.history.append({skill: skill_name, params: params, result: result}) return result def close(self): self.browser.close() self._playwright.stop()调度循环里每一轮都从history里提取最近几条操作记录连同当前页面URL、标题一起发回给模型。这样模型看到上下文时就知道“我刚才已经登录过了”“我现在位于个人中心页面”从而做出下一步决策。我把这种上下文称为“浏览器状态快照”。实际跑下来发现不需要把所有页面内容都塞给模型那会撑爆上下文窗口。只需要提供URL、页面标题、可见的几个关键元素描述就足以支撑模型做判断了。3.4 把截图返回给AI做视觉判断截图不只是给人看的还可以让AI“看”。这里要区分两种模型能力文本模型和视觉模型。如果用的是多模态视觉模型可以直接把Base64图片作为消息内容的一部分发送。以常见的消息格式为例图片消息会被转换成包含图像数据的特定结构。模型就能告诉你“页面上显示了一个绿色的成功提示框”或“图片中心有一个人物头像”。如果用的是纯文本模型也不是完全没法利用截图。可以把截图存储后再用一个轻量级的图像描述模型或者OCR工具把图片内容转成文字描述再喂给文本模型。比如对页面截图做OCR提取出其中可见的标题、按钮文字模型照样能进行后续操作。我在实际项目中两种方式都试过。视觉模型的效果当然更直接但成本和响应时间会高一些。OCR转文字的方式虽然多一道工序但胜在稳定和通用。我的建议是如果任务的决策点不多OCR转文字完全够用如果涉及复杂的视觉判断比如“找出页面上颜色不一样的按钮”那就必须上视觉模型。4. 踩坑记录与问题排查速查表4.1 常见异常与解决方案这一个月跑下来我整理了一份问题排查表。以下问题基本覆盖了新手会遇到的绝大多数情况。现象根本原因解决方案浏览器启动后白屏网络环境无法访问目标站点或页面依赖了大量外部资源先手动用浏览器打开目标URL确认可访问必要时加长等待时间元素定位不到频繁报错页面采用JS动态渲染元素不在初始DOM里改用wait_for_selector显式等待而不是固定sleep频繁登录失效浏览器实例每次重启Cookie丢失使用同一个BrowserContext或持久化Cookie到本地文件截图内容模糊不清设备缩放比设置问题截图分辨率偏低显式设置device_scale_factor2提高截图清晰度大模型调用超时上下文消息过长模型推理时间变长精简历史记录只保留最近3-5条操作摘要页面操作被风控拦截自动化特征明显浏览器指纹被识别使用有头模式加入合理的鼠标移动和点击延时这里特别说一下“设备缩放比”这个参数它在普通手动截图时根本不会遇到但自动化截图时特别容易踩雷。某些网站的布局在高分辨率屏幕上显示正常但脚本跑起来截出来的图却分辨率很低、文字发虚就是因为device_scale_factor默认是1。设成2之后截图的清晰度立刻上了一个台阶。代价是图片体积变大建议用JPEG格式压缩后再返回。4.2 三个容易忽略的性能细节第一浏览器实例复用比并发新开高效得多。一开始我天真地想既然要处理很多页面那就每个任务开一个浏览器实例并行跑。结果内存直接爆掉一个Chromium实例就要占用几百MB内存同时跑五个实例16GB内存也撑不住。后来改成单实例多标签页的方式用同一个浏览器进程开多个context每个context互相隔离内存开销小了一个量级。第二图片Base64编码会显著增加上下文体积。一张1280x800的截图转成Base64后大约有几万字符。如果多步操作每轮都把完整截图塞进模型上下文很快就会超出窗口限制。我后来做了分层运行时只把截图作为临时结果保存只有模型明确需要“看图”时才把图片追加到消息里。否则返回给模型的是截图文件路径图片尺寸信息实际的图像数据留在本地存储。第三网络等待策略要有差异化。统一用networkidle是新手最容易犯的错误有些站点加载慢有些站点一直有长连接。我用的是“DOM加载完成关键元素等待兜底睡眠”三层策略先等domcontentloaded再等目标元素出现最后给一个固定延时间作为兜底。这套策略既能保证绝大多数页面的元素可用又不会无脑等太久。4.3 安全边界要提前划好浏览器自动化的能力越强越要约束它触达的范围。我在技能库里明确规定了一个原则自动化进程不处理涉及支付、密码修改、验证码的场景。虽然技术上完全可以实现但这些操作一旦出错后果不是丢一个自动化任务那么简单。密码和敏感凭据绝不能明文出现在技能参数和模型上下文中。模型生成的参数可能会被记录到日志里如果日志里出现了明文密码安全隐患非常大。我的做法是把凭据统一存放在环境变量中技能内部按需读取模型只知道“登录技能需要用户名和密码参数”但看不到真实值。另外技能库的调度器必须设置一个操作白名单。允许执行的是打开已知域名的页面、点击页面元素、填写文本、截图归档这些可控动作。绝对不开放的是下载未知文件并执行、修改系统设置、访问本地文件系统。这些限制在调度器入口处拦截即可一旦识别到模型试图调用未注册的“危险技能”立刻终止整个任务。5. 更进一步把SkillsBot变成个人工作流的自动化中枢5.1 可以扩展的实用技能清单SkillsBot的能力核心在技能库的丰富程度。我目前已经封装好的技能包括技能名作用适用场景open_page_and_capture打开URL并截图网页存档、视觉校验fill_and_submit自动填充表单并提交批量填写信息scroll_and_collect滚动页面并提取可见文本长列表内容采集extract_table_data提取页面表格数据并转为CSV数据汇总、报表导出login_with_credentials根据环境变量中的凭据完成登录需要登录后才能操作的站点wait_and_observe等待指定时间后观察页面变化页面动态刷新监听这些技能单独用各自能干活组合起来就是一套完整的工作流。比如“登录技能”配合“表格提取技能”再加一个每天定时触发就等于一个无人值守的日报生成器。5.2 我的个人体会与后续规划这套系统我搭起来用了不到一个周末但它带来的工作方式改变却是长期的。以前我需要花大量时间盯着的重复性任务现在只需要写好一段自然语言指令然后等结果出来检查一遍就行。更让我意外的是有了截图反馈机制后任务出错了也能迅速定位问题出在哪一步——那一步往往会有一张异常页面截图作为证据。我在实际使用中发现让AI操作浏览器这件事真正难的不是技术而是想象边界。一开始我只让它做“打开-截图-返回”这种简单操作后来慢慢学会让它组合技能、串联步骤现在它会自动翻页、滚动加载、对比前后截图差异甚至能根据页面文案判断是否出现了人工处理过的“特殊状态”。这些多步组合行为本质上就是大模型规划能力和浏览器自动化执行能力的一次握手。后续我想加两件事一是把定时触发机制加进去让技能库可以在无人值守的情况下周期性执行监控任务二是给截图模块接入一个本地的视觉识别组件不依赖云端模型也能做简单的页面异常检测。这两件事做完SkillsBot就能从“听命令干活”进化成“主动发现问题并汇报”。最后分享一个经验新手不要一上来就搞复杂的多步流程先从“打开一个固定网页截图”开始跑通了再加元素定位再加条件判断一步一步把技能库的积木搭起来。稳住每一步组合起来的力量会超出预期。