从 Isaac Gym 到 Isaac Lab:unitree_rl_lab 的迁移逻辑
- 读懂 unitree_rl_lab 的安装方式、任务命名规则和它支持的机型与任务类型
- 看清 manager-based 环境配置这种写法,和继承一个大基类的老写法差在哪
- 理解 GPU 并行仿真、域随机化、奖励塑形这几件事在训练链路里各自的位置
同一个厂商,为同一批机器人,维护了两个强化学习训练仓库。
这件事第一眼看上去像是没收拾干净的历史包袱。但把两个仓库的目录树摊开并排看,你会发现它们不是新旧版本的关系,更像是同一道题在两套工具链下各解了一遍——而两次解题之间隔着的,正是这几年机器人仿真训练框架变化最大的那部分。
上一篇我们把 unitree_rl_gym 那条 Train → Play → Sim2Sim → Sim2Real 的流水线从头走了一遍。这篇不重复那四步,只做一件事:把 unitree_rl_lab 读一遍,然后拿它跟前一个仓库逐项对照,看差异出在哪、为什么会出在那儿。
先把边界说清楚:本文基于这两个仓库的公开 README 与文件树结构,我手上没有真机,也没有跑过训练,所以不会有任何「效果如何」「多久收敛」的描述。凡涉及运行结果的地方,一律以官方仓库文档的说法为准。
它建在什么上面
unitree_rl_gym 的致谢里列的是 legged_gym、rsl_rl、MuJoCo、unitree_sdk2_python——训练框架来自苏黎世联邦理工的 legged_gym,跑在 Isaac Gym 上。
unitree_rl_lab 的 Overview 第一句就换了地基:这是一套建在 IsaacLab 之上的强化学习环境。致谢里列了四项:IsaacLab(训练与运行代码的基础)、MuJoCo(仿真能力)、robot_lab(项目结构与部分实现的参考)、whole_body_tracking(一个用于动作跟踪的全身控制框架)。
两份致谢清单只有 MuJoCo 是重合的。这不是换了个库那么简单——训练侧的整套抽象换了一层,后面目录结构上的所有差异都是从这里长出来的。
README 里当前写明的支持机型是 Go2、H1 和 G1-29dof。
装它比装前一个费劲
unitree_rl_gym 的安装是常规 Python 项目那一套。unitree_rl_lab 不是,它的安装步骤本身就在告诉你这个仓库的定位:
第一步是先按官方指南把 Isaac Lab 装好。第二步才轮到这个仓库,而且 README 特意强调了一句——要把它 clone 到 IsaacLab 目录之外,作为独立的 standalone 环境包:
git clone https://github.com/unitreerobotics/unitree_rl_lab.git
conda activate env_isaaclab
./unitree_rl_lab.sh -i
# 装完要重启 shell 让环境变更生效
用的是一个装了 Isaac Lab 的 Python 解释器,以 editable 模式装进去。这个「独立于 Isaac Lab 目录之外、但依赖它的解释器环境」的安排很典型:仓库把自己定位成 Isaac Lab 的一个扩展包,而不是一个自带训练框架的完整项目。仓库里 source/unitree_rl_lab/config/extension.toml 这个文件也印证了这一点,扩展包才需要这种描述文件。
机器人模型不在仓库里
这是我读这个仓库时第一个觉得值得停一秒的地方。
unitree_rl_gym 把机器人资产全塞在仓库里,resources/robots/ 下装着 g1、go2、h1、h1_2 的 URDF、MJCF 和一大堆 STL 网格文件——文件数量上,网格占了那个仓库的大头。
unitree_rl_lab 里没有 resources/。模型要你自己下,README 给了两条路:
方法一,用 USD 文件。 从 Hugging Face 上的 unitree_model 数据集把 USD 克隆下来,保持目录结构不变,然后去 source/unitree_rl_lab/unitree_rl_lab/assets/robots/unitree.py 里改 UNITREE_MODEL_DIR 指向它。
方法二,用 URDF 文件(README 标注为推荐)。 从 unitree_ros 克隆机器人描述文件,同样在 unitree.py 里改 UNITREE_ROS_DIR。README 注明这条路要求 Isaac Sim 版本不低于 5.0,具体以官方仓库当前文档为准。
把几百 MB 的网格资产从代码仓库里挪出去,是个很务实的决定。资产更新频率跟代码不一样,混在一起既撑大仓库体积,又让「机器人模型改了」和「训练逻辑改了」在提交历史上分不清。代价是安装步骤多了一步、多了一个要手改的路径常量——这个取舍在工程上通常划算。
USD 和 URDF 并存也有讲究。URDF 是 ROS 生态的通用机器人描述格式,可移植性好;USD 是 Isaac Sim 原生的场景描述格式,加载效率高但绑定平台。仓库同时给两条路,是把「跨生态复用」和「原生性能」的选择权交给了使用者。
任务是怎么命名和跑起来的
unitree_rl_gym 的任务名是 --task=go2、--task=g1 这种短标识,可选值就四个。
unitree_rl_lab 换成了这样:
./unitree_rl_lab.sh -l # 列出所有可用任务
./unitree_rl_lab.sh -t --task Unitree-G1-29dof-Velocity # 训练
./unitree_rl_lab.sh -p --task Unitree-G1-29dof-Velocity # 推理
README 说明 -l 是比 isaaclab 自带方式更快的任务列举,任务名支持自动补全。这几条 .sh 命令都是包装,底层等价于:
python scripts/rsl_rl/train.py --headless --task Unitree-G1-29dof-Velocity
python scripts/rsl_rl/play.py --task Unitree-G1-29dof-Velocity
Unitree-G1-29dof-Velocity 这种全称式的任务 ID,是 Isaac Lab 沿用 Gym 注册表的命名习惯:厂商-机型-关节配置-任务类型。四段结构意味着注册表里预期会有很多条目——同一台机器人的不同关节配置是不同任务,同一套关节配置的不同任务目标也是不同任务。短标识 --task=g1 在只有四个选项时够用,条目一多就撑不住了。
一个专门做「列任务」的命令行开关,本身就是任务数量增长的信号。
目录结构:从「一个型号一个类」到「一堆可组合的项」
这是两个仓库差别最大的地方,也是这篇文章真正想讲的东西。
前一个仓库的写法:继承
unitree_rl_gym 的训练侧长这样:
legged_gym/envs/base/ base_task.py legged_robot.py legged_robot_config.py
legged_gym/envs/go2/ go2_config.py
legged_gym/envs/g1/ g1_config.py g1_env.py
legged_gym/envs/h1/ h1_config.py h1_env.py
legged_gym/envs/h1_2/ h1_2_config.py h1_2_env.py
基类 legged_robot 是一个足式机器人的通用环境实现,每个型号继承它,改配置或者覆写方法。go2 只有 config 没有 env,人形三个型号都各带一个 env——这个不对称我们上一篇分析过。
这套写法的直觉很顺:新增机型就是新增一个子类。但它有个绕不开的问题——观测怎么组成、奖励有哪几项、随机化做哪些,全都散落在基类的方法体和配置类的嵌套字段里。你想给某个型号单独加一项奖励,得找到基类里那个计算奖励的方法,看它怎么遍历配置,再决定是加配置项还是覆写方法。想在两个型号之间复用一半的奖励项,就更麻烦。
这个仓库的写法:管理器式配置
unitree_rl_lab 的训练侧长这样:
source/unitree_rl_lab/unitree_rl_lab/
assets/robots/
unitree.py —— 机器人资产定义与模型路径常量
unitree_actuators.py —— 执行器模型
tasks/
locomotion/
agents/rsl_rl_ppo_cfg.py
mdp/
commands/velocity_command.py
curriculums.py
observations.py
rewards.py
robots/
go2/velocity_env_cfg.py
h1/velocity_env_cfg.py
g1/29dof/velocity_env_cfg.py
mimic/
agents/rsl_rl_ppo_cfg.py
mdp/
commands.py events.py observations.py
rewards.py terminations.py
robots/g1_29dof/...
utils/
export_deploy_cfg.py
parser_cfg.py
对比着看,几件事一目了然:
第一,出现了 mdp/ 这个目录层。 MDP 是马尔可夫决策过程的缩写,强化学习的标准数学框架。这个目录里放的就是构成一个 MDP 的各个部件:观测(observations)、奖励(rewards)、终止条件(terminations)、指令(commands)、事件(events)、课程(curriculums)。它们各自是独立文件,而不是某个基类的方法。
第二,每个机型下面只剩一个 velocity_env_cfg.py。 没有 env.py 和 config.py 的分裂了,型号目录里就是一份环境配置。配置的内容是「从 mdp 里挑哪些项、各自权重多少、参数怎么设」。
第三,任务类型成了目录的第一级划分。 locomotion 和 mimic 各有一整套 agents / mdp / robots,两套 mdp 目录里的文件名也不完全一样——mimic 那套多了 events.py 和 terminations.py,少了 curriculums.py。
这就是所谓的 manager-based(管理器式)环境。它的核心思路是:不要让「一个环境」成为一个巨大的类,而是让它成为一张配置表——观测管理器按表拼观测向量,奖励管理器按表逐项加权求和,事件管理器按表在指定时机触发随机化。
这套思路解决的痛点很具体。想给某个任务加一项奖励,你在 rewards.py 里写一个函数,然后在环境配置里加一行把它挂上去,权重写在配置里。不用碰基类,不用担心影响别的型号。想让两个机型共用九成奖励项、只差一项,配置里改那一行就行。想做消融实验看某项奖励有没有用,把那行注释掉。
代价也真实存在:间接层变多了。 看一个环境到底在算什么,你得同时打开 env_cfg 和 mdp 下的几个文件,来回跳。继承式写法虽然笨重,但「这个型号的逻辑在哪」是个能一句话回答的问题。配置式写法把逻辑打散成积木之后,你需要先理解积木的拼装规则,才能读懂任何一块。
学新框架时那种「每个文件都看得懂、合起来不知道在干嘛」的感觉,多半就来自这里。没有办法绕过去,只能先把管理器的调度顺序搞清楚——观测管理器什么时候被调用、事件管理器有哪几个触发时机——之后所有配置文件读起来才会立刻变得直白。
顺便说说环境到底负责什么
既然拆成了这些部件,正好把「环境」在强化学习里的职责讲清楚。不管框架怎么变,一个环境要提供的东西就这么几样:
- 给观测:把机器人当前状态整理成一个定长向量喂给策略网络。典型内容包括关节角度与角速度、机身姿态与角速度、上一时刻的动作、以及用户下达的速度指令。注意这里有个关键约束——观测里只能放真机上也拿得到的量。仿真里你能读到机器人在世界坐标系下的绝对位置,真机上没有这种东西,训练时用了,部署时就抓瞎。
- 收动作:策略网络输出的通常不是关节力矩,而是关节目标位置的偏移量,再由一个位置控制器换算成力矩。
- 算奖励:把这一步做得好不好折算成一个标量。
- 判终止:摔倒了、姿态超限了、跑够时长了,就结束这一回合。
- 重置:把结束的那些环境实例恢复到初始状态,继续下一回合。
mdp/ 目录里的文件,一个个对得上:observations.py、rewards.py、terminations.py、commands.py。这种目录结构最舒服的一点是——它把教科书上的抽象直接摆成了文件名,你不需要在几千行的基类里找「终止条件到底写在哪」。
为什么非要 GPU 并行仿真
这件事值得单独讲,因为它是整套技术路线成立的前提,跟用哪个框架无关。
强化学习的样本效率非常低。它没有监督信号告诉你「正确答案是什么」,只有一个标量奖励告诉你「刚才那下大概是好是坏」。要从这么稀薄的反馈里试出一套能走路的控制策略,需要的交互量是天文数字级别的。
放到真机上算这笔账:一台机器人实时跑,一天二十四小时不停,能积累的交互量跟需求量差着几个数量级——还没算电池、发热、关节磨损和摔坏的部件。真机在线学习走路,这条路在工程上不成立。
所以只能在仿真里练,而且必须并行练。Isaac Gym 和 Isaac Lab 这类框架真正的价值不在画面好看,在于它们把物理仿真本身做成了 GPU 上的批量张量运算——几千个机器人实例的状态是一个大张量,一次积分把所有实例往前推一步。策略网络的前向推理本来就在 GPU 上,仿真也在 GPU 上,中间不用把数据搬来搬去。
这就是为什么 unitree_rl_gym 的训练参数里有 --num_envs,也是为什么两个仓库的 README 都建议训练时用 headless 模式:渲染是给人看的,跟训练无关,还占着算力。
顺带说一句,unitree_rl_lab 的 .sh 包装脚本里,-t 对应的命令行默认就带上了 --headless。这种把最佳实践写进默认值的做法,比在文档里叮嘱一句有效得多。
域随机化:策略能上真机的真正原因
仿真练出来的策略为什么能在真机上用?
严格说,不能——如果你只在一个固定参数的仿真环境里练。仿真器里的摩擦系数、连杆质量、电机响应、传感器读数,全都是精确到小数点后若干位的理想值;真机上这些量每一个都不准,而且会随着温度、磨损、负载变化。策略在理想世界里学到的那套精细配合,一到现实里就全乱套。这就是 sim2real gap。
域随机化是目前最主流的应对手段,思路简单到有点粗暴:既然仿真参数一定不准,那就别用固定值,每次重置环境时都随机抽一组。
常见的随机化维度包括:
- 地面摩擦系数:从瓷砖到地毯的范围内随机
- 各连杆质量与质心位置:在标称值附近抖动,模拟制造公差和负载变化
- 执行器参数:位置控制器的增益、力矩上限、响应延迟
- 观测噪声与延迟:给传感器读数加噪声,把观测延迟若干个控制周期
- 外部扰动:训练途中随机给机身一个推力
这样练出来的策略,面对的不是「一个仿真环境」,而是「一族参数各异的环境」。它没法依赖任何一组具体参数,只能学出对参数变化本身鲁棒的行为——而真机,不过是这一族环境里的又一个采样点。
代价是训练变难、收敛变慢,随机化范围放太宽策略可能干脆学不会走路,太窄又挡不住 sim2real gap。这个范围怎么定,是这个方向上最吃经验的部分之一。
回到目录结构:unitree_rl_lab 的 mimic 任务下有 mdp/events.py。在 Isaac Lab 的抽象里,事件管理器正是负责在环境重置或指定时机触发这类随机化的部件。把随机化做成一组可挂载的事件项,而不是写死在环境类的 reset 方法里,是管理器式组织带来的直接好处——每一项随机化都可以单独开关、单独调范围。
奖励为什么这么难写
rewards.py 在两个任务目录下都存在,而且大概率是整个仓库里改动最频繁的文件。原因在于奖励设计是这套方法里最不像工程、最像调参艺术的一环。
难点有两层。
第一层是稀疏与稠密的矛盾。 你真正想要的目标(「按指令速度稳稳地走」)如果直接写成奖励,信号太稀疏——机器人一开始只会摔倒,永远拿不到奖励,也就永远学不会。于是必须做奖励塑形(reward shaping):把大目标拆成一堆能持续给出反馈的小项,速度跟踪奖励、姿态保持奖励、关节力矩惩罚、动作变化率惩罚、足底滑动惩罚、腾空时间奖励……每项配一个权重。权重之间的比例关系直接决定策略长什么样,而这些比例往往没有理论依据可循。
第二层是 reward hacking。 神经网络是彻底的机会主义者,它优化的是你写下的那个奖励函数,不是你心里想的那个目标。这两者之间只要有缝,它就会钻。上一篇讲 Sim2Sim 时提过一种钻法——策略去薅仿真器物理模型的漏洞。奖励项本身写得不严密也会被钻:只奖励前进速度,它可能学会往前扑倒再爬起来;加了姿态惩罚,它可能学会用一种极其僵硬别扭的姿势蹭着走,因为那样惩罚最小。
判断一个奖励设计好不好,标准不是「奖励涨得快不快」,而是「拿满这个奖励的唯一办法是不是就是做对事情」。 这个标准很难达到,所以实践中的做法是反过来的:训练、观察行为、发现它在钻哪个空子、补上对应的惩罚项、再训练。奖励函数是被一轮轮的失败磨出来的,不是一次设计出来的。
这也解释了为什么把奖励项拆成独立函数值得——你会反复动它,而且每次只动一两项。
部署侧:换了个数量级的工程量
训练侧的差异讲完了,部署侧的差异同样大,甚至更能说明问题。
unitree_rl_gym 的部署是 Python 为主:deploy/deploy_mujoco/deploy_mujoco.py 做 Sim2Sim,deploy/deploy_real/deploy_real.py 做真机,另外在 cpp_g1/ 下附了一份基于 LibTorch 的 C++ 实现作为示例。硬件通信走 unitree_sdk2_python。
unitree_rl_lab 的 deploy/ 目录里没有 Python 部署脚本。全是 C++:
deploy/include/FSM/
BaseState.h CtrlFSM.h FSMState.h
State_FixStand.h State_Passive.h State_RLBase.h
deploy/include/isaaclab/
envs/manager_based_rl_env.h
envs/mdp/actions/joint_actions.h
envs/mdp/observations/observations.h
envs/mdp/terminations.h
manager/action_manager.h
manager/manager_term_cfg.h
manager/observation_manager.h
assets/articulation/articulation.h
algorithms/algorithms.h
deploy/include/
unitree_articulation.h unitree_joystick_dsl.hpp
LinearInterpolator.h param.h
deploy/robots/
b2/ g1_23dof/ g1_29dof/ go2/ go2w/ h1/ h1_2/
deploy/thirdparty/onnxruntime-linux-x64-.../
这里有三个细节值得单独拎出来。
其一,deploy/include/isaaclab/ 这个目录。 名字和内部结构都在明示:他们用 C++ 把 Isaac Lab 那套管理器抽象在部署侧重新实现了一遍——观测管理器、动作管理器、manager_term_cfg,连 mdp/observations、mdp/actions 的路径层次都对齐了训练侧的 Python 包。
这个设计意图我觉得很清楚:让部署时的观测拼装顺序、动作换算方式,与训练时严格同构。 强化学习部署最经典的一类 bug 就是观测向量对不齐——训练时第 17 位是左膝角速度,部署时因为拼接顺序写错成了右髋,网络照样输出数字,机器人照样动,只是动得莫名其妙,而且不报任何错。用同一套管理器抽象在两侧各实现一遍,等于把这类错误的发生面积压到最小。这一步的工作量不小,但省下的调试时间大概率更多。
其二,模型格式从 .pt 换成了 .onnx。 仓库里 deploy/thirdparty/ 直接内置了 ONNX Runtime 的 Linux x64 发行包,deploy/robots/g1_29dof/config/policy/ 下的模型文件都是 policy.onnx。而 unitree_rl_gym 的 C++ 示例依赖 LibTorch,预训练模型是 .pt。
从 LibTorch 换到 ONNX Runtime,换掉的是一整个深度学习框架的运行时依赖。ONNX 是跨框架的模型交换格式,ONNX Runtime 是一个专做推理的轻量运行时,体积和依赖复杂度都比 LibTorch 小一截,跟训练框架也彻底解耦了——训练侧以后再换框架,只要还能导出 ONNX,部署侧一行不用改。
其三,出现了 FSM(有限状态机)。 State_Passive(被动/阻尼态)、State_FixStand(固定站立)、State_RLBase(强化学习策略)三个状态,加上 CtrlFSM 调度器。这是真机控制程序的标准骨架:开机进被动态、遥控器给指令切到站立、站稳了再切到策略控制、出事随时切回被动态。策略网络在这个结构里只是众多状态中的一个,而不是整个程序。
unitree_joystick_dsl.hpp 这个文件名也有意思——为手柄输入做了个小型 DSL(领域特定语言)。对照 README 里 Sim2Sim 的操作说明就明白它在干嘛了:先按 L2 + Up 让机器人站起来,再按 R1 + X 让策略跑起来。组合键映射到状态切换,用 DSL 描述比一堆 if-else 清爽。
Sim2Sim 的实现方式也变了
unitree_rl_gym 的 Sim2Sim 是一个 Python 脚本直接调 MuJoCo,加载 YAML 配置里指定的策略文件。
unitree_rl_lab 的做法是:先按 unitree_mujoco 的说明把那个独立仿真器装起来,在 simulate/config.yaml 里把 robot 设成 g1、domain_id 设成 0、enable_elastic_hand 设成 1、use_joystck 设成 1,启动 ./unitree_mujoco;然后在另一个终端跑真机同一个控制程序 ./g1_ctrl。
差别在最后这句。真机部署的命令是 ./g1_ctrl --network eth0,Sim2Sim 是同一个 g1_ctrl 不带网卡参数。同一个二进制,同一套 FSM,同一份 ONNX 模型,只是对面接的是仿真器还是真机。 这意味着 Sim2Sim 验证的不只是策略网络,而是整个部署程序——观测拼装、状态机切换、手柄响应,全都一起验了。上一个仓库里 Python 脚本跑 Sim2Sim、另一套代码跑真机,中间那段差异是验不到的。
配置里那个 enable_elastic_hand,对应 README 操作步骤里的「按 9 关掉弹性带」——仿真里先用一根虚拟吊带把机器人吊着,站稳之后再撤掉。这是调试足式机器人的常规手段,省得每次失败都要手动扶起来。
真机那边 README 有一句必须注意的前提:要用这个程序直接控制机器人,得先确保机载的原厂控制程序已经关闭。 两个程序同时往关节发指令是什么后果,不用试也知道。它们之间的通信底层,走的还是 unitree_sdk2 那套 DDS——安装步骤里明确要求先把 unitree_sdk2 编译安装到系统目录,再编译各机型的控制器。
一个不重合的地方,我不去猜原因
有个观察值得如实记下来:README 的 Overview 里当前写明支持 Go2、H1、G1-29dof 三个机型,训练侧 tasks/locomotion/robots/ 下也确实是这三个目录。但 deploy/robots/ 下的控制器工程有七个:b2、g1_23dof、g1_29dof、go2、go2w、h1、h1_2。
两边对不上。这可能意味着部署侧走得比训练侧靠前,也可能意味着某些机型的训练配置还没公开,也可能只是 README 没跟上。我不去猜作者的考虑。 但从使用者角度有个实际的提醒:以你实际能列出来的任务为准(./unitree_rl_lab.sh -l),别看到 deploy 目录下有某个机型就假定训练侧也现成。
另外,unitree_rl_lab 的 License 徽章是 Apache 2.0,unitree_rl_gym 是 BSD 3-Clause。两者都是宽松许可,但条款细节不同——真要基于它们做产品,这个差异得让懂法务的人看一眼,别凭印象办事。
还有一类任务是前一个仓库没有的
unitree_rl_lab 的 tasks/ 下除了 locomotion,还有 mimic。
从文件构成能读出它在干什么:tasks/mimic/robots/g1_29dof/ 下按动作分目录,每个目录里有一个 .bvh 转出来的 CSV 动作数据文件和一份 tracking_env_cfg.py。配套的 scripts/mimic/ 下有 csv_to_npz.py 和 replay_npz.py——把动作数据转成训练用格式,以及回放检查。
BVH 是动作捕捉领域的常用格式。所以这类任务的形态大致是:给定一段人类动作数据,训练策略让机器人去跟踪它。致谢里那个 whole_body_tracking(动作跟踪的全身控制框架)正对应这条线。
mimic/mdp/ 的文件构成也印证了它跟 locomotion 不是一回事:多了 events.py 和 terminations.py,少了 curriculums.py。行走任务需要课程学习(从平地慢慢加难度),动作跟踪任务的难度由动作数据本身决定,不需要那套;反过来,跟踪任务需要更细的终止判据——跟丢了就该重来。
同一套 mdp 抽象,两个任务各挑各的项。 这正是管理器式组织想要的效果:新增一个任务类型不需要动既有任务的任何代码。
那么该学哪个
同一个厂商维护两个训练仓库,学的时候到底看哪个——这是个实际问题,我按能核到的事实给几个判断依据,不做优劣排名。
看你的仿真环境。 这是最硬的约束。unitree_rl_gym 建在 legged_gym / Isaac Gym 上,unitree_rl_lab 要求先装好 Isaac Lab 和 Isaac Sim,而且 URDF 那条路对 Isaac Sim 版本有下限要求。你的机器能装起来哪套,很大程度上就决定了。
看你想学什么。 如果目标是搞懂「强化学习控制机器人」这件事本身的链路——训练怎么跑、策略怎么导出、为什么要 Sim2Sim——unitree_rl_gym 的结构更直白,四步流水线在 README 里写得清清楚楚,Python 部署脚本读起来也快。如果目标是理解现代仿真训练框架的组织方式,或者你打算在上面做比较深的定制(自定义奖励、自定义任务类型、多机型复用),unitree_rl_lab 的管理器式结构更值得投入。
看你要不要碰真机。 两个仓库都覆盖了到真机的完整链路,但形态不同:一个 Python 为主、附 C++ 示例,一个纯 C++、带完整 FSM 和 ONNX 运行时。如果你的目标是把策略跑到实际硬件上并长期维护,后者那套「部署侧重建训练侧抽象」的做法本身就值得研究,不管你最后用不用它的代码。
先读哪个也可以不纠结。 两个仓库解的是同一道题,对照读的信息量比单读一个大得多——很多设计意图只有在看到「另一种做法」时才显形。这也是这一卷把它们放在相邻两篇的原因。
收束:三个可以带走的判断
第一,仓库结构的变化方向是「从继承到组合」。 一个大基类加一堆子类,变成一堆独立的 mdp 项加一份挑选它们的配置表。这个方向不是机器人领域独有的,但在这里的收益特别明显——因为奖励项、观测项、随机化项本来就是需要频繁增删和排列组合的东西。代价是间接层变多、上手变慢。
第二,部署侧的抽象向训练侧对齐,是个值得抄的做法。 用 C++ 重建一遍管理器结构,为的是让观测拼装和动作换算在两侧严格同构。任何跨环境部署模型的场景都有这个问题:训练时的数据处理和推理时的数据处理写在两个地方,就迟早会漂移,而且漂移了不报错。
第三,资产和代码分家、推理运行时和训练框架解耦,这两件事都指向同一个判断——这类项目正在从「一个研究用的实验仓库」往「一个要长期维护的工程产品」演化。研究仓库怎么方便怎么来,工程产品得考虑仓库体积、依赖复杂度、升级路径。
想把整条路线的另一半补上,可以看卷 U 的目录里那条不用强化学习的传统控制线,两种思路对着读,各自的成本和边界会清楚很多。基础还不牢的话,机器人与具身智能卷里有从 ESP32 起步的完整阶梯,具身智能是什么那篇可以当入口。