文档导航

把 Laya 微调成浏览器智能体决策头

把 Laya 微调成浏览器智能体决策头

一个完整、可完全复现的示例:为 Laya 无法零样本胜任的一类决策做专门训练,即挑选下一个浏览器 动作(操作 + 目标元素),服务于 browser-use/jev-ultrafast,其 /v1/systemone 请求格式和 Agent.predict(state, questions) 一样。下面所有内容都跑在一张 RTX 4070 Ti SUPER (16 GB)上,没有付费 API;权重、代码和每次运行的结果在 huggingface.co/cklxx/laya-browser。

结果

typed-decisions,零样本 微调后
留出页面上的元素 top-1(2,734 个决策,每个约 45 个候选) 0.10(随机) 0.66(421M)/ 0.63(322M)
操作准确率(CLICK / TYPE_TEXT / SELECT / DONE) 0.54 0.88–0.89
16 个真实浏览器任务,每个跑 3 次 0 % 62 %(322M)、50 %(421M)
每步延迟(3 个问题,30–65 个候选) 50–200 ms 41–50 ms(421M)、17–23 ms(322M)

实况套件是双峰的:10 个任务 3/3 通过(分类 / 标签页 / 页面导航,复选框,<select>,某些站点 上的搜索 + 提交),6 个任务 3/3 失败(先输入再挑建议的流程、需要先滚动才能翻页、Google Flights)。真实站点上的运行间方差比两个骨干之间的差距还大,所以把它们当作等价的,按延迟挑。

checkpoint 就是普通的 Laya checkpoint 目录:

agent = laya.load("laya-browser/v10s")                       # after huggingface-cli download cklxx/laya-browser
agent.cfg["head_max_len"] = agent.cfg["head_max_len_train"]  # 768; the config records the input format too

流水线

每一步都是 Hub 仓库 code/finetune/ 里的一个脚本;run_v10.sh / run_v10s.sh 端到端跑完它们。

  1. 抓取 421 个真实页面(Wikipedia、GitHub、HN、arXiv、HF、演示商店、表单密集的测试站点), 用 jev 的 DOM 读取器,保留元素表和页面文本。
  2. 反向生成目标(5,244 个):挑一个元素作为答案,让本地的 Qwen3-8B 写出一个用户为表达这个 需求会说出的目标。没有教师需要解决任何东西,所以标签是干净的。
  3. 真实的 DONE 状态(700 个):在浏览器里执行点击,记录落地页连同历史作为一个 DONE 用例。
  4. 第 2 步的负样本(659 个):在这些落地页上给出新目标并保留历史,于是「有历史」不再能预测 DONE。
  5. Mind2Web(osunlp/Mind2Web,7,296 步):候选重新渲染成元素表,动作历史取自 action_reprs,类型的值显示为该字段的当前值。
  6. 在策略修正(DAgger,177 个):用当前模型跑真实任务,每一步问一个本地 LLM,把它的判断连 同模型自己的状态一起保留。
  7. 构建 → 训练 → 校准 → 评估:Laya 的 RLCD 配方(金标分布软目标 + 带噪 logit 策略梯度 + 软 CE),单张 GPU,不用梯度 checkpointing,4 个 epoch(421M 约 2 h,322M 约 1 h),事后温度, 留出的页面 / 网站用于评估。

最关键的是什么:输入格式

把 jev 的状态原样传进去(页面文本 + 整张元素表作为 JSON 放进 state),1,024 token 的窗口会截断 表的大部分,于是模型常常根本看不到它该挑的那个候选。把元素从状态里移出、放进选项列表(完整 标签 + role + 当前值,head_max_len 512 → 768;状态保留 title / URL / history / 1.2–1.5k 字符 的文本)比任何数据改动都更值钱:同一份数据上,Mind2Web 点击 top-1 从 0.44 → 0.51,实况套件从 6/16 → 10/16。

没起作用的东西(免得你重蹈覆辙)

  • 模板化的 DONE 目标(“Open the page titled X, stop once it is open”)会泄漏措辞;模型学到的是 一旦有停止 ⇒ DONE。DONE 样本必须是执行动作之后的真实落地页。
  • 如果每个 DONE 样本都恰好有一个前置动作、而每个点击样本一个都没有,模型就会学到任何历史 ⇒ DONE。补上任务中途的负样本。
  • 只靠 Mind2Web 会毁掉 DONE / TYPE_TEXT(那里没有 DONE,CLICK 占主导)。给稀有的操作加权 (DONE ×4,TYPE_TEXT / SELECT ×3)。
  • 把页面文本截到 3,000 字符什么也没省下(head 主导序列)却损失了 0.04 top-1。
  • torch.compile 在变长批次上会按形状重新编译:慢 6 倍。真正白赚的是关掉梯度 checkpointing (1.25 倍)。
  • 用置信度门控升级到本地 8B 或 27B LLM 反而让结果更差;在这些页面上,微调后的 322M 模型是更好 的决策者(27B 用 300 token 思考预算:0.861 操作准确率 / 0.603 top-1,每步 4.7 s,而 322M 是 0.890 / 0.623,21 ms)。要想进一步从 DAgger 获益,需要一个更强的教师。
  • jev 的 DOM 读取器按设计隐藏密码字段,也永远看不到折叠的菜单;有些「失败」是框架的问题,不是 模型的问题。

复现

huggingface-cli download cklxx/laya-browser --local-dir laya-browser
cd laya-browser/code && uv sync --extra fast
uv run python verify.py v10s            # downloads the checkpoint, answers one recorded browser step

那个仓库里的 code/finetune/README.md 有从第一次尝试到最后一次尝试的每个中间数字,results/ 放着支撑上表的每次运行的套件 JSON。