ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

用Jev本地推理模型重构网页自动化判断层:从规则堆叠到语义理解的实践

用Jev本地推理模型重构网页自动化判断层:从规则堆叠到语义理解的实践 1. 从改一个 CSS 选择器改到凌晨三点说起传统网页自动化的判断层为什么总在拖后腿上个月我花了一整个通宵修一条航班查询脚本起因只是航司网站把页面上“低价日历”的按钮从div换成了button。这种问题听着很小但如果你做过网页自动化一定知道它背后的连锁反应CSS 选择器失效、点击动作落空、后续等待逻辑全部失焦最后脚本停在“等待航班列表”这一步直到超时。那天之后我做了一次统计过去三个月这套航班查询脚本的维护时间里面有将近 40% 花在改选择器、补分支、调等待时间上真正和“查询业务”有关的代码改动反而很少。这让我下定决心用 Jev 把网页自动化里的“判断层”彻底重构一遍。这里的判断层指的是脚本里负责回答“当前页面是什么状态”“下一步该做什么”的那部分逻辑不是采集也不是执行而是两者之间的决策中枢。我先把话说清楚这篇内容不是讲怎么用某个框架去爬航班而是讲怎么把一套又慢又脆的判断层换成一个能用语义理解页面、能快速输出下一步动作的本地推理模型。如果你也在做网页自动化尤其是需要处理动态加载、弹窗、改版频繁的站点这篇文章值得你看完。1.1 航班查询里到底有多少个“判断点”很多人以为航班查询自动化的难点在于登录、验证码、接口加密但真实跑到生产环境后你会发现最大的工作量反而是那些不起眼的“判断点”。以一次最简单的单程查询为例打开航司或OTA站点、输入出发地目的地、选择日期、点击查询、等待结果、翻页或筛选、提取航班信息。光这一步流程我数了数至少需要十一个判断页面是否加载完毕下拉框里的城市选项是否已经出现选完日期后日历组件有没有回填成功点击查询后是进入了加载态还是被弹窗挡住了当前是否出现了验证码或风控拦截结果列表是已经渲染还是仍在异步请求当前页面到底是真的“无结果”还是结果还没加载出来如果有“更多航班”按钮它是否可点点击翻页后列表是更新了还是原地不动价格区域显示的数字是真实价格还是占位符“--”请求超时后是重试整个流程还是恢复当前步骤这些问题传统思路几乎全用“规则”来实现写if element.exists()、匹配一段正则、判断某段文本是否包含关键词然后再叠加一堆time.sleep去等待。我把这种实现称为“规则堆成的积木塔”。单看每一块都不复杂但层数一多彼此之间的耦合就会变得非常严重。今天加一个判断明天改一个等待后天发现原来的分支走不到了最后谁都不敢动这段代码。1.2 规则型判断层的两个死穴时序敏感与语义敏感规则型判断层真正让人头疼的不是代码写得多复杂而是它对“时序”和“语义”极度敏感。先说时序敏感。大家应该都遇到过脚本在本地环境跑得好好的一到服务器上就频繁出错。原因往往是网络延迟变了、页面渲染速度变了而脚本里的等待还是固定的三秒五秒。快的时候页面还没加载完你就去点慢的时候页面早就出来了脚本还在傻等。为了解决这个问题有人把所有等待时间都调大结果单次查询从十秒拖到三十秒有人用显式等待去轮询某个元素出现但判断条件本身还是死的换个页面布局就失效。再说语义敏感。规则判断本质上是靠关键词和结构匹配去猜页面意图比如“检测到‘抱歉’就认为无结果”。但页面里出现“抱歉网络开小差”和“抱歉没有找到符合您条件的航班”业务含义完全不同出现“暂无可售航班”和“航班销售一空”也不一样。靠正则去覆盖这些表达永远追不上的因为前端文案改一次你的正则就要跟着改一次。再加上动态加载越来越普遍一个页面从骨架屏到文字出现再到数据回填、价格刷新中间有几个不同的中间状态。规则判断根本没法优雅地描述“这个页面已经稳定下来”这件事。你只能不停地试错然后不停堆sleep。1.3 “不敢快”才是查询耗时居高不下的真凶说到耗时很多人的第一反应是“网络慢”“服务器慢”“接口慢”。但在我这个场景里真正让航班查询从五六秒变成十几二十秒的恰恰是判断层太脆弱导致执行层“不敢快”。因为拿不准当前页面处于什么状态脚本只能每个步骤都预留大量冗余等待时间。点了查询之后等三秒翻页之后再等三秒看到“加载中”又等三秒。多等一两秒看起来没什么但在一次几十步的查询流程里累加起来单次查询就变成了二十秒起步。我当时的思路一直有个误区总觉得自动化脚本的核心是“脚本本身”判断逻辑只是陪衬。直到我把耗时拆开分析才发现真正的时间大头是等待和错误重试而不是发起请求和执行动作本身。如果判断层能更快、更准确地回答“现在可以点了吗”“现在可以提取了吗”整个流程的时间立刻就能大幅压缩。这也就是我想用 Jev 重构判断层的直接动机不是觉得规则代码没法维护而是希望判断层拥有对页面状态的理解能力让执行层敢快、敢在正确的时间点做正确的事。2. Jev 进场判断层重构的边界到底应该划在哪里听到“用模型替代规则”的时候很容易走两个极端。一种是把所有业务逻辑全部交给模型规则代码全删另一种是认为模型不可靠只敢拿它做关键词匹配的增强版。这两种我都试过也都吃过亏。我的结论是重构判断层不是把原来的规则全部推翻而是重新划分职责。让 Jev 负责它擅长的事情——理解页面语义、判断状态、给下一步决策让规则继续负责它擅长的事情——高频、稳定、可枚举的硬校验。2.1 Jev 解决的是“知道现在该怎么办”不是“去点哪里”先说我这里用到的 Jev 是什么角色。它不是一个能操作浏览器的RPA框架也不直接负责抓数据、渲染页面它更像一个可以本地运行的推理内核。你给它当前页面的结构化摘要、操作历史、再加上一组候选动作它返回的是当前页面属于什么状态、下一步执行什么动作、置信度有多高。所以 Jev 解决的核心问题可以用一句话概括让自动化脚本知道“现在该怎么办”。传统判断层里这个“知道”靠的是if和else的堆叠。改版一次选择器一换规则就崩。而 Jev 靠的是对页面文本、控件类型、交互历史的综合理解来做判断。同样是面对“列表空白”规则会认为“没找到元素无结果”Jev 会结合“页面刚点完查询”“顶部出现 Loading 动画”“页面底部有分页组件”这些信息得出“当前不是无结果而是还在加载”的判断。这个区别在航班查询场景里非常关键。因为航班网站上“暂无航班”“查询失败”“加载中”“请稍后重试”之间的界限并不是靠某一个关键词就能区分的它需要结合上下文。2.2 新架构采集层、判断层、执行层重新分工重构之后我把整个网页自动化按职责拆成三层采集层只负责把当前页面的“信息摘要”抓出来包括可见文本、表单值、按钮可用状态、页面标题、加载动画等整理成一个紧凑的 JSON 结构。它不负责判断这些东西意味着什么。判断层是我的核心改造对象交给 Jev 做。它接收采集层传来的 JSON 摘要和最近几步的操作历史输出一个结构化的决策比如“当前是结果列表页下一步提取数据”或者“当前是异常页需要返回上一步重试”。执行层拿到判断层的输出后才调用 Playwright 去执行具体动作比如点击某个按钮、滚动到某个位置、提取价格文本。它不负责思考只负责把动作做出来同时把执行结果反馈给判断层。这样拆完之后最大的变化是页面改版时采集层可能只需要改选择器名执行层可能只要改定位方式判断层几乎不用动。我做过一次验证某个航司页面把价格区域的class名字从price-now改成了sale-price采集层更新了选择器执行层没动判断层依然能根据“文本以¥开头、带小数、在航班卡片内”这些语义特征正常判断。这放在以前的规则堆叠里至少要改三四处判断条件。2.3 为什么我选了本地推理模型而不是在线大模型有人可能会问既然要上语义理解为什么不用 GPT、Claude 这类在线大模型我一开始也考虑过但实际跑下来发现几个很现实的问题第一是延迟。在线大模型一次推理通常要几百毫秒到两三秒如果判断层每次决策都要请求一次远程 API整个流程的耗时会被严重拖慢。而 Jev 可以本地部署单次推理能压到几十到一百毫秒级别这个量级才能嵌入到实时网页自动化流程里。第二是成本。航班查询这种场景单次流程可能要调用判断层七八次高频跑起来在线模型的 token 消耗很快。本地部署虽然前期要花点时间配置但之后每次决策基本是零边际成本。第三是数据隐私。航班查询会涉及一些账号信息、订单数据、常旅客信息我不太希望这些内容被拼进远程 prompt 发送出去。本地模型就没有这个问题页面摘要全部在机器内部流转。第四是稳定性。在线模型有时候会因超时、限流直接挂掉这对自动化流程是致命的。本地推理只要机器不宕机服务就是稳定可用的。当然Jev 也不是万能的它对复杂长文的语义理解能力不如大模型但网页判断层本来就只需要处理“页面状态”和“下一步动作”这种短文本分类任务不是做长文章总结。用对场景比参数大小更重要。提示判断层选型的核心指标不是模型智商而是延迟、稳定性、可观测性。宁可单次判断能力弱一点也要保证每次判断足够快、足够稳定。3. 航班查询落地实例状态机、Prompt 与主循环代码边界想清楚之后我就开始动手改造那套航班查询脚本。这里我不会把完整代码贴出来因为里面有不少业务配置但我会把核心设计完整交代清楚。你照着这个思路完全可以复刻到自己的项目里。3.1 先把页面归纳成六个稳定状态网页自动化最怕“不可枚举”的页面状态。所以我在重构之前花了一个下午把航班查询流程里所有可能出现的页面表现整理了一遍收敛成六个状态状态含义可能出现的页面特征INIT查询还没开始或在首页有出发地、目的地输入框QUERYING已提交查询等待结果出现加载动画、骨架屏或按钮变为禁用RESULT_LIST航班列表已渲染出现航班卡片、价格文本、航班号EMPTY_RESULT查询成功但无航班页面有“无航班”“没有找到”等语义提示BLOCKED被验证码或风控拦截出现滑块、验证图片、异常提示UNKNOWN判断层无法确认的状态既不像结果页又不像加载页定义状态本身不难难的是怎么可靠地识别它们。传统规则的做法是查关键词有“验证码”就认为是BLOCKED有“航班”字样就认为是RESULT_LIST。但这么做很容易误判比如结果页里也有“验证价格”这种按钮文本。我现在的做法是让 Jev 基于页面摘要判断状态而不是匹配单个关键词。比如下面这段摘要{ url: https://example.com/flight/list, title: 上海-北京 航班列表, has_loading_spinner: false, visible_text_count: 47, cards: [ {airline: MU, price: ¥580, time: 08:00-10:30}, {airline: CA, price: ¥620, time: 09:15-11:40} ], has_pagination: true }Jev 看到这个结构后几乎不会把它判断成QUERYING因为页面里已经有多个航班卡片了。这就是语义判断比关键词匹配可靠的地方它不只看有没有“航班”两个字还看整体结构是否符合“结果页”的样子。3.2 Prompt 设计让判断层输出 JSON不给散文用 Jev 做判断层Prompt 设计非常关键。一开始我写得很随意让它“请告诉我当前页面是什么状态”结果它经常返回“页面看起来像是一个航班列表你可以尝试提取航班信息……”这种散文。散文没法直接驱动执行层还得再解析一遍。后来我改成了严格的输出协议只允许输出 JSON并且强制包含state、action、confidence、evidence四个字段。下面是我目前实际在用的 Prompt 模板做了脱敏简化你是网页自动化流程的判断器。你的任务是根据当前页面摘要和操作历史 判断页面状态并输出下一步动作。 页面摘要 {page_summary} 最近操作历史 {recent_actions} 候选状态 - INIT查询未开始 - QUERYING查询中等待结果 - RESULT_LIST结果列表已展示 - EMPTY_RESULT查询成功但没有航班 - BLOCKED被验证码或风控拦截 - UNKNOWN无法确认 候选动作 - wait继续等待 - extract提取航班列表 - click_more点击“更多航班” - retry_query重新发起查询 - stop终止流程 你必须只输出以下 JSON不要输出任何其他内容 { state: RESULT_LIST, action: extract, confidence: 0.97, evidence: 页面存在多个航班卡片含航班号和价格且有分页组件 }注意我特意加了evidence字段。这个字段不是为了给用户看而是为了可排查。当判断出错时我能看到 Jev 是依据什么做出判断的这对定位问题帮助极大。3.3 主循环与执行器的骨架代码有了状态定义和 Prompt 协议整个主循环就变得非常简单。核心逻辑用伪代码表示就是采集页面摘要交给 Jev 判断得到动作执行动作再采集再判断直到状态变成RESULT_LIST或者达到最大轮数。# 示例代码接口按伪代码抽象 from playwright.sync_api import sync_playwright MAX_ROUNDS 8 def run_query(flight_query: dict) - list: with sync_playwright() as p: browser p.chromium.launch(headlessFalse) page browser.new_page() controller FlightQueryController(page) state INIT action init results [] for round_index in range(MAX_ROUNDS): summary controller.collect_summary() decision jev_judge(summary, recent_actionscontroller.action_history()) state decision[state] action decision[action] controller.log_decision(round_index, decision) if state RESULT_LIST: results controller.extract_flights() break elif state EMPTY_RESULT: results [] break elif state BLOCKED: controller.handle_blocked() break elif state UNKNOWN: controller.wait_for_stable(1000) continue elif action wait: controller.wait_for_stable(500) elif action click_more: controller.click_more_flights() elif action retry_query: controller.re_run_query(flight_query) else: controller.wait_for_stable(500) browser.close() return results这里的jev_judge是我对 Jev 推理调用的一个封装。实际项目里它接收一个字符串形式的 Prompt 和一个 JSON Schema然后返回解析好的字典对象。整个循环跑下来正常情况下 3 到 5 轮就能完成一次查询提交查询、等待渲染、判断为结果页、提取数据。3.4 第一版实测功能通过了耗时却让人崩溃第一版改造完成之后功能上是通的原来动不动因为选择器失效而崩掉的查询改版后明显稳了。但性能并不理想单次查询平均要 17 秒左右。我把耗时拉出来看问题主要集中在三个方面一是交互轮数太多。每次判断只返回一个动作导致遇到加载状态时要连续好几轮“判断-等待-再判断”才能进入下一个阶段。每轮 Jev 推理本身只要 200 毫秒左右但每一轮之间我还设了固定等待加起来就慢了。二是固定等待时间仍然存在。旧代码里到处是time.sleep(3)重构第一版我没有完全清理干净结果这些 sleep 依然在吃时间。三是 Jev 的推理延迟没有做优化。当时我用的是默认参数单个判断请求在 CPU 上要跑 300 到 800 毫秒虽然不算夸张但一轮流程要推理四五次累计起来也很可观。所以第一版跑出来是“功能全通耗时不合格”这反而让我有了一个明确目标在不牺牲判断能力的前提下把单次查询压进 7 秒。4. 把单次查询压进7秒我实际用过的五个优化手段先放结论最后单次查询稳定在 7 秒以内不是靠某一个“大招”而是靠五个优化叠加出来的。每一项都不是黑科技但合在一起效果非常明显。4.1 优化一减少交互轮数让一次推理输出更多信息我把决策协议改成了“复合输出”不再让 Jev 只输出一个动作而是让它一次输出当前状态 主动作 建议的等待条件 是否完成。举个例子以前遇到QUERYING状态需要连续判断三次才能进入RESULT_LIST。现在 Jev 可以直接返回“当前处于加载状态预计还需要等待建议轮询某类元素出现不要点击”执行层拿到这个建议后可以在一个循环内按条件等待直到条件满足然后立刻进入提取阶段不用每 500 毫秒回来问一次。这么一改正常查询的轮数从 6 到 8 轮压缩到了 3 到 4 轮整体耗时立刻减少了三成以上。4.2 优化二用动态等待替换固定 sleep这是耗时下降最大的一步航班查询里最浪费时间的就是那些“固定 sleep”。我的改造方式很简单把time.sleep(3)全部换成“条件等待”。执行层定义一个wait_for(predicate, timeout)函数以 50 毫秒为粒度轮询页面状态条件成立就立刻往下走不成立就继续等。def wait_for_condition(condition_fn, timeout_ms8000, interval_ms50): deadline time.time() * 1000 timeout_ms while time.time() * 1000 deadline: if condition_fn(): return True time.sleep(interval_ms / 1000) return False条件本身不一定非要写成复杂的选择器判断也可以直接交给 Jev判断层返回“建议等待直到出现航班卡片”执行层就可以轮询“页面摘要里 cards 数组长度大于 0”这个条件。这样比time.sleep(3)快得多因为结果是 2 秒出来的话2 秒后就继续不用硬等满 3 秒。这一项优化做完单次查询平均耗时从 17 秒降到了 11 秒左右。4.3 优化三压 Jev 单次推理延迟第三个优化是把 Jev 本身的推理速度提上去。这里面有几个操作立竿见影模型量化。把模型权重从 FP16 量化到 INT8 或 INT4精度略有损失但推理速度可以提升两到三倍。对判断层这种短文本分类任务来说精度损失基本可以忽略。限制输出长度。把max_tokens从默认的 512 压到 64。Jev 只需要输出一个 JSON 对象没必要留这么多生成空间限制之后单次延迟直接下降。开启 KV Cache。重复的 Prompt 前缀会被缓存连续多轮判断时能省不少计算量。缩小 Prompt 体积。页面摘要不要一股脑全塞进去只保留关键字段比如可见文本数、卡片数量、价格区域文本、有无加载动画。摘要越小推理越快。做完这些之后单次 Jev 判断从平均 300 到 800 毫秒压到了 80 到 150 毫秒。这个量级放在整个查询流程里基本可以忽略不计了。4.4 优化四查询级缓存与异常状态快速退避航班查询的业务特点是重复查询率很高。同一个航线、同一天、同一舱位用户可能在半小时内反复查三四次。所以我在判断层之外加了一层查询级缓存如果当前查询条件在 30 分钟内已经成功跑过并且拿到了结果列表就直接返回缓存不重新跑浏览器流程。这个优化看似和判断层无关但它对“平均耗时”的贡献非常大。命中缓存时一次查询可以在 2 秒内完成没命中缓存时才需要走完整的浏览器查询流程。加上缓存之后整体平均耗时会被显著拉低。我同时也做了“异常状态快速退避”出现BLOCKED或UNKNOWN状态时不再傻傻地重试三次而是第一次先等待 1.5 秒第二次 3 秒第三次直接放弃并记录快照。这样即使遇到风控或页面抖动也不会把一次查询拖到三四十秒。4.5 优化五最终实测数据与一个关键注意点五个优化全部上线后我对标了一下优化前后数据版本平均耗时最慢耗时判断轮数缓存命中重构前规则堆叠23 秒41 秒无概念无第一版 Jev 重构17 秒25 秒7 轮无优化后6.8 秒9.5 秒3.5 轮约 40% 请求命中7 秒这个目标不是靠单次流程优化出来的而是由“更少的判断轮数 动态等待 更快推理 部分缓存命中”共同贡献的。在完全没有缓存的情况下单次完整查询大约是 8 到 9 秒加上缓存平均值下降到 6 到 7 秒。提示如果你也想做类似优化记得先做耗时打点再动手优化。否则你只会凭感觉改改完了也不知道哪一步真正有效。我在每个判断轮次、每次等待、每次推理上都打了埋点所有数字才有说服力。5. 跑了两周之后判断层重构那些值得提前知道的坑改造上线后我没有立刻去庆祝“耗时达标”因为网页自动化真正的考验是持续性。果然跑了两个星期新的问题开始浮出水面。这里挑几个我认为最值得提前知道的坑给后来者打个预防针。5.1 模型幻觉空结果页被当成正常结果页最典型的一次事故是某天凌晨脚本查询一条航线页面明明显示“当前日期无航班”但 Jev 却把它判断成了RESULT_LIST并且还尝试去提取航班列表。结果自然是提取失败还浪费了整整一轮请求。我后来看evidence字段才发现问题页面摘要里有“航班”两个字模型就被带偏了以为有航班列表。从那之后我做了两个改进。一是 Prompt 里明确加了一条规则当页面出现“无航班”“没有找到”“暂无”等语义且没有航班卡片时必须输出EMPTY_RESULT。这看起来是在用关键词但实际上是让模型在模糊判断时优先考虑业务强信号。二是在执行层加了一个硬校验Jev 判断为RESULT_LIST时执行层还要检查页面摘要里是否有至少一个包含航班号或价格的卡片结构。如果没有就退回让 Jev 重新判断。这个校验本质上是给模型加了一道“安全带”。5.2 DOM 摘要不做降噪Jev 再强也会被噪声带偏第二个坑是输入噪声。一开始我把整个页面的正文文本全部塞进摘要以为“信息越全判断越准”。结果恰恰相反页面里的广告、推荐文案、footer 链接、实时弹窗信息混进来之后Jev 经常被带偏。后来我重新设计了采集层只保留和当前任务相关的信息页面标题、当前 URL、可见的主要文本块、航班卡片结构、按钮可用状态、加载动画状态。那些埋在 HTML 深处的脚本、样式、隐藏元素、广告 iframe一概不进摘要。这一步做与不做差别非常大。做好降噪之后Jev 的判断准确率从 92% 左右提升到了 98% 以上。不要指望模型自己在噪声里挑重点网页自动化的场景里你给它什么它就信什么采集层的质量直接决定判断层的上限。5.3 规则层不能拆它是 Jev 的“安全带”上线初期我一度想把规则判断全部删掉让 Jev 全权负责。后来发现这是错的。不是因为 Jev 不够好而是因为有些判断根本不需要语义理解比如“页面 URL 包含 login 说明在登录页”“提交按钮处于 disabled 状态说明正在提交中”。这些用规则判断既快又准何必让模型绕一圈呢所以我最终的架构是规则优先模型兜底。能枚举的高频状态用规则直接判断枚举不了或者容易变化的语义状态交给 Jev。两者冲突时如果规则的置信度足够高就以规则为准只有规则无法判断时才采信模型输出。这个设计让我在后续改版中省了很多事大多数页面改版只是改样式和选择器不会推翻整个业务语义所以 Jev 的判断模型不需要经常重新训练规则层的小范围修补就够了。5.4 每一次判断都必须可回放快照与日志最后一点也是我最想强调的一点判断层重构之后可观测性必须跟上。判断层是决策中枢如果它出错了你光看最终结果很难知道是在哪一步错的。所以我给自己的系统加了一个“黑匣子”每次 Jev 判断都记录四样东西——当时的页面摘要快照、实际的页面截图、输入给 Jev 的完整 Prompt、以及 Jev 的输出 JSON。这样一来线上任何一次查询异常我都能像回放录像一样复盘它在哪一步判断错了依据是什么当时的页面长什么样。很多问题不需要上线调试看快照就能定位。后来我还顺手把这些快照整理成了数据集用来继续校准 Prompt 和微调模板。这才是判断层持续变好的闭环不是跑一次就完事而是每次出错都在给下一次判断积累经验。最后分享一个小习惯现在每次新增一个航司或者改版一次页面我不会急着改代码而是先把采集层抓出的页面摘要打印出来看一遍再人工判断一次“如果我是 Jev我看到这些信息会怎么判断”。这样做过几轮之后你对模型能力边界会有很实在的感知Prompt 设计也会越来越顺手。网页自动化的判断层重构说到底不是把代码换成模型而是把维护一个系统的思路从“防着页面变化”换成“理解页面变化”。这个思路转过来之后耗时降到 7 秒反而是最不重要的收获了。
返回列表