机器人调度平台 机器人调度平台实战

一套任务模型适配所有品牌:我们是怎么设计参数化命令数组的

分享到微博

上一篇我们分享了通讯架构选型的踩坑经历——从 UDP 到 WebSocket + MQTT 双通道。这一篇我们深入到任务模型——怎么描述一个机器人任务?不同品牌的任务格式千差万别,有的用 JSON、有的用文本命令、有的用 XML、有的只支持图形化拖拽。我们试过好几种方案,最后定了「参数化命令数组」。本文分享这个设计过程中的踩坑和经验,以及标识符、单位、时间这些「小事」为什么决定了平台的可维护性。


一、一开始的混乱:四个品牌四种任务格式

做统一调度平台,绕不开的一个问题是:怎么描述一个机器人任务?

刚开始对接四个品牌的时候,我们发现每个品牌的任务格式完全不一样:

品牌 A(巡检机器狗)用 JSON,格式是这样的:

```json

{

"task_type": "patrol",

"waypoints": [

{"x": 1.2, "y": 0.5, "action": "take_photo"},

{"x": 3.4, "y": 1.2, "action": "read_meter"}

],

"speed": 0.5

}

```

品牌 B(人形机器人)用文本命令,一行一个命令:

```

MOVE 1200 500

GRIP bin_A force=35

PLACE bin_B

SAY "任务完成"

```

品牌 C(配送机器人)用 XML:

```xml

<mission>

<step index="1">

<command name="goto">

<param name="pos_x">1200</param>

<param name="pos_y">500</param>

</command>

</step>

<step index="2">

<command name="load">

<param name="cargo_id">C-123</param>

</command>

</step>

</mission>

```

品牌 D(协作机械臂)更绝——只支持图形化拖拽编程,根本没有文本格式的任务描述,要创建任务只能在它的软件里用鼠标拖。

四种格式,四种结构,四种单位(品牌 A 用米,品牌 B/C 用毫米),四种命令命名方式。

这种情况下,想做一个统一的任务编排界面,让用户在一个界面里创建涉及四个品牌的复合任务——根本不可能。


二、踩坑实录:我们试过的三种「错误」方案

方案一:每种品牌单独做一套任务编辑器

一开始我们想:「既然每个品牌格式不一样,那就每个品牌单独做一套任务编辑器呗。」

于是我们给品牌 A 做了一个 JSON 表单编辑器,给品牌 B 做了一个文本命令输入框,给品牌 C 做了一个 XML 可视化编辑器,给品牌 D 做了一个「跳转到品牌 D 软件」的按钮。

结果呢?

  • 用户体验稀碎——创建品牌 A 的任务用表单,创建品牌 B 的任务用文本框,创建品牌 C 的任务用可视化编辑器,创建品牌 D 的任务要跳到另一个软件。用户要学会四种操作方式。
  • 跨品牌编排不可能——想做一个「品牌 A 巡检完 → 品牌 B 去处理」的复合任务,只能分别创建两个任务,然后人工接力。
  • 维护成本爆炸——四种编辑器,四种格式校验,四种错误提示。新增一个品牌就要再做一套编辑器。

这个方案用了两个月,我们自己都受不了了,推翻重来。

方案二:用「最强大」的格式,其他品牌转换成它

第二种思路:「选一个功能最强大的格式作为平台标准,其他品牌的任务都转换成这个格式。」

我们选了品牌 A 的 JSON 格式作为标准——因为它结构最清晰、扩展性最好。然后写了一堆转换函数:

  • 品牌 B 的文本命令 → 品牌 A 的 JSON
  • 品牌 C 的 XML → 品牌 A 的 JSON
  • 品牌 D 的图形化任务 → 导出成品牌 A 的 JSON

结果又踩坑了:

  • 品牌 A 的格式不是为所有品牌设计的——它是巡检机器狗的格式,里面有 waypoints、patrol 这些巡检特有的概念。人形机器人的「抓取」「放置」「语音」这些命令,硬塞进巡检格式里,非常别扭。
  • 转换过程信息丢失——品牌 B 的文本命令里有 force=35(抓取力),品牌 A 的格式里没有这个字段,转换的时候只能丢掉。等再转回品牌 B 的时候,抓取力就没了。
  • 品牌 A 升级格式就全乱——品牌 A 升级了 SDK,改了任务格式,我们所有的转换函数都要跟着改。

用某个品牌的格式做标准,本质上还是被这个品牌「绑架」了。 这个方案也不行。

方案三:自定义一套「超级格式」,什么都能表达

第三种思路:「既然别人的格式都不合适,那我们自己定义一套超级格式,什么品牌、什么任务都能表达。」

于是我们设计了一套非常复杂的任务格式——有任务类型、有步骤、有条件分支、有循环、有变量、有函数调用……几乎把编程语言的概念都搬进来了。

结果呢?

  • 太复杂了,用户不会用——我们自己的开发人员都要看好久才能搞明白怎么写,客户的运维人员更是一头雾水。
  • 实现成本极高——要支持条件分支、循环、变量,就得做一个「任务解释器」,工作量巨大。
  • 品牌适配器更难写——把这么复杂的格式转换成各品牌的私有格式,比登天还难。大部分品牌根本不支持条件分支和循环,只能在平台端解释执行,但平台端又不知道机器人的实时状态……

过度设计是另一个极端。 这套「超级格式」最后也被我们砍掉了。


三、最终方案:参数化命令数组

踩了这么多坑之后,我们终于找到了一个平衡点——参数化命令数组

核心思想很简单:

  1. 一个任务就是一组命令的有序列表(数组)
  2. 每个命令有一个命令字(cmd)和参数对象(params)
  3. 命令字是平台定义的标准动作语义,不是任何品牌的私有命令
  4. 参数是可扩展的 JSON 对象,不同命令有不同参数

长这样:

```json

{

"mission_id": "MSN-20260823-001",

"robot_id": "RB-0001",

"commands": [

{"cmd": "navigate", "params": {"x_mm": 1200, "y_mm": 500, "yaw_mdeg": 90000}},

{"cmd": "manipulate", "params": {"target": "bin_A", "grip_force_n": 35}},

{"cmd": "voice", "params": {"text": "物料已送达"}}

],

"priority": 5

}

```

就这么简单。但简单的结构背后,有几个关键的设计决策。

决策一:用扁平数组,不用嵌套的命令链

很多品牌的任务格式是嵌套的——{action: "navigate", next: {action: "grip", next: {action: "place"}}},像链表一样一层套一层。

我们为什么用扁平数组?

  • 解析简单——顺序遍历数组就行,不用递归
  • 修改方便——在中间插入、删除、替换一个步骤,直接操作数组
  • 人类可读——一眼就能看出任务有几步、每步干什么
  • 可扩展——未来要加并行执行、条件分支,可以在命令对象里加 parallel: truecondition: "..." 字段,不需要改整体结构

嵌套结构看起来「高级」,但实际用起来处处别扭。扁平数组是最简单也最实用的选择。

决策二:命令和参数分离,不用「命令字符串」

有些品牌用「命令字符串」——"navigate 1200 500 90000",把命令和参数都塞在一个字符串里。

我们为什么要分离成 cmd + params

  • 类型安全——参数可以是 int、float、string、bool、对象、数组,不用都转成字符串再解析
  • 自描述——x_mm: 12001200 清晰得多,一看就知道是 x 坐标、单位是毫米
  • 支持可选参数——需要的参数写,不需要的不写,不用为了占位写一堆空值
  • 方便校验——可以用 JSON Schema 校验每个命令的参数类型和范围

「命令字符串」看起来简洁,但每一个用它的人都在重新实现一个「命令解析器」,而且容易出错(参数顺序错了、多了少了,运行时才发现)。分离成 cmd + params 是结构化数据的基本设计原则。

决策三:参数名带单位后缀,不用「约定俗成」

你可能注意到了,我们的参数名带单位后缀——x_mm(毫米)、yaw_mdeg(毫度)、grip_force_n(牛顿)、duration_ms(毫秒)。

为什么要这么做?因为「约定俗成」是 bug 的温床

我们踩过这个坑——一开始参数名只叫 xyyawforce,结果:

  • 开发人员 A 以为 x 的单位是米,写了 1.2
  • 开发人员 B 以为 x 的单位是毫米,写了 1200
  • 品牌适配器以为 x 的单位是厘米,收到 1200 以为是 12 米
  • 机器人直接冲到了墙上去

排查这种 bug 特别痛苦——你看到坐标是 1200,但不知道它代表 1.2 米还是 1200 毫米还是 1200 厘米。

参数名带单位后缀,从源头上消除了歧义。 多花几个字节写 _mm,能省下无数小时的调试时间。这笔账太划算了。

我们的单位标准是:

  • 坐标、尺寸、位移 → 毫米(mm),整数
  • 角度 → 毫度(mdeg),整数(1 度 = 1000 毫度)
  • 速度 → 毫米/秒(mm/s)
  • 力 → 牛顿(N)
  • 时间 → UTC 毫秒(13 位整数)
  • 持续时间 → 毫秒(ms)

为什么坐标用毫米不用米?因为机器人的定位精度通常在毫米级,用整数避免浮点精度误差。为什么角度用毫度不用度或弧度?因为整数精确、人类可读(90000 毫度 = 90 度,一眼就懂)。

决策四:命令字是平台标准语义,不是品牌私有命令

commands[*].cmd 的值(navigatemanipulatevoice)是平台定义的标准动作语义,不是任何品牌的私有命令字。

为什么?因为如果允许品牌私有命令字直接出现在平台任务里,那任务格式又变成「品牌相关」的了——品牌 A 的任务里有 GOTO,品牌 B 的任务里有 move_to,平台要认识所有品牌的所有命令字,这就回到了「格式不统一」的老问题。

标准命令语义就像「普通话」——不管你是哪里人(什么品牌),到了平台上都说普通话。品牌私有协议是「方言」,由驱动适配器负责在普通话和方言之间翻译。

目前我们定义的标准命令有:

  • navigate——导航到指定位置
  • manipulate——操作(抓取、放置、拧等)
  • voice——语音播报
  • wait——等待指定时间
  • charge——去充电桩充电
  • emergency_stop——紧急停止

新增命令类型需要平台统一规划和定义,不能某个品牌自己加。这样才能保证所有品牌的任务都能用同一套命令语义描述。

决策五:任务 ID 可读可排序,不用纯 UUID

mission_id 的格式是 MSN-YYYYMMDD-XXX,比如 MSN-20260823-001

为什么不用 UUID?UUID 虽然全局唯一,但人类不可读——8d98b920-4c8f-4f2f-9ba4-6a0f66fc4d55,谁看得出来这是什么时候的任务?

MSN-YYYYMMDD-XXX 格式的好处:

  • 可读——一眼看出是 2026 年 8 月 23 日的第 1 个任务
  • 可排序——按字符串排序就是按时间排序
  • 可追溯——客户说「8 月 23 日那个任务出问题了」,直接搜 MSN-20260823 就能找到
  • 前缀区分——MSN 前缀说明是任务 ID,和 RB(机器人)、MSG(消息)区分开

ID 不仅是给机器用的,也是给人用的。一个可读的 ID 格式,能大幅提升排查问题的效率。


四、上下行消息规范:任务怎么发,反馈怎么收

任务模型定义了「任务长什么样」,但任务在网络上传输时,还要包装成「消息」。我们的消息结构是 header + action + payload 三层:

```json

{

"header": {

"msg_id": "8d98b920-4c8f-4f2f-9ba4-6a0f66fc4d55",

"trace_id": "d4fc4f8f5c6d42b7a2f9b58dbf2f4f90",

"sent_at_utc_ms": 1724380800000

},

"action": "EXECUTE_MISSION",

"payload": {

"mission_id": "MSN-20260823-001",

"robot_id": "RB-0001",

"commands": [...]

}

}

```

  • header:所有消息共有的元数据——消息 ID(幂等用)、追踪 ID(全链路溯源用)、发送时间戳
  • action:消息类型(执行任务、取消任务、暂停任务、紧急停止)
  • payload:具体内容,不同 action 的 payload 结构不同

这种分层结构是分布式系统消息设计的标准模式——header 统一处理(日志、追踪、幂等),action 决定处理逻辑,payload 承载具体数据。清晰、可扩展、易维护。

反馈消息也是类似的结构,payload 里包含任务状态、当前步骤、进度、事件类型等。


五、踩坑总结:任务模型设计的几点经验

回头看任务模型设计这段经历,总结几点:

1. 不要用某个品牌的格式做标准

用某个品牌的格式做平台标准,本质上还是被这个品牌绑架——品牌升级格式你就要跟着改,其他品牌的特有信息在转换中会丢失。一定要定义平台自己的、品牌无关的标准格式。

2. 不要过度设计

「超级格式」什么都能表达,但也什么都难用。任务模型要满足 80% 的常见场景,剩下 20% 的复杂需求可以通过扩展字段或外部编排解决。简单、清晰、好用,比「功能强大」重要。

3. 扁平数组 + cmd/params 分离是实用之选

扁平数组解析简单、修改方便、人类可读;cmd/params 分离类型安全、自描述、方便校验。这两个选择看起来「朴素」,但实际用起来最顺手。

4. 参数名带单位后缀,从源头消除歧义

「约定俗成」的单位是 bug 的温床。多花几个字节写 _mm_mdeg_n,能省下无数小时的调试时间。全平台统一单位标准(毫米、毫度、UTC 毫秒),不要混用。

5. 标准命令语义是「普通话」,品牌适配器做「翻译」

平台任务里只能出现标准命令字,不能出现品牌私有命令。品牌私有协议由驱动适配器负责转换。这样才能保证任务格式真正品牌无关。

6. ID 要可读,不要只追求唯一性

UUID 虽然唯一,但人类不可读。MSN-YYYYMMDD-XXX 这种可读的 ID 格式,能大幅提升排查效率。ID 是给机器用的,也是给人用的。


写在最后

任务模型看起来是「数据结构设计」,但它本质上是平台和机器人之间、模块和模块之间、团队和团队之间的「沟通语言」。一套好的任务模型,能让协作顺畅、bug 减少、维护成本降低;一套差的任务模型,会让系统充满「方言」和「歧义」,后期维护成本指数级增长。

我们在这个问题上踩了不少坑——从四种格式各做一套编辑器,到用某个品牌的格式做标准,到过度设计的「超级格式」,最后才找到参数化命令数组这个平衡点。这些经验都是我们(越微智能)在实际项目中一点点试出来的。

我们团队一直在做机器人二次开发、ROS2 系统开发、多品牌调度、工业 AI 视觉这些落地工作,任务模型是每天都要面对的问题。如果你也在做机器人任务编排、ROS2 开发、多品牌调度相关的工作,欢迎交流,我们一起踩坑、一起进步。


关于作者: 越微智能(Yuewell)是一支专注具身智能与工业 AI 视觉落地的技术团队,提供全品牌机器人二次开发(适配宇树/优必选/智元/傅利叶等)、ROS2 系统开发、工业级视觉算法定制(30+ 算法)、具身机器人调度管理平台、RK3588 边缘一体机等产品和服务,支持从算法、硬件到产线实机部署的全栈交付。
下一篇预告: 《任务状态机与事件驱动:我们是怎么告别轮询、实现毫秒级监控的》,我们将分享任务生命周期状态机的设计、Redis 事件缓冲层的作用、前端静默刷新的实现、以及幂等/重试/超时这些可靠性机制的实战经验,敬请关注。