资讯动态

LLM+P:用PDDL连接大模型与经典规划器,让AI规划既智能又可靠

发布时间:2026/9/19 16:01:56 来源:尧图企业网站定制
简介《大型语言模型与经典规划器融合增强复杂任务规划能力》为一份PDF论文资料系统介绍LLMP框架该框架将自然语言问题描述转换为PDDL规划领域定义语言借助经典规划器快速求得最优解再翻译回自然语言用于增强大型语言模型在复杂长期规划任务中的表现。资料面向机器学习和自动规划领域的科研人员及高级开发者适用于机器人导航、任务分配、操作序列规划等需将自然语言转化为具体行动计划的场景。压缩包仅1个PDF、大小269KB便于快速获取核心内容。文档涵盖LLMP架构设计、PDDL中间转换流程、多组基准测试搭建与实验结果可对比LLMs在零样本与引入规划器后的规划能力差异并为后续自动检测应用时机、减少人工信息依赖提供研究思路。目前已有95人学习。1. 为什么GPT-4能写诗却规划不了积木GPT-4能写十四行诗、能改代码但面对一个5个积木的blocks world问题它给出的6步方案里第2步就把b3放到了还压着b1的b5上——这种错误连5岁孩子都不会犯。根源在于LLM学的是token的统计相关性而非世界的状态转移规则。经典规划器如Fast Downward、FF恰恰相反一旦问题被格式化为PDDL它们就能用状态空间搜索保证找到可行解甚至最优解但格式化这一步需要人工完成。LLMP这篇论文做的事情就是把LLM当作自然语言到PDDL的翻译器、把规划器当作求解器、再把解回译成人话全程不微调任何参数。对做机器人任务编排、运维自动化的从业者而言这意味着让LLM负责听懂需求让规划器负责保证正确——两条腿走路。下面从PDDL的结构边界讲起再逐步拆解这条管线每一环的具体做法与参数选择。2. PDDL双文件结构与LLM翻译边界2.1 为什么PDDL适合做LLM与规划器之间的中间语言LLMP选择PDDL而非自定义JSON schema最直接的原因是PDDL有现成的规划器生态。PDDL将一个问题拆成domain和problem两个文件domain文件描述世界的规则不针对具体实例包含谓词predicates和动作actions每个动作有前提preconditions与效果effectsproblem文件描述一个具体问题包含对象objects、初始状态init和目标条件goal。这种分离天然匹配LLMP的场景——domain文件由领域专家一次性写好对同一领域的所有问题实例复用LLM只需要为每个新问题生成problem文件因为这本质上是把自然语言描述翻译成结构化事实列表。把LLM定位成翻译器是整条管线成立的关键。GPT-4这类模型在机器翻译、文本改写上的表现远超其在推理上的稳定性而PDDL problem文件的生成——把自然语言中的状态描述映射为(:init ...)里的原子事实、把目标映射为(:goal ...)里的合取条件——在形式上与翻译任务高度同构。反过来如果让LLM直接输出计划它需要基于一组状态转移规则做多步前瞻搜索这正是自回归模型最不擅长的组合泛化问题。论文中的实验数据显示在没有PDDL辅助时GPT-4在绝大多数基准问题上连可行计划都产不出而LLMP在同样的问题上几乎全部给出最优解。差距的来源就是任务重心的迁移从推理迁移到翻译加查询。经典规划器的另一个优势来自形式语义保证。PDDL动作的precondition和effect都由逻辑原子组成规划器在搜索时通过状态转移函数逐动作推进任意一个满足所有前提的动作序列若最终状态满足goal条件就被判定为可行计划。若配置最优搜索如A*配合可采纳启发式输出的是步数最少的计划。这个可验证性是LLM直接输出自然语言计划所不具备的——你无法证明一段英文步骤序列在逻辑上无冲突但可以机械地检查PDDL计划中每一步是否合法。理解了这一点就能明白为什么论文要把生成问题描述和求解彻底解耦。2.2 domain与problem文件的正确写法下面给出论文案例使用的blocks world域。domain文件描述动作模型其中holding谓词虽然在初始状态中不出现但作为抓取的中间状态必须定义否则规划器无法表达手正拿着积木这一状态(define (domain blocksworld) (:requirements :strips) (:predicates (on ?x ?y) (on-table ?x) (clear ?x) (holding ?x) (arm-empty)) (:action pick-up :parameters (?x) :precondition (and (clear ?x) (on-table ?x) (arm-empty)) :effect (and (not (on-table ?x)) (not (clear ?x)) (not (arm-empty)) (holding ?x))) (:action put-down :parameters (?x) :precondition (holding ?x) :effect (and (not (holding ?x)) (clear ?x) (arm-empty) (on-table ?x))) (:action stack :parameters (?x ?y) :precondition (and (holding ?x) (clear ?y)) :effect (and (not (holding ?x)) (not (clear ?y)) (clear ?x) (arm-empty) (on ?x ?y))) (:action unstack :parameters (?x ?y) :precondition (and (on ?x ?y) (clear ?x) (arm-empty)) :effect (and (not (on ?x ?y)) (clear ?y) (holding ?x) (not (clear ?x)))))这里值得注意的细节是unstack的定义它要求手为空、积木x在y上且x顶部无遮挡效果是x被拿起、y顶部变空。若漏掉(clear ?y)这个效果规划器在连续两次unstack时会认为y顶部仍然不透明导致搜索空间被错误裁剪。pick-up与unstack的区别在于对象来源前者从桌面拿后者从另一个积木上拿两者在PDDL里是不同动作缺一不可。problem文件描述具体实例。以下对应论文中的P1问题(define (problem p1) (:domain blocksworld) (:objects b1 b2 b3 b4 b5 - block) (:init (on-table b1) (on b2 b1) (on b3 b4) (on b4 b2) (on b5 b3) (clear b5) (arm-empty)) (:goal (and (on b1 b2) (on b3 b5) (on b4 b1))))objects显式声明类型- blockinit列出所有成立的事实goal是要同时满足的条件合取。注意init中没有(clear b4)也没有(holding ?x)因为这些事实在当前状态不成立——PDDL采用封闭世界假设未声明即为假。这个约定与数据库的NULL语义不同LLM在生成时经常在这个地方出错。2.3 无示例时LLM生成PDDL的典型错误模式论文做了一个非常直观的对比实验不给任何上下文直接让GPT-4把P1的自然语言描述写成problem文件生成的PDDL语法完全正确却包含一个编造的谓词(empty)——domain里根本没有这个谓词同时漏掉了b1在桌面上的初始条件。这类错误不是偶发而是系统性的常见模式可以归纳为下表错误类型具体表现后果规避手段编造谓词写出domain中不存在的(empty)规划器解析失败在prompt中提供完整domain内容遗漏初始事实漏掉(on-table b1)计划不可执行让LLM逐一核对每个对象的初始位置对象类型缺失objects未标注(objects b1 - block)类型检查报错在示例中固定类型标注格式goal语义偏差目标谓词参数顺序写反求解出错误计划把goal逐条与自然语言对照这些错误说明LLM对PDDL格式的模仿能力强但对语义完整性没有感知——它不知道一个积木必须落在某处不知道封闭世界假设意味着漏掉的事实就是假。解决办法也不是微调模型而是给一个同类问题的人工正确示例让模型照着结构改写这正是下一章要展开的in-context learning。3. In-Context Learning驱动的PDDL生成与兜底验证3.1 示例构造与Prompt模板LLMP对每个问题域假设三样东西一份domain PDDL文件、一个自然语言描述与对应problem PDDL的配对示例、以及新问题的自然语言描述。示例的作用是告诉模型三件事谓词怎么拼写、对象类型怎么标注、init和goal的排列习惯。下面是用Python组装prompt的完整做法import openai # 假设domain文件已读取为字符串 DOMAIN_TEXT open(blocksworld-domain.pddl, encodingutf-8).read() # 领域内所有谓词都必须出现在这个示例里且格式与domain严格一致 CONTEXT_EXAMPLE Natural language: You have 5 blocks. b2 is on top of b5. b5 is on top of b1. b1 is on top of b4. b3 is on top of b2. b4 is on the table. b3 is clear. Your arm is empty. Your goal is to move the blocks. b4 should be on top of b3. Problem PDDL: (:objects b1 b2 b3 b4 b5 - block) (:init (arm-empty) (on b1 b4) (on b2 b5) (on b3 b2) (on-table b4) (on b5 b1) (clear b3)) (:goal (and (on b4 b3))) def build_pddl_prompt(nl_problem: str) - str: # 把domain放在最前让LLM看得到合法谓词集合 return f{DOMAIN_TEXT} {CONTEXT_EXAMPLE} Natural language: {nl_problem} Provide me with the problem PDDL file that describes the planning problem directly without further explanations. resp openai.ChatCompletion.create( modelgpt-4, messages[{role: user, content: build_pddl_prompt( You have 5 blocks. b5 is on top of b3. ...)}], temperature0.0, top_p0.1, max_tokens512, ) print(resp[choices][0][message][content])两个采样参数需要特别注意。temperature设为0.0是为了让解码过程退化为贪心搜索避免同一问题两次生成出不同的PDDL——如果同样的自然语言描述每次产出不同的问题文件后续排错会非常痛苦。top_p设为0.1是进一步限制候选token池让输出集中在高概率路径上。对于PDDL这种必须精确的任务多样性是敌人而不是朋友与此相对如果你在让LLM做头脑风暴式的创意任务才需要把temperature调到0.7以上。把domain文件放在prompt最前面是一个容易被忽略的细节。直接给模型看合法谓词集合比用文字描述只能使用这些谓词更有效因为模型对代码块的模仿能力远强于对指令的遵从能力。示例中刻意让b3作为唯一clear的积木覆盖了clear只标注最顶端积木的书写习惯。如果你的领域有更复杂的谓词如机器人域的(robot-at ?r ?loc)示例里必须出现至少一次否则模型很可能自己发明表达方式。3.2 示例选择的三个约束示例不是随便找一个就行论文中的做法可以归纳为三个硬性约束。第一示例必须覆盖domain中的所有谓词。以blocks world为例如果示例中只出现on和clear而没出现on-table模型就有概率在生成时省略on-table事实导致初始状态不完整。第二示例的目标条件必须足够简单简单到一眼能看出目标就是这三个条件避免目标本身包含需要推理的子结构那样会干扰模型对goal格式的模仿。第三示例与目标问题共享同一组对象命名风格——都用b1、b2这类短名字不要让示例用a、b、c而目标问题用block1、block2命名风格不一致会显著增加翻译错误的概率。示例属性推荐值原因谓词覆盖率100%未出现的谓词大概率被替换或漏用goal复杂度2~3个原子条件让模型专注于目标格式而非内容推理对象命名与目标问题同风格减少无关的格式迁移压力示例数量1个即可论文实验证明1个充分多个反而可能引入冲突上表最后一行是论文里一个反直觉的结论示例数量不是越多越好。多个示例如果来自不同难度的实例模型可能试图从多个示例中总结出某种模式反而在个别谓词上产生混用。一个精心挑选的示例让模型把它当作翻译模板来套用效果最稳定。这个问题在论文的消融实验中有所体现单示例配置下GPT-4生成的problem文件成功率最高追加更多示例后成功率没有提升个别问题上反而下降。3.3 生成结果的自动化校验LLM生成的PDDL文件不能直接信必须过一道机器校验。最有效的校验方式不是检查语法而是直接把它丢给规划器跑一遍——语法错误会立刻暴露语义错误如漏了初始事实会导致找不到解。在bash里可以这样写# 用Fast Downward做可解性验证把标准输出丢弃只看退出码 fast-downward.py domain.pddl problem.generated.pddl \ --search astar(lmcut()) /dev/null 21 \ echo SOLVABLE \ || echo UNSOLVABLE_OR_PARSE_ERROR退出码0表示规划器成功找到计划非0则需要进一步诊断。常见情况是Fast Downward报错Parsing failed——说明LLM生成的PDDL有括号不匹配或谓词未定义。如果退出码为0却找不到计划则多半是初始状态漏了事实。此时的做法不是重新随机生成而是把规划器的报错信息作为新的上下文追加到prompt里让LLM针对错误做一轮修正。这个self-correction循环通常两轮内就能收敛。实际操作中我还会在送入规划器之前做一层快速前置检查统计生成的文本中左括号和右括号数量是否相等再用正则确认(:objects、(:init、(:goal三个关键字各出现一次。这类轻量检查能拦住大约三成的低级错误省下调用规划器的开销。但注意前置检查永远不能替代真正的规划器验证——只有搜索器告诉你有解或无解才是最终结论。4. 接入经典规划器Fast Downward实战与结果对比4.1 安装与基本调用经典规划器有很多选择Fast Downward是学术界使用最广的规划器之一支持PDDL 2.2的子集且有高效的启发式实现。安装方式如下git clone https://github.com/aibasel/downward.git cd downward ./build.py # 编译完成后核心入口是如下命令 ./fast-downward.py ../domain.pddl ../problem.pddl \ --search astar(lmcut())build.py会自动检测系统依赖并编译翻译模块与搜索模块。如果机器上没有cmake或g需要先通过系统包管理器安装。编译成功后fast-downward.py就是统一入口它接收三个参数domain文件路径、problem文件路径、--search指定的搜索配置。搜索配置是引号包裹的完整字符串Fast Downward支持丰富的组合这里用的astar(lmcut())表示A*搜索配合landmark cut启发式。4.2 搜索配置对结果的影响搜索配置直接决定两个指标解的最优性和求解时间。astar(lmcut())是默认推荐lmcutlandmark cut是可采纳启发式保证A*找到的是步数最优解。但可采纳启发式往往计算成本高复杂问题上可能几十秒甚至几分钟不出结果。如果业务场景只需要有可行解而不强求最短路径可以把配置换成贪心搜索# 快速得到可行解但不保证最优 ./fast-downward.py ../domain.pddl ../problem.pddl \ --search greedy(lmcut()) # 折中方案带权重A*速度接近贪心质量接近最优 ./fast-downward.py ../domain.pddl ../problem.pddl \ --search lazy_wastar(lmcut(), w5)lazy_wastar的w参数控制次优性上界w5表示返回的解步数不超过最优解的5倍但搜索速度比w1快一个数量级。对实际系统来说w5通常是最实用的配置——它把最坏情况控制在可接受范围内又避免了lmcut启发式在复杂domain上的计算瓶颈。值得注意的是规划器输出的计划文件默认写在当前目录的sas_plan文件里每一行是一个动作实例比如(unstack b5 b3)这样的形式。搜索配置最优性相对速度适用场景astar(lmcut())最优慢小规模问题的精确求解astar(blind())最优很慢教科书级对比基线greedy(lmcut())非最优快大规模快速出解lazy_wastar(lmcut(), w5)5倍次优上界中快生产环境默认选择这里有个容易踩的坑Fast Downward在求解结束后会用一条Plan length: N打印出步数但这个步数是动作总数不是unique状态数。如果你拿它跟论文中的最优步数对比确保两边用的是同一个指标。论文里报告的optimal均指最短动作序列长度与astar(lmcut())在搜索完备且启发式可采纳的前提下的输出一致。4.3 重新审视论文的实验结论LLM直接规划与LLMP的差距本质上可以拆成两个独立的失败率。第一阶段LLM生成problem PDDL时语义错误导致规划器找不到解第二阶段即便PDDL正确LLM直接输出的自然语言计划也经常违反动作前提。论文的benchmark横跨blocks world、logistics、ferry等多个经典规划域结果显示LLMP在大多数问题上能找到最优解而纯LLM在多数问题上连可行解都得不到。这个结果的工程含义非常直接规划器提供的sound和complete性质即保证输出逻辑合法、保证有解必能找到正是LLM最缺少的可靠性。你不需要相信模型觉得计划没问题因为规划器已经把每一步验证过了。把这一步接入现有系统相当于给LLM配了一个永不疲劳的检查员——它不产生创意但保证每一个创意都落地正确。5. 计划回译与触发判断的落地细节规划器输出的sas_plan是PDDL动作序列不能直接展示给用户。比如(unstack b5 b3)这种表达非技术用户看不懂。回译的做法是构造一个翻译prompt把原始自然语言问题、PDDL计划、以及一段用自然语言逐条解释每一步的指令组合起来再次调用LLMplan open(sas_plan, encodingutf-8).read() translation_prompt fThe following is a plan for a blocks world problem. Problem: {nl_problem} Plan: {plan} Describe each step in plain English. Keep the same order. Do not add extra steps. resp openai.ChatCompletion.create( modelgpt-4, messages[{role: user, content: translation_prompt}], temperature0.2, # 回译允许少量表达变化但不要让模型自由发挥 )回译时的temperature可以比生成PDDL时略高因为自然语言表达有多种正确方式但必须用Do not add extra steps锁定动作数量防止模型自行增删步骤。接下来是触发判断。论文明确承认没有让LLM自动识别这个问题该走LLMP这是留待未来的方向。实际落地时我建议在LLM前面加一个独立的意图分类模块用一段简短prompt让模型判断输入是否属于已知规划域。分类结果只有两种属于已知域则走LLMP管线否则走普通对话。这个判断器的准确率直接决定系统的可靠性——如果误判把不在domain里的问题硬塞给规划器会得到无解或者荒谬的计划。你在自己的系统里务必给分类器加一条拒绝路径不确定性大于阈值时宁可让用户提供更多信息也不要擅自调起规划器。最后记住一个边界domain文件仍然需要领域专家维护LLMP并没有消灭建模成本它只是让每一次新问题的求解不再重复建模。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价