Ajuste fino de Laya como cabeza de decisión para agente de navegador
Un ejemplo trabajado y totalmente reproducible de especialización de Laya para una familia de decisiones
que no puede hacer sin ejemplos previos: elegir la siguiente acción del navegador (operación + elemento
objetivo) para browser-use/jev-ultrafast, cuyo formato de
solicitud /v1/systemone es el mismo que Agent.predict(state, questions). Todo lo de abajo se ejecutó en
una sola RTX 4070 Ti SUPER (16 GB) sin API de pago; los pesos, el código y los resultados por ejecución
están en huggingface.co/cklxx/laya-browser.
Resultado
| typed-decisions, sin ejemplos previos | ajustado | |
|---|---|---|
| top-1 de elemento en páginas reservadas (2,734 decisiones, ~45 candidatos cada una) | 0.10 (azar) | 0.66 (421M) / 0.63 (322M) |
| precisión de operación (CLICK / TYPE_TEXT / SELECT / DONE) | 0.54 | 0.88–0.89 |
| 16 tareas reales de navegador, 3 ejecuciones cada una | 0 % | 62 % (322M), 50 % (421M) |
| latencia por paso (3 preguntas, 30–65 candidatos) | 50–200 ms | 41–50 ms (421M), 17–23 ms (322M) |
La suite en vivo es bimodal: 10 tareas pasan 3/3 (navegación por categoría / pestaña / página, checkbox,
<select>, búsqueda + envío en algunos sitios) y 6 fallan 3/3 (flujos de escribir-y-luego-elegir-una-sugerencia,
paginación que necesita un desplazamiento primero, Google Flights). La varianza entre ejecuciones en sitios
en vivo es mayor que la diferencia entre los dos backbones, así que trátalos como equivalentes y elige por
latencia.
Los checkpoints son directorios de checkpoint de Laya normales:
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 paso es un script en code/finetune/ del repositorio del Hub; run_v10.sh / run_v10s.sh lo
ejecutan de principio a fin.
- Rastrear 421 páginas reales (Wikipedia, GitHub, HN, arXiv, HF, tiendas de demostración, sitios de prueba con muchos formularios) con el lector de DOM de jev, conservando la tabla de elementos y el texto de la página.
- Generar objetivos a la inversa (5,244): elige un elemento como respuesta y pide a un Qwen3-8B local que escriba el objetivo que un usuario enunciaría para necesitarlo. Ningún profesor tiene que resolver nada, así que las etiquetas son limpias.
- Estados DONE reales (700): ejecuta el clic en el navegador y registra la página de destino con el historial como un caso DONE.
- Negativos del paso 2 (659): nuevos objetivos en esas páginas de destino con el historial conservado, para que “tener un historial” deje de predecir DONE.
- Mind2Web (osunlp/Mind2Web, 7,296 pasos):
candidatos re-renderizados como tabla de elementos, historial de acciones desde
action_reprs, valores tipados mostrados como el valor actual del campo. - Correcciones on-policy (DAgger, 177): ejecuta tareas reales con el modelo actual, pregunta a un LLM local en cada paso, conserva su veredicto con el propio estado del modelo.
- Construir → entrenar → calibrar → evaluar: la receta RLCD de Laya (objetivos suaves de distribución dorada + gradiente de política sobre logits ruidosos + entropía cruzada suave), una sola GPU, sin gradient checkpointing, 4 épocas (~2 h para 421M, ~1 h para 322M), temperatura post-hoc, páginas / sitios web reservados para la evaluación.
Lo que más importó: el formato de entrada
Con el estado de jev pasado tal cual (texto de la página + toda la tabla de elementos como JSON dentro de
state) la ventana de 1,024 tokens trunca la mayor parte de la tabla, así que el modelo a menudo nunca ve
el candidato que debería elegir. Mover los elementos fuera del estado y a la lista de opciones (etiqueta
completa + rol + valor actual, head_max_len 512 → 768; el estado conserva título / URL / historial /
1.2–1.5k caracteres de texto) valió más que cualquier cambio de datos: top-1 de clic de Mind2Web 0.44 →
0.51 y la suite en vivo 6/16 → 10/16 con los mismos datos.
Lo que no funcionó (para que no lo repitas)
- Los objetivos DONE con plantilla (“Open the page titled X, stop once it is open”) filtran el fraseo; el modelo aprende parar cuando ⇒ DONE. Las muestras DONE deben ser páginas de destino reales tras una acción ejecutada.
- Si cada muestra DONE tiene exactamente una acción previa y cada muestra de clic ninguna, el modelo aprende cualquier historial ⇒ DONE. Añade negativos a mitad de tarea.
- Mind2Web por sí solo mata DONE / TYPE_TEXT (no hay DONE ahí, CLICK domina). Repondera las operaciones raras (DONE ×4, TYPE_TEXT / SELECT ×3).
- Truncar el texto de la página a 3,000 caracteres no ahorró nada (la cabeza domina la secuencia) y costó 0.04 de top-1.
torch.compilesobre lotes de longitud variable recompila por forma: 6× más lento. Desactivar el gradient checkpointing fue la verdadera ganancia gratis (1.25×).- La escalada con gating por confianza a un LLM local de 8B o 27B empeoró los resultados; en estas páginas el modelo ajustado de 322M es el mejor decisor (27B con un presupuesto de 300 tokens de pensamiento: 0.861 de precisión de operación / 0.603 de top-1 a 4.7 s por paso, frente a 0.890 / 0.623 a 21 ms). Se necesita un profesor más fuerte para obtener más ganancias de DAgger.
- El lector de DOM de jev oculta los campos de contraseña por diseño y nunca ve los menús contraídos; algunos “fallos” son del framework, no del modelo.
Reproducir
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 ese repositorio tiene todos los números intermedios desde el primer intento
hasta el final, y results/ contiene los JSON de la suite por ejecución que hay detrás de la tabla de arriba.