UI-Voyager: A Self-Evolving GUI Agent Learning via Failed Experience

Paper: arXiv:2603.24533 Code: ui-voyager/UI-Voyager Code reference: main @ 67b65e2b (2026-04-03)

1. Motivation(研究动机)

移动端 GUI agent 已经从“看图回答”推进到真实手机环境中的多轮操作:agent 需要理解屏幕、调用点击/滑动/输入等工具、在动态 UI 中完成跨 app 的任务。论文指出,AndroidWorld 这类任务有三个让训练变难的特征:界面布局和初始状态会随机变化,交互动作丰富且带异步状态转移,任务 horizon 可到约 30 步;因此,一个早期错误可能让整条轨迹失败,而最终 reward 只告诉模型“整条失败”。

现有方法的核心瓶颈不是没有失败样本,而是失败样本没有被有效利用。常规 SFT 往往只保留成功轨迹,失败轨迹被丢弃;GRPO/PPO 这类 RL 方法虽然能使用成功/失败信号,但在 GUI long-horizon sparse reward 下存在严重的 credit assignment(信用分配)问题:整条轨迹里的所有 token/action 共享同一个 trajectory-level advantage,无法定位到底是哪一步造成失败。论文用一个直观例子说明:第 5 步一个错误动作会让 30 步轨迹得 0 分,但其他 29 个本来正确的动作也被一起惩罚。

UI-Voyager 要解决的具体问题是:如何让移动 GUI agent 在没有人工逐步标注、没有外部 teacher policy 的情况下,从同一任务的成功/失败 rollouts 中抽取“关键错误步”的监督信号。这个问题值得研究,因为一旦可以自动把失败经验改造成高质量 step-level SFT 数据,GUI agent 就能形成数据—模型自进化闭环:模型越强,采样到的成功轨迹越多;成功轨迹越多,失败轨迹中可纠正的 fork point 越多;新的纠错样本又继续提升模型。

Figure 1 解读:这张图把 UI-Voyager 放在不同参数规模的 GUI agents / VLMs 中比较。关键点不是单纯“最高”,而是 4B 模型达到 81.0% Pass@1,超过 Human 的 80.0%,也高于 MAI-UI-235B-A22B 的 76.7%、UI-Tars-2 的 73.3% 和 Qwen3-VL-235B-A22B 的 63.7%。论文的主张因此集中在训练机制的数据效率,而不是扩大模型规模。

2. Idea(核心思想)

核心洞察:同一任务的多条 rollout 往往会在某些屏幕状态上“相遇”,但后续动作不同;如果其中一条最终成功、另一条最终失败,那么这些相同状态后的分叉点就是最有价值的 step-level supervision。UI-Voyager 不再把失败轨迹整体丢弃,也不只给整条轨迹一个 0/1 reward,而是把成功轨迹在 fork point 的正确动作蒸馏到失败轨迹的上下文里。

关键创新是一个两阶段自进化框架:第一阶段 Rejection Fine-Tuning(RFT)用自动生成任务、多轨迹采样和 rule-based verifier 筛出成功轨迹做 SFT;第二阶段 Group Relative Self-Distillation(GRSD)在同一任务的成功/失败组内用 SSIM 找到相同屏幕状态,构造“失败上下文 + 成功动作”的自纠错样本,再用标准 autoregressive SFT 训练。

和 EvoCUA 一类依赖外部 VLM 合成 correction trace 的方法相比,UI-Voyager 的差别在于它把“同组成功轨迹”当作 teacher,不需要额外模型来判断或重写失败步骤;和 GRPO/PPO 相比,GRSD 不直接优化稀疏 trajectory reward,而是先把稀疏成败信号转化为明确的 step-level imitation target。

3. Method(方法)

3.1 Overall framework:两阶段 self-evolving pipeline

Figure 2 解读:左半部分是 RFT:prior policy / base policy 在 AndroidWorld 环境中对每个 query 生成 条轨迹,rule-based verifier 只保留成功轨迹,得到 SFT 样本并更新 policy。右半部分是 GRSD:同一 query 的多条 rollout 被分成成功组和失败组,Fork Point Detection 用 SSIM Matching 找到相同 observation 后动作不同的位置,再把成功轨迹的 action/reasoning/tool call 替换到失败轨迹对应上下文中,最后用 mixed samples 继续 SFT。

从直觉上看,RFT 解决的是“先拿到一个足够会做任务的 warm-start agent”,GRSD 解决的是“如何利用 warm-start agent 仍然会产生的大量失败样本”。如果没有 RFT,后续 rollouts 的成功样本太少,成功 teacher 不稳定;如果没有 GRSD,失败轨迹中最有信息量的局部错误又会被丢掉或只以 sparse reward 的形式弱监督。

3.2 Task formulation 与 action space

论文把一个移动 GUI 任务表示为 query 下的多步交互轨迹,step 的核心输入是当前屏幕 observation 和历史 ,输出是动作 或 assistant response。AndroidWorld 的预定义动作空间包括:click(x,y)long_press(x,y)swipe(x,y,x',y')open_app(app_name)input_text(text)keyboard_enter()navigate_back()navigate_home()wait()status(goal_status)answer(text)。这个高层动作空间让 verifier 和轨迹采样可控,但也带来 §5 中提到的低层触控泛化限制。

3.3 Stage 1:Rejection Fine-Tuning(RFT)

RFT 包含三步。

  1. Trajectory generation:seed task generator 对原始任务模板中的时间约束、数量、文件实体等关键参数做扰动,生成新的任务实例;GUI agent 自动执行这些任务,产出多条轨迹。
  2. Rejection sampling:rule-based verifier 检查轨迹是否达到预定义目标或通过任务完成验证,只保留成功轨迹。这样得到的 SFT corpus 同时保证了 trajectory structure 和 action correctness。
  3. Iterative training:第一轮用 Qwen3-VL 系列生成轨迹,并以 Qwen3-VL-4B-Instruct 作为 SFT backbone;后续轮次用上一轮更新后的模型继续生成新轨迹,筛选成功样本,再训练下一轮模型。论文报告三轮后 Pass@1 从 37% 提升到 73%,并选择第三轮 RFT checkpoint(Pass@1 = 73.2%)作为 GRSD/GRPO/PPO 的起点。

RFT 的作用不是解决失败归因,而是把模型推到“能经常产生成功轨迹”的状态。只有当同一任务下同时存在成功和失败 rollouts 时,GRSD 才能抽取可靠的 teacher action;所以 RFT 是 GRSD 的数据前提。

3.4 Stage 2:为什么 GRPO/PPO 在这里低效

论文先写出 GRPO/PPO 的标准形式,用来说明 sparse reward 下的信用分配问题。GRPO 对每个任务 采样 ,目标为: 其中 PPO 目标为: 对 GUI agent 来说,基础 reward 只有成功为 1、失败为 0。于是同一条失败轨迹中所有 token/action 的 advantage 都一样,模型不知道哪个 step 是真正错误点,也不知道正确 action 应该是什么。GRSD 的设计就是把这类 trajectory-level signal 转换成局部的 action-level target。

3.5 Fork Point Detection:用 SSIM 找到“同屏不同动作”的关键点

Figure 3 解读:成功轨迹 和失败轨迹 来自同一个任务。Fork point 不是任意失败 step,而是满足两件事的位置:当前 observation 与成功轨迹某一步足够相似,即 ;但执行各自动作后,下一步 observation 发生 divergence,即 。这样的点说明失败轨迹“看到了和成功轨迹一样的屏幕,却做了不同决策”。

给定成功轨迹 和失败轨迹 论文定义 observation equivalence: 其中 是截图预处理 pipeline。released code 中这个 pipeline 更具体:cores/fork_utils.py 先裁掉顶部状态栏比例 ,再把截图 resize 到 并转成灰度;为了加速,先用 mean-hash 做预过滤,hash similarity 低于 0.80 的 pair 直接丢弃,再计算 SSIM。

对每个失败 step ,算法先做 transition alignment:如果存在成功 step ,使得当前屏幕和下一步屏幕都匹配,即 ,说明两条轨迹还在相同转移上,失败 step 不是错误点,算法跳过并推进最小成功索引

如果不是 aligned transition,则寻找候选 teacher step。论文定义 divergence: 候选集合为 。teacher step 选择为: 也就是优先选当前屏幕最相似的成功 step,并用较小 打破 tie。算法还保持 monotonicity:后续失败 step 只能匹配不早于当前 teacher index 的成功 step,避免后面的失败状态倒退匹配到更早成功状态。

3.6 Step-Level Self Distillation:把失败上下文改造成成功监督

对每个 fork point ,GRSD 保留失败轨迹在 step 的 prompt/history/screenshot,上下文不变,只把 response 替换为成功轨迹 step 的 assistant response: 训练目标是只在 response tokens 上计算 next-token prediction loss: 其中 是构造出的 self-corrective samples, 是 prompt tokens, 是 response tokens。论文实验中 GRSD 作为唯一训练目标替代 GRPO/PPO,核心假设是:对 GUI long-horizon 任务,来自成功 peer trajectory 的局部正确 action,比 trajectory-level advantage 更直接、更低方差。

Figure 7 解读:BrowserMaze 的失败轨迹在 Step 12 错误地继续点 “Right”,成功轨迹在相同状态下选择 “Down”。GRSD 构造样本时不是重新生成整条轨迹,而是把失败轨迹当前 step 的 user prompt、历史进度和 screenshot 保留下来,只替换 assistant response,包括 reasoning、action 和 tool call。这使失败经验变成监督样本:模型看到“同样的失败前上下文”时学会输出成功动作。

3.7 Pseudocode(基于论文与 released code)

released repo main@67b65e2b 主要公开了 AndroidWorld evaluation、fork point detection 和 fork SFT data generation;RFT/GRSD 的完整训练 launch/config 没有随 repo 发布。因此下面把已发布代码路径实现的部分与论文级训练流程分开标注。

def rft_iteration(policy, seed_task_generator, env, verifier, optimizer, k=8):
    """Paper-level RFT loop; released repo does not include the training launcher."""
    sft_samples = []
    for task_template in seed_task_generator.templates:
        task = seed_task_generator.perturb(
            task_template,
            fields=["temporal_constraints", "quantities", "file_entities"],
        )
        trajectories = [policy.rollout(env, task) for _ in range(k)]
        for traj in trajectories:
            if verifier.is_success(task, traj):
                sft_samples.extend(traj.to_supervised_steps())
 
    loss = supervised_finetune(policy, sft_samples, optimizer)
    return policy, loss, sft_samples
import cv2
import numpy as np
from skimage.metrics import structural_similarity as ssim
 
SSIM_RESIZE_DIM = (480, 270)
CROP_TOP_RATIO = 25 / 878
HASH_SIZE = 16
HASH_FAST_REJECT_THRESHOLD = 0.80
 
def obs_similarity(image_path_a, image_path_b):
    """Reflects cores/fork_utils.py::obs_similarity."""
    a = cv2.imread(image_path_a)
    b = cv2.imread(image_path_b)
    if a is None or b is None:
        return -1.0
 
    def preprocess(img):
        h = img.shape[0]
        img = img[int(h * CROP_TOP_RATIO):]
        thumb = cv2.resize(img, SSIM_RESIZE_DIM, interpolation=cv2.INTER_AREA)
        gray = cv2.cvtColor(thumb, cv2.COLOR_BGR2GRAY).astype(np.float64)
        return gray
 
    def mean_hash(img):
        h = img.shape[0]
        img = img[int(h * CROP_TOP_RATIO):]
        small = cv2.resize(img, (HASH_SIZE, HASH_SIZE), interpolation=cv2.INTER_AREA)
        gray = cv2.cvtColor(small, cv2.COLOR_BGR2GRAY)
        return (gray >= gray.mean()).astype(np.uint8).flatten()
 
    hash_sim = (mean_hash(a) == mean_hash(b)).mean()
    if hash_sim < HASH_FAST_REJECT_THRESHOLD:
        return float(hash_sim)
 
    ga, gb = preprocess(a), preprocess(b)
    if ga.shape != gb.shape:
        return -1.0
    if np.array_equal(ga, gb):
        return 1.0
    return float(ssim(ga, gb, data_range=255.0))
def backward_state_match(success_steps, failed_steps, ssim_threshold=0.95):
    """Reflects cores/fork_algorithm.py::_backward_state_match."""
    matches = []
    min_success_idx = 0
 
    for failed_idx, failed_step in enumerate(failed_steps):
        failed_img = get_step_image_path(failed_step)
        if failed_img is None:
            continue
 
        transition_aligned = False
        if failed_idx + 1 < len(failed_steps):
            failed_next = get_step_image_path(failed_steps[failed_idx + 1])
            for success_idx in range(min_success_idx, len(success_steps) - 1):
                succ_img = get_step_image_path(success_steps[success_idx])
                succ_next = get_step_image_path(success_steps[success_idx + 1])
                if (obs_similarity(succ_img, failed_img) >= ssim_threshold and
                    obs_similarity(succ_next, failed_next) >= ssim_threshold):
                    min_success_idx = max(min_success_idx, success_idx + 1)
                    transition_aligned = True
                    break
        if transition_aligned:
            continue
 
        candidates = []
        failed_tool = get_step_tool_call(failed_step)
        for success_idx in range(min_success_idx, len(success_steps)):
            succ_step = success_steps[success_idx]
            succ_img = get_step_image_path(succ_step)
            sim_current = obs_similarity(succ_img, failed_img)
            if sim_current < ssim_threshold:
                continue
            if get_step_tool_call(succ_step) == failed_tool:
                continue
 
            diverged = True
            if success_idx + 1 < len(success_steps) and failed_idx + 1 < len(failed_steps):
                sim_next = obs_similarity(
                    get_step_image_path(success_steps[success_idx + 1]),
                    get_step_image_path(failed_steps[failed_idx + 1]),
                )
                diverged = sim_next < ssim_threshold
            if diverged:
                candidates.append((success_idx, sim_current))
 
        if candidates:
            best_success_idx, _ = max(candidates, key=lambda x: (x[1], -x[0]))
            matches.append((failed_idx, best_success_idx))
            min_success_idx = best_success_idx
    return matches
def build_fork_sft_entry(fail_step, success_step, task_name, succ_id, fail_id,
                         failed_step_idx, success_step_idx, sim_current, sim_next):
    """Reflects cores/fork_algorithm.py::_build_fork_sft_entry."""
    entry = deepcopy(fail_step)
    teacher_response = get_assistant_content(success_step)
    if teacher_response is None:
        return None
 
    for message in entry.get("conversations", []):
        if message.get("role") == "assistant":
            message["content"] = teacher_response
            break
 
    entry["fork_info"] = {
        "task_name": task_name,
        "succ_id": succ_id,
        "fail_id": fail_id,
        "failed_step_idx": failed_step_idx,
        "base_step_idx": success_step_idx,
        "sim_current": round(sim_current, 4),
        "sim_next": round(sim_next, 4),
    }
    return entry
import torch
import torch.nn.functional as F
 
def grsd_sft_train_step(model, tokenizer, batch, optimizer):
    """Training objective from Eq. (7); full GRSD launcher is not released."""
    input_ids = batch["input_ids"]
    attention_mask = batch["attention_mask"]
    labels = batch["labels"]
 
    logits = model(input_ids=input_ids, attention_mask=attention_mask).logits
    shift_logits = logits[:, :-1].contiguous()
    shift_labels = labels[:, 1:].contiguous()
    loss = F.cross_entropy(
        shift_logits.view(-1, shift_logits.size(-1)),
        shift_labels.view(-1),
        ignore_index=-100,
    )
    loss.backward()
    optimizer.step()
    optimizer.zero_grad()
    return loss.item()

3.8 Released code 与论文公式的对应和差异

论文公式与 released code 实现差异:released repo main@67b65e2b 只公开了 fork point detection / fork SFT 数据生成和 AndroidWorld evaluation;RFT 与 GRSD 的完整训练 launcher、optimizer 配置和混合数据训练脚本未在 repo 中出现。因此 RFT/GRSD training-loop 伪代码只能按论文描述与 arXiv source 的 hyperparameter table 还原,不能声称来自 released launch script。

论文公式与 released code 实现差异:论文把 概括为 crop-resize-grayscale,并把 SAME/DIVERGE 写成 SSIM threshold 判断;代码在 cores/fork_utils.py 中补充了工程细节:顶部裁剪比例 、resize 到 mean-hash 预过滤、hash 阈值 0.80、默认 SSIM threshold 0.95。代码还在 _backward_state_match 中显式过滤 same_tool_call 候选;这不是论文公式 (4) 的一部分,但与“相同动作不提供纠错信息”的意图一致。

Code reference: main @ 67b65e2b (2026-04-03) — pseudocode and mapping based on this commit

Paper ConceptSource FileKey Class/Function
论文/模型/评测入口README.mdrelease note、model link、AndroidWorld quick start
AndroidWorld 并行评测run_android_world.shCONFIG_NAMEMODEL_NAMENUM_WORKERS、AVD copy、result/log directory
GUI agent inference wrapperandroidworld/android_world/agents/mgui_agent.pyMGUIAgentprocess_image、tool-call prompt
fork point analysis CLIcores/run_fork_analysis.shDATA_DIRSSIM_THRESHOLD=0.95MODE=shortest_baseNUM_WORKERS=32
fork analysis drivercores/fork_main.pyargument parsing、parallel task analysis、fork_steps.jsonl / report output
teacher/failure pair selectioncores/fork_algorithm.py_select_pairs_shortest_base_select_pairs_all
SAME/DIVERGE matchingcores/fork_algorithm.py_backward_state_match
self-corrective SFT samplecores/fork_algorithm.py_build_fork_sft_entryanalyze_single_task
screenshot preprocessing / SSIMcores/fork_utils.pycrop_top_get_gray_thumbnail_get_image_hashobs_similarity
trajectory data loading / logscores/fork_utils.pyload_task_repeatssave_fork_log

4. Experimental Setup(实验设置)

Dataset / benchmark

  • AndroidWorld:主评测 benchmark,包含 116 个真实移动应用任务,任务有随机初始化参数。训练时跟随 MobileRL 使用 AndroidWorld training sets,规模为 over 7000 tasks
  • BrowserMaze / SystemBluetoothTurnOff 等任务:用于 fork point detection 和 self-corrective sample case study;BrowserMaze 示例显示中间状态 fork point,SystemBluetoothTurnOff 显示初始状态 fork point。

Model / evaluation metrics

  • Backbone:Qwen3-VL-4B-Instruct。
  • Main metric:Pass@1 success rate / success rate,即 AndroidWorld 任务是否完成的平均成功率。
  • RFT analysis metric:Pass@1 与 Pass@k,用来观察多轮 RFT 后采样成功率随 的变化。
  • Reproducibility setting:UI-Voyager 的 81.0% 是 64 independent runs / 64 random seeds 下的平均成功率;baseline numbers 来自已有论文。

Baselines

论文比较了开源与闭源 GUI agents / VLMs:Qwen3-VL series、MAI-UI、ScaleCUA、Ferret-UI Lite、Step-GUI、UI-Tars / UI-Tars-1.5 / UI-Tars-2、UI-Venus / UI-Venus-1.5、GUI-Owl / GUI-Owl-1.5-8B-Thinking、Seed1.5-VL、Seed1.8、Gemini-2.5-Pro,以及 Human baseline。训练方法对比中还比较了从同一 RFT checkpoint 初始化的 PPO、GRPO 和 GRSD。

Training / implementation config

论文正文明确给出:RFT 第三轮 checkpoint 的 Pass@1 为 73.2%,后续 GRSD/GRPO/PPO 都从该 RFT model 出发;Qwen3-VL-4B-Instruct 是 backbone;AndroidWorld training sets 超过 7000 tasks;episode 最长可到约 30 interaction steps。

arXiv source chapters/7_appendix.tex 给出 GRPO/PPO 主要超参(注意该 appendix table 在源文件中存在;released GitHub 没有对应训练 launch script):

GRPO hyperparameters(source appendix)

  • Data:Max Prompt Length 8192;Max Response Length 512;Train Batch Size 256。
  • Actor / Policy:FSDP;PPO Micro Batch Size/GPU 8;LR ;Gradient Clipping 1.0;Clip Ratio low/high 0.2/0.28;PPO Epochs 1。
  • Rollout & Sampling:Temperature 1.0;Max New Tokens 512;Number of Samples ;Max Turns 30;History Length 30。

PPO hyperparameters(source appendix)

  • Data:Max Prompt Length 8192;Max Response Length 512;Train Batch Size 256。
  • Actor / Policy:FSDP;PPO Micro Batch Size/GPU 4;LR ;Gradient Clipping 1.0;Clip Ratio 0.2;PPO Epochs 1。
  • Rollout & Sampling:Temperature 1.0;Max New Tokens 512;Max Turns 30。

Released code execution config(不是训练超参)

  • run_android_world.sh 默认 CONFIG_NAME=Qwen3-VL-4B-InstructMODEL_NAME=Qwen3-VL-4B-InstructNUM_WORKERS=4N_REPEATS=1,使用 AndroidWorldAvd 并为 worker 复制 AVD。
  • cores/run_fork_analysis.sh 默认 SSIM_THRESHOLD=0.95MODE=shortest_baseNUM_WORKERS=32MAX_LOG_SEEDS=5,输入目录格式为 eval_results/{MODEL_NAME}/results/{TIMESTAMP}/seed_{N}/{TaskName}/repeat_{N}_succ.jsonl / repeat_{N}_fail.jsonl,输出 fork_analysis_report.jsonfork_steps.jsonl

论文未详细说明完整硬件配置;released repo 也没有训练 launch script,因此不应从默认配置推断 GPU 型号/数量。

5. Experimental Results(实验结果)

5.1 Main AndroidWorld results

UI-Voyager 在 AndroidWorld 上达到 81.0% Pass@1 success rate,超过 reported Human 80.0%。论文强调这是 4B 模型,与更大模型相比也更高效。

ModelParamsSuccess Rate
Qwen3-VL-2B2B36.4
MAI-UI-2B2B49.1
ScaleCUA-3B3B23.7
Ferret-UI Lite-3B3B28.0
Qwen3-VL-4B4B45.3
Step-GUI-4B4B63.9
GUI-Owl-7B7B66.4
Step-GUI-8B8B67.7
MAI-UI-8B8B70.7
GUI-Owl-1.5-8B-Thinking8B71.6
UI-Venus-1.5-30B-A3B30B77.6
MAI-UI-32B32B73.3
UI-Tars-2230B73.3
Qwen3-VL-235B-A22B235B63.7
Gemini-2.5-Pro-69.7
Seed1.8-70.7
MAI-UI-235B-A22B235B76.7
Human-80.0
UI-Voyager4B81.0

5.2 RFT self-evolving analysis

Figure 4 解读:左图显示 Qwen3-VL-4B-Instruct 经过四轮 RFT 后,Pass@1 和 Pass@k 持续提升;论文选择第三轮 RFT checkpoint(Pass@1=73.2%)作为后续训练基础。右图显示如果直接从 Qwen3-VL-4B-Instruct 做 GRPO/PPO,约 175 training steps 才达到单轮 RFT 的 64.0% 水平,说明直接 RLVR from scratch 在 long-horizon GUI 中样本效率很低。

5.3 Fork point case studies

Figure 5 解读:BrowserMaze 中失败和成功轨迹都到达 Step 12 的相同屏幕状态;失败轨迹点击 “Right”,但右侧有墙导致无效;成功轨迹点击 “Down” 完成关键转移。这个例子展示了中间状态 fork point:错误不是整条轨迹都错,而是在某个共享 observation 下做了错误 action。

Figure 6 解读:SystemBluetoothTurnOff 的 fork point 出现在 Step 0 初始状态。失败轨迹试图通过上滑打开 settings app,成功轨迹则从顶部下滑打开 notification shade 并进入 quick settings。这个例子说明 fork point detection 不只适用于中间状态,也能发现一开始就分叉的错误决策。

5.4 GRSD versus GRPO/PPO

Figure 8 解读:三种方法都从同一个 RFT model(73.2% success rate)开始。GRSD 提升到 81%,而 GRPO/PPO 进展慢并在约 76% 附近 plateau。论文把差距归因于两点:GRPO/PPO 没有定位关键错误 step 的机制;GRSD 能通过 fork point detection 把失败轨迹中的 decisive action 直接改写为监督样本。

Figure 9 解读:论文挑选 RFT baseline 成功率最低的十个代表任务,包括 RecipeDeleteDuplicateRecipes2、SystemCopyToClipboard、AudioRecorderRecordAudioWithFileName、TasksCompletedTasksForDate、VlcCreateTwoPlaylists、SportsTrackerActivitiesOnDate、SportsTrackerTotalDistanceForCategoryOverInterval、RetroCreatePlaylist、TasksDueOnDate、RecipeDeleteMultipleRecipesWithConstraint。GRSD 在这些低成功率任务上整体高于 PPO/GRPO,说明它特别适合“成功样本稀少但同组轨迹可对齐”的 sparse-reward 场景。

5.5 Limitations and practical caveats

论文讨论了两个主要限制。

  1. SSIM-based matching 在实时环境中不完美:移动 GUI 是异步执行的,截图会受动画、键盘弹出、loading、toast、cursor blinking、时钟变化等影响。相同逻辑状态可能 SSIM 偏低,不同状态也可能因局部相似而 SSIM 偏高。论文建议未来做 time-aware matching、短 temporal window 匹配、对高方差区域做 mask,并融合 OCR/layout/accessibility-tree signals。
  2. 高层 predefined action space 限制泛化:AndroidWorld 的 click/swipe/type/navigation 等高层动作让训练和验证更稳定,但忽略了 AndroidEnv 强调的低层触控动态,例如 gesture duration、trajectory shape、release timing、actuation noise。未来可以用 hierarchical action modeling:先用高层动作获得样本效率,再在 post-training 中引入低层手势扰动增强迁移。

总体结论:UI-Voyager 的贡献不是提出一个更复杂的 reward model,而是把同一任务的成功/失败 rollout 关系转化为可训练的 step-level self-distillation 数据。结果上,4B 模型在 AndroidWorld 的 116 个任务上达到 81.0% Pass@1;机制上,RFT 提供 warm-start,GRSD 负责从失败经验中抽取纠错监督,是两者共同支撑 self-evolving GUI agent 的性能提升。