
1. 从7.1秒订机票说起浏览器Agent到底在解决什么问题第一次看到7.1秒完成机票预订这个数字我的反应是怀疑。做过RPA和自动化测试的人都知道一个完整的机票预订流程——打开航司或OTA页面、输入出发到达城市、选日期、点搜索、在结果列表里挑航班、选舱位、填乘机人信息、处理各种弹窗和二次确认、最后走到支付页——光是页面加载和网络往返就不止7秒。所以这个7.1秒一定不是指从零到支付完成的端到端时间而是指Agent在已经加载好的页面上从决定下一步做什么到执行完这个动作的决策执行闭环时间。这个区分非常关键也是理解浏览器Agent动态索引动作空间这个概念的入口。传统浏览器自动化比如Selenium脚本、Playwright录制回放的本质是预先写死的动作序列第1步点这里第2步填那里。页面结构一变脚本就崩。而浏览器Agent要解决的核心问题是让模型在运行时自己看懂页面自己决定点哪里、填什么并且这个决策过程要足够快、足够准。动态索引动作空间就是为这个决策过程服务的一套机制。简单说页面上可交互的元素可能有几百个按钮、输入框、链接、下拉框、单选框……如果每次都把整个页面的DOM或者截图丢给模型让它选token消耗巨大、延迟极高、准确率还差。动态索引的思路是在每一步根据当前任务目标和页面状态动态地筛选、编号、排序出一小批当前最可能用到的候选动作把动作空间从几百个压缩到十几个甚至几个模型只需要在这个小集合里做选择。这就是7.1秒能实现的底层原因——不是模型变快了而是喂给模型的选择变少了。这篇内容适合三类人看一是正在做浏览器自动化、想从脚本时代升级到Agent时代的技术同学二是做AI应用、想理解Agent落地时动作空间这个工程细节的产品和算法同学三是单纯好奇AI怎么自己上网办事的爱好者。我会把动态索引动作空间的原理、实现思路、实测中的坑、以及怎么复现一个类似流程尽量讲透。关键词里的Jev Ultrafast、浏览器Agent、动态索引动作空间、机票预订我会在对应章节里逐一拆开讲。2. 拆解动态索引动作空间为什么不能把整个页面丢给模型2.1 动作空间的本质把网页变成一道选择题先建立一个心智模型。一个网页对Agent来说本质上是一个巨大的、带状态的、可交互的图。每个可点击、可输入、可选择的元素都是一个动作节点。Agent的任务就是在这个图里找到一条从当前状态到目标状态比如订单提交成功的路径。问题在于现代网页的交互元素数量非常夸张。一个典型的机票搜索页光是筛选条件航空公司、起飞时段、中转次数、舱位等级就可能上百个可交互项加上导航栏、广告位、推荐位、页脚链接轻松突破300个。如果Agent每一步都要在这300个里选会发生什么Token爆炸把300个元素的描述标签、类型、位置、属性序列化轻松上万token每步都传一遍成本和延迟都受不了。注意力稀释候选太多模型的注意力被分散选错的概率大幅上升。这跟人一样给你30个按钮让你选比给你3个按钮选出错率高得多。无关干扰页脚关于我们、广告限时特惠这些元素对订机票这个任务毫无价值但会持续干扰决策。动态索引动作空间要做的就是在每一步动态地做减法。它不是一次性把页面所有元素都索引好而是随着任务推进根据当前子目标和页面变化实时生成一个精简的候选集。2.2 动态两个字的重量静态索引为什么不够用有人会想那我提前把页面元素都索引好建个固定映射表不就行了这就是静态索引也是早期方案常踩的坑。静态索引在机票预订这种场景下会迅速失效原因有三第一页面是分步的。搜索页、结果页、填单页、支付页是完全不同的DOM结构。你在搜索页索引的出发城市输入框到了结果页根本不存在。静态索引要么为每个页面单独建表维护成本爆炸要么就得覆盖所有页面又回到候选过多的问题。第二同一页面的状态会变。比如结果页里你选了直飞筛选后航班列表整个重渲染之前索引的元素ID可能全变了。再比如日期选择器点开前和点开后是两个世界。第三任务目标在推进。第一步的目标是填出发地第二步是填目的地第三步是选日期。每一步真正需要的动作集合是不同的。静态索引无法感知这种目标驱动的变化。所以动态的核心含义是索引的粒度、范围、排序都随任务阶段和页面状态实时调整。这也是为什么它能把动作空间压到极小——因为它在正确的时刻只呈现正确的候选。2.3 动态索引的三层过滤逻辑结合我自己的实践和对这类方案的理解动态索引动作空间通常会经过三层过滤我把它总结成一张表过滤层作用典型手段压缩效果第一层可见性过滤剔除不可见、不可交互元素检查display/visibility/尺寸/disabled属性300 → 80左右第二层语义相关性过滤剔除与当前子目标无关的元素用轻量模型或规则匹配元素文本与子目标80 → 20左右第三层任务阶段过滤只保留当前阶段该出现的动作类型阶段状态机 元素类型白名单20 → 5~10三层下来模型每步面对的候选通常只有个位数到十几个。这就是7.1秒里决策部分能压到毫秒级的根本原因。注意这三层不是串行死板的实际工程里第二层和第三层经常融合在一起做用一个打分函数统一排序。提示第一层过滤看似简单但坑最多。很多元素在DOM里存在但被CSS隐藏、被其他元素遮挡、或者在视口外。如果不过滤模型会去点一个用户根本看不到的按钮然后困惑为什么没反应。3. Jev Ultrafast的快从哪来决策延迟的四个来源3.1 把快拆开看7.1秒到底花在哪要理解Jev Ultrafast这类方案为什么能快得先把一次Agent动作的耗时拆开。一次完整的感知-决策-执行循环时间花在四个地方页面感知获取当前页面状态DOM快照、可访问性树、或截图。动作空间构建从页面状态里提取、过滤、排序候选动作。模型决策把候选动作喂给模型让它选一个并生成参数比如往哪个框填什么。动作执行与验证执行点击/输入等待页面响应确认是否成功。传统方案里第2步往往被忽略直接全量喂第3步就成了瓶颈。而Ultrafast类方案的核心优化恰恰是把功夫下在第2步——用工程手段把第3步的输入压到最小从而让第3步的延迟从秒级降到百毫秒级。3.2 感知层的取舍DOM、可访问性树还是截图感知层选什么直接决定后续所有环节的成本。三种主流方案各有取舍原始DOM信息最全但噪声最大序列化后体积巨大。适合做底层数据源不适合直接喂模型。可访问性树Accessibility Tree浏览器为辅助功能维护的一棵树天然过滤了大量装饰性元素保留了语义角色button、textbox、link。这是目前浏览器Agent最常用的感知源信噪比高。截图 视觉模型最接近人类操作但每次都要跑视觉模型延迟和成本都高且对文字密集的页面比如航班列表识别精度不稳定。我的经验是以可访问性树为主DOM属性为辅截图作为兜底验证。可访问性树能直接告诉你这是个按钮名字叫搜索这比从DOM里猜class名靠谱得多。Jev Ultrafast这类追求速度的方案大概率也是走可访问性树这条路因为它能在感知阶段就完成第一层过滤。3.3 动作空间构建的工程细节编号、排序与去重候选动作怎么呈现给模型是个被低估的细节。我试过几种方式差异很大纯文本列表[1] button 搜索 [2] textbox 出发城市 ...。简单但模型容易数错编号。带坐标的列表加上元素位置帮助模型理解空间关系但增加token。分组呈现按输入类按钮类选择类分组模型定位更快。实测下来带简短编号 元素角色 可读名称的组合最稳。编号要短1、2、3而不是element_001名称要截断超过20字的部分砍掉并且每步重新编号——不要跨步复用编号否则模型会混淆。去重也很关键。可访问性树里经常出现嵌套元素比如一个按钮里套了个span两个节点都可点击。如果不去重候选里会出现两个几乎一样的选项模型随机选一个行为不稳定。去重的规则通常是如果父元素和子元素的可点击区域高度重叠只保留语义更明确的那一个。3.4 模型侧小模型做选择大模型做规划7.1秒能实现还有一个常被忽略的点不是所有决策都需要大模型。一个成熟的浏览器Agent架构通常是分层的规划层用能力强的模型做任务分解先搜航班再选航班再填信息频率低可以慢。执行层用轻量模型或甚至规则小分类器做这一步点哪个频率高必须快。动态索引动作空间把执行层的候选压到个位数后执行层甚至可以用一个很小的模型或者一个微调过的分类头来完成选择延迟自然就下来了。这就是Ultrafast里ultra的来源——不是单点技术突破而是整条链路的协同瘦身。4. 机票预订这个场景为什么是块试金石4.1 机票预订的复杂度一个天然的Agent压力测试为什么拿机票预订来展示因为这个场景几乎集齐了浏览器Agent要面对的所有难点多步流程搜索 → 结果 → 选舱 → 填单 → 确认 → 支付至少6步每步页面结构不同。动态内容航班列表是异步加载的价格会变余票会变。表单复杂乘机人信息、证件号、联系方式字段多且有格式校验。干扰密集各种低价推荐保险搭售会员优惠弹窗专门诱导误点。状态敏感选了一个航班后价格可能已经变了需要重新确认。能在这个场景下把动作空间管好、把流程跑通基本说明这套Agent机制是能打的。反过来如果一个方案在机票预订上表现稳定迁移到酒店预订、火车票、外卖下单这些场景通常也不会差。4.2 动态索引在机票场景的具体应用拿选航班这一步举例看看动态索引怎么工作。结果页可能有50个航班卡片每个卡片里有选择按钮、价格、时间、航司信息。如果全量喂给模型50个卡片就是50个候选动作加上筛选器、排序器轻松上百。动态索引会这么做可见性过滤只保留视口内或可滚动到的卡片通常一屏5~8个。语义过滤如果用户说要最便宜的直飞那么带经停标签的卡片、价格明显偏高的卡片相关性打分降低。阶段过滤当前阶段是选航班那么筛选器、排序器这些改条件的动作降权只保留选择类动作。最后模型面对的可能就是5~8个选择按钮每个附带简短描述航司时间价格。模型选一个执行进入下一步。整个过程干净利落。4.3 实测中最容易翻车的三个点我在类似场景里踩过的坑按翻车频率排序第一异步加载没等完就索引。点了搜索后结果列表是分批次渲染的。如果索引动作跑得太早可能只抓到前3个航班模型选了其中一个但其实后面还有更合适的。解决办法是等网络空闲或等某个标志性元素出现再触发索引而不是固定sleep。第二弹窗遮挡。机票页面特别爱弹限时优惠领取优惠券。这些弹窗会盖住主内容但DOM里主内容的元素还在。如果索引时不检测遮挡模型会去点被盖住的按钮点了没反应。检测遮挡的简单办法是取元素中心点坐标用document.elementFromPoint看返回的是不是它自己。第三价格和余票的时效性。模型基于索引时的价格做了决策但执行时价格变了。这在机票场景很常见。稳妥的做法是在关键动作如提交订单前做一次二次确认把最新价格读出来再决定是否继续。注意这三个坑的共同点是页面状态和索引快照不一致。动态索引的动态不仅是过滤动态还包括索引的有效期管理——索引出来的候选要标注它是什么时候抓的超过一定时间或页面发生重大变化就作废重抓。5. 复现思路自己搭一个精简版动态索引Agent5.1 技术选型别一上来就上重型框架如果你想自己动手复现一个类似机制我的建议是先用最轻的工具把核心链路跑通再考虑工程化。具体选型浏览器控制PlaywrightPython或Node都行。它比Selenium更现代对可访问性快照的支持更好page.accessibility.snapshot()能直接拿到可访问性树。模型执行层用一个响应快的小模型规划层可以用能力更强的模型。如果只是验证机制执行层甚至可以用规则先顶着。索引逻辑自己写不要依赖现成框架。因为动态索引的核心就是针对你的场景做过滤和排序这部分必须自己掌控。5.2 核心代码骨架下面是一个精简版的骨架展示感知 → 索引 → 决策 → 执行的循环。注意这是示意实际要补大量异常处理。from playwright.sync_api import sync_playwright def get_action_space(page, sub_goal): # 1. 拿可访问性树 snapshot page.accessibility.snapshot() candidates [] def walk(node, depth0): if not node: return role node.get(role, ) name (node.get(name) or ).strip() # 2. 可见性 角色过滤 if role in (button, textbox, link, combobox) and name: candidates.append({role: role, name: name[:20]}) for child in node.get(children, []): walk(child, depth 1) walk(snapshot) # 3. 语义相关性打分简化版关键词匹配 def score(item): s 0 if any(k in item[name] for k in sub_goal.split()): s 10 if item[role] button: s 1 return s candidates.sort(keyscore, reverseTrue) # 4. 截断到前N个重新编号 top candidates[:8] for i, item in enumerate(top, 1): item[id] i return top def decide(action_space, sub_goal): # 这里替换成你的模型调用 # 输入action_space sub_goal输出选中的id和参数 prompt f目标{sub_goal}\n候选动作{action_space}\n选择最合适的id # chosen_id call_model(prompt) return 1 # 示意 def run_task(page, task_steps): for sub_goal in task_steps: space get_action_space(page, sub_goal) chosen decide(space, sub_goal) # 执行动作示意点击 target space[chosen - 1] page.get_by_role(target[role], nametarget[name]).first.click() page.wait_for_load_state(networkidle)这段代码的价值不在能直接跑而在于它把动态索引的四个关键动作都体现出来了拿可访问性树、按角色和可见性过滤、按语义打分排序、截断重编号。你可以在这个骨架上把打分函数换成更聪明的实现把决策换成真实模型调用。5.3 从能跑到好用三个必须补的工程能力骨架跑通后离7.1秒还有距离。要补的能力第一索引缓存与失效。不是每步都要重新抓整棵树。如果页面没发生大变化比如只是输入框里多了几个字可以复用上一步的索引只做增量更新。这能省掉大量感知时间。第二动作结果验证。点了一个按钮后怎么知道成功了不能假设一定成功。要检查页面是否出现了预期的变化比如URL变了、某个元素出现了、某个元素消失了。验证失败要能回退重试。第三失败恢复。Agent跑飞是常态。要有一套机制连续N步没有进展就重新规划页面出现意外弹窗就优先处理弹窗等等。这部分最考验工程功底也是Demo和产品之间的鸿沟。6. 动态索引动作空间的边界与常见误解6.1 它不是万能的哪些场景不适合动态索引动作空间在元素多、流程长、结构相对规范的场景电商、OTA、表单填写效果最好。但有几类场景它力不从心强视觉依赖的场景比如验证码、图形验证、需要看图的场景。这些靠可访问性树解决不了得靠视觉。高度动态的富交互比如在线文档协作、复杂图表操作元素状态变化太快索引刚建好就失效。需要精确坐标的场景比如画图、拖拽动作空间是连续的不是离散的选项。认清边界很重要别指望一套机制打天下。6.2 常见误解动态索引不等于每次都重新抓全页这是我最想纠正的一个误解。很多人一听动态就以为是每一步都把整个页面重新解析一遍。恰恰相反高效的动态索引是增量的。它维护一个页面状态的表示只在检测到变化时才更新相关部分。全量重抓是兜底策略不是常规操作。理解了这一点才能理解为什么它能快。6.3 和录制回放的本质区别最后说个对比。录制回放传统RPA是空间换时间前期录制成本低但页面一变就废维护成本随时间指数上升。动态索引Agent是时间换空间前期要投入做索引逻辑、做验证、做恢复但一旦跑通对页面变化的鲁棒性高得多。选哪条路取决于你的场景变化频率。变化慢、流程固定的录制回放够用变化快、页面复杂的动态索引才是正解。我在实际项目里的体会是不要追求一步到位的全自动Agent。先把动态索引这套机制在单个高频场景比如就订机票打磨到稳定把失败恢复做扎实再考虑横向扩展。7.1秒这个数字背后是大量工程细节的堆叠而不是某个单点魔法。把机制理解透比记住数字有用得多。