☰
分层世界模型H-WM:打通机器人任务规划与运动规划的关键架构
2026/10/17 17:13:06 网站建设 项目流程

做机器人任务规划和运动控制的人,一定都经历过这样的场景:仿真环境里一切正常,一换到真实场景,机器人要么原地发呆,要么反复重规划,要么干脆撞上前方障碍物。问题往往不是某个单独算法写得不好,而是你把感知、任务规划、运动规划全塞进了同一个状态空间里去处理,不同时间尺度、不同抽象级别的信息互相干扰,最终整个系统被拖垮。H-WM(分层世界模型)的思路,是把机器人的“大脑”拆成语义层、状态层、运动层三个明确的层次:任务规划管“做什么”,运动规划管“怎么做”,中间靠状态模型衔接,让每一层都在自己适合的抽象级别和时间尺度上运转。这篇文章就围绕H-WM这个架构,把分层背后的原理、工程落地方式、参数调校思路,以及我自己实际调试中踩过的坑,一次性讲清楚。适合正在做轮式移动机器人、机械臂抓取或者复合机器人项目的朋友参考,也适合想打通任务规划与运动规划之间隔阂的人看看。

1. 为什么需要分层世界模型:从单体感知到“三层大脑”的跃迁

1.1 传统世界模型在复杂任务面前的崩溃现场

拿一个非常具体的场景说起:一台服务机器人要完成“去厨房拿一瓶水,再放到客厅桌上”的任务。传统做法通常是把所有信息揉进一个统一的世界模型里——视觉SLAM得到的栅格地图、物体识别输出的品类和位姿、机械臂的关节角度、任务意图的符号状态,全部塞进同一个向量空间处理。看起来简单,实际上处处是矛盾。

首先是状态空间维度爆炸。地图本身就是几百维的数组,加上物体属性、机器人状态、动态障碍物信息,状态空间动辄上千维。其次是规划尺度不一致,任务规划需要在秒级甚至分钟级的尺度上做推理,运动规划却要在几十毫秒的周期里输出轨迹,混在同一层之后,任务层的决策频率会被运动层拖慢,反过来运动层又会被任务层的重规划逻辑干扰。最麻烦的是语义和几何的冲突:任务层说“拿水”,几何层说“水在架子上,够不着”,中间缺少一个能同时理解“拿水”这个意图和“够不着”这个事实的中间层。

最终你会发现,每个模块单独跑都问题不大,凑到一起就是不动、乱动、反复重规划。这不是算法能力不够,而是分层结构设计不合理。H-WM解决的就是这个问题:在“任务意图”和“关节动作”之间,插入一个结构化的中间层,让符号化的高层推理和几何化的底层执行各归各位。

1.2 分层到底分的是什么:一个日常类比

用一个特别朴素的生活类比,你从办公室回家。顶层是“任务层”:我要回家,先出公司,走到地铁站,坐几站,出站,走回家。这一层只关心步骤顺序和依赖关系,完全不管迈哪条腿、步幅多大。底层是“运动层”:迈左腿、抬右腿、保持身体平衡、避开前方水坑。这一层只关心怎么执行,不关心为什么走这条路。中间层是“状态层”:你现在在哪个位置,地铁是否拥挤,哪条路临时封了。它把世界的实时状况翻译成上下两层都能理解的格式。

H-WM就是把这套思维工程化,把人脑天然具有的分层抽象,变成机器可计算的结构。核心收益是:每一层都能在适合的时间尺度、用适合的表示做决策,互不干扰,又通过定义良好的接口协作。需要提醒的是,分层规划这个概念并不新,但“分层世界模型”和传统分层规划有个关键区别:它不仅分层规划,还分层建模。模型本身是三层的,状态更新和预测在每一层以不同的抽象程度进行,决策只是模型的一种使用方式。这个点很多人会忽略,导致最后实现的其实是一个分层模块的堆叠,而不是真正统一的世界模型。

2. H-WM的整体架构设计与核心模块拆解

2.1 三层世界模型各自负责什么

H-WM里最常见的工程定义是三层结构。

第一层是语义层,也叫符号层,它用类似PDDL谓词的方式表示世界的高层状态,比如“(robot_at kitchen)”“(object_on table)”“(door_open)”。这一层的数据格式适合任务规划器做逻辑推理,它不关心机器人具体在厨房的哪个坐标,只关心“是不是在厨房”这个符号事实。

第二层是状态层,负责维护物体的位姿、速度、可通行区域等几何信息,通常是欧拉向量加协方差的形式。这一层承担两个职责:把语义层的谓词“翻译”成具体的几何目标,比如把“(object_on table)”解析成“水瓶位姿是(x, y, z, roll, pitch, yaw)”;把运动层的结果“抽象”成语义事件,比如“机器人到达目标点”这个几何事实转换成“(robot_at kitchen)”这个符号谓词。

第三层是运动层,直接对接机器人控制,包含当前关节角、里程计、碰撞检测结果等。这一层面向控制回路,使用局部规划器、状态机来完成短时间尺度的执行,比如导航里的TEB、机械臂里的RRT*或者MPC。

三个层次的典型时间尺度是这样的:语义层1到10秒,状态层100毫秒到1秒,运动层10到50毫秒。

层级表示方式更新频率典型算法
语义层PDDL谓词 / 符号图秒级HTN、PDDL规划器、LLM任务规划
状态层几何状态 + 概率表示百毫秒级卡尔曼滤波、粒子滤波、成本地图
运动层关节空间 / 位姿空间十毫秒级RRT*、TEB、MPC、DWA

2.2 任务规划与运动规划如何在分层中被连接

任务规划器在语义层工作,输入是一组初始谓词和目标谓词,输出是一串抽象动作序列,例如“移动到厨房 -> 打开柜门 -> 抓取水瓶 -> 移动到客厅 -> 放置”。这串动作是符号级的,不能直接发给底盘和机械臂,因为它们不知道“移动到厨房”在地图上的具体坐标是什么。

所以在H-WM里,最核心的设计是跨层接口。每个抽象动作都要配一个状态层的“子目标规范”。举例来说,“移动到厨房”在状态层被转化为:目标点位于地图坐标(x, y),朝向theta,允许位置误差0.2米,角度误差0.1弧度,超时30秒。“抓取水瓶”在状态层则被转化为:水瓶当前位姿,机械臂末端可达性检查,一组可抓取姿态集合。

这个转化过程是H-WM实现中工作量最大、最容易出错的地方。我的建议是,把子目标规范设计成独立的数据结构,而不是在任务层代码里硬编码。这样做的好处是扩展性:以后新增一个“搬运箱子”任务,不需要改任务规划器,只需要在状态层写一个新的子目标解释器。

运动层拿到子目标规范后,启动对应算法,执行完的结果由状态层做事件检测,再反馈回语义层更新谓词。闭环跑起来后,任务规划器和运动规划器实现了比较彻底的解耦:任务层看不到关节角的细节,运动层看不到任务语义,各自的搜索空间大幅缩小,规划速度和鲁棒性同时提升。

2.3 为什么推荐HTN加采样规划,而不是端到端

我这两年踩过最大的坑,就是一开始迷信端到端。端到端学习在封闭场景的感知到动作映射上效果不错,但室内任务场景里,任务层需要长时程的逻辑推理,比如“如果客厅桌子被占用了,就放到餐桌上”。这种逻辑用纯学习方案很难保证一致性,而且场景一换,重训成本非常高。

HTN(层次任务网络)的优势在于它天然支持分层。一个“完成一次送水”的方法里,可以把子任务定义为“导航到厨房-抓取水瓶-导航到客厅-放置水瓶-任务完成”,每一层都可以继续向下分解成原语动作。任务层的搜索空间比平面PDDL搜索小很多,因为HTN利用领域知识把大块的搜索跳过了。

运动层则采用RRT* + TEB这类采样加优化的混合路线。原因非常实际:不需要非常精确的环境模型,只要成本地图和碰撞检测就能跑;实现简单,调参透明,出了bug好定位。当然,H-WM并不绑定具体算法,你完全可以把任务层的HTN换成LLM加语义解析,运动层换成MPC,只要跨层接口保持一致,系统的其他部分不动也能转。架构的意义在解耦,不在某一种具体实现。

3. 实操落地:从仿真到真机的完整路径

3.1 环境与工具链选型

实际做H-WM项目,我建议首选轮式移动平台,比如差速底盘加2自由度机械臂加RGBD相机。选这个平台是因为导航和抓取两个子问题足够典型,又不会像双足机器人那样把运动控制的复杂度顶上来。

工具链方面,我常用的组合是:仿真环境用开源机器人仿真器,地图与定位用当前主流的SLAM方案加自适应蒙特卡洛定位,成本地图用三层叠加的方式,任务规划用HTN库加自定义域文件,路径规划用采样规划器库加时间弹性带,状态层用行为树或状态机承接事件流。这里有个现实的约束:选型时一定要确认各库之间的版本兼容性,我见过不少项目在联调阶段才发现通信协议对不上,非常崩溃。先把仿真里最简版本跑通,再逐步加复杂度,这是最稳妥的路径。

3.2 分层状态表示与地图体系的关键细节

状态层的设计我强烈建议用统一的“世界模型节点”来管理,而不是散落在各个模块里各维护一份。每一层的数据结构不同,但通过消息传递机制保持同步。语义层维护谓词库,状态层维护物体和机器人位姿、可通行区域网格,运动层维护里程计、关节角和碰撞检测结果。

地图覆盖层的设计也要讲究。我通常把栅格地图拆成三层:静态地图层、动态障碍物层、膨胀层。静态层来自SLAM构建,动态层实时更新行人和移动障碍物,膨胀层根据机器人外形对静态和动态层做处理。导航规划器查询的是三项叠加后的成本地图。

膨胀半径的设置需要根据机器人实际外形来定,不建议拍脑袋。我常用的公式是:

膨胀半径 = 机器人外接半径 + 安全余量

安全余量一般在0.1到0.3米之间,取决于场景中人的密度和机器人的最高速度。设置过小会导致贴墙剐蹭,设置过大则会让机器人无法通过正常宽度的门。我在一个通道1.2米、机器人宽0.6米的场景里实测,安全余量取0.1米机器人能过门,但转弯时后轮偶尔蹭到门框;取0.15米体感最稳。这类参数必须现场标定。

3.3 任务层HTN分解与子目标生成实例

以“送水到客厅”为例,HTN域可以设计成这样:

  • 方法:bring_object(target_container, target_location)

    • 前提:机器人位于厨房,水杯在柜子上
    • 分解子任务:
      • navigate_to(kitchen_table)
      • grasp_object(water_bottle)
      • navigate_to(living_room_table)
      • place_object(water_bottle)
  • 方法:grasp_object(object)

    • 前提:物体位姿已知,机器人可达
    • 分解动作:移动到抓取位,执行抓取,状态层检测“物体在手”

这些方法描述的是领域知识,写好后HTN规划器会以状态层的当前事实作为输入,输出一棵可执行的任务树。任务层的输入输出都是符号,而符号的真假由底层的状态估计来更新,这就是分层交互的重点。

子目标规范的数据结构可以写成这样:

class SubGoal: action_type: str # "navigate" / "grasp" / "place" / "open_door" target: dict # { "x": 3.2, "y": 1.8, "theta": 0.785 } tolerance: dict # { "pos": 0.2, "angle": 0.1 } timeout: float # 30.0 preconditions: list # [ "object_at(water_bottle, kitchen_table)" ] on_success: list # [ "set_fact(robot_at, kitchen_table)" ] on_failure: list # [ "set_fact(navigation_failed, kitchen_table)" ]

这个结构把“做什么”和“怎么做”之间的翻译变成了一个固定接口。以后每增加一种任务类型,只需要写对应的子目标解释器,任务规划器和运动规划器几乎不动。我在多个项目里验证过,这种设计能显著降低系统的维护成本。

3.4 运动层规划参数调校:RRT* 与 TEB 的实际调整心得

运动规划这一层是最容易出玄学问题的地方,我给出两个最常见规划器的调参经验。

RRT*用于机械臂和全局路径规划,核心参数有三个。步长(step size)一般在0.05到0.2米之间,步长太大会导致窄障碍物区采样点跳过可通行区域,太小则搜索太慢,我常用的初始值是0.1米。最大采样次数取决于地图复杂度,简单房间5000次足够,密集货架区我开到2万次以上,否则找不到可行路径。到达半径(goal tolerance)根据任务需求设置,移动底盘取0.1米,机械臂抓取前取0.02米,并且最后会加一段轨迹优化来平滑路径。

TEB用于局部导航,关键参数是时间最优权重(weight_optimaltime)和避障权重(weight_obstacle)。这两个参数直接决定了机器人是贴着障碍物快跑,还是绕远路慢跑但更安全。我一般从weight_optimaltime=1.0、weight_obstacle=100开始,再根据实测微调。最大速度和加速度参数必须与底盘真实能力匹配,否则规划出的轨迹电机跟不上,机器人就会出现抖动、打滑、轨迹异常。

参数调校的本质是取舍。用一个代价函数来理解就是:

J = w1 * 完成任务时间 + w2 * 障碍物距离惩罚 + w3 * 速度平滑惩罚

权重不同,行为完全不同。希望机器人更稳妥,就把w2调大;希望它高效,就把w1调大。没有任何一组参数能通吃所有场景,必须在任务部署场景里做现场标定。我的做法是在仿真里先标定一组大概参数,再放到真机上做3组对照测试,取行为最符合预期的那组做微调。

3.5 闭环与重规划触发策略:避免系统震荡的关键

我见过不少方案在任务层、运动层都能跑,但整体闭环逻辑一团糟。H-WM的闭环应该是这样:语义层监控任务谓词是否成立,比如“物体是否到达目标位置”;状态层定时或事件触发地做感知刷新,更新物体位姿和地图变化;如果状态层发现当前路径被动态障碍物挡住,或者执行结果与预期不符,就触发运动层重新规划;如果状态层检测到子目标长期无法实现,比如超时3次,就上报语义层,让任务规划器重新做高层决策。

这里的问题是重规划触发条件设计不当会导致系统震荡。我最开始把触发条件设得过于灵敏,机器人在狭窄走廊里反复重启规划器,原地打转根本没法走。后来加了两道防护:重规划冷却时间(同一原因10秒内最多触发2次),以及三级降级策略(再规划 -> 降速 -> 停止并上报)。加上这两条之后,系统的稳定性提升非常明显。

4. 常见问题与排查技巧实录

4.1 任务层规划了物理上不可行的动作

现象:任务层让机器人“穿过走廊到达储物间”,但走廊实际宽度小于机器人宽度,运动层始终找不到路径,超时上报后,任务层又给出了另一个同样不可行的子目标,陷入死循环。

排查思路是这样的。先检查状态层的机器人尺寸和地图的可通行宽度是否一致,很多情况是尺寸设置和真实底盘的外轮廓对不上。其次,在语义层的每个动作下发前做一次可行性前置校验,“navigate_to(目标点)”先查目标点是否落在成本地图的免费区域,并且目标点周围的邻域是否足够容纳机器人。最后,把“可行性校验失败”作为一个谓词反馈给任务规划器,让它把这个失败当成一个可解决的子问题来处理,比如换一扇门、换一条路线,而不是盲目重试同一个方案。

我在实际项目中增加了状态层的“前置条件检查器”,在任务规划器输出每个抽象动作后、下发运动层之前统一做校验。校验通过才执行,不通过直接返回失败原因。这个改动省掉大量无效运动规划,也让任务层的失败分支变得肉眼可见。

4.2 状态估计误差跨层放大的问题

现象:视觉定位对目标物体的位姿估计有5厘米误差,状态层把抓取点发给机械臂,机械臂按这个点规划轨迹,每次抓空。每一层单独看都正常,但误差在“语义层只知道物体在桌上”和“状态层只知道位姿大概是(x, y, theta)”之间的传递过程中被放大了。

根源是层间翻译精度的不匹配,语义层只关心“在不在”,状态层关心“在哪”,运动层关心“怎么抓”。解决思路有两条。第一,状态层不单给一个位姿估计,还给协方差矩阵,抓取规划时把协方差融合进“可抓取位姿集合”里。第二,运动层的RRT*可以直接把末端姿态容差作为目标区域的半径,比如抓取目标不是单点,而是半径5厘米、角容差0.05弧度的集合。

真机部署时还有一个技巧值得分享:在执行抓取前增加一个“视觉伺服微调”阶段。机械臂运动到预抓取位之后,用相机主动重新识别物体位置,再把末端对准后执行抓取。这一步能吃掉90%的位姿误差。我加了这一步之后,抓取成功率从不到60%直接拉到90%以上。

4.3 重规划触发过于频繁导致系统震荡

现象:动态场景中机器人每遇到一个新障碍物都要重新规划,全局路径频繁切换,小车原地打转或者呈Z字形前进。

这个问题的根源是把重规划决策交给了直觉判断,而不是明确的度量标准。最终我用了三层触发阈值来解决。第一层是路径碰撞,成本地图上当前路径被障碍物占据超过阈值,比如连续3个栅格被占,才触发重规划。第二层是路径偏离,机器人相对期望轨迹的横向偏移超过0.3米才触发。第三层是子目标不可达,子目标位置在成本地图上被判定为障碍区才触发。

加上冷却时间和三级降级策略之后,整个系统的行为变得可预测。规划和重规划的频率,不应该由拍脑袋决定,而是要把每种异常情况都定义成可度量的条件,由状态层统一做决策。

4.4 抽象粒度到底怎么选

分层模型里被问得最多的问题就是:任务要不要分更多层?每个子任务拆多细才算合理?

我的经验法则是:如果任务层输出的一个动作,状态层要花超过5秒去解释,说明这个抽象动作太粗。如果任务层输出的动作序列超过20步,说明抽象粒度太细,应该把连续同类型的动作聚合起来。一个合理的抽象动作,应该满足状态层可以为其定义明确的前置条件、执行时间估计和后置条件,并且运动层可以找到一个相对独立的规划策略来执行它。

分层不是越细越好。有一次我把一个导航任务拆成四个子导航动作,结果任务层的规划速度没有提升,反而因为接口转换次数变多,整体延迟还增加了一些。适度抽象,才是分层世界模型的关键。

5. 参数速查表与极简代码骨架参考

5.1 参数速查表:从项目实践中收敛出来的初始值

我把前面提到的关键参数整理成了一张速查表,适合在项目初期直接抄作业:

模块参数推荐初始值备注
膨胀层膨胀半径0.15米按机器人实际尺寸标定
全局规划最大采样次数10000密集货架场景提到2万+
RRT*步长0.1米窄道场景降低到0.05
TEB避障权重100行人密集场景可调高
TEB时间最优权重1.0效率优先场景调高
重规划冷却冷却时间10秒同一原因10秒内最多2次
子目标超时超时时间30秒按任务时长调整
视觉伺服预抓取距离0.1到0.2米在预抓取位先视觉对齐再抓

这些参数只能作为起点,到真机阶段必须根据场地条件实测调整。每次调整只动一个参数,改完就测一组实验,不然参数之间互相耦合,出了问题根本定位不到原因。

5.2 极简H-WM代码骨架:展示三个层次怎么协作

# 语义层:任务规划 class TaskPlanner: def build_domain(self): # 用HTN/PDDL描述bring_object等抽象动作 # 返回任务方法定义 pass def plan(self, facts, goal): # 输入状态层的符号事实,输出子目标序列 return subgoal_list # 状态层:子目标翻译与状态维护 class StateModel: def interpret(self, subgoal): # 把抽象动作翻译成几何子目标 if subgoal.action_type == "navigate": target_pose = self.map.get_pose(subgoal.target) return GeometricGoal(target_pose, tolerance=0.2) if subgoal.action_type == "grasp": obj_pose = self.object_db.get_pose(subgoal.object_id) grasp_poses = self.inverse_kinematics(obj_pose) return GraspGoal(grasp_poses, cov=obj_pose.cov) def perceive(self): # 更新事实库、物体位姿、地图障碍信息 pass # 运动层:执行几何子目标 class MotionPlanner: def plan(self, geometric_goal): if geometric_goal.type == "navigation": path = rrt_star(global_costmap, robot_pose, geometric_goal.target) return teb_follow(path) if geometric_goal.type == "grasp": traj = rrt_star_arm(arm_state, geometric_goal.grasp_poses) return traj # 主循环:闭环执行 state_model = StateModel() while not task_done: subgoals = task_planner.plan(facts, goal) for sg in subgoals: ok, result = execute_subgoal(sg, state_model, motion_planner) if not ok: retry_or_replan(sg, result)

这段骨架去掉了大量工程细节,只保留三个层次最核心的交互关系。实际项目中,每个环节都要考虑失败分支、日志记录、超时处理、状态同步。代码结构理清后,H-WM的价值会体现得非常直接:新增一个任务类型时,你不需要改运动规划器,只需要在状态层写一个新的子目标解释器。

6. 最后分享一点我的个人体会

分层世界模型本质上不是某个惊艳的算法,而是一种工程组织方式。我做了几个机器人项目之后再回头看,最大的感受是:分层带来的收益不是性能提升,而是可调试性。出问题时,你不需要在一个几千维的状态空间里大海捞针,而是先看语义层的谓词对不对,再看状态层的估计准不准,最后查运动层的路径有没有问题。每一层都可以单独验证,再逐层联调,问题范围大幅缩小。

另一个很实际的建议是:先用最简闭环跑通一个导航子任务,再逐步扩展成完整的H-WM。我第一次尝试直接搭好三层并接满传感器,结果在联调阶段花费了大量时间处理接口问题。如果当初先做一个“导航到桌子”的最小闭环,验证好跨层接口的稳定性,后面的进展会顺利得多。如果你正在做一个类似的实物机器人项目,建议优先把“子目标规范”这个跨层接口数据结构设计好,后面你会感谢自己在早期做的这个决定。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询