资讯动态

基于LLM与Playwright的网页自动化任务处理器:架构设计与实践

发布时间:2026/10/2 5:29:59 来源:尧图企业网站定制
1. 为什么我把日常脚本重写成了Paperclip这样的任务处理器先交代一下背景。我以前有个很典型的痛公司里每天有大量网页操作是重复的——早上把供应商后台的报价拉下来整理成表格中午去几个数据平台把关键指标截图存下来下午再逐个填一堆表单。这些事我最早都用Playwright写死脚本每个网站一套选择器每次页面改版脚本就废光维护就有几十个版本。后来我把这套东西整个重写了内部代号就叫Paperclip。名字的灵感其实就是办公桌上那个回形针——它没有任何魔法但能把一堆散落的单据夹成一条清楚的链。我的Paperclip做的事情也一样把用户一句大白话指令比如“去后台把今天的新增订单导出成Excel”拆解成浏览器里的一个个动作打开哪个页面、点击哪个按钮、等哪个元素出现、抓哪段数据然后自动执行完。它本质上是一个跑在你自己机器或服务器上的任务处理器底层接了大模型API来负责“读懂指令和拆动作”用浏览器自动化框架负责“动手点击”再用一个任务队列来保证长任务不丢、失败能重试。这篇内容适合两类人看。一类是被重复网页操作折磨的普通办公族你可以不写代码把Paperclip当成一个能听懂人话的网页机器人另一类是写过一些自动化脚本但总在维护选择器上翻车的开发者你会更关心我这套分层设计和踩过的坑。先说清楚边界Paperclip不是RPA厂商那种企业级平台也不是什么万能爬虫它解决的是自己可控的网站和公开页面的日常重复操作。登录态、验证码这类硬骨头我的做法是尽量绕开或者让人工介入而不是硬刚。想清楚了这一点后面所有设计才不会跑偏。2. 三层架构任务队列、浏览器控制层、LLM调度层各干各的Paperclip一开始我写得很粗暴就是把“大模型返回的JSON动作”直接塞给Playwright去跑。结果跑一个长任务就出问题网络断了任务没了、浏览器崩了不知道从哪继续、LLM偶尔抽风生成了非法动作整个流程就卡死了。后来我老老实实把系统拆成了三层各自管各自的事。2.1 任务队列层把“人话需求”变成不丢失的持久化任务这一层对应的是数据库和消息队列。用户说一句话之后我先把它转成一个标准的任务对象落到SQLite里。结构大概是这样的{ task_id: tsk_20250312_001, user_intent: 去供应商后台把今天的报价单下载到本地, status: pending, actions: [], retry_count: 0, max_retries: 3, created_at: 2025-03-12 09:00:00 }任务状态我用了一套有限状态机pending排队中、running执行中、waiting_confirm需要人工确认、succeeded完成、failed失败每个状态变化都写进SQLite。为什么非要落库而不是放在内存里因为LLM调度一个长任务往往要好几分钟中间浏览器随时可能崩。落库之后进程重启可以从running状态恢复执行而不是从头再来。2.2 浏览器控制层所有动作的真实执行者这层我基于Playwright实现但你完全可以用Selenium或者别的框架替换核心在接口抽象。它对外暴露的只有几个动作接口open(url)、click(selector)、fill(selector, value)、wait_for(condition)、extract(selector)、download(dir)。每个接口内部处理等待元素、处理弹窗、处理失败重试这些脏活。关键点在于浏览器控制层是“无脑执行”的它不负责判断“该不该点这个按钮”。所有决策都交给上层LLM调度层这一层只管把动作做稳定。这样设计之后换浏览器框架只需要改这层的实现不会动到上层的决策逻辑。2.3 LLM调度层负责“思考”把指令翻译成动作序列这一层是整个系统的脑子。用户那句大白话进来之后我先把网页当前的可用信息整理成一段上下文包括当前URL、页面标题、可见按钮和输入框的语义描述然后让大模型基于这段上下文输出一个动作序列。动作序列是结构化的JSON格式如下[ {action: open, value: https://supplier.example.com/dashboard}, {action: wait_for, value: text今日报价}, {action: click, value: text导出}, {action: download, value: /data/reports} ]为什么一定要LLM输出JSON动作而不是直接让LLM写一段Python代码去执行我试过后者风险太大——LLM生成的代码很容易包含奇怪的文件操作、网络请求、甚至无限循环等于把一台浏览器权限的机器交给了不可控的代码。JSON动作是固定的、白名单式的、可校验的LLM再怎么跑偏也只能在这几个动作里选出格的话我就拒绝执行。这一层我把OpenAI、国内几个主流大模型都试过结论是任务拆解能力比代码生成能力重要得多。一个能把步骤拆清楚、路径想明白的模型比一个能写花哨代码但经常想当然的模型靠谱得多。3. 技术选型里最让我纠结的几个决定往下层讲之前先把我个人踩过的选型坑写出来。这些决定看似简单实际上直接决定了Paperclip的上限和运维成本。3.1 浏览器控制Playwright、Selenium、原生CDP怎么选这个选择我做了个对比直接上表格维度PlaywrightSelenium原生CDP元素等待机制内置自动等待几乎不用手写sleep需要显式等待写起来啰嗦自己实现麻烦多浏览器支持Chromium/Firefox/WebKitChrome/Firefox/Edge等只支持Chromium系调试手段trace viewer录像回放问题复现能力强截图为主需要自己封装并发多开支持多context隔离支持但资源控制弱支持学习成本中等最低高我最后选了Playwright最核心的原因就是它的自动等待。LLM调度层给出的动作里经常有“点击某按钮”“等待某数据加载”这种模糊指令如果选择器给得不够精确Playwright的自动等待能帮我兜住很多延迟问题而Selenium通常需要我手动写一堆expected_conditions代码量直接翻倍。另外Playwright的trace录像帮我排除了无数个“到底哪一步错了”的疑问。出了问题直接打开回放能看到每一步鼠标落在哪里、页面是什么状态这对和LLM调度层联动排查问题特别重要——你可以直接看到LLM是不是给了一个离谱的点击目标。3.2 LLM API选择稳定性和结构化输出比聪明更重要我试了一轮主流大模型API最后定的选择标准就三条第一必须支持稳定的JSON结构化输出。很多模型支持function calling或者response_format但实际用起来偶尔会多出解释文字或者丢掉字段。我要的是每一次返回都能被pydantic严格校验通过偶尔一次解析失败还能重试而不是概率性的“偶尔能用”。第二上下文窗口要够大。因为Paperclip要把整个操作历史传给LLM做下一步决策如果历史一长就溢出那任务稍复杂一点就废了。我实际跑下来长期任务的历史压缩策略比窗口大小更关键这个后面说。第三延迟要低。拆解一步动作如果耗时超过10秒整个交互体验就毁了。我是先在云端API上跑通后来对隐私要求高的任务切到本地部署的轻量模型效果还可以但需要把网页上下文压缩得足够干净。3.3 本地跑还是云端跑我的落地方式最终我的部署方式是任务队列和调度层跑在一台云服务器上浏览器控制层用Playwright的headless模式跑在同一个环境里。为什么不在自己电脑上跑因为定时任务、长任务执行到一半电脑休眠就全完了。上服务器之后稳定了很多。费用方面纯调用云端大模型API跑日常任务一个月大概几十块钱这个成本能接受。真正贵的是浏览器常驻内存——headless浏览器每个实例大概占500MB内存并发三个任务就需要近2GB选服务器的时候一定要按这个预算来。4. 核心模块实现从一句指令到真实点击的全流程选型定了之后我重点打磨了三个核心模块。这一部分我拆开讲每一步都给出实际能跑的东西。4.1 任务解析流程把意图固化成可校验的动作序列用户输入一句话之后Paperclip不会直接开始执行而是先走一个“意图解析”环节。这一步的Prompt设计很关键我用自己的话描述一下大概的模板结构你是一个网页操作助手。请把用户的最终目标拆解为不超过10步的动作序列。 每步动作只能是以下类型之一open, click, fill, wait_for, extract, download, screenshot。 当前页面状态描述{当前页面的语义描述包含链接文本和按钮文本} 用户目标{用户输入} 只输出JSON数组不要输出其他内容。有个细节我会把页面的语义描述提前用程序生成好而不是让模型直接读HTML。因为HTML太长塞给LLM很容易让模型迷失重点。我用一个页面抽析模块把页面里的a标签文本、button文本、input的placeholder、表单label全提取出来整理成结构化文本。这样一来上下文很短模型定位元素的准确率大幅上升。4.2 动作执行的可靠性选择器策略是核心中的核心这一步是Paperclip翻车率最高的地方。最初我图省事让LLM直接返回CSS选择器结果悲剧了——类名带着hash后缀的网站一改版就全废更别提一些前端框架每次构建都会重新生成随机class。我的改进方案是往Prompt里明确禁止使用class选择器和id选择器强制让LLM从页面语义信息里挑选定位方式优先级是这样优先级定位方式适用场景1get_by_role按钮、链接、复选框这种有明确ARIA角色的元素2get_by_text页面里唯一文本比如“导出”“确认提交”3get_by_placeholder输入框依据placeholder定位4get_by_label表单控件依据label文本定位5备用CSS仅限数据表格实在没有语义特征才用举个例子原来让LLM点击“导出”按钮它可能返回css.export-btn改版后就失效。现在我会在页面语义描述里把这个按钮标成rolebutton, text导出LLM返回click(text导出)代码层再转成get_by_text(“导出”)稳定得多。在API层我还加了元素找不到时的兜底策略第一次点击失败不是直接报错而是重新抓取当前页面语义、再让LLM重新给一次选择器。实测这个“重新审视”机制能把单步成功率从80%左右拉到95%以上。4.3 任务循环成功、失败、还是求助整个调度循环我写成了一个大循环。伪代码如下while task.status running: page_state extract_page_semantics(page) actions llm.decide_next_actions(page_state, task.original_intent, action_history) actions validate_actions(actions) # pydantic校验 for act in actions: success browser.execute(act) if not success: # 如果连续失败两次拉高状态 task.retry_count 1 if task.retry_count 2: task.status waiting_confirm notify_user(f任务卡在第{step}步{act.summary}) break actions llm.re_decide(page_state) # 带着失败原因重新决策 else: action_history.append(act) else: task.status succeeded有个判断很重要什么时候让LLM继续处理什么时候直接举手求助。我的经验是同一个动作连续失败两次或者网络重试三次还是超时就不要再折腾了直接标记为waiting_confirm并通知人工。因为LLM在浏览器环境里其实是个“睁眼瞎”一旦信息不足它越重试越容易陷入自己编造的循环里。5. 上线后踩过的坑才算数我把事故修成了策略这部分我挑几个最有代表性的写都是真实翻过车的。5.1 新标签页和iframe导致点击目标丢失第一次跑通全流程那天我让它自己登录进系统点开一个报表结果它点了半天都点不对。看回放才发现点击“查看报表”之后系统是在新标签页打开的而我的浏览器控制层一直盯着原来的页面找元素当然什么都找不到。解法是在browser层统一做一次“标签页感知”每次执行动作前检查当前活跃标签页如果页面跳转了URL就自动switch过去。iframe更恶心表单嵌在iframe里Playwright的locator默认是探测不到iframe内部元素的。我的做法是给元素定位加一个全局兜底先看主文档找不找得到找不到再遍历当前页面的iframe逐个找。5.2 弹窗和确认框一次手滑提交了不该提交的申请有一次测试一个申请表单流程最后弹了一个原生确认框Playwright默认弹框会拦截而我的动作序列里没有处理confirm的指令结果是任务卡了半小时所有后续动作全在跟一个不存在的弹窗较劲。之后我加了弹窗事件监听遇到confirm、alert、prompt默认全部先拒绝然后把这个弹窗内容作为一条“页面状态”反馈给LLM让它决定要不要点确定。这个策略虽然保守但至少保证了不会在流程不明的时候把不该提交的内容提交出去。所有需要确认的操作我现在都在Prompt里明确要求必须先返回wait_for(弹窗文本)再返回click(确定)宁可多一个步骤也不赌。5.3 登录态失效长任务跑一半被踢下线跑一个需要20分钟的批量任务时系统每隔10分钟踢一次登录态任务卡在一次跳转登录页上。这玩意靠自动化硬解成本高我最终的方案很朴素用Playwright的storage_state把登录后的cookie持久化到本地文件每次任务启动前加载一遍快过期时再安排一次“续期动作”——先自动去首页走一圈看是否被重定向到登录页是的话就通知人工扫码不是的话正常继续。这类验证码、风控、动态token的问题我的经验是不要试图靠Prompt或者脚本去破解。主动把边界画清楚让系统在遇到这些情况时第一时间通知人比训练一个“破解版自动化”靠谱得多也更稳妥。5.4 动作历史过长导致LLM开始胡说八道这个坑最有意思。任务一旦长到上百步我把全部action_history都塞给LLM结果模型经常在中后段开始重复之前做过的动作或者忘了原始目标是什么开始自由发挥。查trace发现它把第10步的点击动作又在第80步重放了一遍直接把页面带偏了。我的解法是做了动作历史的滑动窗口压缩保留最近10步的完整动作更早的步骤则每5步合成一条摘要让LLM只看到“已经完成了什么”而不必纠结每个细节。设置向量归档也可以但实际用下来滑动窗口摘要已经够用成本还低。5.5 幂等性是个大事重复提交不可接受最严重的一个问题因为网络超时浏览器控制层以为点击没有生效实际服务器已经收到了请求。于是任务队列触发了重试同一笔申请被重复提交了两次。这个问题的根子在“动作执行成功”没有唯一标识。我后来在动作层给每个动作生成一个request_id点击表单提交前先记录这个任务的关键数据比如订单号、URL执行完之后强制查一次页面状态确认是否出现成功提示没有成功提示才允许重试否则一律按成功处理。这套幂等判断让重复提交事故基本清零。6. 实测效果与更适合往里塞的业务场景最近我让Paperclip连续跑了一周的日常任务放几个实测数据。最稳定的是数据归集类每天上午从三个平台抓行情数据、清洗、写入表格七天成功率100%平均每个任务耗时4分钟之前人工做要半小时。其次是表单代填类只要页面不弹验证码成功率在95%以上。最不稳定的是那种需要大量判断的复杂审批流程涉及多个角色、多种状态流转成功率掉到70%左右这种场景我现在不敢全自动。用下来我觉得这几个场景是最适合往里面塞的定时巡检每天早上自动打开各个后台看告警数量异常时截图加汇总发出来数据归档把周期性生成的报表按固定命名规则下载存档跨平台录入从A系统读数据再填到B系统中间做格式转换页面功能冒烟改版之后自动把核心路径点一遍确认没挂不太适合的是涉及支付、强实名认证、强风控审核的流程。这类流程一旦自动操作出问题填坑成本比节省的时间高得多。我自己把Paperclip当成了那个办公桌上不停加夹子的回形针架子——今天夹一个巡检明天夹一个归档后天夹一个新需求。本质上它不是什么新发明只是把“网页操作”这件事从硬编码脚本的易碎状态换成了“有脑子的指令翻译可靠的手脚执行”。你要是也有一堆类似的重复网页操作可以按我这个三层分工开始搭先把任务队列做扎实再把选择器策略固化掉最后才接大模型——这个顺序反了后面就是无穷无尽的坑。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑