上一篇我们分享了通讯架构选型的踩坑经历——从 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,改了任务格式,我们所有的转换函数都要跟着改。
用某个品牌的格式做标准,本质上还是被这个品牌「绑架」了。 这个方案也不行。
方案三:自定义一套「超级格式」,什么都能表达
第三种思路:「既然别人的格式都不合适,那我们自己定义一套超级格式,什么品牌、什么任务都能表达。」
于是我们设计了一套非常复杂的任务格式——有任务类型、有步骤、有条件分支、有循环、有变量、有函数调用……几乎把编程语言的概念都搬进来了。
结果呢?
- 太复杂了,用户不会用——我们自己的开发人员都要看好久才能搞明白怎么写,客户的运维人员更是一头雾水。
- 实现成本极高——要支持条件分支、循环、变量,就得做一个「任务解释器」,工作量巨大。
- 品牌适配器更难写——把这么复杂的格式转换成各品牌的私有格式,比登天还难。大部分品牌根本不支持条件分支和循环,只能在平台端解释执行,但平台端又不知道机器人的实时状态……
过度设计是另一个极端。 这套「超级格式」最后也被我们砍掉了。
三、最终方案:参数化命令数组
踩了这么多坑之后,我们终于找到了一个平衡点——参数化命令数组。
核心思想很简单:
- 一个任务就是一组命令的有序列表(数组)
- 每个命令有一个命令字(cmd)和参数对象(params)
- 命令字是平台定义的标准动作语义,不是任何品牌的私有命令
- 参数是可扩展的 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: true或condition: "..."字段,不需要改整体结构
嵌套结构看起来「高级」,但实际用起来处处别扭。扁平数组是最简单也最实用的选择。
决策二:命令和参数分离,不用「命令字符串」
有些品牌用「命令字符串」——"navigate 1200 500 90000",把命令和参数都塞在一个字符串里。
我们为什么要分离成 cmd + params?
- 类型安全——参数可以是 int、float、string、bool、对象、数组,不用都转成字符串再解析
- 自描述——
x_mm: 1200比1200清晰得多,一看就知道是 x 坐标、单位是毫米 - 支持可选参数——需要的参数写,不需要的不写,不用为了占位写一堆空值
- 方便校验——可以用 JSON Schema 校验每个命令的参数类型和范围
「命令字符串」看起来简洁,但每一个用它的人都在重新实现一个「命令解析器」,而且容易出错(参数顺序错了、多了少了,运行时才发现)。分离成 cmd + params 是结构化数据的基本设计原则。
决策三:参数名带单位后缀,不用「约定俗成」
你可能注意到了,我们的参数名带单位后缀——x_mm(毫米)、yaw_mdeg(毫度)、grip_force_n(牛顿)、duration_ms(毫秒)。
为什么要这么做?因为「约定俗成」是 bug 的温床。
我们踩过这个坑——一开始参数名只叫 x、y、yaw、force,结果:
- 开发人员 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 的值(navigate、manipulate、voice)是平台定义的标准动作语义,不是任何品牌的私有命令字。
为什么?因为如果允许品牌私有命令字直接出现在平台任务里,那任务格式又变成「品牌相关」的了——品牌 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 事件缓冲层的作用、前端静默刷新的实现、以及幂等/重试/超时这些可靠性机制的实战经验,敬请关注。