一句英文任务,在 Jetson Orin 上跑完一次真实的航班查询,总共 32.8 秒。其中负责挑动作的 Laya 只用掉 0.5 秒,负责理解任务的 Qwen 用掉 3.5 秒,剩下二十多秒都花在浏览器操作、页面等待、核对和保存上。全程本地推理,人没有接管。

航班演示录屏,助手在 Trip.com 上确认深圳到上海的单程查询、筛出直飞并按低价排序

录屏里这条任务做了 5 次浏览器动作,最后停在匹配航班的票价选项层。它不下单,也不支付。

数字看完了,问题还在。程序要点一个按钮的时候,是让大模型把这个动作写成一句话再解析,还是从程序已经列好的动作里挑一个?这篇把两条路的机制拆开,再给出 Jetson 上的实测配置、安装步骤和两个能自己复现的 demo。

本文实测设备是 reComputer Industrial J4012(Jetson Orin NX 16GB),系统 JetPack 6.2.1、CUDA 12.6,代码在 jetson-browser-assistant。

一、两条路,区别在动作由谁挑

假设你让助手去查航班。它读完任务、打开页面,接下来得决定做什么。页面上可能是出发地、目的地两个输入框,一个日期选择器,还有一个 Search 按钮。程序能把这些控件列成可选动作,但总得有人挑一个。

传统文本生成模型的做法是读任务和页面描述,然后把这个动作写出来。比如它返回一句「点击 Search」,程序解析这段文字,去执行点击。页面变了以后,助手再观察一次,继续问下一步。

生成这段答案分两步。Prefill 阶段处理描述任务和页面的输入,decode 阶段逐个 Token 生成答案,后面的 Token 依赖前面已经输出的内容。一个任务里要连做几次动作,这两步就重复几遍。

这条路能应付各种说法,也能生成新文字。但程序已经知道有哪些动作可选时,还有一个更窄的问法,这些动作里,下一个该做哪个。

两条输出路径对比,上面是自回归模型 prefill 后逐 Token 解码动作文字,下面是决策模型给现成候选打分

上面一行是自回归模型先做 prefill、再逐 Token 解码出动作文字,下面一行是决策模型给现成的候选打分。这张图讲机制,不比较速度。

两条路都要处理输入,都要算。文本模型也能只输出一个简短的动作 ID,光看机制图判断不了谁更快,实际延迟取决于模型、输入长度、候选数量和具体实现。

这个项目两个模型都用上了。Qwen 负责理解用户的要求,需要写字的时候由它来写。Laya 反复从当前候选里挑操作和目标。工具由程序提供,动作由程序执行,结果也由程序核对。

二、决策模型拿到的三样东西

拿白板上的一个例子。画布上有个绿色的框,标签是 Camera,目标是把它的底色改成蓝色,标签和位置都保留。这一次的目标由程序直接交给 Laya,原文如下。

Make the Camera rectangle background blue. Keep its text and position.

程序把当前情况整理成三份输入。

输入Camera 这个例子里是什么
状态 StateCamera 现在是绿色,标签和位置已知
目标 Goal把 Camera 变蓝,标签和位置保留
候选 Candidates此刻可用的红、绿、蓝、黄四个按钮

三样缺一不可。状态说的是现在什么样,目标说的是想要什么样,候选说的是程序此刻能做什么。观察漏掉一个控件,或者正确选项根本没进候选列表,模型再准也选不出来。

本项目固定使用 laya-browser v19s,编码器是 mmBERT-base,后面接一个决策头。编码器结合上下文读这些文字,绿色是现在的颜色,蓝色是想要的颜色,保留位置是一条约束。决策头给候选打分,返回选中的那个。更细的设置见文末的固定配置链接。

状态、目标、候选的文字描述进入 mmBERT-base 双向编码器和决策头,分别输出操作选择与点击目标选择

文字形式的状态、目标和候选进入 mmBERT-base 双向编码器和决策头,分别输出操作选择与点击目标选择。这个实现不把截图像素送进模型。

候选描述每次请求都可能变。这一次选项 3 是蓝色按钮,wrapper 把它映射成动作 ID color_blue;下一次换了页面或画布,3 可能指别的东西。映射由程序按最新一次的观察重新建立。

这次返回把「要做什么」和「对谁做」拆成两个问题,操作选了 CLICK,目标选了选项 3。两个问题可以放进同一批计算,不必固定先推操作再推目标。

一次 Camera 决策记录,操作选了 CLICK 概率 0.9785,点击目标选了蓝色概率 0.9721

一次真实的 Camera 决策,操作选了 CLICK,概率 0.9785,点击目标选了蓝色,概率 0.9721。图上两个色块按保存的场景数值重画,不是截图。

程序点完蓝色按钮后重新读了一次画布,确认 Camera 已经变蓝,标签和位置都还在。

分数只表示模型在这一组候选里更偏向哪个。同一份记录里,目标那一项还有一个 confidence 字段,值是 0.8859。这两个数都不能保证整条任务成功,单条例子也说明不了新任务上的准确率或校准水平。

候选多了,计算量跟着涨。候选集合较大时,固定版本的 wrapper 会分组打分,再拿各组胜出者比一次,所以一次 API 请求背后可能不止一轮模型计算。接口耗时里还有请求准备和传输,不只是模型本身的时间。

三、选出来只是第一步,核对由程序负责

模型推理的时候页面可能已经变了,一次点击也可能没起作用。所以助手跑的是一个循环,观察、选择、核对、执行、再观察。

Agent 循环示意,Qwen 解析请求并生成候选,Laya 选择操作与目标,程序核对后执行界面输入

Qwen 理解要求,Laya 在可用动作里选,执行和核对交给程序。

动手之前,程序先确认选中的目标还在当前页面或画布上。鼠标键盘由 Playwright 执行。做完之后再读一次真实状态,检查要求有没有满足。模型自己返回一个 DONE,不能当作完成凭证。

适配器负责补上具体能力,比如在日历里选完整年月日、拖动白板上的一个对象。白板的核对器还会查对象身份、标签、颜色、位置和箭头绑定,所以在原对象上面盖一个新的框,过不了「保留原对象」这一关。这些工具和检查都写在应用里,加一个新工具就得真写代码。

四、在 Jetson Orin NX 上跑起来

两个演示用同一套配置,模型、浏览器和桌面共用一份内存。

组件实测配置
硬件reComputer Industrial J4012,Jetson Orin NX 16GB
系统Ubuntu 22.04.5,JetPack 6.2.1,L4T 36.4.3
Python / CUDAPython 3.10.12,CUDA 12.6
决策模型laya-browser v19s,PyTorch 2.7.0 CUDA,原始 wrapper,fast=False
文本模型Qwen3.5-4B-MTP-GGUF,Q4_K_M
文本推理CUDA llama.cpp,8192 Token 上下文,单并发,关闭 thinking,MTP 深度 2
浏览器 / 白板Playwright 1.64.1,ARM64 Chromium,Excalidraw 0.18.1

在 Jetson 桌面上用普通用户打开终端,执行下面三行。

git clone https://github.com/zibochen6/jetson-browser-assistant.git
cd jetson-browser-assistant
./setup.sh

安装脚本会检查设备,准备独立的 uv 环境和匹配的 CUDA 版 PyTorch,安装 Chromium,下载固定版本的模型,编译 llama.cpp,再把服务预热好。不要加 sudo 运行,需要装系统库时脚本会自己提权。兼容性、下载和排错见文末的 README 与安装说明。

五、什么场景值得用决策模型

值得考虑的场景有几个共同点,用户的目标会变,当前状态会变,但可用的工具是有限的。航班筛选、改白板上已有的对象都属于这一类。应用这边仍然要负责观察当前状态、把有意义的选项摆出来、做完再去核对。

任务固定、每次都走同一条路的场景,确定性脚本往往更省事。需要开放式回答或新写文字的时候,生成式模型依旧有用。

有一条要说清楚,这个项目没有做同条件的脚本基线,所以这些演示不能证明模型方案比脚本更快或更好维护。

当前版本只覆盖 Trip.com 上的一条有限流程和一张小尺寸白板。其他网站、自由绘图、语音和工业控制都要另外开发和验证。Jetson 的 16GB 是 CPU 与 GPU 共享内存,别的程序和 swap 都会吃掉可用容量。

六、时间花在哪,两个 demo 怎么跑

录屏里这条航班任务总耗时 32.8 秒,Laya 决策累计约 0.5 秒,Qwen 解析约 3.5 秒,剩下的时间在浏览器操作、页面等待、核对和保存上。

三根横条对比任务总耗时 32.8 秒、Laya 决策累计 0.5 秒、Qwen 解析 3.5 秒

三根条是各自独立的计数,不是完整的分项拆分,这次任务还复用了页面上已有的搜索条件,所以不能当成从空白页逐个填表的耗时。

另外做过一组等待逻辑的对照。模型预先加载、浏览器用独立的空白 profile、改掉页面等待判定之后,广州到北京这条前后都成功的任务从 119.4 秒降到 41.6 秒。动的是等待逻辑,模型没换过。

查询航班

启动助手,等提示符 Models ready 和 Task> 出现。

./launch.sh

输入下面这条任务,日期换成未来某一天。

Find one-way flights from Shenzhen to Shanghai on 2026-11-13 for 1 adult in economy. Nonstop only, lowest price first. Show up to 3 options and open the fare options for one matching flight.

启动脚本会同时打开真实网站和一个只读的 Live Flow 窗口,只想要网站时用 ./launch.sh --no-visualize。完成后检查实际候选、Query verified 和 Fare options opened 三项输出。全程停在票价选项层,不会下单或支付。

在同一张白板上连续修改

先结束或取消正在跑的任务,再打开一张空白画布。

./launch-whiteboard.sh --open examples/whiteboard/blank.excalidraw

--open 会替换当前画布,已经导出的文件保留。等到 Task> 后输入下面这条。

Draw three labeled boxes: Camera on the left, Jetson in the center, and Display on the right. Connect Camera to Jetson and Jetson to Display with arrows.

第一条跑完后不要重新打开空白文件,直接提交第二条。

Make Jetson green. Add a box labeled Robot Arm below Jetson and connect Jetson to Robot Arm. Keep everything else.

白板演示录屏,Camera、Jetson、Display 三个框被画出并连线,Jetson 随后变绿,下方新增连好箭头的 Robot Arm

第二句跑完的样子,Jetson 变绿,下方多了一个连好箭头的 Robot Arm,画布上原有的对象都还在。

每条白板任务完成后都会存下可编辑的 scene.excalidraw、一张 PNG、一张截图和一份核对报告,更多可用指令见文末的白板指南。

计时从按下回车开始,包含解析、必要的浏览器打开、推理、界面操作、等待、核对和保存。模型加载、预热和输入等待不计入,人工等待算在总时间里。

任务进行中按 Ctrl+C 取消这一条。空闲时输入 exit 或 quit 退出提示符,模型和浏览器继续留着。./status.sh 查服务,./stop.sh 停止项目服务和模型。航班记录在 runs/,白板记录在 whiteboard-runs/。

七、常见问题

决策模型和让大模型只输出一个动作 ID,区别在哪

输出空间被限定在程序给出的候选里,不用逐 Token 生成一整个答案。输入那一步照样要算,所以真实耗时还是取决于模型大小、输入长度和候选数量。

0.9785 这样的分数能当成功率看吗

不能。它只表示在这一组候选里模型更偏向哪个,confidence 是另一个字段,单条记录也说明不了校准水平。

候选编号为什么每次都要重新映射

页面和画布一变,同一个编号指向的东西就变了。映射由程序按最新一次观察重建,这也是为什么状态、目标、候选三份输入必须来自同一时刻。

换成别的网站能用吗

当前只覆盖 Trip.com 上的一条有限流程。城市选择器、日历这类控件各有各的写法,适配器要单独写代码并验证。

决策只要 0.5 秒,那慢在哪

慢在浏览器操作、页面等待、核对和保存。上面那组 119.4 秒到 41.6 秒的对照,改的全是等待判定,模型没换。

16GB 内存够吗

模型、Chromium 和桌面共用同一份内存。跑白板演示时实测占用 12.29 GiB,另有 2577 MiB swap,其中包含系统里其他程序,实际余量要按自己的环境看。

为什么不让模型自己说任务完成

模型返回 DONE 只代表它认为做完了。完成与否由程序重新读一次真实状态来判定,白板还会核对对象身份、颜色、位置和箭头绑定。

八、参考资料

Logo

更多推荐