Ajuster finement Laya comme tête de décision d’un agent de navigation
Un exemple travaillé et entièrement reproductible de spécialisation de Laya pour une famille de
décisions qu’il ne peut pas faire sans exemples : choisir la prochaine action du navigateur (opération +
élément cible) pour browser-use/jev-ultrafast, dont le
format de requête /v1/systemone est le même que Agent.predict(state, questions). Tout ce qui suit a
tourné sur une seule RTX 4070 Ti SUPER (16 GB) sans API payante ; les poids, le code et les résultats par
exécution sont sur huggingface.co/cklxx/laya-browser.
Résultat
| typed-decisions, sans exemples | ajusté | |
|---|---|---|
| élément top-1 sur des pages mises de côté (2,734 décisions, ~45 candidats chacune) | 0.10 (hasard) | 0.66 (421M) / 0.63 (322M) |
| exactitude d’opération (CLICK / TYPE_TEXT / SELECT / DONE) | 0.54 | 0.88–0.89 |
| 16 tâches de navigateur réelles, 3 exécutions chacune | 0 % | 62 % (322M), 50 % (421M) |
| latence par étape (3 questions, 30–65 candidats) | 50–200 ms | 41–50 ms (421M), 17–23 ms (322M) |
La suite en direct est bimodale : 10 tâches passent 3/3 (catégorie / onglet / navigation de page, case à
cocher, <select>, recherche + soumission sur certains sites) et 6 échouent 3/3 (flux taper-puis-choisir-
une-suggestion, pagination qui exige d’abord un défilement, Google Flights). La variance d’une exécution à
l’autre sur des sites en direct est plus grande que l’écart entre les deux backbones, alors traite-les
comme équivalents et choisis selon la latence.
Les checkpoints sont des répertoires de checkpoint Laya ordinaires :
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
Chaque étape est un script dans code/finetune/ du dépôt Hub ; run_v10.sh / run_v10s.sh l’exécutent de
bout en bout.
- Explorer 421 pages réelles (Wikipedia, GitHub, HN, arXiv, HF, boutiques de démo, sites de test riches en formulaires) avec le lecteur DOM de jev, en gardant la table d’éléments et le texte de la page.
- Générer les objectifs en sens inverse (5,244) : choisir un élément comme réponse, demander à un Qwen3-8B local d’écrire l’objectif qu’un utilisateur formulerait pour en avoir besoin. Aucun enseignant n’a à résoudre quoi que ce soit, donc les étiquettes sont propres.
- États DONE réels (700) : exécuter le clic dans le navigateur et enregistrer la page d’arrivée avec l’historique comme cas DONE.
- Négatifs de l’étape 2 (659) : de nouveaux objectifs sur ces pages d’arrivée avec l’historique conservé, pour que « avoir un historique » cesse de prédire DONE.
- Mind2Web (osunlp/Mind2Web, 7,296 étapes) :
candidats re-rendus comme table d’éléments, historique d’actions depuis
action_reprs, valeurs typées affichées comme valeur courante du champ. - Corrections on-policy (DAgger, 177) : exécuter des tâches réelles avec le modèle courant, demander à un LLM local à chaque étape, garder son verdict avec l’état du modèle lui-même.
- Build → entraînement → calibration → éval : recette RLCD de Laya (cibles souples par distribution gold + gradient de politique sur logits bruités + soft CE), un seul GPU, pas de gradient checkpointing, 4 époques (~2 h pour 421M, ~1 h pour 322M), température post-hoc, pages / sites mis de côté pour l’éval.
Ce qui a le plus compté : le format d’entrée
Avec l’état de jev passé tel quel (texte de la page + toute la table d’éléments en JSON dans state), la
fenêtre de 1,024 jetons tronque la majeure partie de la table, si bien que le modèle ne voit souvent jamais
le candidat qu’il devrait choisir. Déplacer les éléments hors de l’état et dans la liste d’options
(libellé complet + rôle + valeur courante, head_max_len 512 → 768 ; l’état garde titre / URL / historique
/ 1.2–1.5k caractères de texte) a valu plus que n’importe quel changement de données : top-1 de clic
Mind2Web 0.44 → 0.51 et suite en direct 6/16 → 10/16 sur les mêmes données.
Ce qui n’a pas marché (pour que tu ne le répètes pas)
- Les objectifs DONE templatés (« Ouvre la page intitulée X, arrête-toi une fois qu’elle est ouverte ») fuient la formulation ; le modèle apprend arrête quand ⇒ DONE. Les échantillons DONE doivent être de vraies pages d’arrivée après une action exécutée.
- Si chaque échantillon DONE a exactement une action antérieure et chaque échantillon de clic aucune, le modèle apprend tout historique ⇒ DONE. Ajoute des négatifs en milieu de tâche.
- Mind2Web seul tue DONE / TYPE_TEXT (pas de DONE là-dedans, CLICK domine). Repondère les opérations rares (DONE ×4, TYPE_TEXT / SELECT ×3).
- Tronquer le texte de la page à 3,000 caractères n’a rien économisé (la tête domine la séquence) et a coûté 0.04 de top-1.
torch.compilesur des lots de longueur variable recompile par forme : 6× plus lent. Désactiver le gradient checkpointing a été le vrai gain gratuit (1.25×).- L’escalade conditionnée par la confiance vers un LLM local 8B ou 27B a rendu les résultats pires ; sur ces pages, le modèle ajusté 322M est le meilleur décideur (27B avec un budget de réflexion de 300 jetons : 0.861 d’exactitude d’op / 0.603 de top-1 à 4.7 s par étape, contre 0.890 / 0.623 à 21 ms). Un enseignant plus fort est nécessaire pour d’autres gains DAgger.
- Le lecteur DOM de jev masque les champs de mot de passe par conception et ne voit jamais les menus repliés ; certaines « défaillances » sont le framework, pas le modèle.
Reproduire
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 de ce dépôt contient chaque nombre intermédiaire de la première tentative à la
dernière, et results/ contient les JSON de suite par exécution derrière le tableau ci-dessus.