Ajuste fino do Laya como cabeça de decisão de agente de navegador
Um exemplo completo e totalmente reproduzível de especialização do Laya para uma família de decisões
que ele não consegue fazer zero-shot: escolher a próxima ação do navegador (operação + elemento
alvo) para browser-use/jev-ultrafast, cujo formato
de solicitação /v1/systemone é igual a Agent.predict(state, questions). Tudo abaixo rodou em uma
única RTX 4070 Ti SUPER (16 GB) sem API paga; pesos, código e resultados por execução estão em
huggingface.co/cklxx/laya-browser.
Resultado
| typed-decisions, zero-shot | ajustado | |
|---|---|---|
| top-1 de elemento em páginas reservadas (2.734 decisões, ~45 candidatos cada) | 0.10 (acaso) | 0.66 (421M) / 0.63 (322M) |
| precisão de operação (CLICK / TYPE_TEXT / SELECT / DONE) | 0.54 | 0.88–0.89 |
| 16 tarefas reais de navegador, 3 execuções cada | 0 % | 62 % (322M), 50 % (421M) |
| latência por passo (3 perguntas, 30–65 candidatos) | 50–200 ms | 41–50 ms (421M), 17–23 ms (322M) |
A suíte ao vivo é bimodal: 10 tarefas passam 3/3 (navegação por categoria / aba / página, checkbox,
<select>, busca + envio em alguns sites) e 6 falham 3/3 (fluxos de digitar e depois escolher uma
sugestão, paginação que precisa de um scroll antes, Google Flights). A variância entre execuções em
sites ao vivo é maior que a diferença entre os dois backbones, então trate-os como equivalentes e
escolha pela latência.
Os checkpoints são diretórios de checkpoint comuns do Laya:
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
Cada passo é um script em code/finetune/ do repositório do Hub; run_v10.sh / run_v10s.sh o
rodam do início ao fim.
- Crawl de 421 páginas reais (Wikipedia, GitHub, HN, arXiv, HF, lojas de demonstração, sites de teste com muitos formulários) com o leitor de DOM do jev, mantendo a tabela de elementos e o texto da página.
- Gerar objetivos ao contrário (5.244): escolha um elemento como resposta e peça a um Qwen3-8B local que escreva o objetivo que um usuário declararia para precisar dele. Nenhum professor precisa resolver nada, então os rótulos são limpos.
- Estados DONE reais (700): execute o clique no navegador e registre a página de destino com o histórico como um caso DONE.
- Negativos do passo 2 (659): novos objetivos nessas páginas de destino com o histórico mantido, para que “ter um histórico” pare de prever DONE.
- Mind2Web (osunlp/Mind2Web, 7.296 passos):
candidatos re-renderizados como uma tabela de elementos, histórico de ações a partir de
action_reprs, valores digitados mostrados como o valor atual do campo. - Correções on-policy (DAgger, 177): rode tarefas reais com o modelo atual, pergunte a um LLM local a cada passo, guarde seu veredito com o estado do próprio modelo.
- Build → train → calibrate → eval: a receita RLCD do Laya (alvos suaves de distribuição gold + gradiente de política de logit ruidoso + CE suave), GPU única, sem gradient checkpointing, 4 épocas (~2 h para 421M, ~1 h para 322M), temperatura post-hoc, páginas / sites reservados para avaliação.
O que mais importou: o formato de entrada
Com o estado do jev passado ao pé da letra (texto da página + a tabela de elementos inteira como JSON dentro de state), a
janela de 1.024 tokens trunca a maior parte da tabela, então o modelo muitas vezes nunca vê o candidato que deveria
escolher. Mover os elementos para fora do estado e para dentro da lista de opções (rótulo completo + papel + valor atual,
head_max_len 512 → 768; o estado mantém título / URL / histórico / 1.2–1.5k chars de texto) valeu mais
que qualquer mudança nos dados: top-1 de clique no Mind2Web 0.44 → 0.51 e a suíte ao vivo 6/16 → 10/16 nos mesmos dados.
O que não funcionou (para você não repetir)
- Objetivos DONE em template (“Abra a página chamada X, pare quando ela estiver aberta”) vazam fraseado; o modelo aprende parar quando ⇒ DONE. As amostras DONE precisam ser páginas de destino reais depois de uma ação executada.
- Se toda amostra DONE tem exatamente uma ação anterior e toda amostra de clique nenhuma, o modelo aprende qualquer histórico ⇒ DONE. Adicione negativos de meio de tarefa.
- O Mind2Web sozinho mata DONE / TYPE_TEXT (não há DONE lá, o CLICK domina). Repondere as operações raras (DONE ×4, TYPE_TEXT / SELECT ×3).
- Truncar o texto da página para 3.000 chars não economizou nada (a cabeça domina a sequência) e custou 0.04 de top-1.
torch.compileem lotes de comprimento variável recompila por forma: 6× mais lento. Desligar o gradient checkpointing foi o ganho grátis de verdade (1.25×).- Escalonamento com gating por confiança para um LLM local de 8B ou 27B tornou os resultados piores; nessas páginas o modelo ajustado de 322M decide melhor (27B com um orçamento de pensamento de 300 tokens: 0.861 de precisão de op / 0.603 de top-1 a 4.7 s por passo, contra 0.890 / 0.623 a 21 ms). Um professor mais forte é necessário para mais ganhos de DAgger.
- O leitor de DOM do jev esconde campos de senha por design e nunca vê menus recolhidos; algumas “falhas” são o framework, não o modelo.
Reproduzir
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 nesse repositório tem todos os números intermediários da primeira tentativa até a
final, e results/ guarda os JSONs de suíte por execução por trás da tabela acima.