Dokumentation

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.

  1. 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.
  2. 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.
  3. Echte DONE-Zustände (700): den Klick im Browser ausführen und die Landeseite mit der Historie als DONE-Fall aufzeichnen.
  4. Negativbeispiele aus Schritt 2 (659): neue Ziele auf diesen Landeseiten mit beibehaltener Historie, damit „eine Historie haben” aufhört, DONE vorherzusagen.
  5. Mind2Web (osunlp/Mind2Web, 7,296 Schritte): Kandidaten als Elementtabelle neu gerendert, Aktionshistorie aus action_reprs, typisierte Werte als aktueller Wert des Felds gezeigt.
  6. 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.
  7. 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.compile kompiliert 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.