Documentação

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.

  1. 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.
  2. 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.
  3. Estados DONE reais (700): execute o clique no navegador e registre a página de destino com o histórico como um caso DONE.
  4. 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.
  5. 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.
  6. 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.
  7. 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.compile em 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.