4. 经典规划算法:STRIPS、PDDL语言入门、前向搜索与后向搜索

好,咱们进入经典规划的世界。说实话,这部分内容是我个人觉得最「有味道」的——它奠定了现代任务规划的理论基础。你想想看,在深度学习火起来之前,机器人怎么思考?靠的就是这些逻辑清晰、结构分明的符号规划方法。

今天我们要聊三个核心东西:STRIPS(斯坦福研究院问题求解器)、PDDL(规划领域定义语言),以及两种最基本的搜索策略——前向搜索和后向搜索。它们就像规划领域的「三原色」,理解了它们,后面再学HTN、Temporal Planning就轻松多了。

4.1 STRIPS:规划算法的老祖宗

STRIPS是1971年Richard Fikes和Nils Nilsson搞出来的。我刚开始读时觉得这名字挺土,但真正用起来才发现——简洁就是力量。

STRIPS的核心思想很简单:世界由一组逻辑命题描述,动作会改变这些命题的真值。每个动作定义三样东西:

  • 前提条件(Preconditions):动作执行前必须为真的命题
  • 添加列表(Add List):动作执行后变为真的命题
  • 删除列表(Delete List):动作执行后变为假的命题

举个例子,机器人抓取一个物体:

Action: PickUp(robot, obj)
Preconditions: At(robot, loc) ∧ At(obj, loc) ∧ Free(robot) ∧ Not(Heavy(obj))
Add: Holding(robot, obj)
Delete: Free(robot), At(obj, loc)

嗯,这里要注意——STRIPS假设世界是「封闭世界」,也就是说,没明确说为真的命题都视为假。我在项目中遇到过有人忘了这个前提,结果规划器一直报错,排查了半天才发现是初始状态没写全。

STRIPS的核心假设:动作只改变显式列出的命题,其他所有命题保持不变。这就是所谓的「框架问题」的经典解法——虽然简单,但够用。

4.2 PDDL语言入门:让规划可移植

STRIPS虽然好,但每个团队都有自己的描述格式,互相不兼容。1998年,一群规划研究者坐在一起,搞出了PDDL——规划领域定义语言。说白了,就是给规划问题定了个「普通话」标准。

PDDL把规划问题拆成两个文件:

文件 内容 类比
domain.pddl 定义谓词、动作、类型 像编程里的「类定义」
problem.pddl 定义对象、初始状态、目标状态 像编程里的「实例化」

我习惯先写domain再写problem,这样逻辑更清晰。来看一个简单的搬运机器人例子:

;; domain.pddl
(define (domain transport)
  (:requirements :strips :typing)
  (:types location robot package)
  (:predicates
    (at ?r - robot ?l - location)
    (at ?p - package ?l - location)
    (carrying ?r - robot ?p - package)
    (connected ?l1 ?l2 - location)
  )
  
  (:action move
    :parameters (?r - robot ?from ?to - location)
    :precondition (and (at ?r ?from) (connected ?from ?to))
    :effect (and (not (at ?r ?from)) (at ?r ?to))
  )
  
  (:action pick-up
    :parameters (?r - robot ?p - package ?l - location)
    :precondition (and (at ?r ?l) (at ?p ?l) (not (carrying ?r ?p)))
    :effect (and (carrying ?r ?p) (not (at ?p ?l)))
  )
)
;; problem.pddl
(define (problem transport-p1)
  (:domain transport)
  (:objects
    robot1 - robot
    pkg1 - package
    locA locB - location
  )
  (:init
    (at robot1 locA)
    (at pkg1 locA)
    (connected locA locB)
    (connected locB locA)
  )
  (:goal (and (at robot1 locB) (at pkg1 locB)))
)

个人经验:写PDDL时,:requirements一定要写对。我刚开始经常漏写:typing,结果规划器报「未定义类型」的错误。另外,动作的:parameters里变量名前面要加问号,这个细节容易忘。

4.3 前向搜索:从初始状态往前推

前向搜索,顾名思义,就是从初始状态出发,不断应用可用的动作,直到达到目标状态。它的逻辑很直观:

  1. 当前状态 = 初始状态
  2. 找出所有前提条件满足的动作
  3. 选一个动作执行,更新状态
  4. 检查新状态是否满足目标
  5. 如果满足,返回动作序列;否则回到第2步

你想想看,这其实就是广度优先搜索或深度优先搜索在规划问题上的应用。但有个大问题——状态空间爆炸。我记得有一次用前向搜索解一个10个物体的抓取问题,搜索树直接膨胀到几百万个节点,跑了半小时没出结果。

避坑指南:前向搜索虽然直观,但分支因子太大。我曾经在一个仓储机器人项目里用纯前向搜索,结果规划时间比机器人执行时间还长。后来加了启发式函数才解决问题。

4.4 后向搜索:从目标状态往回推

后向搜索的思路正好反过来——从目标状态出发,反向推导出哪些动作能产生这个状态,然后一步步往回走到初始状态。它的好处是:只关注与目标相关的动作,搜索空间通常小很多。

后向搜索的关键概念是「回归」(regression):给定一个状态S和一个动作a,如果a的效果能产生S,那么a的前提条件就是S的「前驱状态」。

举个例子:

  • 目标状态:Holding(robot, obj)
  • 能产生这个状态的动作:PickUp(robot, obj)
  • 该动作的前提条件:At(robot, loc) ∧ At(obj, loc) ∧ Free(robot)
  • 所以前驱状态就是这些前提条件

我个人更喜欢后向搜索,尤其是在目标状态比较明确、初始状态很复杂的情况下。但后向搜索也有坑——它需要处理「相关动作」的筛选,如果领域模型写得不好,容易漏掉关键动作。

4.5 前向 vs 后向:怎么选?

我整理了一个对比表,方便你快速决策:

维度 前向搜索 后向搜索
搜索方向 初始状态 → 目标状态 目标状态 → 初始状态
分支因子 大(所有可用动作) 小(只考虑相关动作)
启发式容易度 容易(有明确距离度量) 较难(目标状态不唯一)
适用场景 初始状态简单、目标复杂 目标明确、初始状态复杂
典型算法 FF、FastDownward BFS(regression)、GSP

我的建议:实际项目中,别死磕纯前向或纯后向。现代规划器(比如FastDownward)都是双向搜索+启发式函数的混合策略。你只需要理解这两种思路,然后交给规划器去选。

4.6 知识体系总览

下面这张图把本章的核心逻辑串起来了——从STRIPS到PDDL,再到两种搜索策略,最后到实际应用:

经典规划算法知识体系 STRIPS 框架 PDDL 语言 前向搜索 后向搜索 启发式搜索 (FF) 回归搜索 (GSP) 任务规划器(实际应用) STRIPS 提供框架 → PDDL 标准化描述 → 前向/后向搜索求解 → 最终落地到规划器

这张图你看懂了吗?从上到下,STRIPS定义了「动作是什么」,PDDL让这些定义可读可写,前向和后向搜索则提供了两种求解思路。最后,它们都汇入到实际的任务规划器中。

好了,经典规划算法的核心就这些。说实话,STRIPS和PDDL虽然老,但思想至今仍在用。我去年做的一个仓储机器人项目,底层规划器用的还是PDDL 2.1标准——经典的东西,生命力就是这么强。