ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

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-features=AutomationControlled'] });

提示:headless: false在调试阶段是刚需。Agent 判断错了,你能立刻看到它点到了哪里;如果开无头模式,你只能对着日志猜,效率差十倍。

3. 227 次操作是怎么堆出来的:任务拆解与执行链路

3.1 把"发一篇稿"拆成 Agent 能执行的原子动作

"发一篇稿"听起来是一件事,但对 Agent 来说,它必须被拆成一串原子动作。我最终的拆解是这样的:

  1. 打开目标站点首页
  2. 判断当前是否已登录(检查头像元素是否存在)
  3. 未登录则点击登录入口,等待登录完成
  4. 进入创作/发文页面
  5. 填写标题
  6. 填写正文(富文本编辑器,需要特殊处理)
  7. 选择分类与标签
  8. 上传封面图
  9. 预览并检查
  10. 点击发布
  11. 确认发布结果

看起来只有 11 步,但实际执行了 227 次浏览器操作。差距在哪?在于每一步内部都有大量的"感知-判断-执行"循环。比如第 6 步"填写正文",富文本编辑器不是简单的input,Agent 需要先点击编辑区、等待工具栏加载、再逐段输入,每输入一段还要确认内容真的进去了。光是正文这一项,就消耗了 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 坑五:上传封面图时文件选择框卡死

现象:点击"上传封面"后,弹出了系统文件选择框,脚本卡住不动。

根因:系统级文件对话框不属于浏览器 DOM,Playwright 无法直接操作。

修复:用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,感知层根据页面特点定制。三层各司其职,整个系统就稳了。

我个人在实际操作中的体会是:别追求一步到位的"全自动",先做到"半自动 + 人工兜底",再逐步把人工环节替换掉。我这次实验里,最后发布那一下其实还是我手动确认的——不是技术上做不到,而是我觉得发布这种不可逆操作,留个人工确认更稳妥。等技术再成熟一点,再考虑全自动也不迟。

最后分享一个小技巧:如果你也想试这套方案,先从最简单的单页表单开始练手,别一上来就挑战富文本编辑器。把感知层、执行层、重试机制都调顺了,再上复杂页面,能少踩一半的坑。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进