
拿到“jev-ultrafast7 秒订机票”这个标签的时候我第一反应是“又一个吹牛项目”。但真把这个目标拆开算了一遍账之后发现这说法不仅不夸张反而是一个很硬核的工程命题。浏览器 Agent也叫 Web Agent、Web智能体大家见得多了大多数演示视频里它慢慢吞吞看一眼页面想一下点一下再等页面加载再截图再想……订一张票花半分钟以上太正常了。能压到 7 秒意味着它把“感知—规划—执行”这三个环节全部重新设计过而不是靠堆显卡。这篇文章我就按自己做浏览器自动化项目的经验把这种极速 Agent 背后的设计逻辑拆开讲清楚适合正在做 WebAgent、自动化测试平台、或者想用大模型驱动浏览器干实事的工程朋友参考。1. 7 秒不是嘴上的 7 秒先把订机票的任务拆成可度量的动作链1.1 为什么订机票是浏览器 Agent 的试金石很多人觉得订机票就是“搜一下点一下”很简单的流程。但实际上这是一个非常典型的复合任务它同时考验感知、决策和执行三段能力要理解自然语言指令里的隐含信息出发地、目的地、日期、舱位、人数甚至“便宜一点”这种模糊偏好要操作一个动态渲染的复杂页面搜索表单、日期控件、航班列表、价格筛选、乘机人信息、最终提交页面元素还会抖低价弹窗、优惠券浮层、倒计时提示、按钮 loading 状态都会让静态规则失效要处理多分支异常没航班、价格变了、登录态过期、提交失败需要回退。这比“帮我查下明天的天气”或者“打开某个网页”这种单动作任务难得多。一个浏览器 Agent 能把订机票在 7 秒内跑完说明它的感知速度、规划效率、执行稳定性都到了一个可用的水平。所以我建议所有做 Web Agent 的人先别追求花哨的演示场景拿订机票当基准测试跑通了再谈别的。1.2 先算账7 秒的预算怎么分配拿到“7 秒”这个目标值我做的第一件事不是找源码而是倒推时间预算。下表是我按自己项目里常见的耗时数据做的一个估算阶段预算耗时优化手段意图理解与参数抽取100-200ms用轻量模型做槽位抽取不走完整对话浏览器会话预热/恢复500-800ms常驻浏览器进程 持久登录态 预加载目标站点首屏语义快照200-300ms抽取 DOM/可访问性树不做整页截图动作序列规划1.0-1.5s一次性生成完整 JSON 动作序列不逐轮调用大模型动作执行与页面交互1.5-3.0s批量执行、局部校验、等待关键节点出现结果确认与订单校验200-500ms校验订单金额/乘机人/日期是否匹配加起来大约 3.5-5 秒。这里还没算真实网络波动和平台本身的反应时间所以给最终网络请求留出 2-3 秒的余量7 秒正好是一个“认真做过工程优化”才能达到的数字。这里我想强调一个原则极速浏览器 Agent 的目标不是“让每一步更快”而是“让大部分工作不走关键路径”。很多时间消耗是可以从用户等待时间里挪走的这在后面的流水线设计里会详细讲。2. 传统浏览器 Agent 慢在哪像素级感知和逐轮推理是最大瓶颈2.1 截屏喂给大模型漂亮的 Demo跑不快的架构现在很多浏览器 Agent 的演示都是这么做的调用无头浏览器截一张图把图片交给视觉语言模型模型标出“点击坐标”然后执行。这个方案 Demo 很好看因为对用户来说Agent 就像真人一样“看”屏幕。但实际跑起来有几个致命问题第一图片的 token 成本极高。一张 1280x720 的截图在多数多模态模型里折算成 token 要几千甚至上万一次“看懂”的延迟就在 1 秒以上。而订机票这种流程动辄需要 20-30 次页面状态更新每次都要重新“看”累积时间根本压不住。第二像素里有大量干扰信息。背景图、渐变、阴影、广告图对模型理解页面结构毫无帮助反而会干扰它对可交互元素的定位。哪怕模型输出一个坐标这个坐标也会因为页面布局微调而失效——今天按钮在 (350, 420)明天一个促销横幅插进来按钮就跑到 (350, 620) 了。我在自己项目里试过纯视觉方案7 秒连一张截图都还没“看完”。所以看到“7 秒订机票”这个目标我立刻判断它一定不是走像素级感知的路线。2.2 逐轮“看-想-做”循环把延迟放大了 N 倍慢的第二个来源是架构。常见 Agent 框架是“ReAct 循环”模型观察当前页面模型决定下一步动作执行动作等待页面变化回到第 1 步。每一步都要走一次“页面快照 LLM 推理 动作执行”。如果一次订票需要 25 个动作每个动作走一轮完整循环LLM 推理 1-2 秒页面操作 加载 1-3 秒快照生成 0.3-1 秒。总耗时直接奔着 30-60 秒去了这还没算 LLM 偶尔想错了要重来。这种架构的瓶颈不在某一单个环节而是循环次数太多了。你把单次推理从 2 秒优化到 0.5 秒总共 25 步也只能省 30 多秒里的 20 秒仍然跑不进 7 秒。2.3 从“手工模式”切到“流水线模式”打个比方传统逐轮方案就像裁缝做一件衣服量体、裁剪、缝纫、锁边每道工序做完都要停下来想一想再开始下一步整体慢而且容易错。而 7 秒目标要求的是流水线先把任务图纸画好一次性规划出全部动作序列然后多个工位并行开工每个工位只做自己那一道工序做完就交给下一个。所以真正快的浏览器 Agent架构上一定是“预规划 确定性执行”的组合。这也是我判断 jev-ultrafast 这类项目核心设计时最看重的一点。3. 把“看页面”改成“读结构”后感知速度提升近一个数量级3.1 语义级快照不截图直接读 DOM浏览器本身早就给页面建立了一份“结构地图”——DOM 树和可访问性树。这里面记录了每个元素的角色按钮、输入框、链接、名称、状态以及层级关系。浏览器 Agent 要做的不是用视觉模型去“认”这个结构而是直接把这份地图抽取出来压缩成紧凑的 JSON。一份订票页面抽出来的语义快照长这样{ url: https://example.com/flights?fromBJStoSHAdate2026-05-20, elements: [ { id: n1, role: button, name: 搜索航班, state: enabled, hint: 出发地 北京 到达地 上海 }, { id: n2, role: listitem, name: CA1831 07:30-09:45 经济舱 ¥680, state: selectable }, { id: n3, role: checkbox, name: 仅看直飞, state: unchecked } ] }粗粒度的 DOM 可能有上万行但语义快照只保留“可交互元素 必要的文本信息”往往能压到 3-6KB 的 JSON。token 成本比截图低两个数量级模型“读”一遍也就几百毫秒。3.2 代码层怎么接上这条链路我这里给一段基于 Playwright 的极简抽取思路真正做的时候你可以封装成服务import { chromium } from playwright; async function getSemanticSnapshot(page) { // 优先走 ariaSnapshot这是 Playwright 1.49 提供的原生语义抽取 const ariaSnapshot await page.locator(body).ariaSnapshot(); // 可选再补一份纯交互元素清单用于定位动作目标 const interactives await page.evaluate(() { const selector button, a, input, select, textarea, [rolebutton], [rolelink]; return [...document.querySelectorAll(selector)] .filter(el { const style getComputedStyle(el); const rect el.getBoundingClientRect(); return style.display ! none style.visibility ! hidden rect.width 0 rect.height 0; }) .map((el, i) ({ id: n${i}, tag: el.tagName.toLowerCase(), role: el.getAttribute(role) || null, text: (el.textContent || el.getAttribute(aria-label) || ).trim().slice(0, 80), type: el.getAttribute(type) || null })); }); return { ariaSnapshot, interactives }; }这里有一个非常关键的操作意图先过滤掉隐藏元素和零尺寸元素。很多页面在 DOM 里存了一堆下拉选项、隐藏表单字段、模板节点如果不过滤快照就会有一半是废信息既拖慢速度又干扰模型。3.3 视觉兜底只有在语义抽取出问题时才打开用语义快照并不意味着完全抛弃视觉。比如某些平台的航班座位图是用 canvas 画的日期选择器是自绘组件这时候 DOM 里可能只有一个canvas标签语义信息不够。我的做法是给系统留两条通道主通道是语义快照承担 95% 以上的页面理解副通道是视觉截图只在出现“无可用交互元素”或“模型规划置信度过低”时触发。这样能兼顾速度和鲁棒性。以前“每步都必须看截图”的方案本质上是把所有压力都压到视觉模型上而正确的做法是让结构信息做主力视觉只做救火队员。4. 流水线化是 7 秒的关键预取、并行、单一规划与动作缓存4.1 会话预热和预取把等待从用户路径上挪走用户发出“帮我订明天北京到上海的机票”这句话时Agent 不应该先傻乎乎地启动浏览器再打开订票网站再输入信息。它应该早就把“弹药”准备好了。我在系统里做了两层预热常驻浏览器进程浏览器不是每次任务现启动的而是一个常驻无头浏览器实例多个任务共享。无头浏览器冷启动普遍要 2-5 秒这么好的性能直接消耗在关键路径上太浪费。常驻之后打开一个 tab、恢复 session 只要几百毫秒。站点预加载根据用户指令里出现的目的地/平台关键词在意图解析完成后立刻并行打开订票首页、预取搜索接口需要的静态资源。等模型规划好动作时页面已经 Ready 了。其实很多“慢”根本不是 Agent 本身慢而是它把准备工作都放在了用户等待时间里面。把准备动作提前用户体验自然就快了。4.2 一次性规划而不是每步规划这是整个设计里最反直觉、也最核心的一点不要让模型走一步想一步而是让模型一口气输出全部动作序列。一次性规划的动作序列长这样[ { action: input, target: n4, value: 北京 }, { action: input, target: n5, value: 上海 }, { action: select_date, target: n6, value: 2026-05-20 }, { action: click, target: n7, desc: 点击搜索航班按钮 }, { action: wait_for, target: [rolelistitem], timeout_ms: 3000 }, { action: click, target: n8, desc: 选择 CA1831 航班 }, { action: click, target: n9, desc: 选择经济舱 }, { action: submit, target: n10, desc: 提交订单 } ]这个方案有两个直接好处LLM 调用次数从 20 次降到 1 次推理延迟直接除以 20动作之间可以预判依赖哪些动作需要等待哪些动作可以提前执行执行器可以做调度优化。它的风险也很明显计划越长后面某个动作失败导致全盘重来的概率越大。所以它必须搭配下面要讲的校验和回退机制不能“一把梭到底”。4.3 并行请求让多条路径同时跑起来订机票场景里天然的并行点非常多搜索航班的同时可以并行请求目标日期前后一天的航班做比价多个候选航班想要展开查看退改规则可以同时展开如果系统支持多个订票平台同一组搜索条件可以同时发给两个平台。在浏览器 Agent 里做并行核心是用 Browser Context 隔离。多标签页并行执行时注意不要共用同一个 DOM 快照否则状态会互相覆盖。我自己的做法是一个任务占用一个 contextcontext 里可以开多个标签页并行跑不同分支最终报告汇总到主流程。4.4 每次动作后只抽局部快照传统方案每次操作后都要重新看整个页面。优化后的方案是执行一个动作后只抽取动作目标附近的局部 DOM用来验证动作是否生效。比如点击“搜索航班”后不需要重新分析整个页面只需要确认航班列表区域有没有出现listitem节点。这种局部校验把“感知—执行”的循环从一个完整页面缩小到一个功能区耗时从 300ms 降到 30ms而且 token 消耗也大幅下降。5. 快而不翻车动作约束、结果校验和故障回退怎么设计5.1 固定动作空间不让模型自由发挥如果要保证“快”的同时“稳”第一步是限制模型能做的动作类型。我会把动作空间收敛成几种动作参数说明clicktarget点击指定语义节点inputtarget, value填入文本selecttarget, option选择下拉选项scrolltarget / direction滚动到某个节点waitselector, timeout等待某类节点出现submittarget提交表单notifymessage通知用户/请求人工介入模型不允许输出任意坐标只能引用快照里的节点 ID。这样一来动作天然是“语义化”的页面设计怎么变只要语义节点还在就能操作。这也让每次动作都非常轻适合流水线执行。5.2 校验逻辑前置不等页面加载完等预期状态出现很多 Agent 慢是因为执行完动作后习惯性“等页面加载完成”。但“加载完成”这个事件在很多现代应用里是永远不触发的——页面会一直有懒加载、轮播、异步请求。正确做法是等“预期状态出现”点了日期就等日期控件的选中样式状态出现点了搜索就等结果节点出现。校验分前置和后置两种前置校验动作执行前检查目标节点是否enabled且visible如果不满足要么跳过要么报错后置校验动作执行后检查关键结果是否出现比如航班列表出现、订单金额和候选航班一致。后置校验还有一个作用防止“假成功”。很多页面点了提交按钮你以为下单成功了其实弹了个“请先同意协议”的提示。所以校验不能只看动作有没有执行要看业务结果有没有达成。5.3 回退策略局部重试而不是全局重来动作序列运行中某个动作失败是必然的。关键是失败后怎么处理才能不把之前省下的时间又浪费掉。我的回退分级如下同一个节点换个方式重试比如点击没反应先尝试 dispatch 鼠标事件再不行用 keyboard 触发再不行用dispatchEvent触发。这个重试只需要 1-2 秒重新抽取快照定位新节点如果原节点已经不在了重新抽一份局部快照匹配语义名称然后重试回退到最近成功检查点如果连续 3 次动作失败回到最后一个已验证的业务里程碑修正规划后继续人工接管遇到验证码、登录滑块、风控直接停下来通知用户绝不硬闯。这套回退逻辑最重要的就是别把重试当成补偿手段。重试策略设计得好能让 Agent 在 95% 以上的情况下都能自己恢复。设计得差就会陷入“失败—重试—失败”的死循环把性能全搭进去。6. 想自己复现这条路可以参考的工具链和最小实现6.1 工具选型对比如果你也想搭一个 7 秒级的浏览器 Agent工具链可以参考下面这套组合环节推荐选项说明浏览器控制Playwright / PuppeteerPlaywright 的ariaSnapshot原生支持语义抽取优先感知层DOM 交互元素抽取 ariaSnapshot主打快结构优先兜底视觉多模态小模型只在语义节点缺失时启用规划层轻量级大模型 JSON 模式输出25 步以内的任务一次规划即可执行层自研确定性执行器不依赖 LLM只做固定动作的执行和校验会话层常驻浏览器进程 持久化 cookie避免每次冷启动规划层这里多说一句不是模型越强越好而是“够用就好”。订机票的动作序列往往不超过 30 步一个 70B 以下、支持 JSON 输出的开源模型配合好的提示词就能完成。用超大模型反而推理延迟压不下来。6.2 最小实现骨架下面是一个最小可运行的 Python 骨架核心逻辑就是“感知—规划—执行—校验”四大模块# web_agent_mini.py - 极简浏览器 Agent 骨架示意 import asyncio import json from playwright.async_api import async_playwright class NanoWebAgent: def __init__(self, llm): self.llm llm # 传入一个支持 JSON 输出的 LLM client self.browser None self.page None async def init(self, url): self.pw await async_playwright().start() self.browser await self.pw.chromium.launch(headlessTrue) # 关键复用 profile 目录保持登录态 self.context await self.browser.new_context() self.page await self.context.new_page() # 预热目标站点 await self.page.goto(url, wait_untildomcontentloaded) async def snapshot(self): 语义快照只抽交互元素 return await self.page.evaluate( () { const nodes document.querySelectorAll( button, a, input, select, textarea, [rolebutton], [rolelink]); return [...nodes].filter(el { const r el.getBoundingClientRect(); const s getComputedStyle(el); return r.width 0 r.height 0 s.visibility ! hidden s.display ! none; }).map((el, i) ({ id: n i, tag: el.tagName.toLowerCase(), text: (el.textContent || el.getAttribute(aria-label) || ).trim().slice(0, 60), })); } ) async def execute(self, action: dict): 确定性执行单个动作 target action.get(target) if action[action] click: # 通过 locator 定位语义节点 await self.page.locator(...).click() elif action[action] input: await self.page.locator(...).fill(action.get(value, )) # ... 其他动作分支 async def run(self, task: str): snapshot await self.snapshot() plan self.llm.generate_plan(task, snapshot) # 一次性规划 for step in plan[steps]: ok, need_refresh await self.execute_with_verify(step) if not ok and need_refresh: snapshot await self.snapshot() # 局部重规划 plan self.llm.replan_failed_step(step, snapshot)这段代码省略了校验和重试细节但已经把核心架构表达清楚了感知产物是结构化 JSON规划产物是结构化动作序列执行器是确定性代码LLM 只出现在规划和重规划环节。6.3 调参经验几个我在实际调优过程中踩出来的参数建议temperature 设到 0或者接近 0动作序列容不得“创意”上下文窗口只保留最近 3-5 个快照浏览器 Agent 的对话历史里塞太多旧页面快照token 消耗会失控而且模型容易被过期信息误导等待策略用轮询而不是固定 sleep每 200ms 检查一次预期节点是否出现比闭眼等 3 秒快得多失败也能更早发现。7. 实际踩坑记录订机票场景里那些会吃掉你几秒的隐形敌人前面讲的是理想设计下面聊聊真刀真枪跑的时候哪些东西会偷偷吃掉你的时间预算。7.1 登录态和风控最贵的隐形开销订机票平台几乎都要登录而登录往往伴随短信验证、滑块验证。这套流程本身就要 5-10 秒而且很多是纯人工环节根本没法自动化加速。我的处理办法是“会话预热”提前用真实账号登录并保存持久化登录态Agent 启动时直接恢复会话跳过登录步骤。特别注意这里说的是用自己的账号、合规使用登录态。任何破解验证码、绕过风控的手段都别碰既不稳妥又违反服务条款。7.2 iframe 和 Shadow DOM语义快照的盲区很多航司网站的日期选择器和支付区域偏偏喜欢用 iframe 或者 Shadow DOM。这会让“一句话查 DOM”的方案失灵——因为元素藏在另一个文档根节点下面。解决办法是快照抽取时显式遍历 key 为for (const frame of page.frames()) { // 对每个 iframe 分别执行抽取逻辑合并到同一个快照 }Shadow DOM 则要在querySelectorAll之前开启穿透或者单独处理 shadow root。这个坑不填Agent 会在某个不可见的日期控件上卡住一卡又是 3-5 秒。7.3 弹窗和动效快照抖动和定位错位的元凶订票页面几乎必有三个东西优惠券弹窗、低价提醒、还有加载遮罩。这些元素会在你抽完快照之后“插队”导致节点 ID 全部往后移一位。如果执行器用固定 ID 去点击就会点错地方。我现在的方案是两招关键动作前做一次“弹窗清理”找到常见弹窗关闭按钮优先点掉动作定位参照“语义名 结构路径”而不是纯 ID这样即使有元素插入模型也能重新匹配到正确目标。7.4 并发请求与限流之间的平衡前面说要多路并行但并行太猛容易触发平台风控请求直接被限流有时候反而比串行更慢。我实测下来订机票场景比较稳的并发窗口是 2-3 个标签页、每个标签页内不超过 2 个并行请求。再往上提速的收益就会快速递减而封禁风险直线上升。7.5 多标签页迷路有些平台会在搜索后新开标签页或者用新页签承载支付流程。如果 Agent 不统一管理 tab就会出现“还在旧 tab 操作但结果在新 tab”的错位。执行器要维护一个 tab 管理器每次动作前确认当前活跃 tab 是不是目标 tab必要时切回来。说在最后如果只让我讲一条做极速浏览器 Agent 的心得那就是别把时间花在提高单次模型推理速度上而是把感知和规划的浪费砍掉。7 秒只是复现出来的结果真正值钱的是那套预算分配思路——哪些步骤必须保留、哪些可以并行、哪些可以提前、哪些可以直接省略。这种思考方式套到查快递、办公自动化和商品比价这些场景里也能用。我下一步正准备把这套语义感知加流水线执行的思路搬到“多平台购物比价”这个任务上跑通之后再回来写一篇对比总结。