Fine-Tuning von Laya als Entscheidungskopf für einen Browser-Agenten
Ein durchgearbeitetes, vollständig reproduzierbares Beispiel dafür, wie Laya für eine
Entscheidungsfamilie spezialisiert wird, die es zero-shot nicht kann: die nächste Browser-Aktion
(Operation + Zielelement) für
browser-use/jev-ultrafast wählen, dessen
/v1/systemone-Anfrageformat dasselbe ist wie Agent.predict(state, questions). Alles unten lief auf
einer einzelnen RTX 4070 Ti SUPER (16 GB) ohne bezahlte API; Gewichte, Code und Ergebnisse pro Lauf
stehen unter
huggingface.co/cklxx/laya-browser.
Ergebnis
| typed-decisions, zero-shot | fine-tuned | |
|---|---|---|
| Element-Top-1 auf zurückgehaltenen Seiten (2,734 Entscheidungen, ~45 Kandidaten je Seite) | 0.10 (Zufall) | 0.66 (421M) / 0.63 (322M) |
| Operations-Genauigkeit (CLICK / TYPE_TEXT / SELECT / DONE) | 0.54 | 0.88–0.89 |
| 16 echte Browser-Aufgaben, je 3 Läufe | 0 % | 62 % (322M), 50 % (421M) |
| Latenz pro Schritt (3 Fragen, 30–65 Kandidaten) | 50–200 ms | 41–50 ms (421M), 17–23 ms (322M) |
Die Live-Suite ist bimodal: 10 Aufgaben bestehen 3/3 (Kategorie- / Tab- / Seitennavigation, Checkbox,
<select>, Suche + Absenden auf manchen Seiten) und 6 scheitern 3/3 (Abläufe, bei denen du tippst und
dann einen Vorschlag wählst, Paginierung, die zuerst ein Scrollen braucht, Google Flights). Die Varianz
zwischen den Läufen auf Live-Seiten ist größer als der Abstand zwischen den beiden Backbones, behandle sie
also als gleichwertig und wähle nach der Latenz.
Checkpoints sind gewöhnliche Laya-Checkpoint-Verzeichnisse:
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
Pipeline
Jeder Schritt ist ein Skript in code/finetune/ des Hub-Repositorys; run_v10.sh / run_v10s.sh führen
es von Anfang bis Ende aus.
- Crawlen 421 echte Seiten (Wikipedia, GitHub, HN, arXiv, HF, Demo-Shops, formularlastige Testseiten) mit jevs DOM-Reader, wobei die Elementtabelle und der Seitentext erhalten bleiben.
- Ziele rückwärts generieren (5,244): ein Element als Antwort wählen und ein lokales Qwen3-8B bitten, das Ziel zu schreiben, das ein Nutzer nennen würde, um es zu brauchen. Kein Lehrer muss etwas lösen, die Labels sind also sauber.
- Echte DONE-Zustände (700): den Klick im Browser ausführen und die Landeseite mit der Historie als DONE-Fall aufzeichnen.
- Negativbeispiele aus Schritt 2 (659): neue Ziele auf diesen Landeseiten mit beibehaltener Historie, damit „eine Historie haben” aufhört, DONE vorherzusagen.
- Mind2Web (osunlp/Mind2Web, 7,296 Schritte):
Kandidaten als Elementtabelle neu gerendert, Aktionshistorie aus
action_reprs, typisierte Werte als aktueller Wert des Felds gezeigt. - On-Policy-Korrekturen (DAgger, 177): echte Aufgaben mit dem aktuellen Modell ausführen, bei jedem Schritt ein lokales LLM fragen und sein Urteil mit dem eigenen Zustand des Modells behalten.
- Bauen → trainieren → kalibrieren → evaluieren: Layas RLCD-Rezept (weiche Ziele aus der gold-Verteilung + Policy-Gradient über verrauschte Logits + weiche CE), eine GPU, kein Gradient-Checkpointing, 4 Epochen (~2 h für 421M, ~1 h für 322M), Post-hoc-Temperatur, zurückgehaltene Seiten / Websites für die Evaluierung.
Was am meisten zählte: das Eingabeformat
Wenn jevs Zustand wörtlich durchgereicht wird (Seitentext + die ganze Elementtabelle als JSON innerhalb
von state), schneidet das 1,024-Token-Fenster den größten Teil der Tabelle ab, sodass das Modell den
Kandidaten, den es wählen sollte, oft nie sieht. Die Elemente aus dem Zustand heraus und in die
Optionsliste zu verschieben (volles Label + Rolle + aktueller Wert, head_max_len 512 → 768; der Zustand
behält Titel / URL / Historie / 1.2–1.5k Zeichen Text) war mehr wert als jede Datenänderung:
Mind2Web-Klick-Top-1 0.44 → 0.51 und die Live-Suite 6/16 → 10/16 bei denselben Daten.
Was nicht funktionierte (damit du es nicht wiederholst)
- Vorlagenhafte DONE-Ziele („Open the page titled X, stop once it is open”) verraten die Formulierung; das Modell lernt stoppen, wenn ⇒ DONE. DONE-Stichproben müssen echte Landeseiten nach einer ausgeführten Aktion sein.
- Wenn jede DONE-Stichprobe genau eine vorherige Aktion hat und jede Klick-Stichprobe keine, lernt das Modell jede Historie ⇒ DONE. Füge Negativbeispiele mitten in der Aufgabe hinzu.
- Mind2Web allein macht DONE / TYPE_TEXT kaputt (dort gibt es kein DONE, CLICK dominiert). Gewichte seltene Operationen stärker (DONE ×4, TYPE_TEXT / SELECT ×3).
- Den Seitentext auf 3,000 Zeichen zu kürzen sparte nichts (der Kopf dominiert die Sequenz) und kostete 0.04 Top-1.
torch.compilekompiliert bei Batches variabler Länge pro Form neu: 6× langsamer. Das Abschalten des Gradient-Checkpointings war der eigentliche kostenlose Gewinn (1.25×).- Konfidenzgesteuerte Eskalation an ein lokales 8B- oder 27B-LLM machte die Ergebnisse schlechter; auf diesen Seiten ist das fine-getunte 322M-Modell der bessere Entscheider (27B mit einem 300-Token-Denkbudget: 0.861 Operations-Genauigkeit / 0.603 Top-1 bei 4.7 s pro Schritt, gegenüber 0.890 / 0.623 bei 21 ms). Für weitere DAgger-Gewinne braucht es einen stärkeren Lehrer.
- jevs DOM-Reader verbirgt Passwortfelder absichtlich und sieht eingeklappte Menüs nie; manche „Fehler” sind das Framework, nicht das Modell.
Reproduzieren
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 in diesem Repository enthält jede Zwischenzahl vom ersten bis zum letzten
Versuch, und results/ enthält die Suite-JSONs pro Lauf, die hinter der obigen Tabelle stehen.