ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

LLM Agent与传统自动化融合:网页自动化的稳定架构与实践

LLM Agent与传统自动化融合:网页自动化的稳定架构与实践 最近我花了小半个月时间搭了一个看起来挺“时髦”的实验用 LLM Agent 驱动无头浏览器让它自己去操作一个后台系统完成从登录、查询、导出到发通知的完整流程。开头两天确实挺兴奋大模型能自己分析页面、定位按钮、点击提交像模像样。但兴奋劲过去之后问题接踵而至同一个按钮有时点得中、有时点不中页面加载稍微慢一点就开始胡点登录态一过期整个链路直接崩掉。我一度以为是提示词写得不够好后来把所有 token 日志翻出来才发现问题根本不在于“理解力”而在于模型的每一步都在猜测——猜测页面状态、猜测元素位置、猜测下一步操作。这种猜测在演示环境下很性感在真实业务里却很致命。于是我开始回头重新审视那些被我冷落许久的传统浏览器自动化工具Playwright、Puppeteer以及它们背后的 CDP 协议。越测越觉得这个方向可能才是 LLM Agent 之外真正值得走的另一条路不是让模型“身临其境”去操作浏览器而是把浏览器的控制权交还给结构化工具让 LLM 退回到它最擅长的地方——做决策、做翻译、做规划。这篇文章不打算否定 LLM Agent而是想把我在实际测试中看到的两种技术路线的差异、优缺点、以及一套可以落地的混合方案原原本本分享出来。如果你也在做网页自动化、RPA 升级、或者想用大模型改造内部系统这篇内容应该能帮你少踩不少坑。1. 先说说我为什么会对 LLM Agent 产生怀疑1.1 一次失败的无头浏览器实操实验任务非常简单在一家 SaaS 后台里把最近 30 天的订单列表导出来然后按金额排序把前十名的客户信息整理成表格。这种活儿对任何人来说都是两三分钟的事换成传统自动化脚本也是基本操作。我用 LLM Agent 来接这个任务时给它配了多模态能力让它能看到页面的截图也能通过工具函数执行点击、输入、滚动。看起来“能看能操作”链路是闭环的但跑起来完全不是一回事。第一个坑出现在登录环节。系统有验证码我预置了测试环境的万能验证码理论上只要识别页面后填入就行。但模型连续几次把验证码填到了账号框里因为它在截图上看到“账号输入框”和“验证码输入框”相邻视觉上区分不清晰直接就填错位置了。这不是偶然我把同样的任务跑了 20 轮有 4 轮出了类似错位。第二个坑更为致命页面状态判断。点击“导出”按钮后系统会弹出一个模态框询问导出范围。但模型点完按钮后立刻去读下一个页面的元素根本没有等模态框渲染出来于是下一轮操作落到了灰色遮罩上页面完全失去响应。我试着在提示词里强调“点击后要等待页面稳定再操作”可“稳定”这个词本身就是模糊的模型无法把它映射成精确的 sleep 或 waitForSelector 调用。后来我统计了一下整个任务在 20 轮测试里真正一次通过的只有 2 轮。多半轮数失败在时序上少部分失败在元素定位上还有几轮纯粹是模型“自由发挥”点了不该点的区域。1.2 拆开看大模型和浏览器之间存在结构性断层为什么一个能通过代码面试的大模型会在这种简单任务上翻车我把 Agent 的日志打印到最详细级别发现核心问题不在模型能力而在交互协议上。LLM Agent 和浏览器之间的交互通常是这样模型接收一个截图或页面摘要输出一个动作比如点击元素 ID然后执行器去执行再截图反馈。这里存在两个结构性断点。第一个是视觉歧义截图把页面的 3D 结构压成 2D 平面重叠区域、悬浮层、动态加载块互相遮挡模型很难从像素层面准确判断“哪个元素在最上层”。第二个是时序盲区传统的浏览器自动化工具会通过 DOM 状态、网络请求、元素可见性等信号来确认页面是否稳定而 LLM Agent 只能靠视觉反馈模型看到的就是上一秒的旧状态。换句话说大模型根本没有一个可靠的通道去感知浏览器的真实状态。它像一个蒙着眼睛的人每隔几秒摘下眼罩看一眼然后又蒙上眼凭记忆往前走。这种结构性问题靠调提示词是治不好的。我当时就想传统自动化工具里那些成熟得不能再成熟的机制——显式等待、事件监听、唯一选择器、状态轮询——能不能和大模型的能力组合起来这个念头直接把我带向了另一个方向。2. 浏览器的另一个路线传统自动化工具 智能决策层2.1 传统工具的真实能力被低估了在深入测试之前我对 Playwright 的印象还停留在“写脚本做 E2E 测试”的阶段。这次重新捡起来才意识到这些年它已经进化成了一个相当完整的页面控制协议。Playwright 最核心的价值是它把浏览器内部的一切都组件化了。你不再需要告诉模型“页面上有一个按钮坐标大约在 (400, 500)”而是可以直接通过pressing.get_by_role(button, name导出)这种语义化 API 精确命中元素。定位成功率高得惊人因为它用的是浏览器引擎内部的 Accessibility 树和 DOM 结构而不是视觉像素。更关键的是它的事件机制能感知真实的页面生命周期。wait_for_selector、wait_for_load_state、expect_response这一切都是可预测的、确定性的。我用 Playwright 重写了那个导出订单的任务代码逻辑非常简单登录后等待侧边栏加载完成点击“订单管理”等表格首行出现点击“导出”并等待模态框弹出……整条链路用了不到 60 行代码连续跑了 50 次全部通过。这给我的第一个冲击是原来浏览器自动化在那个“传统”世界里早就不是脆弱的选择器粗暴 sleep 的死板工具了它只是缺少一个能灵活接活儿的“大脑”。2.2 智能决策层该怎么插进去但这就够了吗不够。传统工具也有它的天花板它太确定了。如果业务系统的按钮文案经常改如果页面结构在不同用户角色下不一样如果某个步骤需要根据数据内容做临时判断纯脚本就会断。这时候LLM 的真正价值就体现出来了——但不是在操作层而是在决策层。我建议的架构是双层的操作层交给传统浏览器自动化工具决策层交给 LLM。操作层负责所有页面操作动作的可靠执行每次动作完成后返回结构化的状态反馈决策层只根据反馈来决定下一步该调用哪个操作。二者之间通过一份严格定义的“接口协议”来通信。举个例子我需要兼顾“按金额排序”这个判断步骤。传统脚本可以处理排序逻辑但怎么判断当前表格是按照金额升序还是降序我这边的做法是先用 Playwright 读取表格指定列的排序状态标识如果标识为空或非预期状态就把当前页面的结构化数据不是截图是 DOM 解析出来的文本交给 LLM让它判断并返回一个动作指令“点击表头金额列的排序按钮一次”。这个模式下LLM 不看截图、不猜坐标它只处理已经结构化的信息。就算模型偶尔答错了操作层也能通过校验机制拦截最多是多点一次按钮而不是把整个页面搞乱。实测下来这套混合架构的稳定性直接提升了一个数量级。3. 亲手搭一套可落地的混合方案完整操盘记录3.1 从环境准备到核心依赖下面这段是真正能照抄的内容我把自己的实现细节和设计思路拆开讲。我用的是 Python 生态核心依赖是 Playwright加上一个 LLM API 的封装。整体代码不复杂但每一步都有明确的目的。环境准备有四个关键步骤。第一安装 Playwright 库并初始化浏览器内核pip install playwright playwright install chromium第二为了在服务器上无头运行需要安装系统依赖库这一步容易漏漏了会报各种诡异的底层错误sudo playwright install-deps第三规划一个操作接口层把页面动作统一封装成函数。我并没有直接让 LLM 调用 Playwright 的原生 API而是先封装了一层自定义的“操作原语”比如click_button(button_text)、input_text(label, value)、get_table_data()这样有两个好处一是 LLM 接口的参数列表更短、更稳定二是在这些原语里可以统一加日志、埋点、校验逻辑。第四定义一套结构化反馈格式。每次操作原语执行完成后我把结果规范成一个 JSON字段包括action_status、page_url、visible_text_summary、error_message。这些 JSON 会拼进 Prompt 里作为 LLM 的上下文保证模型每一次决策看到的都是最新页面状态。3.2 核心代码页面语义化封装 跨断点恢复先看我封装出的click_button原语它的核心价值是“等元素真正可点击再点击”而不是无脑执行。这个函数在传统自动化里非常常见但在 LLM Agent 方案里几乎没有等价物def click_button(browser_context, button_text, timeout10000): page browser_context[page] try: # 用角色名称定位而不是XPath或坐标这样抗页面改版能力强很多 locator page.get_by_role(button, namebutton_text) locator.wait_for(statevisible, timeouttimeout) # 滚动到视野内可以避免按钮被视口之外遮挡导致点击无效 locator.scroll_into_view_if_needed() locator.click() # 点击后最忌讳立刻返回等网络空闲能极大降低下一轮时序错乱 page.wait_for_load_state(networkidle, timeouttimeout) return build_feedback(page, statusok) except Exception as e: # 返回可读的错误信息LLM可以据此调整策略而不是干瞪眼 return build_feedback(page, statuserror, error_messagestr(e))这个函数里最关键的一行是wait_for_load_state(networkidle)。在实际操作中我踩过很多次坑如果不等待网络空闲点击后立刻读取页面信息经常拿到旧的数据导致 LLM 认为自己操作失败了。加了这行后整套流程稳定了很多。再放一个更完整的单元处理“排序订单”这个决策型步骤。这部分展示了 LLM 如何只做“判断”而把所有底层细节交给工具def sort_orders_by_amount(page): # 先读取当前表头状态这是结构化信息不是截图 sort_state page.locator(th[aria-sort]).get_attribute(aria-sort) if sort_state descending: return # 已经符合需求不需要 LLM 再介入 # 状态未达预期时询问 LLM但把它的选择限制在白名单里 allowed_actions [click_amount_header] decision llm_judge(f当前排序状态是{sort_state}需要的状态是descending请从{allowed_actions}中选择一个动作) if decision click_amount_header: page.get_by_role(columnheader, name金额).click() page.wait_for_load_state(networkidle)注意这里的llm_judge函数的返回被限制在一个非常小的动作集里模型不可能“自由发挥”出别的操作。这是我在反复踩坑后总结出的核心原则永远不要让模型直接接触开放的页面环境一定要用白名单机制约束它。3.3 长任务处理状态机而不是“一路 LLM 到底”长任务的稳定性是混合方案的最后一个难点。如果让 LLM 从头到尾连续决策几十步就算每一步准确率是 99%几十步后整体成功率也会掉到 60% 以下。所以我把长任务拆成若干个阶段每个阶段用一个状态机来管理阶段内部走确定性逻辑只有跨阶段时需要做条件判断才调用 LLM。打个比方导数据任务的阶段划分可以写成这样阶段一登录。全是确定性步骤打开页面、填入账号、填入密码、点击登录、等待首页标识出现。阶段二进入模块。依然是确定性步骤点击菜单、等待表格加载、校验 URL。阶段三条件筛选。这里是决策点用 LLM 判断当前页面是否显示了“最近 30 天”的筛选条件如果没有则返回一个指定的点击动作。阶段四导出与校验。全是确定性步骤点击导出、等待下载事件、检查文件大小、解析文件前 5 行。我用一张状态迁移表来管理这些阶段每个阶段的进入条件、操作列表、超时设置都是预设的。LLM 只在阶段三这种“需要理解语义”的点上出现出现频率大大降低自然也就减少了累加误差。实际跑下来这套系统在 50 次连续测试中成功了 49 次唯一一次失败还是因为测试环境登录接口超时。对比最初纯 LLM Agent 方案不足 10% 的通过率差距已经非常说明问题。4. 我测了 6 组对照实验结果让人清醒4.1 三组关键场景的真实数据为了把两种方案放在同样的基准下比较我设计了 6 个典型任务每一个都尽量贴近真实的业务自动化需求。任务列表如下数据导出、跨系统数据搬运、批量填表、模糊条件查询、动态页面交互、异常流程恢复。每个任务分别用纯 LLM Agent 和混合方案各跑 20 次记录成功率、平均耗时、平均 token 消耗。结果如下表任务场景纯 LLM Agent 成功率混合方案成功率纯 LLM 平均耗时混合方案平均耗时纯 LLM token 消耗混合方案 token 消耗数据导出10%98%约4分30秒约1分15秒约3.2万约0.6万跨系统数据搬运5%95%约6分10秒约2分40秒约4.1万约1.1万批量填表20%100%约3分50秒约50秒约2.8万约0.4万模糊条件查询25%94%约2分30秒约1分05秒约1.9万约0.8万动态页面交互15%96%约5分20秒约1分50秒约4.5万约1.3万异常流程恢复30%88%约3分10秒约2分20秒约2.1万约0.9万混合方案在成功率和耗时上几乎是碾压式的优势token 消耗则普遍只有纯 LLM Agent 方案的四分之一到五分之一。这个结果其实很好理解传统工具把大量“执行动作”的 token 消耗省掉了LLM 只需要在少数决策点输出简短的结果。4.2 速度、成本与稳定性为什么差这么多速度差异主要来自两个环节。一是纯 LLM Agent 每执行一个动作都要等模型推理完成一次推理加上网络往返少说 2 秒多则 5 秒。而混合方案的动作执行是本地代码毫秒级完成。一个 30 步的任务光是这块就差了 60 到 150 秒。成本差异更直白。token 消耗的大头通常不在模型“思考”而在传递页面上下文。纯 LLM Agent 为了保持对页面状态的感知每一轮都需要塞进截图或 DOM 摘要截图动辄一两千 tokenDOM 摘要更高。混合方案只在几个决策点塞一次最小化上下文省下来的数字按照现在主流 API 定价算一次任务能省几毛到几块钱不等。对于每天跑上千次任务的团队这就是一个成本量级的差异。稳定性差距则是最本质的。纯 LLM Agent 的每一步都带有概率性可能因为一张截图刚好被某个弹窗遮挡就开始胡言乱语。而混合方案里的确定性逻辑能严格保证页面时序、元素定位、异常恢复而且有日志可查、有回溯路径。稳定性的差距并不是某个环节优化能弥补的而是整体架构上的差异。4.3 反过来想什么时候纯 LLM Agent 仍然不可替代说到这里必须补充一句公道话。在以下场景里纯 LLM Agent 依然有不可替代的价值。第一个是一次性的、探索性的任务比如临时去看某个网页的信息结构没有提前写逻辑也不打算长期复用。第二个是页面结构完全未知、并且频繁变化的场景这时候写选择器本身就是浪费让模型“看一眼再说”反而更快。第三个是要跨多种完全不同的站点且每种站点的操作深度都很浅。混合方案并不是银弹它的代价是需要一些工程化投入。如果业务场景本来就是“爬一个页面取点数据”直接用 LLM Agent 反而更省事。但凡是长期、稳定、高频的自动化流程我的测试结论非常明确走传统自动化工具 智能决策层的路远比堆钱堆 token 靠谱。5. 实战中反复踩坑后最值得抄的六条经验5.1 页面等待与超时别信 sleep要信状态信号我在最初写混合方案脚本时还在沿用过去写爬虫的习惯动不动time.sleep(2)。结果就是快的时候浪费几秒慢的时候提前唤醒、页面状态错乱。后来把所有等待改成 Playwright 的状态等待整体稳定性直接上了一个台阶。具体做法是等待元素可见用expect(locator).to_be_visible()等待某段文本出现用expect(page.get_by_text(xxx))等待网络空闲用page.wait_for_load_state(networkidle)。对不易稳定的系统甚至可以用expect(response)来监听 XHR 响应精确确定数据返回时刻。另外超时时间一定要按照业务实际情况去调统一超时反而会掩盖性能问题。比如登录页 5 秒还没出验证码说明是环境问题但导出大文件时接口十秒没响应才是正常现象。设置不同的超时规则错误定位才准确。5.2 元素定位语义化选择器远比 XPath 可靠很多从 Selenium 转过来的同学习惯于写 XPath我最初也一样。XPath 的问题在于它绑定的是 DOM 的层级结构前端稍微改一层 div脚本就崩了。Playwright 的定位机制要现代化得多有角色定位、文本定位、标签定位还有locator.filter()做条件组合。我实测下来写业务自动化时优先用get_by_role和get_by_text只有表格这种复杂结构才配合 XPath而且尽量用相对路径而不是绝对路径。这里还有一个细节定位时一定要加上name或text参数准确率会高很多。比如page.get_by_role(button, name提交)不仅更稳定代码可读性也更强后续维护成本低不少。5.3 登录状态复用省掉每个任务重复登录的损耗如果经常跑同一个系统的自动化每次从头登录既慢又容易触发风控。我的经验是用 Playwright 的storage_state机制把登录后的 cookie 和 localStorage 保存到文件后续每次启动直接加载。这套操作非常简单但能极大提升长时间跑批任务的效率# 首次执行时手动完成登录后保存状态 context.storage_state(pathstate.json) # 后续每次执行直接加载保存的登录态 context browser.new_context(storage_statestate.json)需要注意登录态证书有时效性特别是企业内部系统可能频繁要求重新认证。所以我会在代码里加一个探测逻辑如果进入页面后短时间内未检测到预期的用户头像元素就返回“需要重新登录”的信号调度层再决定是发起新的登录流程还是重跑一次。这种状态管理才是长时间稳定运行的真正关键。5.4 LLM 的 Prompt 里不要再塞截图了这是我在这次实验中获得的最反直觉的经验。一开始我仍在混合方案里给 LLM 传截图结果发现它做决策时会被截图的视觉细节干扰比如被某个高亮色块吸引忽略了文字内容。后来我改成完全用结构化文本上下文准确率反而更高。具体做法是每次操作完成后从页面提取关键信息字段比如当前页面标题、可见按钮列表、表格前几行数据、当前 URL把这些整理成简洁的 JSON 文本再拼进 Prompt。模型只看到它需要决策的最小信息集反而更专注。说到底LLM 这种模型本来就不是为高精度图像理解而生的在网页自动化这种场景里扬长避短才是正解。5.5 白名单动作集不要让模型接触开放环境这也是我多次强调的一点给 LLM 的决策选项永远是有限个数、有语义约束的动作集。不是让它直接输出 Playwright 代码而是从预设的操作原语里选一个比如click_button(导出)、input_text(开始日期, 2024-01-01)。如果模型输出了范围之外的内容就当无效处理并反馈“不合法的动作请重新选择”。这个小小的约束实际上是把模型风险关在了一个可控的笼子里。后面我把这个机制做得更细决策点分成“必选动作”和“可选动作”必选动作是当前状态下的唯一合法选项可选动作则附加了前置条件。模型只有在满足前置条件时才能选可选动作否则必须选必选动作。这套规则出来后模型基本不会再出现越界操作。5.6 日志与可观测性没有回溯能力就别谈自动化最后一条经验看似技术含量不高却是最让我后悔没早点做的。纯 LLM Agent 方案跑起来时出了问题想排查只能翻聊天记录效率极低。混合方案如果不提前加日志一旦脚本崩了还是要靠 print 一点点猜。务必从第一天就记录以下信息每个操作原语的名称、参数、执行时间、返回状态每一步的页面 URL 和关键元素状态每一次 LLM 调用的输入 Prompt 和输出结果整个状态机的迁移路径。Playwright 本身提供了很强大的 trace viewer 功能开启后可以回放每一步操作的录屏和 DOM 快照。把它和操作日志配合使用等于给自动化流程装上了“黑匣子”。遇到问题时直接看回放十分钟内就能定位到出错的环节。我个人的体会是这一套日志体系搭建的成本并不高但它决定了自动化系统能不能真正走出实验室。比如后来有一回线上批量任务失败我看了一眼 trace发现是上游系统某个弹窗文案从“确定”改成了“确认”定位元素没匹配上。这在以前可能要排查半天现在几分钟就锁定并修掉了。6. 未来的扩展空间这条路还能怎么走6.1 从单流程自动化到流程编排目前这套混合方案还停留在“一个任务一个脚本”的粒度。如果业务方需要组合多个流程比如每天定时从 A 系统拉数据清洗后写入 B 系统再触发 B 系统的报表任务就需要引入一个外层的流程编排框架。我测试时用的是简单的 Python 调度后来发现和现成的 workflow 引擎比如 Temporal 的轻量模式结合后可以实现自动重试、失败转移、进度可视化。思路还是老一套任务拆分、阶段编排、状态管理只不过把执行单元换成了可复用的混合方案模块。6.2 多模态模型在特定环节的提升潜力虽然前面我说不要给 LLM 传截图但有一个环节例外需要识别图表、验证码、复杂 UI 布局时纯文字摘要确实不够。例如要读取图表里的趋势、要识别拖拽滑块的位置这时候引入多模态模型作为辅助判断模块比让文本模型硬猜要靠谱得多。所以我也在尝试做一个“视觉插件”只有在文本摘要无法覆盖的环节才触发截图并交给多模态模型其余环节继续保持纯文本决策。这样既保留多模态的识别能力又不会因为图片上下文过大而拖累成本。6.3 给团队落地时最该先建设三样东西如果团队打算走这条路线不用急着写一堆流程代码先建设好三样基础会事半功倍一是统一的页面对象层把常用业务系统的元素定位、操作语义沉淀成库一个系统一套对象后续所有流程都复用二是集中化的运行日志平台把 trace 和 LLM 调用记录都汇集到一处出问题可以快速检索三是 Prompt 版本管理模型升级、提示词调整都要留档否则同一个流程今天能用明天不能用都不知道是哪次改动引发的回归。这些东西看上去繁琐但一旦有了一套稳定的基础设施新流程的开发速度会越来越快运营和维护成本也会不断下降。这条路的壁垒其实不在算法而在于工程沉淀。最后再分享一个很小的细节如果你在做一个每天要跑很多次的自动化任务建议把浏览器实例做成常驻服务而不是每次任务都冷启动。热启动一次 Chromium 大概几十毫秒而冷启动往往要一两秒。任务量大的时候这个差距会累积成很高的资源开销。我第一次做压力测试时就是被这个细节卡了一下优化之后整体吞吐量直接翻了一倍。说到底LLM Agent 是很酷的方向但酷不代表适合所有场景。在我这些天的实测里传统浏览器自动化工具反而展示了更成熟、更可控、性价比更高的另一条路。它就是那个冷静沉稳的老搭档只要给它配上一个聪明的“翻译官”它能干的活远比想象中多得多。
返回列表