microduck
学无止境。——荀子
一只 800 克的鸭子,和一群替它思考的守护进程
先说尺寸
二十五厘米,八百克。
这个尺寸大概是一只真的小鸭子站起来的高度,也是你能单手拎起来、随手放在桌角、不会觉得桌面上多了台设备的重量。
它是一只双足机器人,靠强化学习策略走路。描述短得只有一句:一只小小的双足鸭子机器人。
项目地址:https://github.com/pollen-robotics/microduck
而文档的第二句话,才是整篇文章真正的入口:
这个仓库是鸭子的大脑。
「大脑」这个词,用得一点不虚
一个机器人项目通常会告诉你它有多少个自由度、用什么电机、续航多久。这份文档先告诉你的是:这只鸭子的思考,被拆成了几个进程,跑在一块 Rockchip RK3566 上。
五十赫兹的控制环,用神经策略驱动十五个舵机;无线电和摄像头;以及一整套负责把新软件送进机器人、同时保证不会把它变成砖头的更新机制。
五十赫兹意味着什么?每二十毫秒,这套系统必须重新决定一次十五个舵机各自该转到哪个角度。快于此,人感觉不到;慢于此,鸭子就会开始发抖、打滑、摔倒。
而它用的不是一套手写的步态方程,是神经策略。这就把整个项目的重心挪到了另一个地方:这里装的不是「怎么走路」,而是「学会了走路的那颗脑子」,以及让这颗脑子能安全地待在一台真实机器里的那套基础设施。
它会做的事
文档用一个表格列了四件事,每件配一段视频。这一节写得极简,但每一行都值得读。
它会走。 拿起手柄,开着它到处跑。
它会滚。 装上轮子,说一句滚轮模式,它就加载另一个脑子。
注意这句话里的「另一个脑子」。滚轮模式不是把腿收起来硬拖,而是换一套策略。走路和滚动是两种完全不同的动力学问题,用一套网络同时学会两件事,不如训练两套、按场景切换来得干净。这个设计选择,比任何参数都更能说明团队的取向。
它会叼东西。 嘴贴到地面,一个按键。
它会自己站起来。 把它推倒,它自己爬回来。
最后这条是四件事里技术上最难、演示上最讨巧的一条。倒地起身意味着机器人必须能在训练中没见过或极少见的状态下恢复——这恰恰是 sim2real 里最考验策略鲁棒性的场景。
除此之外,它还会坐下、踢球、听指令向前滚,以及用一种属于它自己的嗓音叫。
「属于它自己的嗓音」这个说法很妙。它没有说「播放鸭叫音频」,而是说这嗓音是它的。后面提到 robotctl 里有一个叫 chorale 的命令,还有一个叫 theremin 的——一只会唱歌、还能当特雷门琴玩的鸭子。
训练发生在隔壁
这是整份文档里最诚实的一句话:它运行的策略是在隔壁训练的。
隔壁是另一个仓库,microduck_rl。里面是 MuJoCo 和 PPO,是一套 sim2real 的做法,是导出成 ONNX 的流程——而当前这个仓库,负责加载那些 ONNX。
把「训练」和「运行」分成两个仓库,是一个很少见但很清醒的切法。
训练需要 GPU、需要大量试错、需要能随时改奖励函数;运行需要确定性、需要低延迟、需要能在二十毫秒内算完。两者的依赖、迭代节奏、失败模式完全不同。塞进一个仓库,结果是两边互相拖累。
而 ONNX 在这中间扮演了一个很关键的角色:它是一道格式的边界。边界的一侧可以是 Python、是仿真、是随便什么训练框架;另一侧只有一个要求——能读出这个张量,能算出这个结果。
一只八百克的鸭子,和一台训练它的机器,靠一个文件格式握了手。
一群 daemon,一套语言
「Under the hood」那一节只有一段话,但信息密度极高。
整个系统是 Rust,没有框架,一个 workspace。然后是一个个守护进程,各管一摊:
robotd 管控制环和电机总线。这是最核心的一个,二十毫秒的心跳就在它手里。
updaterd 装签名的发行版,并且在机器人启动后状态不健康时把它回滚。
configd 管 wifi 和身份。
btd 是手机走的那条蓝牙通路。
padd 读手柄。
mediad 用 WebRTC 推摄像头画面。
tofd 提供深度传感器。
七个进程,七种职责。这份分工有个很明显的特点:每一个都对应一种「可能单独坏掉」的东西。
控制环会卡,更新会失败,wifi 会连不上,蓝牙会掉,手柄会没电,视频会断流,深度传感器会读不到数。把每一种故障源关进自己的进程里,坏一个不至于带走整只鸭子。
然后是那句真正让它成立的句子:它们通过同一个 JSON-RPC 契约在 Unix socket 上对话,而每一个客户端——app、控制台、手柄、你写的脚本——发出的调用完全一样。
这句话的含金量在于「完全一样」。很多系统会给内部调用留一条快车道,给外部调用留一条慢车道,然后两条道慢慢长出不同的语义。这里没有:手机 app 让鸭子往前走,和你在终端里敲一行命令让鸭子往前走,走的是同一条路,同一个参数,同一个返回。
一条契约,七个进程,四种客户端。这是把「可测试」和「可脚本化」当成架构约束而不是事后补丁的做法。
更新是件会被认真对待的事
给一台机器人推新固件,和给一台服务器推新版本,风险完全不是一个量级。
服务器推坏了,你还能 ssh 进去修。机器人推坏了,它可能就趴在地上,连不上网,也不接受任何指令——你手里剩下的只有一个八百克的塑料摆件。
所以 updaterd 的职责被写得很重:安装签名的发行版,并且在机器人启动后不健康时回滚。
文档在「你有一只鸭子」那一节里,把更新单独列了一行,三个动词:安装、回滚、固定版本。然后给了一句总结——
每一次更新都被验证、被健康门控、并且可逆。
验证,解决的是「这个包是不是官方的」。健康门控,解决的是「装完之后它还活着吗」。可逆,解决的是「如果前面两条都过了但还是坏了呢」。
三道关卡,对应三种不同的失败。这不是谨慎,这是被现实教育过的痕迹。
没有鸭子也能开发
这一节大概是整份文档里最浪漫的部分。
桌面上没有机器人怎么办?跑 scripts/duck-sim。
它让真实的守护进程跑在一个 MuJoCo 里的身体上。不是一个模拟接口的假实现,是那些真的 daemon,接在一个仿真出来的机体上。
两种用法:窗口里一只鸭子,或者四只,当作可以登录进去的机器。
第二条听起来像是给需要同时跑好几套东西的人准备的——四只鸭子,就是四台可以登进去的机器。一只坏了不影响另外三只,测试跑完就销毁。
而第一条,是给某个深夜想试试新步态的人准备的:改一行代码,重启,窗口里那只鸭子开始走,走歪了,摔倒,再改一行。
这个循环越短,人越愿意试。而「愿意试」这件事,是机器人项目里最稀缺的资源。
两类文档:为什么这样,以及哪里出过错
文档目录里有两个并列的文件夹,名字都很朴素:docs/design/ 和 docs/project/。
前者的定位是:为什么事情是现在这个样子。
后者的定位是:什么东西出过错,以及什么能补上它。
第二个特别少见。绝大多数项目文档记录的是「我们做了什么」,很少有人专门开一个目录记录「我们搞砸了什么,还差什么」。
而这份文档不仅开了,还在首页上把它标了出来,当成一个值得一看的入口。
一个愿意公开自己「未解决问题清单」的项目,通常意味着两件事:它的维护者知道问题在哪,以及他们相信读文档的人有能力一起想。
顺手的地方
「你有一只鸭子」那一节里,有几个小东西能看出产品感。
一份 cheat sheet,收录 robotctl 的每一条命令:开车、配置、嗓音、合唱、特雷门琴、wifi、更新、日志。文档自己说,从这里开始。
一张手柄的完整按键映射,以及配对方法——注意那个补充说明,「每个手柄配一次」,还附了「配对不上时怎么办」。
还有一个叫 duckctl 的东西:从笔记本通过蓝牙控制鸭子,不需要网络,不需要 ssh。
最后这一条解决的是一个很具体的场景。机器人连不上 wifi 了,你又进不去它,怎么办?答案是不进它——直接用蓝牙在近处跟它说话。这是把「救援通道」做成了产品的一部分,而不是留到论坛帖子里让人教你按复位键。
最后
回到那句「这个仓库是鸭子的大脑」。
这句话之所以准确,是因为它没有夸大。这里没有机械结构图,没有注塑模具,没有电池选型。这里装的是:一套二十毫秒一次的判断,一群各管一摊的进程,一条所有客户端共用的契约,一套推坏了能退回来的更新机制,一只可以在窗口里被推倒又爬起来的仿真鸭子。
二十五厘米,八百克,十五个舵机。硬件给了它身体,这个仓库给了它反射、记忆和语言。
而文档的最后一行是这么写的:
没有鸭子在这只机器人的制作过程中受到伤害。咨询了好几只。
一只会自己站起来、会踢球、会用特雷门琴唱歌的机器人,最后留下的是这么一句话。这大概就是做玩具和做工具之间那条模糊的线——你得先觉得它可爱,才愿意为它写七个守护进程。
