Дообучение Laya как головы решений для браузерного агента
Проработанный и полностью воспроизводимый пример специализации Laya для семейства решений,
которое она не может выполнять zero-shot: выбор следующего действия браузера (операция + целевой
элемент) для browser-use/jev-ultrafast, чей формат
запроса /v1/systemone совпадает с Agent.predict(state, questions). Всё ниже выполнялось на
одной RTX 4070 Ti SUPER (16 ГБ) без платного API; веса, код и результаты по каждому запуску лежат
на huggingface.co/cklxx/laya-browser.
Результат
| typed-decisions, zero-shot | дообученная | |
|---|---|---|
| top-1 элемента на отложенных страницах (2,734 решения, ~45 кандидатов в каждом) | 0.10 (случайно) | 0.66 (421M) / 0.63 (322M) |
| точность операции (CLICK / TYPE_TEXT / SELECT / DONE) | 0.54 | 0.88–0.89 |
| 16 реальных браузерных задач, по 3 запуска | 0 % | 62 % (322M), 50 % (421M) |
| задержка на шаг (3 вопроса, 30–65 кандидатов) | 50–200 мс | 41–50 мс (421M), 17–23 мс (322M) |
Живой набор бимодален: 10 задач проходят 3/3 (навигация по категории / вкладке / странице,
checkbox, <select>, поиск + отправка на некоторых сайтах), а 6 проваливают 3/3 (сценарии
«набрать-затем-выбрать-подсказку», пагинация, требующая сначала прокрутки, Google Flights).
Разброс между запусками на живых сайтах больше, чем разрыв между двумя бэкбонами, поэтому
считайте их эквивалентными и выбирайте по задержке.
Чекпойнты — это обычные каталоги чекпойнтов 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
Пайплайн
Каждый шаг — это скрипт в code/finetune/ репозитория на Hub; run_v10.sh / run_v10s.sh
выполняют его от начала до конца.
- Обход 421 реальной страницы (Wikipedia, GitHub, HN, arXiv, HF, демо-магазины, сайты с обилием форм) с помощью DOM-ридера jev, сохраняя таблицу элементов и текст страницы.
- Обратная генерация целей (5,244): выберите элемент как ответ и попросите локальный Qwen3-8B написать цель, которую пользователь сформулировал бы, чтобы он ему понадобился. Ни одному учителю не нужно ничего решать, поэтому метки чистые.
- Реальные состояния DONE (700): выполните клик в браузере и запишите страницу приземления вместе с историей как случай DONE.
- Негативы из шага 2 (659): новые цели на этих страницах приземления с сохранённой историей, чтобы «наличие истории» перестало предсказывать DONE.
- Mind2Web (osunlp/Mind2Web, 7,296 шагов):
кандидаты перерисованы как таблица элементов, история действий из
action_reprs, типизированные значения показаны как текущее значение поля. - Коррекции on-policy (DAgger, 177): запустите реальные задачи с текущей моделью, спрашивайте локальный LLM на каждом шаге, сохраняйте его вердикт вместе с собственным состоянием модели.
- Сборка → обучение → калибровка → оценка: рецепт RLCD от Laya (мягкие цели в виде gold-распределения + градиент политики по зашумлённым логитам + мягкая перекрёстная энтропия), одна GPU, без gradient checkpointing, 4 эпохи (~2 ч для 421M, ~1 ч для 322M), пост-хок температура, отложенные страницы / сайты для оценки.
Что важнее всего: формат входа
Когда состояние jev передаётся дословно (текст страницы + вся таблица элементов как JSON внутри
state), окно в 1,024 токена обрезает большую часть таблицы, поэтому модель часто вообще не видит
того кандидата, который должна выбрать. Перенос элементов из состояния в список вариантов
(полная метка + роль + текущее значение, head_max_len 512 → 768; состояние хранит заголовок /
URL / историю / 1.2–1.5k символов текста) дал больше, чем любое изменение данных: top-1 клика на
Mind2Web 0.44 → 0.51, а живой набор 6/16 → 10/16 на тех же данных.
Что не сработало (чтобы вы не повторяли)
- Шаблонные цели DONE («Open the page titled X, stop once it is open») утекают формулировкой; модель учится останавливаться, когда ⇒ DONE. Образцы DONE должны быть реальными страницами приземления после выполненного действия.
- Если в каждом образце DONE ровно одно предшествующее действие, а в каждом образце клика — ни одного, модель учится любая история ⇒ DONE. Добавьте негативы из середины задачи.
- Mind2Web сам по себе убивает DONE / TYPE_TEXT (там нет DONE, доминирует CLICK). Перевзвесьте редкие операции (DONE ×4, TYPE_TEXT / SELECT ×3).
- Урезание текста страницы до 3,000 символов ничего не сэкономило (голова доминирует в последовательности) и стоило 0.04 top-1.
torch.compileна батчах переменной длины перекомпилирует под каждую форму: в 6 раз медленнее. Отключение gradient checkpointing было настоящим бесплатным выигрышем (1.25×).- Эскалация с gating по уверенности к локальному LLM на 8B или 27B сделала результаты хуже; на этих страницах дообученная модель 322M — лучший решатель (27B с бюджетом в 300 токенов на размышление: 0.861 точности операции / 0.603 top-1 при 4.7 с на шаг, против 0.890 / 0.623 при 21 мс). Для дальнейшего выигрыша от DAgger нужен более сильный учитель.
- DOM-ридер jev скрывает поля паролей по замыслу и никогда не видит свёрнутые меню; некоторые «сбои» — это фреймворк, а не модель.
Воспроизведение
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 в том репозитории содержит все промежуточные числа от первой попытки до
финальной, а results/ хранит JSON-файлы набора по каждому запуску, стоящие за таблицей выше.