把 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 端到端跑完它們。
- 抓取 421 個真實頁面(Wikipedia、GitHub、HN、arXiv、HF、演示商店、表單密集的測試站點), 用 jev 的 DOM 讀取器,保留元素表和頁面文本。
- 反向生成目標(5,244 個):挑一個元素作為答案,讓本地的 Qwen3-8B 寫出一個使用者為表達這個 需求會說出的目標。沒有教師需要解決任何東西,所以標籤是乾淨的。
- 真實的 DONE 狀態(700 個):在瀏覽器裡執行點選,記錄落地頁連同歷史作為一個 DONE 用例。
- 第 2 步的負樣本(659 個):在這些落地頁上給出新目標並保留歷史,於是「有歷史」不再能預測 DONE。
- Mind2Web(osunlp/Mind2Web,7,296
步):候選重新渲染成元素表,動作歷史取自
action_reprs,型別的值顯示為該欄位的當前值。 - 在策略修正(DAgger,177 個):用當前模型跑真實任務,每一步問一個本地 LLM,把它的判斷連 同模型自己的狀態一起保留。
- 構建 → 訓練 → 校準 → 評估: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。