
PDDL 这门语言我最早接触是在研究规划算法的时候。当时导师扔给我一份规划竞赛的题目说“你先把这套描述问题的方式搞清楚”于是我在一个周末里对着文档和示例硬啃从一脸懵到能独立建模一个完整的物流规划问题。回头再看PDDL 这个缩写——Planning Domain Definition Language规划领域定义语言——其实就是一套用来“描述问题”而非“解决问题”的形式化语言。你用它把现实世界里的对象、状态和动作抽象成逻辑符号剩下的搜索和推理交给规划器planner去完成。这篇文章我打算从一个实践者的角度把 PDDL 这条入门路径完整走一遍。你会搞明白它到底解决什么问题domain 文件和 problem 文件怎么拆谓词和动作怎么写又怎么把一套模型跑起来最后我还会把自己踩过的坑一并倒出来。如果你正在学经典规划、想做自动任务编排或者单纯对“机器怎么从一堆动作里找出方案”有好奇这篇文章应该能帮你省下不少摸索时间。1. PDDL 是什么以及为什么值得花时间学1.1 一套“描述问题”的领域语言先打一个比方。你让一个朋友帮你取快递你只需要说明“哪个快递、在哪家驿站、你住哪栋楼、驿站几点关门”朋友自己会规划路线、安排时间。PDDL 做的事情完全一样它不关心用什么算法去找答案只负责把“快递在哪、你要去哪、你能做什么操作”这些信息结构化地写出来。从学术上讲PDDL 最初是 1998 年国际规划竞赛IPC为了统一各规划器输入格式而设计的语言。发展到现在主流是 PDDL2.1经典部分支持谓词逻辑、类型系统、动作的 precondition前置条件和 effect效果后续扩展还加入了数值变量、时序约束等高级特性。不过入门阶段只要吃透经典部分就能做很多事了。这套语言的价值在于“领域与问题分离”。domain 文件描述的是这个世界的“规则”比如“拿起积木需要手空着”“放下积木后手就不空了”problem 文件描述的是某个具体场景比如“现在桌上有三块积木目标是搭成一座塔”。同一个 domain 可以配无数个 problem这和编程里的“类”与“实例”的关系非常像。1.2 学它到底能干什么PDDL 最直接的应用场景是自动规划。物流公司用在配送路径和装车顺序上机器人在仓储环境里规划抓取和移动的次序游戏 AI 用它做任务编排甚至工业流水线的工序调度也可以建模范式。学术界几乎所有规划算法的论文都会用 PDDL 基准测试集来跑实验所以你想读懂这些论文PDDL 就是第一道门槛。不过我更想强调的是学 PDDL 对思维方式的训练价值。它强迫你去掉所有语义上的模糊明确“什么条件成立时动作可用”“动作执行后哪些事实发生了变化”。这种把直觉变成形式化描述的思维方式写代码、设计系统时都极其有用。尤其是你习惯了写命令式代码第一次接触“声明式建模”会觉得整个视角都被打开了。1.3 你需要具备哪些基础说实话PDDL 的语法门槛不高比大多数编程语言都简单。只要你理解布尔逻辑的基本概念知道“谓词”大致是个真/假判断就足够开始学了。有一点编程经验当然更好因为你能更快理解“变量实例化”和“参数绑定”这些概念但即便你只会用电脑写文档照着示例敲也能跑起来第一个规划任务。2. 环境搭建与第一次运行2.1 规划器怎么选PDDL 写完之后需要交给规划器来求解。规划器就是做搜索和推理的程序——你给它 domain 和 problem它返回一个动作序列也就是“计划”。比较常用的几个规划器有 Fast Downward、FF、LAMA这些在国际规划竞赛里都属于经典选手基本能满足入门和中等规模问题的求解需求。如果你是第一次跑我个人的建议是直接用在线编辑器。比如 planning.domains 网站提供的在线环境左边写 domain右边写 problem点一下 solve 就能看到规划结果不需要你在本地配置任何依赖。这个在线环境用的也是 Fast Downward 系列规划器和本地跑的效果基本一致。如果要在本地跑我建议装 Fast Downward因为它是目前学术界用得最广泛的开源规划器之一支持 PDDL2.1 甚至更高的特性。下载源码后编译一次后面跑实验就很方便了。编译过程在 Linux 和 macOS 上都很顺畅Windows 用户建议用 WSL 或者 Docker 跑。2.2 第一个 demo积木世界本地环境配好之后我们用一个最经典的“积木世界”来跑通整个流程。这个场景在规划领域相当于编程界的 Hello World几乎所有教材都会用到。domain 文件长这样(define (domain blocksworld) (:requirements :strips) (:predicates (on ?x ?y) (ontable ?x) (clear ?x) (handempty) (holding ?x) ) (:action pick-up :parameters (?x) :precondition (and (clear ?x) (ontable ?x) (handempty)) :effect (and (not (ontable ?x)) (not (clear ?x)) (not (handempty)) (holding ?x)) ) (:action put-down :parameters (?x) :precondition (holding ?x) :effect (and (not (holding ?x)) (clear ?x) (handempty) (ontable ?x)) ) (:action stack :parameters (?x ?y) :precondition (and (holding ?x) (clear ?y)) :effect (and (not (holding ?x)) (not (clear ?y)) (clear ?x) (handempty) (on ?x ?y)) ) (:action unstack :parameters (?x ?y) :precondition (and (on ?x ?y) (clear ?x) (handempty)) :effect (and (not (on ?x ?y)) (not (clear ?x)) (clear ?y) (handempty) (holding ?x)) ) )problem 文件长这样(define (problem blocks-1) (:domain blocksworld) (:objects a b c) (:init (clear a) (on a b) (on b c) (ontable c) (handempty) ) (:goal (and (on a b) (on b c) (ontable c))) )这个例子的初始状态是 a 在 b 上b 在 c 上c 在桌上目标状态不变所以理想情况下规划器应该返回空计划。如果你把目标改成(and (on b a) (on c b))规划器就要给出一个几步的搬移方案。第一次看到规划器输出的完整动作序列时你会直观感受到 PDDL 描述能力与推理能力结合的威力。3. 核心语法拆解domain 和 problem 的每一个零件3.1 domain 文件的结构domain 文件定义“世界观”。它里面有几个关键构件首先是:requirements它告诉规划器这个领域用到了哪些语言特性。基础必加的是:strips表示只用经典的 STRIPS 动作表示。如果用了类型系统就要加:typing用到相等判断加:equality用到条件效果加:conditional-effects。我建议只加必要的特性功能越少规划器搜索空间就越小求解速度越快。其次是:predicates也就是谓词表。这里是所有“原子事实”的声明。每个谓词有名字和参数参数用?x这类变量表示前面的?是语法要求不是装饰。谓词实例化之后就变成命题比如(on a b)表示“a 在 b 上”它的真值在状态中被维护。最后是动作定义。每个动作有三段:parameters声明动作需要的对象参数。:precondition声明动作执行前必须为真的条件。:effect声明动作执行后世界发生的变化。precondition里的and表示所有条件必须同时满足effect里的and表示所有效果同时发生。要删除一个事实就在前面加not。这里必须特别注意经典 PDDL 遵循“严格封闭世界假设”即所有未声明为真的命题默认都是假的。所以在 effect 里你需要显式删除那些不再成立的事实否则规划器会认为它们依然成立。3.2 problem 文件的结构problem 文件描述“具体任务”结构相对简单:domain声明使用哪个 domain 文件。:objects列出问题中出现的所有对象名。:init描述初始状态下所有为真的事实。:goal描述目标状态需要满足的条件。这里有个很容易忽略的点:init里列出的是“所有为真的事实”而:goal里的则有不同的语义。目标条件可以放宽为“至少满足这些条件”也就是说额外的真事实不会影响目标判定。比如目标是(on a b)如果状态里还有一个无关的(ontable c)目标依然成立。3.3 类型系统与变量绑定当对象数量变多谓词参数容易出现类型混淆。比如“拿起”动作只能作用于可拿取的物体不能作用于桌子本身。这时候就要引入:typing特性(:requirements :strips :typing) (:types block table) (:constants table - table) (:predicates (on ?x - block ?y - block) ...):types声明类型层级:constants声明领域中的常量对象常量不需要在 problem 里重复声明。变量后面用- 类型来标注类型语法上-两边必须有空格。类型系统不仅能避免无意义的动作实例化更重要的是能大幅缩减规划器的搜索空间。物体一多这个优化是数量级的差别。值得一提的是PDDL2.1 之后还引入了:equality允许在动作参数和条件里使用判断两个变量是否指向同一对象。这个在很多场景下非常关键比如“把一个物体从 a 移到 b要求 a 和 b 不同”。不加这个限制规划器可能产生把东西搬到原地这种蠢计划。4. 从零完成一个物流规划案例理论说得再多不亲手建模等于白学。这一节我带你完整走一遍物流派送问题。场景设定为有两辆车三个地点一个包裹要从 A 地送到 C 地车辆可以在地点之间移动包裹可以被装上车或卸下车。4.1 先梳理谓词再写动作建模的第一步不是写代码而是想清楚“这个世界的状态由哪些事实组成”。我建议拿张纸列出来这对新手来说特别重要。物流场景里车在哪(at ?v - vehicle ?loc - location)包裹在哪(at ?p - package ?loc - location)包裹在车上(in ?p - package ?v - vehicle)接下来是动作。动作就是状态的转换器每个动作要写清楚什么时候能用、用了之后改变什么drive车从一处移动到另一处。前提是车当前在起点效果是车不再在起点到了终点。load把包裹装上车。前提是车和包裹在同一地点效果是包裹不再在该地点而是在车上。unload把包裹卸下车。前提是包裹在车上且车在目标地点效果是包裹不再在车上而是在该地点。4.2 完整代码实现domain 文件(define (domain logistics) (:requirements :strips :typing) (:types vehicle package location - object) (:predicates (at ?x - object ?y - location) (in ?p - package ?v - vehicle) ) (:action drive :parameters (?v - vehicle ?from ?to - location) :precondition (at ?v ?from) :effect (and (not (at ?v ?from)) (at ?v ?to)) ) (:action load :parameters (?p - package ?v - vehicle ?l - location) :precondition (and (at ?v ?l) (at ?p ?l)) :effect (and (not (at ?p ?l)) (in ?p ?v)) ) (:action unload :parameters (?p - package ?v - vehicle ?l - location) :precondition (and (at ?v ?l) (in ?p ?v)) :effect (and (not (in ?p ?v)) (at ?p ?l)) ) )problem 文件(define (problem logi-1) (:domain logistics) (:objects truck1 truck2 - vehicle pkg1 - package A B C - location ) (:init (at truck1 A) (at truck2 B) (at pkg1 A) ) (:goal (at pkg1 C)) )这个模型有一个值得留意的设计我把at谓词的第一个参数定义成了object类型而vehicle和package都继承自object。这样同一套at既能表达车的位置也能表达包裹的位置。如果两个谓词分开写反而会让规则变复杂。类型继承用在这里是一个实实在在的建模技巧。4.3 运行与结果解读把上述两个文件丢到在线编辑器或者本地 Fast Downward 里跑你应该会看到类似输出表述可能略有差异Step 0: (drive truck1 A B) Step 1: (drive truck1 B C) Step 2: (load pkg1 C)等等这里就有问题了。规划器给出的方案直接先把车开到 C 再去装包裹但包裹明明在 A车到 C 后是空车。出现了这个结果说明我的模型有一个严重的语义漏洞load动作的前提是车和包裹必须同处一地而车在 C、包裹在 A 并不满足这个条件所以这不是合法方案。实际规划器应该输出Step 0: (load pkg1 truck1 A) Step 1: (drive truck1 A B) Step 2: (drive truck1 B C) Step 3: (unload pkg1 truck1 C)这才是装货、运输、卸货的完整链路。为什么前面我会故意写一个错误的解读因为这恰好说明了一个关键现象规划器不会脑补你没写清楚的世界。它严格执行规则但绝不会“理解”规则背后的意图。如果模型里没有定义“包裹必须随车移动”它就会找到某种利用规则漏洞的诡异方案。接着你可以试一个有趣的修改把目标改成(and (at pkg1 C) (at truck2 A))看看规划器怎么调度两辆车。结果通常是先派 truck1 送包裹再派 truck2 去 A。这里规划器会自动处理并行事务的先后顺序就像你同时安排两个人办事一样只不过它在状态空间里搜索出来的。4.4 参数越多问题越难的错觉很多新手容易以为只要问题写得出来规划器就一定解得出来。这个想法趁早纠正。经典规划在很多问题上是 PSPACE 完全的物体一多状态空间指数爆炸规划器可能要跑几十秒甚至几分钟才能出结果。我见过有人拿物流模型跑了 20 个包裹结果规划器半天没反应。这时不是模型写错了而是问题的计算复杂度本身就高。所以如果发现求解特别慢优先考虑优化模型加更强的类型限制、减少无关谓词、缩小动作的参数范围、删掉永不使用的动作。模型越精简搜索空间越小效果立竿见影。这也是为什么 PDDL 建模被称作一门“手艺”——同一问题新手可能让规划器跑一天老手建模后几秒钟出结果。5. 我刚入门时常犯的错误整理成一份避坑清单5.1 效果里忘记删掉旧事实这是所有 PDDL 新手最容易踩的坑我自己也栽过。比如移动动作的 effect 只写了(at ?v ?to)忘了写(not (at ?v ?from))结果在规划器眼里车同时出现在两个地方。由于封闭世界假设只有显式not掉的事实才会不再成立所以这不是小细节而是会直接导致规划器给出荒谬方案的严重模型错误。写效果时我建议在脑子里过一遍“这个动作执行后哪些以前成立的事实不再成立”然后逐一对应写not。列一张检查表每个 effect 里的正向事实都有对应的“旧状态删除项”吗没有的话返回去改。5.2and的位置和括号搭配PDDL 用的是 Lisp 这类前缀表达式括号和and的位置非常容易写错。我见过一个常见错误在一个只需要单条件的动作里给:precondition直接写:precondition (clear ?x)这没问题但当条件多于一个时必须写成:precondition (and (clear ?x) (handempty))少了and或括错层规划器直接报语法错误。要避免这类问题一开始建议把括号当作“嵌套结构”来看不要试图手工匹配。用支持括号高亮的编辑器写 PDDL会舒服很多。我在本地用的是 VS Code 加 PDDL 插件在线环境则直接在 planning.domains 里写它也会即时提示括号匹配错误。5.3 谓词设计过度复杂新手容易把谓词设计得非常“重”什么状态都塞进一个大的对象里。比如想表达“红色的大箱子在仓库”就建一个(red-box-in-warehouse ?x)这其实是三个独立事实的压缩。模块化谓词设计的好处是动作建模时灵活否则每增加一个属性你都得重新写一套规则。把原子事实拆小一点代价只是多写几行谓词声明但换来的是建模的弹性。5.4 目标条件只写了一半目标条件必须覆盖所有要求少一个条件规划器就觉得任务完成了。比如实际要求“包裹到达 C 且车辆回到 A”你只写了(at pkg1 C)规划器就会把车随意扔在路上。值得强调的是goal只要求“至少满足”不会强求“恰好满足”所以多余的事实不影响目标成立缺少的事实却会让目标不成立。写目标时逐条核对需求别想当然。5.5 默认值的陷阱封闭世界假设下[:init]中没写的谓词一律为假。很多时候我们觉得“车默认都在仓库吧”但如果你没在:init里写出(at truck1 depot)规划器就认为车不在仓库仓库在场的条件全部无法触发。所以初始化状态要写全别漏掉“默认成立”的事实。5.6 在线编辑器与本地规划器的差异在线编辑器方便但有个问题它默认的规划器可能不支持某些高级特性或者对错误信息的输出比较简略。本地跑 Fast Downward 时错误信息相对更丰富能帮你更快定位语法问题。建议本地装一个哪怕是 WSL 里的环境。这也是为什么我愿意多花十分钟配置本地环境——调试体验完全不一样。6. 继续深入的方向与我的个人体会到这里PDDL 的核心知识已经全部过了一遍。你能看懂 domain 和 problem 的结构能写出谓词和动作能独立跑通一个物流规划案例也知道了该怎么排查常见错误——这些足够支撑你开始解决自己手头的问题了。如果你打算继续深入有几个方向可以走一是研究更复杂的语言特性比如数值变量和动作代价这可以用于最短路径类的规划问题二是看看时序规划动作有持续时间且可以重叠三是去读 Fast Downward 的文档了解不同搜索算法的适用场景同一问题在不同算法下性能差异非常大。再往后你可以参加一些规划竞赛的题目集或者尝试把 PDDL 引入你自己的工作流。最后说点个人体会。我最初学 PDDL 碰到了不少挫折最大的一次心理冲击是我以为“只要模型对了答案自然就对了”结果被一组反直觉的怪异计划弄得怀疑人生。后来我才明白PDDL 模型是对真实世界的一个抽象是你对问题认知的编码。计划质量差往往不是规划器笨而是模型不够精确。想通这一点之后我在建模时总是先问自己这个世界里究竟有哪些对象哪些状态是原子性的哪些操作是真正可以独立执行的这些问题想清楚了剩下的语法不过是记录答案的工具而已。