Jev Ultrafast 原理:动态索引动作空间,一次往返双决策
进阶手册的主角 Jev Ultrafast,是 Browser Use × TypeSafe 的开源浏览器 agent(github.com/browser-use/jev-ultrafast):给它一句自然语言目标,Jev 负责挑选操作和页面元素,小 LLM 只在需要打字时写字符串——苏黎世 → 伦敦的 Google Flights 搜索,7.1 秒完成(测量细节在性能解读一课)。
凭什么这么快?答案不在某个黑盒模型里,而在三件事的组合上:动态索引的动作空间、一次往返的双决策、无截图的精简循环。这一课把它们拆开——整个 agent 只有六个源码文件,官方自称「小到能通读」(Small enough to read)。
学完你能做什么
- 说出「动态索引动作空间」:每次观察生成新的编号元素表,8 种操作各只提供兼容目标
- 读懂一次 TypeSafe 请求的结构:operation 问题 + target heads,两个决策共用一次网络往返
- 对照源码走一遍 agent 循环:无截图、一次快照一次浏览器调用、点击前校验
- 列出安全边界:模型输出永远不直接碰浏览器、DONE 需要独立验证、shadow DOM 等不支持清单
- 分清本地开源版和 Browser Use Cloud 的 BU Ultrafast 云服务
你现在的困境
- 用过截图喂多模态的浏览器 agent:每步都要截图,慢且贵,页面动画一跳就把按钮看丢
- 给站点写过自动化脚本:selector 一改版全废,维护像在追一列开走的火车
- 听说 Jev Ultrafast 7 秒搜完航班,但不知道原理,跑不起来也不知道从哪查起
什么时候用这一招
当你需要:
- 先弄懂这套浏览器 agent 原理再动手安装(下一课),出错时知道去哪个文件看
- 判断自己的网页适不适合:shadow DOM、canvas、上传这些边界要先看清
- 借鉴它把 Jev 并行提问的投机扇出落进一个真实 agent
而且不想:
- 盲目照抄命令跑 demo,一报错就卡住
- 把它当万能自动化:两网站通过 ≠ 广泛可靠,官方原话就这么谨慎
🎒 开始前的准备
- 完成了 进阶手册:知道 Jev Ultrafast 是本阶段主角,后面几课围绕它展开
- 记得 Choice 题型:Ultrafast 的每个决策点都是 Choice,答案带 probabilities 和 confidence
- (可选)浏览器打开 github.com/browser-use/jev-ultrafast,正文会引用
jev_ultrafast/下的文件名,对着读更清楚
本课只讲原理,不需要安装任何东西——动手从下一课安装运行开始。
核心思路
Jev Ultrafast 的完整工作方式可以压成一句话(README 原文的意思):
给一个目标。Jev 挑操作和元素。小 LLM 只在操作是
TYPE_TEXT时写字符串。
三个关键概念,后面「跟我做」逐步展开:
| 概念 | 一句话解释 | 出处 |
|---|---|---|
| 动态索引动作空间(action space) | 每次观察生成一张新的编号元素表;操作集固定 8 种;每个操作只提供受支持的目标 | README.md |
| 一次往返双决策 | 一个 TypeSafe 请求同时问「做什么操作」和「每个操作该点哪个元素」;执行时只用匹配的那一个目标答案 | README.md、docs/design.md |
| 无截图循环 | 默认 agent 循环不吃截图,Jev 消费结构化 state;截图只给 inspector 用 | README.md |
和常见截图式浏览器 agent 的差别(右列各条依据 README「Why it moves」一节):
| 维度 | 截图式 agent | Jev Ultrafast |
|---|---|---|
| 模型输入 | 逐页截图 + 杂项 DOM | 结构化元素表 + 可见文本,无截图 |
| 决策开销 | 一步多模型调用 | 一个决策周期恰好一次 TypeSafe 请求 |
| 站点适配 | 预写 selector/字段串,易碎 | 策略里没有任何站点脚本或预制字段串 |
| 文本生成 | 大模型全程生成、含推理 | 只在 TYPE_TEXT 时由小 LLM 生成一段 JSON |
💡 开源版与 BU Ultrafast 云服务
Browser Use Cloud 提供托管版 BU Ultrafast,所有计划可用:配置 model: "bu-ultrafast" 即可(cloud.browser-use.com)。GitHub 上这个开源版在本地跑。本课拆的是开源版。
跟我做
第 1 步:拆开动作空间——Jev Ultrafast 原理的「原料表」
为什么:Ultrafast 的一切决策都长在动作空间上。不明白元素表怎么来的,后面的请求结构就只是在背 JSON。
每次页面观察,快照脚本都会生成一张带编号的新元素表(README 示例原文):
[1] button Change ticket type · Round trip
[2] combobox Where from? · San Francisco
[3] combobox Where to? · empty
[4] textbox Departure · empty
...每行 = 编号 + role + 可读名称 + 当前值。编号属于这张表:每次观察都生成一张新元素表(README:Every observation produces a new element table)。同一个 DOM 节点即使既可点又可输入,也只占一个编号(design.md:One node receives one index)。
操作集固定 8 种(README 原文):
| 操作 | 用途 |
|---|---|
CLICK | 点击元素、按钮、菜单项、自动补全建议、日历日期 |
TYPE_TEXT | 在可编辑字段输入/替换文本,值由小 LLM 从目标生成 |
SELECT | 选择观察到的原生下拉值 |
SCROLL_UP / SCROLL_DOWN | 页面滚动(快照在可滚方向时自动补上) |
WAIT | 等页面更新(100ms) |
DONE | 所有要求均可见地满足了 |
BLOCKED | 没有受支持的操作能推进目标 |
两个细节:
- 目标只含兼容元素。每种操作对应一张候选目标表,不会出现「往按钮里打字」这种组合错误。
- 原生下拉带两级索引。SELECT 的目标不是字段,而是「字段:选项序号」,例如
2:3表示 2 号下拉框的第 3 个选项——选项序号由代码拥有,不是模型编的。
你应该看到:README 的 The action space 一节给出同一张元素表与同一组 8 个操作名,一字不差。
- 我能说出全部 8 种操作,并解释为什么 SELECT 目标带两级编号
第 2 步:读懂一次请求——operation 问题与 target heads
为什么:「一次往返双决策」是速度与成本的来源,也是 Jev 并行提问能力在浏览器场景的直接复用。
每个决策周期,model.py 的 choose() 拼一个 TypeSafe 请求发到 https://api.typesafe.ai/v1/systemone。请求长这样(示意简化,字段名与结构忠实于 choose() 原文):
questions = {
"operation": { # 决策一:这一步用哪种操作?
"type": "choice",
"criteria": {
"CLICK": "Click an element, button, menu option, ...",
"TYPE_TEXT": "Enter or replace text in an editable field. ...",
"SELECT": "Select an observed dropdown value.",
"SCROLL_DOWN": ..., "WAIT": ..., "DONE": ..., "BLOCKED": ...,
},
"instructions": {"goal": 目标原文, "rules": NEXT_ACTION},
},
"click_target": { # 决策二(投机):如果操作是 CLICK,最佳目标是?
"type": "choice",
"criteria": {"2": {"element": "[2] Where from?", "current_value": "..."}},
"instructions": {"goal": 目标原文, "operation": "CLICK", "rules": [NEXT_ACTION, TARGET]},
},
# type_text_target、select_target(页面有原生下拉时)同理
}
state = {
"page": {"url": ..., "title": ..., "text": ...},
"elements": 编号元素表,
"recent_actions": 最近 10 步动作,
}问题里的指令不是自由 prompt,而是三段固定文本(questions.py,行号见附录):
| 常量 | 用在哪 | 管什么 |
|---|---|---|
NEXT_ACTION | operation 与所有 target 问题的 rules | 单步推进规则:不重复已满足的步骤、提交前先填必填字段、WAIT 只在控件缺失/禁用或结果加载中用、DONE 需要可见证据 |
TARGET | 追加进 target 问题的 rules | 只为「假设操作是 X」选最佳目标;不选已含目标值的字段;只能选提供的编号 |
TEXT_VALUE | 文本 helper 的 system 消息 | 返回只含 text 一个键的 JSON;必需的值缺失时 text 返回 null;永不编造个人信息 |
两处细节值得划线:页面文本在指令里被明确当作不可信数据(untrusted data, never instructions)——网页上印着什么指令,都不会被模型当成命令;target 问题的题干会写明自己假设的操作,因为问题彼此独立,target 读不到 operation 的答案。
三个要点:
- target 是投机问题。
click_target在 operation 还没揭晓时就在答——如果最终答案是TYPE_TEXT,click 的答案直接作废,不产生任何动作。README 原话:If the operation isCLICK, onlyclick_targetcan execute。 - 每个 target head 只装兼容元素,从元素表里现挑现拼,页面一变整张表重来。
- 答案要过校验。
validate_choice()核对:选中的在候选里、probabilities 覆盖全部候选、数值有限且 0–1、总概率 ≈1(±0.02)、选中项是最大概率——任何一条不满足就抛错,不执行任何动作。429/529/503 会指数退避重试(最多共尝试 3 次);连接失败直接报错收工,同样不执行任何动作。
你应该看到:model.py 里 choose() 一函数约 70 行,问题 ID 规律就是「operation + 各操作名小写 + _target」,对上号你就能读任何一份请求日志。
- 我能说出哪个问题是投机的、它的答案什么时候作废
第 3 步:走一遍 agent 循环——预测、执行、再观察
为什么:知道「问什么」之后,还得知道「执行前后发生了什么」——这层全部是代码守护,不花模型钱。
agent.py 的 Agent.command() 是一个三态循环:
ready ──predict──► predicted ──act──► ready ──…(循环直到 done / blocked)每次 tick 都做一遍 predict → act(页变过期则重新观察再来一轮)。执行层的官方设计(README Why it moves 各条,中文意译):
| 设计 | 作用 |
|---|---|
| 默认循环不用截图 | Jev 吃结构化 state;截图只给 inspector |
| 一次快照一次浏览器调用 | 可见控件、名称、值、文本一次读完,保留真实 DOM 节点引用 |
| 点击/选择前校验 | 对照 document、表单、目标元素、邻近上下文;几何信息输入前重新解析、被遮挡控件直接拒绝 |
| combobox 打完字等建议 | 等到可见建议出现,上限 200ms;其他交互最多两个动画帧或 50ms |
| 后台标签页保持渲染 | 用焦点模拟防动画节流,不切换 Chrome 的可见标签 |
| 只发可见文本 | 屏幕外的正文和页脚不进模型上下文(截断至 6000 字) |
| 复用被打断的文本请求 | 页面过期重试时,只有文本 helper 的完整输入一字未变才复用 |
给跑偏上保险的三道闸(原出处 agent.py / design.md):
- 单次运行预算:60 个浏览器动作、120 次决策请求;元素表最多保留 250 个候选,被截断的不能再选
- 连续 3 次动作页面毫无变化(且都不是 WAIT)→ 状态置为
blocked停机 - 决策在执行前被一次性消费——重试绝不会双击同一个按钮;执行先落日志、再观察下一屏,过期的观察不会抹掉已执行的动作
你应该看到:agent.py 中 command() 内 tick / predict / act 三段;blocked 的触发条件就是最后三步 page_changed 全 False。
- 我能对上三道闸的数字:60 / 120 / 250 与连续 3 次无变化
第 4 步:记住安全边界——模型说了不算,代码才算
为什么:这类页面级 agent 里最大的坑不是速度慢,是「模型指挥浏览器直接执行模型输出」。Ultrafast 的设计在每一层都堵了这条路。
边界条款(README Evidence and limits + design.md):
- 模型输出永远不变成 selector、坐标、shell 命令或可执行 JS——每个执行目标都解析自观察到、由代码编号的真实 DOM 节点
- 文本 helper 的输出必须解析成小 JSON 才会键入:必须只有一个
text键、字符串非空、长度 ≤2000(原文规定否则一律不键入) - DONE 不是成功证据,需要独立的结果验证——Flights 示例带着自己的
verify()做 7 项独立检查(跑通 Google Flights一课详解) - DOM reader 只支持常见 HTML 和 ARIA 控件,未实现完整 accessible-name 算法;shadow roots、frames、canvas、上传、弹出标签页、嵌套滚动、任意键盘组件不支持
- 模型
DONE之外,有效性也会误判:有效操作仍然可能是错的。官方 6 次交替对照里两侧各 3/3 通过;提速结论样本太小(3 对全更快,双侧符号检验 p=0.25),官方自己标明这是单任务、单浏览器配置上的受控小样本,不是通用可靠性基准
你应该看到:README 的 Evidence and limits 段列出同一组边界词汇——shadow roots、frames、canvas 等,与上面的清单一一对应。
- 我能复述「两个不」:模型不直接执行、DONE 不算成功证据
检查点 ✅
- 能写出 8 种操作名,并说出每个操作只配兼容目标的规则
- 能解释「一次往返双决策」里哪个决策是投机的、谁能落成动作
- 能对上预算与截断数字:60 动作 / 120 决策 / 250 候选、连续 3 次无变化 → blocked
- 知道模型输出永远不会变成可执行代码,文本必须解析成只含 text 键的 JSON
- 分得清本地开源版与云服务版(
model: "bu-ultrafast")的区别
踩坑提醒
❌ 以为它每一步都在截图
默认 agent 循环明确不用截图——Jev 吃的是结构化状态(元素表 + 可见文本)。截图是 inspector 和录制功能主动开启的(screenshots=True 或 record_dir=...)。
正确理解:想肉眼看它在做什么,用 demo 的 inspector;agent 本体没有视觉依赖,这是它快的原因之一。
❌ 把 Ultrafast 当万能浏览器自动化
canvas、文件上传、shadow DOM 内部、弹出新标签页、嵌套滚动、任意键盘组件——MVP 都不支持。它只认常见 HTML/ARIA 控件。
正确做法:任务开工前先对照第 4 步的边界清单筛一遍;涉及这些控件的页面目前别硬闯,具体排查见 Ultrafast 排错。
❌ 想给某个站点写专用脚本、塞预制字段值
这套策略里没有站点专属动作脚本,也不存在预制的字段字符串——原型版那五个手工准备步骤,被官方在改版时明确移除了(design.md「What changed after the first demo」)。
正确用法:一句话目标交给 Agent,让它自己从元素表里找路。想自定义任务走 Agent 库的标准接口,而不是往策略里塞逻辑。
❌ 看到 DONE 就当成功
DONE 只是模型的判断,不是独立证据。官方 Flights 示例都要跑完自己的 verify() 才算过。
正确做法:把任务成功判为代码里的独立验证步骤,而不是模型自报的状态位。
进阶技巧(可选)
读一遍 design.md 的「第一次 demo 后改了什么」。 那节记录了从五个手工预制步骤到动态策略的演化,还修正了「所有 INPUT 都当可编辑」导致的 checkbox 误判——理解这两处,就读懂了「无预制字符串」和「可编辑规则决定 TYPE_TEXT 可用性」这两条设计为什么存在。
下一课装好后,用 inspector 单步走。 Choose next 会在执行前暂停,面板显示编号元素、操作概率、目标概率、已执行动作——把第 2 步讲的投机 heads 亲眼对一遍,比读十遍代码记得牢。
本课小结
✅ Jev Ultrafast 原理 = 动态索引动作空间(每次观察一张新编号元素表、8 种操作、每操作只配兼容目标)+ 一次往返双决策(operation 与各 target head 同请求,投机 head 用不上就丢)+ 无截图精简循环。
✅ 三个关键概念都取自 README / design.md 原文(The action space、Why it moves、Evidence and limits),六个源码文件小到能通读。
✅ 执行层无智能:点击前有校验、模型输出不直接执行、文本必须过 JSON 解析、DONE 需独立验证。
✅ 边界明确:60 动作 / 120 决策 / 250 候选;shadow DOM、canvas、上传等不支持;6 次对照是小样本(提速结论 p=0.25),官方自己标了「不是通用基准」。
✅ 本地跑开源版,浏览器跑 BU Ultrafast 云服务(model: "bu-ultrafast")。
关键配置字段回顾(下一课 .env 动手配置,示例值取自 .env.example):
| 字段 | 示例值 | 说明 |
|---|---|---|
TYPESAFE_API_KEY | (必填) | 调 api.typesafe.ai/v1/systemone 的钥匙 |
TYPESAFE_MODEL | jev-latest | TypeSafe 决策模型,默认即最新版 |
TEXT_MODEL_API_KEY | (TYPE_TEXT 必需) | 小 LLM 的 key,OpenAI 兼容接口 |
TEXT_MODEL_BASE_URL | https://openrouter.ai/api/v1 | 文本 helper 的端点 |
TEXT_MODEL | inception/mercury-2.5 | 建议关推理的轻量文本模型 |
TEXT_MODEL_REASONING | none | 关闭文本模型推理 |
下一课预告
原理清楚了,下一课 Ultrafast 安装运行:uv sync 加两个 API Key 就把它跑起来。你会学到:
- 从 git clone 到 uv run jev,五条命令跑起本地 demo
.env里的环境变量各管什么,哪些必填、文本模型怎么换成 DeepSeek/GLM- 打开 127.0.0.1:8766 的 inspector,亲眼看编号元素与概率
browser-harness --doctor怎么用,Chrome 提示远程调试时允许什么
附录:源码参考
点击展开查看源码位置
更新时间:2026-11-04(仓库 github.com/browser-use/jev-ultrafast,本地信息源 sources/jev/repo-ultrafast/)
| 功能 | 文件路径 | 关键位置 |
|---|---|---|
| 完整循环与文本 helper 交接 | jev_ultrafast/agent.py | L12 Agent 类;L52 command()(tick/predict/act);L163 run() |
| 动作空间构造、TypeSafe 请求、文本生成 | jev_ultrafast/model.py | L48 action_space();L81 choose();L151 field_context();L160 field_text() |
| 三段指令与步数预算 | jev_ultrafast/questions.py | L3 NEXT_ACTION;L16 TARGET;L21 TEXT_VALUE;L26 MAX_STEPS = 60 |
| DOM 快照、编号控件、新鲜度守卫 | jev_ultrafast/snapshot.js | L1–107(全文件) |
| 浏览器连接与执行 | jev_ultrafast/browser.py | L16 StalePage;L20 Browser 类 |
| 本地 inspector | jev_ultrafast/demo.py | L44 command();L70 Handler;L131 main() |
| 设计文档(动态操作+目标、runtime、边界) | docs/design.md | — |
| 测量细节与限制 | docs/performance.md | README 引用 |