Ajuste fino do Laya como cabeça de decisão de um agente de navegador
Um exemplo trabalhado e totalmente reproduzível de especialização do Laya numa 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 o
browser-use/jev-ultrafast, cujo formato de pedido
/v1/systemone é igual a Agent.predict(state, questions). Tudo o que se segue correu numa única
RTX 4070 Ti SUPER (16 GB) sem API paga; os pesos, o código e os resultados por execução estão em
huggingface.co/cklxx/laya-browser.
Resultado
| typed-decisions, zero-shot | ajuste fino | |
|---|---|---|
| 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 da 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 suite em direto é bimodal: 10 tarefas passam 3/3 (navegação por categoria / separador / página,
checkbox, <select>, pesquisa + submissão em alguns sites) e 6 falham 3/3 (fluxos de escrever e
depois escolher uma sugestão, paginação que precisa de um scroll primeiro, Google Flights). A
variância entre execuções em sites em direto é maior do que a diferença entre os dois backbones,
por isso trata-os como equivalentes e escolhe pela latência.
Os checkpoints são diretórios de checkpoint normais 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
executam-no de ponta a ponta.
- Rastrear 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 inversamente (5,244): escolhe um elemento como resposta, pede a um Qwen3-8B local que escreva o objetivo que um utilizador enunciaria para precisar dele. Nenhum professor tem de resolver nada, por isso as etiquetas são limpas.
- Estados DONE reais (700): executa o clique no navegador e regista 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» deixe de prever DONE.
- Mind2Web (osunlp/Mind2Web, 7,296 passos):
candidatos renderizados novamente como tabela de elementos, histórico de ações a partir de
action_reprs, valores tipados mostrados como o valor atual do campo. - Correções on-policy (DAgger, 177): executa tarefas reais com o modelo atual, pergunta a um LLM local a cada passo e guarda o seu veredicto com o estado do próprio modelo.
- Construir → treinar → calibrar → avaliar: a receita RLCD do Laya (alvos suaves de distribuição gold + gradiente de política com logits ruidosos + CE suave), uma única GPU, 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 à letra (texto da página + toda a tabela de elementos como JSON dentro
de state), a janela de 1,024 tokens trunca a maior parte da tabela, por isso o modelo muitas
vezes nunca vê o candidato que devia escolher. Mover os elementos para fora do estado e para a
lista de opções (etiqueta completa + função + valor atual, head_max_len 512 → 768; o estado
mantém o título / URL / histórico / 1.2–1.5k caracteres de texto) valeu mais do que qualquer
alteração nos dados: top-1 de clique no Mind2Web 0.44 → 0.51 e a suite em direto 6/16 → 10/16 nos
mesmos dados.
O que não funcionou (para não o repetires)
- Objetivos DONE em modelo («Abre a página com o título X, para quando estiver aberta») deixam escapar o fraseado; o modelo aprende parar quando ⇒ DONE. As amostras DONE têm de ser páginas de destino reais após uma ação executada.
- Se cada amostra DONE tiver exatamente uma ação anterior e cada amostra de clique nenhuma, o modelo aprende qualquer histórico ⇒ DONE. Acrescenta negativos a meio da tarefa.
- Só o Mind2Web mata DONE / TYPE_TEXT (não há DONE lá, CLICK domina). Repondera as operações raras (DONE ×4, TYPE_TEXT / SELECT ×3).
- Truncar o texto da página para 3,000 caracteres não poupou nada (a cabeça domina a sequência) e custou 0.04 de top-1.
- O
torch.compileem lotes de comprimento variável recompila por forma: 6× mais lento. Desligar o gradient checkpointing foi o verdadeiro ganho grátis (1.25×). - O escalonamento com gate de confiança para um LLM local de 8B ou 27B tornou os resultados piores; nestas páginas o modelo de 322M com ajuste fino é o melhor decisor (27B com um orçamento de raciocínio de 300 tokens: 0.861 de precisão de operação / 0.603 de top-1 a 4.7 s por passo, contra 0.890 / 0.623 a 21 ms). É preciso um professor mais forte para mais ganhos com DAgger.
- O leitor de DOM do jev esconde os campos de palavra-passe por design e nunca vê menus recolhidos; algumas «falhas» são a 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
O code/finetune/README.md desse repositório tem todos os números intermédios, da primeira
tentativa à final, e results/ contém os JSONs da suite por execução por detrás da tabela acima.