0. 简介

Zetta 面向抓取、搬运、放置等机器人操作任务,处理的是基础策略执行过程中失去接触、抓取滑落和进度停滞等问题。清华大学 AIR 与 Z-Trans AI 的团队把运行时监测、恢复动作和工具封装成可演化的 Harness(执行支撑系统),保持底层策略权重与裁决代理固定,通过失败诊断、代码修补和独立验证积累能力。论文 v1 在 LIBERO-Pro 两个 Goal 设置上报告平均成功率从34.5%升至90.8%,在 RoboCasa 的18项任务上从73.56%升至93.56%。结合公开源码,可以具体看到这些能力如何进入动作循环,以及一段恢复代码需要经过哪些检验才能成为正式技能。项目主页在Github上。

1. 从一次搬运失败理解执行闭环

1.1 模型知道目标,执行状态仍可能偏离

设想一个任务:机器人把桌上的物体放到指定容器里。模型先识别目标,再靠近、闭合夹爪、抬起并搬运,看上去已经完成了最难的语义理解。途中物体轻微滑落,后续动作却继续朝容器推进,最终夹爪在目标上方打开,任务失败。这类问题往往始于一个局部物理事件,却会破坏后面整段操作的前提。对机器人而言,“夹爪已经闭合”“物体已经抓稳”“搬运期间物体仍在手中”是三个不同事实,可靠执行必须持续验证它们,不能用最初的动作意图代替后续状态。

这里要厘清两个层次的闭环。许多 VLA,也就是视觉—语言—动作模型,本来就会反复读取图像和机器人状态,然后预测下一段动作,所以不能笼统说所有 VLA 都是开环。Zetta 主要补充的是监测与恢复的闭环:系统要明确判断当前技能的物理前提是否仍成立,发现异常后切换到恢复过程,并验证何时恢复正常策略。基础模型每次看见新图像,并不保证它能及时识别滑落、终止尚未执行的动作块,或者调用一个经过验证的重抓工具。

1.2 事后反思与动作期间干预的时间差

把整段任务执行记录称为 Rollout,其中从重置环境到成功、失败或超时的一次完整尝试称为 Episode。任务结束后再分析录像,可以帮助下一次尝试,但当前回合的物体可能已经落到不可达位置。核心问题在于,物理故障的演变速度与大模型推理的延迟不匹配:接触变化可能需要及时响应,而多模态推理、生成工具调用和等待网络返回通常需要更长时间。因此,Zetta 把频繁执行的状态检查写成轻量代码,让大模型主要承担证据判断、失败诊断与候选修补。

直觉理解:可以把机器人任务看成开车。导航系统知道目的地,但轮胎打滑时还需要及时反馈车身状态;等抵达不了目的地再复盘,已经错过当次纠偏机会。Zetta 给执行过程增加持续工作的监测与恢复机制,复杂判断则放在较慢的层级处理。

2. 整体架构:固定策略,演化执行支撑系统

2.1 三个可更新组件与两个固定组件

论文把可演化部分写成 H = { C , R , T } \mathcal{H}=\{C,R,\mathcal{T}\} H={C,R,T}。其中 C C C 是运行时 Critic 集合,负责把状态变化转成异常证据; R R R 是 Recovery Playbook,规定遇到某种故障时应该怎样恢复; T \mathcal{T} T 是可执行工具集合,承载规划、抓取、接触控制等实际操作。这里的 Critic 是监测函数,不是强化学习中的价值网络。底层动作策略 π \pi π 和在线裁决代理 A o r c h \mathcal{A}_{\mathrm{orch}} Aorch​ 在演化过程中保持固定,变化发生在它们可读取的证据与可调用的恢复能力中。这意味着系统可以增加新的行为能力,而不必更新策略参数。

优化目标也围绕这一区分展开。下面的 J J J 表示期望任务成功率, g g g 是任务目标, s 0 s_0 s0​ 是初始状态, D \mathcal{D} D 是评估时采样的任务与状态分布, τ \tau τ 是执行产生的轨迹。冻结策略的约束用 θ ( k + 1 ) = θ ( k ) \theta^{(k+1)}=\theta^{(k)} θ(k+1)=θ(k) 表达更直接,避免把“不更新参数”误解成模型输出对参数的数学导数恒为零。工程上需要保存模型与 Harness 的版本,才能确认某次成功率变化究竟来自策略替换,还是来自外部执行机制的改进。

max ⁡ H J ( H ) = E ( g , s 0 ) ∼ D [ Success ⁡ ( τ ) ∣ π θ , A o r c h , H ] , θ ( k + 1 ) = θ ( k ) . \max_{\mathcal{H}} J(\mathcal{H}) =\mathbb{E}_{(g,s_0)\sim\mathcal{D}} \left[\operatorname{Success}(\tau)\mid \pi_{\theta},\mathcal{A}_{\mathrm{orch}},\mathcal{H}\right], \qquad \theta^{(k+1)}=\theta^{(k)}. Hmax​J(H)=E(g,s0​)∼D​[Success(τ)∣πθ​,Aorch​,H],θ(k+1)=θ(k).

2.2 三个时间尺度分别完成什么

动作循环最频繁:执行动作、采集状态、运行 Critic,必要时进入恢复。批次优化循环较慢:收集一批成功与失败轨迹,按故障机制聚类,诊断并提出小范围候选修改。验证与更新循环最慢:候选接受回归测试和独立种子评估,通过后发布新的技能版本。进一步看,这三层分别回答“现在是否异常”“应该增加什么能力”“这个能力是否值得长期保留”。它们构成的是不同时间尺度的协作,论文方法章节的 Phase I、II、III 则是离线演化的工作阶段,两组划分不能简单逐项等同。

在这里插入图片描述

图左侧是当前版本的执行系统,右侧是从失败中形成下一版本的演化流程。成功轨迹提供正常执行的参照,失败轨迹提供诊断证据,经过验证的修补再回到左侧。

2.3 用三条时间带读懂能力如何积累

原论文架构图展示了组件关系,理解运行顺序还需要把动作期间、回合批次之间和版本发布之前分开看。下图把这三个时间尺度展开为三条带状流程,上方锁定策略与裁决角色,下方画出经过验证的技能如何回到执行端。
在这里插入图片描述

3. 证据层:先知道机器人实际发生了什么

3.1 动作指令与真实位移分别记录

要识别卡顿,系统必须比较“发出了移动指令”与“末端实际移动了多少”。在 robots/libero/critic_runtime.py 的 extract_libero_critic_features 中,当前末端位置来自机器人状态,实际位移由前后两步位置相减得到。下面摘录第88—100行,代码把指令幅度、真实位移和两者方向关系转成可供 Critic 使用的标量特征。输入中的 action 是七维动作,states[:3] 是末端位置,输出是一个特征字典,因此监测器可以判断是否持续存在“有命令却没有运动”的现象。

# robots/libero/critic_runtime.py:88-100
if previous_eef is not None:
    realized = states[:3] - np.asarray(previous_eef, dtype=np.float64)
    realized_norm = float(np.linalg.norm(realized))
    directional = command_norm > 1e-9 and realized_norm > 1e-9
    features["command.realization.direction_available"] = directional
    features["command.realization.eef_motion_m"] = realized_norm
    features["command.realization.stalled"] = (
        command_norm >= 0.05 and realized_norm <= 0.0005
    )
    if directional:
        features["command.realization.direction_cosine"] = float(
            np.dot(command[:3], realized) / (command_norm * realized_norm)
        )

这段实现有一个容易忽略的单位问题:指令幅度采用控制接口的动作尺度,真实位移以米计算,二者不能直接相减当成同单位误差。0.0005米对应0.5毫米,它与动作阈值一起组成当前实现的启发式卡顿判断。阈值需要结合控制频率、动作缩放和机器人动力学校准,不能原样搬到另一台设备。方向余弦只有两个向量都足够大时才计算,也避免了机器人基本静止时因数值噪声产生毫无意义的方向证据。

3.2 仿真真值与策略观测之间的边界

仓库把 privileged.* 特征放在独立的 Critic 证据通道中,允许读取仿真器里的物体、接触或任务关系状态,同时避免将这些量偷偷插入原有 VLA 观测。这种安排有助于清楚回答“谁获得了额外信息”,但仍需要认识到,完整系统使用了比基础策略更多的观测与工具能力。公开实现对 Role1 可见的信息也有过滤与授权规则,部分配置允许有限的进度和接触证据。迁移到真机时,这些仿真状态必须换成视觉、力觉或融合估计,估计误差会直接影响监测与恢复质量。

这里的关键是,证据既要足够具体,又要能追溯到传感器或仿真状态。论文将状态、动作、语义里程碑与物理辅助信号一起保存,并把基础设施故障和有效任务失败分开:模拟器崩溃、网络中断、视频缺失不应自动成为“机器人没学会”的样本。若无效运行被丢弃或随意更换种子,成功率统计还可能偏离原来的任务分布。因此,运行记录需要保留逻辑种子、初始状态、策略版本和有效性判断,才能支撑后续诊断及配对评估。

4. 时序 Critic:把异常证据接入动作循环

4.1 激活条件、连续证据与冷却时间

一个“物体没有随手移动”的规则,应该在抓取已经建立之后起作用,否则接近阶段也会不断报警。 zetta/evolution/critic.py 中的 TemporalCritic.evaluate 为每条规则保存连续计数、历史窗口和冷却状态,并先检查 activation_conditions。下面摘录第62—71行:激活条件不成立时清空历史,重新开始一个时序阶段。换句话说,抓取前的静止证据不会混进抓取后的停滞判断,这比给单帧状态加一个阈值更接近真实任务的逻辑。

# zetta/evolution/critic.py:62-71
if not all(
    self._predicate(condition, observation)
    for condition in rule.activation_conditions
):
    # A false activation guard starts a fresh temporal epoch. This
    # prevents pre-contact history satisfying a post-contact rule.
    state.consecutive = 0
    state.cooldown_remaining = 0
    state.history.clear()
    continue

激活之后,普通阈值规则需要连续满足 dwell_steps 才触发,避免单步噪声导致频繁介入;触发后再用 cooldown_steps 限制重复提案。停滞规则略有不同,它直接检查一个完整窗口里的数值变化范围,所以窗口一旦满足条件即可提案。下面摘录同一文件第128—133行,history 是窗口数据,threshold 是允许的变化幅度。这里的“停滞”仅表示该特征在窗口内近似不变,是否构成任务异常还取决于激活条件与特征的物理意义。

# zetta/evolution/critic.py:128-133
def _condition(rule: CriticRule, value: Any, history: list[Any]) -> bool:
    if rule.operator == "stagnant":
        if len(history) < rule.dwell_steps:
            return False
        numbers = [_numeric(item) for item in history]
        return max(numbers) - min(numbers) <= _numeric(rule.threshold)

4.2 动作块内部及时中断

不少策略一次预测多个动作,再由运行时按顺序执行。如果监测器只在整块完成后运行,虽然策略仍周期性读取观测,故障后的剩余动作却已经被执行。Zetta 当前 LIBERO 后端在 _chunk_step_one 中逐动作推进环境,并在每次动作之后调用 _evaluate_critic。下面是 rollout_runtime/backends/rlinf_env.py 第1618—1633行:发现提案且开启中断选项时立即退出内部循环,把尚未执行的动作留在块外,交给上层处理。

# rollout_runtime/backends/rlinf_env.py:1618-1633
step_proposals = self._evaluate_critic(
    slot, obs, reward=reward_value, action=action
)
if step_proposals:
    step_info["critic_proposal_rule_ids"] = [
        row["rule_id"] for row in step_proposals
    ]
    proposals.extend(step_proposals)
per_step_info.append(step_info)
frames.append(self._observation(slot_index, obs))
if step_proposals and slot.critic_interrupt_on_proposal:
    # Hitting a proposal breaks out of the physical action loop
    # early: matches the legacy ``critic_chunk_step``'s
    # ``interrupt_on_proposal`` semantics, so the remaining
    # action block is not executed.
    break

动作频率监测有明确的实现边界:这段代码检查的是每个环境动作执行后的状态,并不意味着覆盖仿真器每个物理积分子步,也不证明实现了千赫兹硬实时控制。检查时已经发生的那一个动作仍可能造成损伤,Critic 的作用是减少异常继续扩大,并为后续恢复提供证据。RoboCasa 的会话实现也有逐动作评价路径,但其他后端可能只有动作块级证据,因此必须逐个后端核对执行粒度,不能把统一接口误认为统一的实时性能。

工程价值:动作块像一串提前排好的路线指令。逐动作监测相当于每经过一个路口都检查道路是否仍通畅,一旦发现封路就停止执行后面的路线;它缩短了发现问题后的继续行驶距离,但无法撤销已经经过的那个路口。

4.3 沿着动作时间轴看一次中断

概念图2把代码里的 break 放回时间轴:策略先给出一个动作块,运行时执行其中一个动作后读取状态,满足时序条件才产生提案。图中选第三个动作为示意触发点,仅用于区分已执行和未执行的部分,不能理解成检测器固定等待三步。下方证据链继续拆出状态提取、特征计算、规则评价与裁决职责,帮助读者把“及时中断”和“获准恢复”分成两个相继发生的事件。真正运行时的窗口长度、激活阶段与冷却规则,都应从相应技能配置读取。

在这里插入图片描述

5. 从提案到恢复:执行权与重入条件

5.1 监测器提交证据,裁决路径决定执行

Critic 输出包含规则编号、触发步、特征、观测值、建议方案,以及 environment_write=False。这些字段把它限制在提案与证据生成职责内;真正推进环境的是获得执行权限的工具。论文将这层判断交给 Orchestrator,当前 LIBERO 恢复路径则通过 Role1 适配器提交事件。下面摘录 robots/libero/role1_recovery.py 第268—277行,显示裁决结果仍受冻结恢复步骤约束,不能凭一次代理输出直接宣布任务结束或绕过指定工具。

# robots/libero/role1_recovery.py:268-277
effect = self.adapter.decide(event, image_payloads=images)
if effect.termination.approved or effect.direct_action is not None:
    raise LiberoRecoveryActorError(
        "Role1 cannot terminate or bypass a frozen recovery step"
    )
selected = effect.selected_tool or event.current_tool
if selected != required_role1_tool:
    raise LiberoRecoveryActorError(
        "Role1 did not select the current frozen recovery tool"
    )

这段代码对应的是候选已冻结后的执行流程,允许调用的工具与当前恢复步骤都事先确定,Role1 的自由度受到限制。若允许修改工具参数,运行记录还会保存基础参数和实际参数的哈希,便于核查行为变化。这里的关键是,异常发现、证据裁决与环境动作具有分开的责任边界。不过权限分离只减少某些错误路径,无法保证证据一定正确,也无法替代机器人驱动层的限位、急停与碰撞约束;在真机上仍需要独立的安全控制机制。

5.2 恢复要跨动作块持续,交回策略要验证状态

一次恢复可能包含重新接近、重新抓取、确认接触等多步操作。若每次收到新的 VLA 动作块就把恢复状态清空,系统会反复开始第一步,或者在恢复尚未结束时被普通策略覆盖。RoboCasa 的 RecoveryController 用执行编号、技能包哈希、当前步骤和状态保存回合内的恢复进度。下面摘录 robots/robocasa/recovery_controller.py 第149—156行:步骤只有在指定工具确实执行,或明确验证无需动作时才能前进,避免把“函数返回了”当作“恢复已经发生”。

# robots/robocasa/recovery_controller.py:149-156
step = self._rule["steps"][self.state.current_step_index]
required_tool = str(step.get("tool", ""))
if selected_tool != required_tool:
    raise ValueError("Actor did not execute the frozen recovery step tool")
if executed_horizon < 1 and not bool(no_op_verified):
    raise ValueError("recovery step produced no environment action")
next_index = self.state.current_step_index + 1
complete = next_index >= len(self._rule["steps"])

完成工具列表仍然只是程序层面的进度,物体可能继续晃动,抓取也可能没有真正稳定。论文提出 VLA 重入契约 Ψ ( s t ) \Psi(s_t) Ψ(st​),要求原来的异常证据已清除,同时状态稳定性满足条件,才把控制交回基础策略。下面的 1 \mathbf{1} 1 表示真假条件对应的指示值, γ \gamma γ 是稳定性门槛。若 Stability 定义为“越大越稳定”的得分,就使用大于关系;若直接使用接触力矩的振荡幅度,通常应该检查它低于阈值。两种定义不能混用,否则恢复结束条件会被写反。

Ψ ( s t ) = 1  ⁣ [ FailureCleared ⁡ ( s t ) ] ∧ 1  ⁣ [ Stability ⁡ ( s t ) > γ ] . \Psi(s_t) =\mathbf{1}\!\left[\operatorname{FailureCleared}(s_t)\right] \land \mathbf{1}\!\left[\operatorname{Stability}(s_t)>\gamma\right]. Ψ(st​)=1[FailureCleared(st​)]∧1[Stability(st​)>γ].

恢复步骤完成与物理重入条件成立需要分别验证。上述控制器负责跨动作块维持进度,它本身并没有在这几行代码里实现通用的接触平衡判据,也不负责进程崩溃后的仿真状态恢复。对于具体任务,异常清除、接触稳定和目标完成要由相应工具与状态谓词落实,并保存在审计记录里。换句话说,复现时需要沿着真实调用链检查每个条件是否执行,不能只在技能描述文件中写一句“恢复稳定后返回”就认为契约已经落地。

在这里插入图片描述

图中按一次搬运回合中的先后顺序展示恢复:抓取丢失后重新接近,发现重抓姿态不可行后调用 GraspGen,接近目标时再执行稳定放置。这里的 PnP 指 Pick-and-Place,也就是抓取与放置,CAP 指基于代码的策略。图中的每个节点都应理解成带前置条件与结果验证的工具调用,后续运输依赖前一步重新建立的物理状态。这样的案例解释了长任务成功率为何会受到多次局部纠错影响,但单个展示回合仍不能替代批量统计与独立验证。

难点提示:重入契约像维修后的试车验收。更换零件只是完成维修步骤,车辆还要通过实际运转检查才能重新上路;恢复工具执行完毕与抓取状态真正稳定也存在同样的区别。把交接条件写成可检查谓词,才能减少恢复结束时的二次故障。

5.3 把程序完成与物理验收画成两个步骤

下图将“工具步骤结束”放在两项物理检查之前,目的是让控制权交接的条件可见。恢复控制器可以正确执行全部工具,接触状态却仍未达到任务要求;反过来,传感器瞬时看见一次接触,也不能自动说明搬运已经稳定。图中用一个明确的“且”连接故障清除与状态稳定,只有两项均成立才返回普通策略,否则保留在受约束恢复路径,按预算继续检查或退出。这是对论文重入契约的解释,具体后端是否实现这些检查,还要回到工具和状态谓词的真实调用链核对。

在这里插入图片描述

6. 从失败记录到候选技能:诊断与最小修补

6.1 首个缺失里程碑与最早发散点

…详情请参照古月居

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐