【VLA工程】(3)—— 运行时的动作限幅与安全停
文章目录
- 【VLA工程】(3)—— 运行时的动作限幅与安全停
-
- 1. 原始输出与下发命令要分开
- 2. 动作限幅的四层约束
-
- 2.1 工作空间盒
- 2.2 单步步长与速度
- 2.3 关节限位
- 2.4 夹爪开合
- 3. 安全停的触发条件
- 4. 停机优先级
- 4.1 锁存与复位的状态机口径
- 5. 日志字段与回合数据对齐
- 6. 可运行示意:限幅与安全监视
- 7. 人机协同与复位纪律
- 8. 适用边界
- 9. 小结
摘要:策略模型给出的动作,不能默认等于可以下发到机械臂的命令。本篇把 限幅(按工作空间、步长、关节与夹爪约束裁剪动作)与 安全停(急停、超时、通信中断等条件下清零并锁存)写成运行时的固定层,并约定结果码与日志字段,便于和回合数据对齐复盘。贯穿示例仍是桌面摆放:把红色方块放到蓝色托盘。适合已经能跑通仿真或示教回放、准备上受控真机试用的工程师。读完可以落地一版「先裁剪、再监视、再下发」的控制周期壳。

前文见 【VLA工程】(1)—— 上真机前的工程准备 与 【VLA工程】(2)—— 回合数据与轨迹的组织方式。第 1 篇要求动作有边界、安全可切断;第 2 篇要求入库记下实际下发值。本篇把这两条落到控制周期里。
1. 原始输出与下发命令要分开
桌面摆放任务里,模型可能一次给出过大的末端增量,或在托盘边缘给出指向桌外的位移。若把原始向量直接送给控制器,常见后果是:撞限位、扫倒物块、夹爪在空中乱开合。演示视频往往剪掉这些片段;工程侧必须假设它们会发生。
因此运行时至少拆成两路字段:
| action_raw | 策略本步原始输出 |
| action_cmd | 经限幅与安全逻辑后实际下发的命令 |
复盘时若只看 action_raw,会误判「限幅没生效」;只看视频,又说不清停机是人按的还是超时触发的。两路字段与第 2 篇的轨迹步对齐后,版本对比才站得住。
也可以只在调试期同时落两路,量产评测只强制 action_cmd。但只要做过「模型改了、限幅没改」或「限幅改了、模型没改」的对比实验,就会发现缺少 action_raw 时很难解释成功率波动来自哪一层。
2. 动作限幅的四层约束
限幅不是「加个 clip 函数」一句话能概括的。桌面任务建议按层声明,并写进同一份配置文件。

2.1 工作空间盒
末端位置(或增量积分后的目标位姿)必须落在允许区域内。桌面摆放示例可先用轴对齐盒:桌面上方一定高度范围内、托盘周围留安全边距。盒外目标不「尽量靠近边界继续跑」也可以,但团队要事先选定一种策略并写死,避免每个人本地改阈值。
2.2 单步步长与速度
策略频率常低于控制频率。单步最大位移、最大转角、最大夹爪开合变化量要有上限,抑制模型一次给出的跃迁。限幅后的命令仍可能被控制器插值;入库仍记限幅后的目标,而不是插值中间点(除非评测明确要求轨迹密采样)。
2.3 关节限位
末端合法不等于关节合法。冗余臂或奇异附近,逆解可能把某一关节推到硬限位。运行时应在关节空间再裁一次,或在无合法逆解时直接进入安全停,而不是反复尝试发散的解。
2.4 夹爪开合
夹爪建议单独限幅:最大开度、最小开度、单步开合变化量。许多桌面失败来自「到点了但没夹紧」或「夹紧后过早松开」;开合边沿应能从日志判断,而不是淹没在连续开度噪声里。
四层可以在同一函数里顺序执行,但配置项与日志标志建议分开,便于统计「到底哪一层最常触发」。
桌面摆放的起步参数可以先偏紧:例如单步最大位移 1~2 cm、工作空间盒只覆盖托盘与取物区、夹爪开度限制在夹具行程内。偏紧的代价是任务更难完成;偏松的代价是撞机。受控试用阶段优先接受前者。
限幅层应独立于模型仓库版本:权重可以一天换三次,Limits 配置的变更要单独记版本号(例如 limits_ver),并写入回合元数据。否则「成功率掉了」无法回答是模型退步还是有人把盒子放大了。
3. 安全停的触发条件
安全停指:本回合不再继续执行策略动作,命令清零或切到预定义保持姿态,并写 result=safety_stop(或更细的原因码),直到人工复位或会话明确重启。它与「抓空失败后自然结束」不同:后者可以记 fail_grasp,手臂不必锁死;前者默认锁存,防止下一周期策略又把臂甩出去。

建议至少覆盖:
| 人工急停 | 硬件按钮或软件急停置位 | safety_stop,safety_reason=e_stop |
| 任务超时 | 超过约定时长未达完成定义 | timeout 或 safety_stop(团队选一种写死) |
| 持续越限 | 连续 N 步限幅后命令与原始输出差过大 / 无合法逆解 | safety_stop,safety_reason=limit |
| 通信中断 | 策略进程或控制器心跳超时 | safety_stop,safety_reason=comm |
| 力矩 / 碰撞 | 关节力矩或外部碰撞传感器超阈 | safety_stop,safety_reason=wrench |
超时是否并入 safety_stop,第 1 篇已允许两者分立。本篇建议:对外统计成功率时,timeout 与 safety_stop 都不要算进 success;对内排障时分开计数,避免把「模型慢」和「人拍了急停」混成一个数字。
「持续越限」不要第一步越限就停:偶发裁剪是限幅的正常工作。更稳妥的规则是:连续 N 步(例如 10 步)clip_flags 非空且命令范数接近零,或逆解连续失败,再升为安全停。N 写进配置,并在日志里带上计数,避免口头争论「抖了几下算不算要停」。
通信心跳建议双向:策略进程要确认控制器在线,控制器也要确认策略在线。只做单向心跳时,常见盲区是策略卡死在推理里,控制器仍以为「上一命令有效」而继续跟踪旧目标。
4. 停机优先级
多条件同时成立时,需要明确优先级,否则日志里会出现互相矛盾的原因码。

推荐顺序:
同级冲突时取更严动作:清零优先于「限幅后继续」。复位流程也要写清:谁有权复位、复位后是否允许自动续跑。桌面试用阶段,默认 复位后不自动续跑,由人确认初始布局再开新回合。
4.1 锁存与复位的状态机口径
把安全层看成很小的状态机有助于实现一致:
| RUNNING | 限幅后下发 | 触发任一条安全条件 → LOCKED |
| LOCKED | 只允许零命令或保持姿态 | 显式 reset_safety() 且人确认布局 → RUNNING |
| FAULT | 与 LOCKED 相同,但要求检修记录 | 人工清故障码后进入 LOCKED 或直接回 RUNNING |
不必上完整状态机框架;用枚举加几条转移规则即可。关键是:普通控制周期不得把 LOCKED 清成 RUNNING。曾经出现过「超时停了,下一秒策略又动起来」的现场事故,根因往往是把复位写进了默认分支。
桌面摆放试用时,也可以把 FAULT 与 LOCKED 合并,先保证「停下就不动」,再细分故障码。
5. 日志字段与回合数据对齐
限幅与安全停若不落日志,第 2 篇的轨迹表就缺关键列。最小集合如下。

| action_raw / action_cmd | 区分模型意图与实际命令 |
| clip_flags | 本步触发了哪些限幅层(可用位标志或字符串列表) |
| safety_reason | 停机原因;未停机可为空或 none |
| result | 回合级结果,含 safety_stop / timeout |
| t_stop | 停机时间戳,便于和视频对齐 |
批跑统计时,除成功率外,建议固定输出:safety_stop 次数、各 safety_reason 计数、限幅触发率(多少步至少触发一层)。这些数字比「感觉臂在抖」更适合做版本对比。
与第 2 篇目录对齐时,可以把逐步字段写进 steps.jsonl,把 safety_reason、t_stop、limits_ver 写进 meta.json。这样过滤「所有因 e_stop 结束的回合」只需要扫元数据,不必解开整段轨迹。
6. 可运行示意:限幅与安全监视
下面用最小 Python 结构表达「单步限幅 + 超时安全停」。真实系统还要接硬件急停与力矩通道;此处先把软件壳钉死。
from __future__ import annotations
from dataclasses import dataclass
from typing import List, Optional
@dataclass
class WorkspaceBox:
x_min: float
x_max: float
y_min: float
y_max: float
z_min: float
z_max: float
@dataclass
class Limits:
box: WorkspaceBox
max_step: float # 末端单步最大位移(米)
grip_min: float
grip_max: float
@dataclass
class Action:
dx: float
dy: float
dz: float
grip: float
def clip_action(pose, raw: Action, lim: Limits) –> tuple[Action, List[str]]:
"""对末端增量与夹爪做最小限幅,返回命令与触发标志。"""
flags: List[str] = []
dx, dy, dz = raw.dx, raw.dy, raw.dz
step = (dx * dx + dy * dy + dz * dz) ** 0.5
if step > lim.max_step and step > 1e-9:
scale = lim.max_step / step
dx, dy, dz = dx * scale, dy * scale, dz * scale
flags.append("step")
nx, ny, nz = pose[0] + dx, pose[1] + dy, pose[2] + dz
b = lim.box
if not (b.x_min <= nx <= b.x_max and b.y_min <= ny <= b.y_max and b.z_min <= nz <= b.z_max):
nx = min(max(nx, b.x_min), b.x_max)
ny = min(max(ny, b.y_min), b.y_max)
nz = min(max(nz, b.z_min), b.z_max)
dx, dy, dz = nx – pose[0], ny – pose[1], nz – pose[2]
flags.append("box")
grip = min(max(raw.grip, lim.grip_min), lim.grip_max)
if grip != raw.grip:
flags.append("grip")
return Action(dx, dy, dz, grip), flags
@dataclass
class SafetyState:
e_stop: bool = False
t0: float = 0.0
timeout_s: float = 20.0
locked: bool = False
reason: Optional[str] = None
def check(self, now: float) –> Optional[str]:
if self.locked:
return self.reason
if self.e_stop:
self.locked, self.reason = True, "e_stop"
return self.reason
if now – self.t0 > self.timeout_s:
self.locked, self.reason = True, "timeout"
return self.reason
return None
def control_step(pose, raw: Action, lim: Limits, safety: SafetyState, now: float):
reason = safety.check(now)
if reason:
zero = Action(0.0, 0.0, 0.0, pose_grip_hold(pose))
return zero, [], reason
cmd, flags = clip_action(pose, raw, lim)
return cmd, flags, None
def pose_grip_hold(pose) –> float:
"""示意:停机时保持当前夹爪开度,具体从状态向量读取。"""
return float(pose[3]) if len(pose) > 3 else 0.0
要点:
- 安全条件成立时 先停再记,不要先下发再补日志。
- clip_flags 与 safety_reason 分开:限幅是常态裁剪,安全停是回合级事件。
- 仿真与真机共用同一套 Limits 与原因码;换机只换驱动,不换判定口径。

7. 人机协同与复位纪律
受控真机试用阶段,建议固定三条纪律:
示教回放接入限幅层时同样有效:回放轨迹越限,应能触发与在线策略相同的停机路径,用来验收壳本身,而不是只验收神经网络。
联调顺序建议:先用脚本动作(固定点到点)验证限幅与急停链路,再接示教回放,最后接 VLA 权重。三步都用同一份 Limits 与原因码。若只有第三步才第一次接通急停,排障时很难判断是模型问题还是壳未就绪。
8. 适用边界
本篇适合:
- 仿真已能跑通,准备上桌面臂受控试用
- 已有策略输出,但缺少统一限幅与停机码
- 需要把安全事件写进与第 2 篇兼容的回合日志
本篇不展开:
- 完整功能安全认证(ISO 等)与电气急停回路设计细则
- 力控 / 柔顺控制算法本身
- 多臂协调与移动基座的安全区规划
桌面摆放可以把工作空间盒设得偏紧:先保证「出界就停」,再随标定与夹具稳定逐步放宽。过早把盒开得很大,等于把约束推回模型,运行时层形同虚设。
与前文衔接:第 1 篇声明了动作边界与安全停结果码;第 2 篇要求轨迹记实际下发值;本篇给出控制周期内如何生成这些字段。没有本篇,前面的口径仍停在配置表与口头约定。
常见验收口诀可以写成三句话:急停一按必须停;超时必须停;出盒命令不得原样下发。三句都能在仿真里用脚本动作复现,再上真机,成本通常低于「先看模型会不会抓」。
9. 小结
若只能先做一件事:给当前控制循环加上工作空间盒与单步步长限幅,并把越限或急停写成 safety_stop。这一步不提高模型上限,但能把「偶然没撞」和「约束生效」区分开。
把这层壳接到仿真批跑后,再统计:success 比例、safety_stop 比例、各 safety_reason 占比。三组数字同时看,比单独盯成功率更不容易误判「模型变强了」。
限幅与安全停稳定之后,下一篇适合把同一套结果码接到版本对比与批跑评测上,让「换权重是否更安全、更成功」变成可核对的表,而不是群里的观感。
系列导航:
- 上一篇:【VLA工程】(2)—— 回合数据与轨迹的组织方式
- 下一篇:版本对比与批跑评测的口径(撰写中)




