嫌投简历太耗时间,于是开始研究浏览器自动化。

GitHub 上有个著名的开源自动投简历项目,体验很差——拿源码编译、配一堆依赖,跑起来各种报错。不喜欢,自己动手做了 JobPilot:找 CSS 选择器写死自动化流程,把抓到的数据丢给 AI 判断 JD 是否匹配、生成打招呼语。勉强能用。

后来 Boss 直聘升级了 antbot 反爬机制,检测到 Playwright 连接就直接关网页或者无限刷新。写死选择器的路子彻底失效——你精心调试的定位逻辑,对方一个更新就全废了。

再后来用了 Comet——Perplexity 做的浏览器,思路打开了:不用写死选择器,让 AI 直接看着网页操作。Comet 是包月的,有用量额度,但没筛几个简历,日/周用量就没了。每一步操作都是 AI 在做视觉推断:看截图,想一下该点什么,点完再看截图,再想下一步。一个完整的投简历流程跑几十步,每一步都在消耗用量。

单模型方案在面对网页时必须选择旗舰模型,成本高,而且慢。

于是,我想到了把“思考”和“执行”拆开,用两个 AI 来分别负责。

主管-执行器双层架构 Link to heading

主管层(Workspace / Supervisor)用的是旗舰级推理模型,比如 GLM 5.1、Gemini 3 Pro。它不负责具体操作,只负责理解用户意图,与用户对话,拆解任务,派发任务给执行器。调用频率低,但每次调用都做重要决策。

执行器层(ExecutionTask / Executor)用的是足够便宜的模型。对它的要求不高——能看明白 snapshot,能在网页截图里找到应该点哪个按钮就行。它不关心全局目标,只拿着主管给的指令,看着网页,一步步执行点击、输入、滚动这些动作。调用频率高,但每次调用的成本很低。

中间还有一层 UI 交互层,接收用户的模糊意图,实时展示 AI 的执行步骤和截图。做 Agent 不能做成黑盒——用户得能看到它在看什么、在想什么,否则出了问题无从排查。

主管-执行器双层架构

这个分层源于对成本的现实妥协。在单模型方案下,每步交互都用旗舰模型进行视觉推理,成本完全不可控。

拆分后,高频、重复的执行步骤交由廉价模型,而低频的主管层仅做关键决策。两三次的旗舰模型调用配合几十次的廉价模型调用,将总成本拉回了合理区间。

廉价模型的现实 Link to heading

执行层必须用便宜模型,因为网页交互要不断抓 snapshot、做判断——如果每一步都用旗舰模型,每一步都在烧钱。但便宜模型到底能不能用?

实测场景很简单:在 Boss 直聘页面上选择“公主岭市”。就这么一个操作。

在 NVIDIA NIM 平台上测了一圈开源模型,加上 GLM 的免费模型——没有一个能正确找到或者点到位。(注意,这是 NIM 平台上的模型,官方版本我没测过,不排除官方部署有差异。)唯一跑通的是 Gemini 2.5/3 Flash。

区区选择一个“公主岭市”,几乎所有开源模型都搞不定。网页交互的层级嵌套和多级联动远比想象中复杂——你要先点开城市选择器,在弹出的面板里找到地级市,点了之后再在下一级面板里找县级市,每一级都可能触发新的 DOM 变化。模型一进这种真实场景就迷失了。

这就形成了一个矛盾:必须用便宜模型(否则每一步都在烧钱),但便宜模型连基础操作都做不好。如果这个矛盾不解决,双层架构的执行层就是个空壳——拆开了规划和执行,但执行层做不了事。

Skill:SOP + Drill 的两段式设计 Link to heading

Skill 是这个矛盾的解决方案。

在 AI Agent 行业,Skill 是解决"模型能力不够但成本必须可控"这个问题的标准思路:让模型根据描述自己判断该不该用某个能力模块,匹配到了就调用,没匹配到就靠模型自己推断。调用方式是标准的——主管 AI 看到所有 Skill 的描述,自己判断当前任务该不该用、用哪个,然后把 Skill ID 传给执行器,执行器用 ID 取出内容作为系统提示词。于是我也给 BrowserPilot 加上了挂载 Skill 的能力。BrowserPilot 的 Skill 内部装载的是一个完整的工作流——分两段。

前半段是 SOP,给执行模型一份明确的操作路线图。其实 SOP 不是必须的,它完全取决于模型的能力:模型能力越强,SOP 可以越简单,比如只需要一句话——“搜索结果页面选完筛选器以后调下面 JSON”;模型能力越差,就越需要 SOP 详细、完整,直接告诉它:

  1. 导航到 Boss 直聘(给出 URL)
  2. 在搜索框输入目标名称并点击搜索
  3. 跳转到搜索结果页面后,在城市中选择目标地级市
  4. 目标地级市选择完毕后,在区域里选择目标县级市/行政区
  5. 通过筛选器筛选目标薪资范围

模型跟着路线图走就行,不需要自己找路。这就是 SOP 的作用——降低对模型推断能力的要求。廉价模型搞不定选择“公主岭市”,不是因为它的"智力"不够,而是因为它在没有任何指导的情况下面对一个复杂的嵌套 DOM 无从下手。有了路线图,它照做就行了。

后半段是 Drill——SOP 执行完之后,调用一段声明式脚本做批量操作。滚动加载更多岗位、提取岗位列表、逐个打开岗位详情页、提取 JD 内容、让主管 AI 判定是否匹配——全是确定性流程,脚本直接跑,执行模型不参与。

Skill 的两段式设计不是一刀切——不是"要么全靠 AI推断,要么全靠脚本硬编码",而是在同一个流程里让两种机制衔接:SOP 部分有 AI 参与,但降低了推断门槛;Drill 部分 AI 完全退出,确定性执行。甚至在 Drill 内部,还能通过 ask_ai 动作回调主管模型做判断——比如"这个 JD 是否匹配我的简历",Drill 把内容提取出来,丢给主管模型判定,判定结果直接决定是否继续处理这个岗位。AI 和确定性流程不是割裂的,而是无缝衔接的。

Drill 引擎 Link to heading

Drill 是一个嵌入在 AI Agent 里的声明式工作流引擎。结构分两阶段:

steps 是页面级操作——滚动加载更多内容、提取列表数据、打开链接。这些操作在整个页面上执行一遍。

item_steps 是逐条迭代——对 steps 提取出来的每一项,逐个开新 Tab、提取详情内容、AI 判定、条件终止。每一项是一个完整的小流程。

几个关键动作:

  • ask_ai:Drill 运行中回调主管模型做业务判断,不用人工介入。比如"这个 JD 和我的简历匹配吗",模型返回 {match: true/false},Drill 直接拿结果决定下一步。
  • abort_item:条件表达式判断,匹配 {res.match} == false 的岗位直接关 Tab 跳过,不浪费后续步骤。
  • extract_list:按 CSS 选择器提取页面列表,支持字段映射(从每条岗位卡片里提取标题、链接、薪资等)。
  • repeat_interval_ms:设了这个参数,Drill 就变成后台监控模式,按间隔定期轮询——比如 Boss 直聘的消息监控,每隔一段时间检查有没有新的沟通消息。

这不是"写个脚本跑一下"的层次。它是一个有分支、有回调、能确定性执行也能动态推断的小型工作流引擎。嵌入在 Agent 里,让 AI 推断和确定性流程在同一条流水线上协作,而不是割裂成两个独立的系统。

JCEF vs Camoufox:一个产品思维的取舍 Link to heading

BrowserPilot 的 UI 框架选了 Compose Multiplatform——它是目前各方面最平衡的跨平台方案:JVM 生态成熟稳定,Skiko 自绘引擎保证全平台 UI 一致,Compose 和 SwiftUI 几乎一样。我不喜欢追热门,我喜欢学习成本和实用平衡。

浏览器引擎的选择则不是纯粹的"哪个好用选哪个"。

JCEF(Java Chromium Embedded Framework)我完全实现并跑通了。用户体验很好——浏览器内嵌在应用里,不用额外集成,而且天然跨平台,只要 Compose Multiplatform 支持的平台 JCEF 就能跑。但它有个问题:CDP(Chrome DevTools Protocol)连接会崩,底层没开放的 API 只能靠 JS 注入操作。

Camoufox 是基于 Firefox 的魔改浏览器,反检测能力强,连 OpenAI 都在用这个方案。但要单独集成一个浏览器——Linux 和 Windows 的包体积 800~900MB,macOS 的也有 600MB。用户体验不如内嵌,但换来的是开箱即用:设置好 API 直接就用了,啥都不用管。

JCEF 崩了一次我就没再深入排查了。不是因为懒——而是做了一个判断:即使我把 CDP 崩的问题修了,这条路也不值得走。JCEF/Chromium 路线面对反爬的未来不可预期。反爬技术在持续进化,Chromium 的自动化指纹特征是公开的,今天能跑明天可能就废了。而 Camoufox/Firefox 路线有生态背书——OpenAI 在用,Camoufox 社区在持续维护反检测补丁,它有可预期的未来。

放弃了更好的即时体验(内嵌浏览器、零额外依赖),选择了更确定的长期路线(独立的魔改浏览器、有持续维护预期)。纯写代码的人会选 JCEF——更好用、更优雅、更"正确"。但做产品的人会选 Camoufox——今天的体验差一点可以忍受,明天整个方案被反爬更新废掉是不可忍受的。

一些工程细节 Link to heading

几个在真实场景里踩出来的工程决策,不展开讲,但值得提一下:

SnapshotCleaner + Viewport 裁剪。Playwright 给的 Aria 快照噪声很大——备案信息、加载提示、空 wrapper 节点占了一大半。Boss 直聘还用 PUA 私用区字符混淆薪资数字。SnapshotCleaner 清掉噪声、折叠 wrapper、裁剪到可视区域——给模型的是精炼的信息,不是浏览器给的原始噪声。另外设置了 500ms 的 boundingBox 超时:Playwright 默认对 stale ref 等 30 秒才放弃,几个失效节点就能让整个流程卡几分钟。500ms 超时后视为不在可视区域,安全裁掉。

Tab 生命周期管理。每个 Tab 有 Owner,Tab 关闭自动杀死关联的执行任务,弹出页继承父页的 Owner。Per-tab 分片锁,不同 Tab 的操作可以真正并行,不会因为一个 Tab 在滚动就阻塞另一个 Tab 的点击。

人类模拟鼠标。不是 page.mouse().click(x, y) 一步到位。从随机起点开始,20 步贝塞尔曲线移动到目标坐标,每步加随机抖动,模拟真实的手部轨迹。反爬检测不只看结果,还看过程——一条直线移动到按钮上的鼠标轨迹,跟一条有弧度有抖动的轨迹,在检测模型眼里是两个物种。

Telegram Bot 遥控。用户不在电脑前也能通过手机触发预设任务,SOP 任务通过 inline button 直接选择。投简历的时候人可能不在电脑前,但想让 Agent 后台跑。这不是技术炫技,是解决真实场景。


BrowserPilot 不追求花哨,是在 AI 的灵活性与工程的成本、稳定性之间,找一个能实际干活的平衡点。这是我对探索 AI Agent 的一次重要的工程实践——对我来说,它给我带来的认知提升远超过它本身的价值。