YAOTU INSIGHTS

AI Agent 浏览器自动化实战:Playwright 全自动发稿的 9 个坑

AI Agent 浏览器自动化实战:Playwright 全自动发稿的 9 个坑
1. 一次让AI替我干活的真实实验从想法到落地去年年底我在整理自己的技术输出节奏时发现一个很尴尬的问题我写代码、做项目、踩坑复盘的速度其实不慢但把这些东西整理成一篇能发出去的稿子再登录社区、排版、配图、选标签、点发布这一整套流程下来往往要吃掉我大半个下午。真正写的时间可能只占三成剩下七成全是机械操作。于是我就冒出一个念头能不能让一个 AI Agent 自己把这件事从头到尾干完不是帮我写而是帮我发——自己打开浏览器、自己登录、自己填标题正文、自己点发布全程我只在旁边看着。这个实验最后跑通了但代价是227 次浏览器操作、4.3 小时的实际耗时、以及 9 个把我卡到怀疑人生的坑。这篇文章就是把这 4.3 小时里发生的所有事情摊开讲清楚我用了什么工具、为什么这么选、每一步在干什么、坑是怎么踩的、又是怎么爬出来的。关键词里出现的WorkBuddy、AI Agent、腾讯云社区、浏览器自动化、Playwright这几个词基本就是这次实验的全部骨架。先说清楚这件事适合谁看。如果你只是想让 AI 帮你写一段文案那这篇对你价值不大直接用聊天窗口就够了。但如果你想让 AI真正去操作一个网页——登录、填表、点击、翻页、上传、提交并且要稳定、可复现、能处理各种意外弹窗和验证那这篇就是给你准备的。它涉及的是AI Agent 与浏览器自动化框架的结合是让模型做决策、让自动化框架做执行的典型架构。哪怕你用的不是 WorkBuddy而是别的 Agent 平台底层的思路也是通用的。我先把结论摆前面免得你看到一半才发现方向不对让 AI 全自动发一篇稿技术上完全可行但全自动这三个字是有水分的。真正跑起来你会发现AI 负责的是判断下一步该点哪里、该填什么而稳定性、重试、异常兜底这些脏活累活还是得靠 Playwright 这类自动化框架来扛。两者分工不清就会像我一样在某个弹窗上卡四十分钟。下面我按实际推进的顺序把整件事拆开讲。2. 为什么是 Playwright 而不是别的自动化方案2.1 选型背后的三个硬性约束在动手之前我对比过几种主流方案纯 Selenium、Puppeteer、Playwright以及一些基于浏览器插件的录制回放工具。最后选 Playwright不是因为它新而是因为它刚好满足我这次实验的三个硬性约束。第一个约束是现代前端页面的兼容性。腾讯云社区这类站点是典型的前端渲染应用页面元素大量依赖异步加载DOM 结构在首屏之后还会持续变化。Selenium 那套基于 WebDriver 的老路子在等待策略上要写大量显式等待稍不留神就抓到还没渲染完的空节点。Playwright 自带的**自动等待auto-waiting**机制会在点击、填值前自动检查元素是否可见、是否可交互、是否稳定这一条就省掉了我至少一半的等待代码。第二个约束是多浏览器与多上下文隔离。实验里我需要同时保持已登录的会话和干净的测试会话还要能在 Chromium、Firefox、WebKit 之间切换验证兼容性。Playwright 的BrowserContext概念天然支持这种隔离——每个 context 相当于一个独立的隐身窗口cookie、localStorage 互不干扰。这一点在我排查为什么登录态丢了那个坑的时候起了决定性作用。第三个约束是和 AI Agent 的对接友好度。Playwright 提供了清晰的 API 边界Agent 只需要输出选择器 动作这样的结构化指令框架负责执行。相比之下录制回放工具生成的脚本可读性差Agent 很难动态修改而纯 Selenium 的 API 又太啰嗦Agent 生成的指令容易出错。2.2 Playwright 在这个项目里到底扮演什么角色很多人会混淆一件事Playwright 不是 AI它只是手。它不会自己判断现在该点登录还是该点注册它只会忠实地执行你给它的指令。真正做判断的是上层的 AI Agent——也就是 WorkBuddy 这类平台里的决策模块。所以整个架构其实是两层层级角色负责的事典型工具决策层AI Agent看当前页面状态决定下一步动作WorkBuddy / 大模型执行层自动化框架把动作翻译成真实浏览器操作Playwright感知层页面快照把 DOM 转成 Agent 能读的文本accessibility tree / DOM 摘要我踩的第一个认知坑就在这里一开始我天真地以为AI 自动化就是让模型直接操控浏览器结果发现模型根本看不见页面。它看到的是一段被压缩过的页面文本描述然后基于这段描述猜下一步。页面描述的质量直接决定了 Agent 的判断准确率。这也是为什么后面我会花大量时间在怎么把页面喂给 Agent这件事上。2.3 环境准备里最容易被忽略的两个细节环境搭建本身不复杂npm init playwright一把梭但有两个细节我建议你一开始就处理好否则后面会反复返工。第一个是浏览器版本锁定。Playwright 默认会下载它自己配套的浏览器内核但如果你机器上已经装了系统级 Chrome很容易出现代码里用的是 Playwright 内核手动调试时用的是系统 Chrome两边行为不一致的情况。我的做法是统一用 Playwright 自带的内核并且在playwright.config.ts里显式指定channel避免歧义。第二个是持久化用户目录。登录态这种东西如果每次跑脚本都重新登录不仅慢还容易触发风控。Playwright 支持launchPersistentContext把用户数据目录固定下来登录一次之后 cookie 就留在本地。这个目录的位置要提前规划好别放在临时目录里否则系统一清理就前功尽弃。// 持久化上下文的关键配置 const context await chromium.launchPersistentContext(./user-data, { headless: false, // 调试阶段一定要开有头模式 viewport: { width: 1440, height: 900 }, args: [--disable-blink-featuresAutomationControlled] });提示headless: false在调试阶段是刚需。Agent 判断错了你能立刻看到它点到了哪里如果开无头模式你只能对着日志猜效率差十倍。3. 227 次操作是怎么堆出来的任务拆解与执行链路3.1 把发一篇稿拆成 Agent 能执行的原子动作发一篇稿听起来是一件事但对 Agent 来说它必须被拆成一串原子动作。我最终的拆解是这样的打开目标站点首页判断当前是否已登录检查头像元素是否存在未登录则点击登录入口等待登录完成进入创作/发文页面填写标题填写正文富文本编辑器需要特殊处理选择分类与标签上传封面图预览并检查点击发布确认发布结果看起来只有 11 步但实际执行了 227 次浏览器操作。差距在哪在于每一步内部都有大量的感知-判断-执行循环。比如第 6 步填写正文富文本编辑器不是简单的inputAgent 需要先点击编辑区、等待工具栏加载、再逐段输入每输入一段还要确认内容真的进去了。光是正文这一项就消耗了 60 多次操作。3.2 感知层Agent 到底看到了什么这是整个实验里最值得讲的部分。Agent 不是用眼睛看页面它读的是一份页面结构摘要。我最初用的是最朴素的方案——把整个 DOM 的innerText抓出来喂给模型。结果非常糟糕模型经常把导航栏的文字当成正文把页脚链接当成按钮。后来我改成了基于accessibility tree无障碍树的摘要方案。无障碍树的好处是它天然过滤掉了大量装饰性元素只保留可交互、有语义的节点并且每个节点自带 role按钮、输入框、链接和 name。Agent 拿到这份摘要后判断准确率肉眼可见地提升了。// 抓取页面可交互元素的语义摘要 const snapshot await page.accessibility.snapshot(); // 递归提取 role name压缩成 Agent 可读的列表但这里有个坑无障碍树对富文本编辑器的描述非常模糊。编辑器区域在无障碍树里往往只是一个role: textbox的大节点Agent 根本不知道光标在哪、当前段落是什么格式。这就是我后面要专门讲的第 4 个坑。3.3 执行层为什么每次操作都要重新定位Playwright 有个特性叫locator 的惰性求值。你写page.locator(.submit-btn)的时候它并不会立刻去查 DOM而是在真正执行点击时才去定位。这个设计本来是为了应对动态页面但在 Agent 场景下会带来一个副作用如果 Agent 缓存的元素引用过期了操作就会失败。我的应对策略是每次操作前都重新定位绝不缓存元素句柄。Agent 输出的指令格式统一成选择器 动作 参数执行层每次拿到指令都重新locator一次。这样虽然多了一点开销但稳定性提升非常明显。227 次操作里因为元素过期导致的失败从最初的十几次降到了 0。3.4 4.3 小时的时间都花在哪了我把这 4.3 小时做了个粗略的分布统计结果挺反直觉的阶段耗时占比说明环境与登录调试约 25%登录态反复丢失排查了很久页面感知方案迭代约 30%从 innerText 换到无障碍树富文本编辑器处理约 20%最大的单点难题异常弹窗与重试约 15%各种意料之外的遮挡真正发文本身约 10%反而最快看到没真正干活的时间只占一成九成都在处理怎么让 Agent 稳定地干活。这个比例我觉得很有代表性——任何想做浏览器自动化的朋友都要有这个心理预期。4. 九个坑的完整排查链路4.1 坑一登录态在第二次运行时凭空消失现象第一次跑登录成功发文顺利。第二次跑脚本一启动就发现是未登录状态又得重新登录。排查过程我先怀疑是 cookie 没保存检查了user-data目录发现文件确实在。然后我打印了 context 的 cookie发现登录相关的 cookie 是session类型没有expires字段——也就是说它是会话级 cookie浏览器一关就没了。而我第一次跑完脚本后进程正常退出会话 cookie 自然被清空。根因会话级 cookie 不落盘。修复改用launchPersistentContext并确保进程不是被强杀而是走正常的context.close()。同时把关键 cookie 手动持久化一份下次启动时注入。// 保存并复用登录态 const cookies await context.cookies(); fs.writeFileSync(auth.json, JSON.stringify(cookies)); // 下次启动 await context.addCookies(JSON.parse(fs.readFileSync(auth.json, utf-8)));4.2 坑二Agent 把登录和注册点反了现象Agent 在首页看到两个相邻的按钮一个登录一个注册它点了注册。排查过程我回看 Agent 的决策日志发现它拿到的页面摘要里两个按钮的文本分别是登录和注册但摘要里没有包含它们的位置信息。模型在纯文本层面判断偶尔会选错。根因页面摘要丢失了空间位置信息模型只能靠文本语义猜。修复在摘要里给每个可交互元素加上序号和相对位置比如顶部导航第 3 个按钮并且在 Agent 的提示词里明确要求优先选择语义完全匹配的元素不确定时先输出候选列表。4.3 坑三富文本编辑器填不进去现象标题能填正文死活填不进去。Agent 报告已输入但页面上一个字都没有。排查过程这是耗时最长的一个坑。我先试了fill()无效试了type()无效试了pressSequentially()还是无效。后来我用 Playwright 的 trace 功能录了一次操作发现 Agent 点击编辑区后焦点确实进去了但输入的内容被编辑器的某个事件监听器拦截了。根因这个富文本编辑器监听的是beforeinput和input事件而 Playwright 的fill()是直接设置value不触发这些事件。修复改用逐字符模拟真实键盘输入并且在输入前先click()编辑区确保焦点输入后再用page.keyboard.press(Space)触发一次事件。await editor.click(); await page.keyboard.type(content, { delay: 30 }); // 关键触发编辑器的 input 事件 await page.keyboard.press(End);注意delay参数别设太小。我一开始设 0编辑器直接丢字符设 30ms 之后稳定了。这个值因编辑器而异需要实测。4.4 坑四无障碍树读不到编辑器内容现象Agent 想确认正文是否填对了但无障碍树里编辑器区域是空的。根因富文本编辑器用的是contenteditable无障碍树对它的支持不完整。修复对编辑器区域单独走一条感知通道——直接读innerText而不是走无障碍树。也就是说感知层要分区域用不同策略不能一刀切。4.5 坑五上传封面图时文件选择框卡死现象点击上传封面后弹出了系统文件选择框脚本卡住不动。根因系统级文件对话框不属于浏览器 DOMPlaywright 无法直接操作。修复用page.on(filechooser)事件拦截直接setFiles()绕开系统对话框。page.on(filechooser, async (chooser) { await chooser.setFiles(./cover.png); }); await page.click(.upload-cover-btn);4.6 坑六标签选择器的下拉框点不开现象Agent 点击标签输入框下拉列表不出现。排查过程trace 显示点击事件确实触发了但下拉列表的 DOM 节点始终是display: none。后来发现这个下拉框是鼠标悬停触发的不是点击触发。修复改用hover()而不是click()。这个坑的教训是不要假设所有交互都是点击悬停、长按、右键都可能。4.7 坑七发布按钮被浮层遮挡现象Agent 找到了发布按钮点击却报错元素被遮挡。根因页面右下角有个帮助浮层正好盖住了发布按钮。修复点击前先检测是否有遮挡元素有的话先关闭浮层。Playwright 的click({ trial: true })可以预演点击不实际执行用来检测可点击性非常方便。try { await page.locator(.publish-btn).click({ trial: true }); } catch { await page.locator(.help-float .close).click(); } await page.locator(.publish-btn).click();4.8 坑八Agent 陷入重试死循环现象某个操作失败后Agent 不断重试同一个动作连续重试了 40 多次把时间全耗光了。根因Agent 的决策逻辑里没有失败次数上限和换策略机制。修复在 Agent 的提示词里加入硬性规则——同一动作连续失败 3 次必须换一种策略或上报人工。同时在执行层加一个计数器超过阈值直接中断。4.9 坑九发布成功后 Agent 不知道成功了现象稿子其实发出去了但 Agent 还在页面上找发布成功的提示找不到就判定失败又点了一次发布。根因成功提示是个 toast几秒后就消失了Agent 感知的时候已经错过。修复不要依赖瞬时提示改用结果页 URL 变化或列表页出现新文章作为成功判据。这个思路很重要判断成功要用稳定信号不要用瞬时信号。5. 让 Agent 稳定干活的几条硬经验5.1 感知层要分区域定制别指望一套方案打天下前面反复提到页面感知是 Agent 的眼睛。我最后的方案是分区感知导航区走无障碍树正文区走 innerText表单区走 DOM 属性提取弹窗区单独监听。每个区域用最适合它的方式描述Agent 拿到的信息才准确。这件事没有银弹只能一个区域一个区域地调。5.2 给 Agent 的提示词要写成操作规程不是任务描述我一开始给 Agent 的提示词是帮我在腾讯云社区发一篇稿结果它各种自由发挥。后来我把它改写成一份操作规程每一步的输入是什么、输出是什么、失败怎么办、什么情况下必须停下来问人。改完之后Agent 的行为可预测多了。提示提示词里一定要写不确定时输出候选并暂停否则 Agent 会硬着头皮瞎点。5.3 重试要有策略梯度不能原地打转重试不是简单地再来一次。我的做法是给每个动作准备三档策略第一档原样重试第二档换选择器比如从 class 换成文本第三档换交互方式比如从 click 换成 hover 再 click。三档都失败才上报人工。这样能覆盖绝大多数偶发问题。5.4 全程留痕trace 是你的救命稻草Playwright 的 trace 功能一定要开。它会把每一步的截图、DOM 快照、网络请求全录下来。我排查富文本那个坑全靠 trace 回放。没有 trace我可能还在瞎猜。await context.tracing.start({ screenshots: true, snapshots: true }); // ... 执行操作 await context.tracing.stop({ path: trace.zip });6. 这套方案还能怎么用从发稿到任何重复网页操作跑通发稿这件事之后我意识到这套架构的价值远不止发稿。任何需要登录、需要填表、需要多步操作、且步骤相对固定的网页任务都可以套这个模式。比如批量整理后台数据、定时提交表单、跨系统的信息同步等等。关键是把任务拆成感知-决策-执行三层然后针对每一层做稳定性优化。决策层交给 AI执行层交给 Playwright感知层根据页面特点定制。三层各司其职整个系统就稳了。我个人在实际操作中的体会是别追求一步到位的全自动先做到半自动 人工兜底再逐步把人工环节替换掉。我这次实验里最后发布那一下其实还是我手动确认的——不是技术上做不到而是我觉得发布这种不可逆操作留个人工确认更稳妥。等技术再成熟一点再考虑全自动也不迟。最后分享一个小技巧如果你也想试这套方案先从最简单的单页表单开始练手别一上来就挑战富文本编辑器。把感知层、执行层、重试机制都调顺了再上复杂页面能少踩一半的坑。