3. 任务规划基础:状态空间表示、动作与效果、规划域定义

各位好,欢迎来到任务规划的第一课。说实话,很多做机器人路径规划的朋友,一听到「任务规划」就觉得头大,觉得那是搞AI的人的事。我以前也这么想,直到有一次在产线上调试一个复合机器人——它明明能绕过障碍物,却不知道先抓A零件还是B零件,结果卡在那来回晃。那一刻我意识到:轨迹规划管的是「怎么走」,任务规划管的是「做什么」和「按什么顺序做」。缺了后者,机器人就是个有肌肉没脑子的大力士。

这一章我们就把任务规划的地基打牢。说白了,就是三件事:怎么描述世界状态、怎么定义动作、怎么把整个问题框起来。嗯,我们一个一个来。

3.1 状态空间表示:给世界拍一张快照

任务规划的第一步,是让机器人理解「世界现在长什么样」。你不能指望它像人一样看一眼就全明白,得给它一个结构化的描述。这个描述,就叫状态空间表示。

我个人习惯用一组命题(propositions)来描述状态。每个命题是一个布尔变量,真或假。举个例子:

状态 S = {
    at(robot, kitchen) = True,
    at(cup, shelf) = True,
    holding(robot, nothing) = True,
    clean(cup) = False
}

这四句话就拍了一张快照:机器人在厨房,杯子在架子上,机器人空手,杯子没洗。简单吧?但这里有个坑——状态空间的大小会爆炸。如果你有N个布尔命题,理论状态数就是2^N。我在一个仓储项目里见过有人定义了50个命题,结果状态空间比银河系的星星还多,规划器跑了一宿没出结果。

避坑指南: 我曾经犯过一个错误——把「杯子在桌上」和「杯子不在桌上」同时作为两个命题。这等于给了规划器一个矛盾的世界,它永远找不到解。记住:命题之间要互斥且完备,别自己给自己挖坑。

除了命题逻辑,还有一种常用的表示叫一阶逻辑。它允许你用谓词和对象来描述,表达能力更强。比如:

状态 S = {
    At(Robot, Kitchen),
    On(Cup, Shelf),
    Holding(Robot, None),
    Clean(Cup) = False
}

你看,这里把对象(Robot, Cup, Shelf)和关系(At, On, Holding)分开了。好处是:如果你有10个物体,不需要为每个物体单独定义「在桌上」的命题,一个On(x, y)谓词就能搞定。我建议你在对象数量超过5个时,果断用一阶逻辑,否则命题列表会写到让你怀疑人生。

3.2 动作与效果:机器人能做什么

有了状态描述,下一步就是告诉机器人它有哪些「本事」。每个动作由三部分组成:前提条件(preconditions)、效果(effects)、以及参数(parameters)。

拿「抓取」这个动作举例:

Action: Pick(robot, obj, from)
  Parameters:
    robot - 机器人
    obj   - 要抓的物体
    from  - 物体当前所在位置
  Preconditions:
    At(robot, from) = True
    At(obj, from) = True
    Holding(robot, None) = True
  Effects:
    Holding(robot, obj) = True
    At(obj, from) = False
    Holding(robot, None) = False

你想想看,这个定义其实就是在说:「如果机器人和物体在同一位置,而且机器人空着手,那么抓取之后,机器人手里就有了物体,物体不在原位置了,机器人也不再空手了。」——这不就是常识吗?但机器没有常识,你得一字一句写清楚。

我的经验: 写动作效果时,一定要把「被改变」和「被解除」的命题都列全。我见过有人只写了Holding变成True,忘了把At(obj, from)设成False。结果规划器认为物体还在原地,机器人抓了个寂寞。嗯,调试这种bug特别痛苦。

动作效果通常用STRIPS风格来描述,也就是「添加列表」和「删除列表」。上面的例子用STRIPS写就是:

Action: Pick(robot, obj, from)
  Pre: {At(robot, from), At(obj, from), Holding(robot, None)}
  Add: {Holding(robot, obj)}
  Del: {At(obj, from), Holding(robot, None)}

这种格式简洁明了,也是经典规划器(如FF、FastDownward)的标配。我个人觉得,初学者从STRIPS入手最不容易出错。

3.3 规划域定义:把问题框起来

有了状态和动作,最后一步就是定义规划域(Planning Domain)。说白了,就是告诉规划器:你在哪个世界里干活,你的目标是什么。

一个完整的规划域包含:

  • 对象类型:比如机器人、位置、物体
  • 谓词定义:比如At(?x ?y)、Holding(?r ?o)
  • 动作定义:所有可用的动作模板
  • 初始状态:世界一开始的样子
  • 目标状态:世界应该变成的样子

下面是一个完整的PDDL(Planning Domain Definition Language)示例,这是规划领域的「普通话」:

(define (domain kitchen-world)
  (:types robot location object)
  (:predicates
    (at ?r - robot ?loc - location)
    (at ?obj - object ?loc - location)
    (holding ?r - robot ?obj - object)
    (clean ?obj - object)
  )
  (:action pick
    :parameters (?r - robot ?obj - object ?from - location)
    :precondition (and (at ?r ?from) (at ?obj ?from) (holding ?r nothing))
    :effect (and (holding ?r ?obj) (not (at ?obj ?from)) (not (holding ?r nothing)))
  )
  (:action place
    :parameters (?r - robot ?obj - object ?to - location)
    :precondition (and (holding ?r ?obj) (at ?r ?to))
    :effect (and (at ?obj ?to) (not (holding ?r ?obj)) (holding ?r nothing))
  )
)

然后是一个具体的问题定义:

(define (problem wash-cup)
  (:domain kitchen-world)
  (:objects
    robot1 - robot
    cup1 - object
    kitchen - location
    sink - location
  )
  (:init
    (at robot1 kitchen)
    (at cup1 kitchen)
    (holding robot1 nothing)
    (not (clean cup1))
  )
  (:goal
    (and (at cup1 sink) (clean cup1))
  )
)

你看,这个定义说得很清楚:机器人叫robot1,杯子叫cup1,初始都在厨房,杯子没洗。目标是把杯子放到水槽里,并且洗干净。规划器拿到这个定义后,就会自动搜索出一系列动作序列,比如:Pick(robot1, cup1, kitchen) → Move(robot1, kitchen, sink) → Place(robot1, cup1, sink) → Wash(robot1, cup1)。

核心要点: 规划域定义的质量,直接决定了规划器能不能找到解、找到的解好不好。我见过太多人把域定义得过于复杂,结果规划器在无关动作上浪费大量时间。记住一个原则:只包含与目标相关的动作和谓词。比如你的目标是洗杯子,就别定义「煮咖啡」的动作——哪怕你的机器人确实会煮咖啡。

下面这张图总结了本章的知识体系,你可以对照着梳理一下思路:

任务规划基础:三大核心模块 状态空间表示 命题逻辑 一阶逻辑谓词 布尔变量组合 状态空间大小控制 动作与效果 前提条件 添加/删除列表 STRIPS 风格 参数化动作模板 规划域定义 对象类型 谓词定义 初始状态 目标状态 三者关系 状态空间描述「世界是什么样」→ 动作定义描述「世界如何改变」→ 规划域把问题框定在特定场景 PDDL 示例:厨房世界 (define (domain kitchen-world) (:types robot location object) (:predicates (at ?r - robot ?loc - location) ...) (:action pick :parameters (?r - robot ?obj - object ?from - location) :precondition (and (at ?r ?from) (at ?obj ?from) (holding ?r nothing))

最后说一句:任务规划的基础,说白了就是「用逻辑语言描述常识」。你定义的状态越精确、动作越完整、域越聚焦,规划器就越聪明。别小看这一步——我见过太多项目,算法调得天花乱坠,结果栽在状态表示上。地基没打好,楼盖得再高也得塌。