LLM与经典规划器融合:复杂任务规划的正确性保障
2026/9/19 12:54:27 网站建设 项目流程

简介:面向机器学习和自动规划研究人员,这份PDF收录了LLM+P框架论文,提出将经典规划器优势融入大语言模型(LLM)的方法。其核心思路是把自然语言问题转换为PDDL格式,借助经典规划器求出最优解,再转回自然语言,适用于机器人导航、任务分配、操作序列规划等长时规划场景。全文共1个PDF文件,压缩包大小269KB,便于随时查阅;目前已有95人学习。读者可获得完整的框架设计、多类基准测试任务的详细设置、以及实验对比结果;论文不仅展示了LLM+P能在多数问题上给出最优计划,还揭示了原始LLM在长时规划中的可行性短板,并探讨了自动判断应用时机、减少对外部人工信息依赖等未来方向。适合正在探索LLM在复杂规划任务中落地路径的科研人员与高级开发者。

1. 大模型算不动搜索,经典规划器看不到语义:融合是复杂任务规划的必然路径

复杂任务规划有个尴尬的现状:大语言模型擅长把自然语言目标翻译成步骤列表,但一旦步骤超过十几步、状态约束稍微纠缠,LLM 的输出就开始「看起来合理、执行时撞墙」。经典规划器(如 Fast Downward、PDDL 求解器)相反——它们在形式化状态空间里做搜索,解得动几百步的严格规划,却读不懂自然语言,更不会帮你把一堆非结构化需求整理成形式化问题。所谓融合,不是让 LLM 去学搜索,也不是让规划器去读自然语言,而是各干各最擅长的事:LLM 负责把自然语言目标和环境信息翻译成规划器需要的领域描述(PDDL Domain)与问题描述(PDDL Problem),经典规划器做状态空间搜索,最后再由 LLM 把规划结果解释回自然语言动作序列。这个分工在机器人任务规划、智能工厂调度、物流路径规划里都是同一个套路,也是当前复杂任务规划最能落地、最可复现的技术路线。

2. 搞清楚两者边界:LLM 的上下文窗口不是搜索空间,规划器的形式化语言才是约束本源

2.1 为什么纯 LLM 规划在长程任务上必然「假性正确」

LLM 在单步动作生成上表现很好,但它生成计划的方式本质是自回归式的贪心采样——每一步都基于前面生成的内容做概率预测。问题在于,复杂任务规划的约束往往是全局性的:某个动作执行后占用的资源要到最后一步才释放、两个子目标共享同一台设备、同一中间状态可能被多个前置条件引用。这一类约束在 LLM 的注意力窗口里跨度过大,模型很难在生成第 3 步时就精确记住第 17 步需要的状态条件。

一个典型的失败模式是「先锁后解」:LLM 给出的计划里,资源在步骤 5 被锁定,步骤 8 就尝试再次锁定同一个资源,理由是「需要在后续步骤中继续使用」——这在语义上没错,但规划器视角下这是非法动作,因为状态里该资源已经是 locked。另一个失败模式是子目标冲突:两个子目标分别可达,但共享一个有限的中间缓存,LLM 各自生成路径时不感知对方,合到一起就死锁。

提示:在本地做一个小实验就能验证这一点——给 LLM 一个 20 步的积木堆叠任务(Blocksworld),要求它逐步输出动作。大多数人会得到 15 步前后就开始出现重复搬移或者逻辑跳跃的结果。这不是模型笨,是自回归生成的固有局限。

2.2 经典规划器真正的优势与真正的门槛

经典规划器解决的是 STRIPS / PDDL 框架下的状态空间搜索问题。以 Fast Downward 为代表的规划器在任务可解时能找到最优或近似最优的动作序列,而且是可证明正确的——它遍历状态空间,每个动作都要验证前置条件在当前状态下成立,输出序列中每个状态迁移都是合法迁移。这个性质远超 LLM 的概率性输出。

但经典规划器的门槛也很直接:它要的是 PDDL 格式的形式化描述。定义一个领域(Domain)需要写清楚谓词(predicates)、动作(actions)、前置条件(preconditions)和效果(effects);定义一个具体问题(Problem)需要写清楚对象(objects)、初始状态(init)和目标状态(goal)。这个转换过程对非形式化背景的人来说很难上手,一个 30 步的搬运任务光写正确的 PDDL 问题描述就可能花掉半天。融合方案的第一个核心价值就在这个环节:让 LLM 干翻译,让规划器干搜索。

2.3 融合架构的四个组件与数据流

一个可落地的「LLM + 经典规划器」融合系统,通常由四个模块组成:

  1. 语义解析模块:LLM 接收自然语言目标和环境描述,输出结构化的中间表示(通常是 JSON),包括对象列表、初始状态事实、目标状态事实。
  2. 格式转换模块:把 JSON 映射为 PDDL 的 problem 文件,domain 文件可以预置或者由 LLM 生成后人工校验。
  3. 经典规划求解模块:调用 Fast Downward、Pyperplan 或 FF 规划器,在领域与问题描述上做搜索,输出原始动作序列。
  4. 结果解释与校验模块:LLM 把动作序列解释回自然语言,并做一轮回读自检——把规划器的结果反向描述成状态变化,与原目标比对。

这个数据流里,LLM 被限制在「读自然语言、写中间表示」和「读动作序列、写自然语言」两个上下文边界极其明确的环节,搜索和约束检查全部交给规划器。系统的正确性由规划器保证,系统的可用性由 LLM 的翻译能力兜底。

3. 动手实现一个 LLM + 经典规划器融合的最小系统:JSON 到 PDDL 的转换闭环

3.1 准备基础环境与经典规划器选型

本地环境只需要 Python 3.10+ 和一个可用的 PDDL 规划器。规划器选型上,Pyperplan适合学习和验证(纯 Python 实现,安装即用),Fast Downward适合追求求解效率和最优解的场合(C++ 实现,需要编译或使用 Docker 镜像)。先在终端验证 Pyperplan 可用:

pip install pyperplan # 测试一个最简单的 PDDL 问题 pyperplan domain.pddl problem.pddl

参数说明:domain.pddl是领域文件,描述动作模型;problem.pddl是问题文件,描述具体场景的初始状态和目标。两个缺一不可,Pyperplan 输出的.soln文件就是规划出的动作序列。如果这一步报错,优先检查 PDDL 语法,最常见的问题是括号不匹配和谓词参数个数与动作定义不一致。

提示:做融合实验时,建议先用 Pyperplan 跑通全链路,再去换 Fast Downward。因为 Pyperplan 的错误信息更接近 Python 栈,方便你理解是 LLM 生成的 PDDL 写错了,还是规划器本身的限制。

3.2 用 LLM 把自然语言目标转成结构化中间表示

融合的关键第一步,是让 LLM 输出一个结构完整的 JSON,而不是直接输出 PDDL。原因是:直接生成 PDDL 语法错误率太高,LLM 对括号嵌套和谓词作用域的掌控能力很差;JSON 是 LLM 生成最稳定的格式之一,先保证结构稳定,再做语法转换,错误面小得多。以下是一个针对「积木堆叠」任务的提示词与解析代码:

import json from openai import OpenAI client = OpenAI() # 替换为你实际使用的 API 配置 prompt = """ 请将下面的自然语言任务转换为 JSON 格式的规划问题描述。 任务:桌面上有红色积木、蓝色积木和绿色积木,先把蓝色积木放到红色积木上, 再把绿色积木放到蓝色积木上。初始状态:所有积木都在桌面上,且互相不叠放。 要求 JSON 必须包含以下字段: - objects: 所有对象名称列表,用英文 - init: 初始状态的谓词列表,用 (predicate-name arg1 arg2) 格式的字符串 - goal: 目标状态的谓词列表,同上 只输出 JSON,不要输出解释。 """ resp = client.chat.completions.create( model="gpt-4o-mini", messages=[{"role": "user", "content": prompt}], temperature=0, max_tokens=800, ) raw = resp.choices[0].message.content parsed = json.loads(raw) print(json.dumps(parsed, ensure_ascii=False, indent=2))

这段代码做了三件事:构造提示词模板、调用大模型接口取得响应、把响应体解析为 JSON 对象。temperature=0是为了保证翻译的确定性——规划问题的中间表示应该尽可能稳定,温度调高会引入随机性,导致同样的自然语言输入每次都得到不同的 JSON 结构,后期难以排查。

实际运行时你会发现,LLM 输出的 JSON 里有一个提示词中没提到的坑:谓词命名不一致。比如同样表示「在桌面上」,模型可能输出(on-table red-block),也可能输出(ontable red-block),带不带连字符、用不用on单独表达,取决于模型的潜意识偏好。这个不一致会在 PDDL 转换时直接导致谓词未定义错误。

3.3 把 JSON 映射为 PDDL Problem 文件:一个可复现的转换函数

拿到结构化 JSON 后,下一步是把它转成 PDDL Problem 文件。这里有一个工程决策:domain 文件不用 LLM 生成,而是由人来预置。

原因很简单——对于一个固定应用场景(比如积木堆叠、托盘搬运、仓库调度),动作模型是稳定的,不会随着任务改变而改变;而 problem 文件记录的是具体任务的初始状态和目标状态,每次都可能不同。让 LLM 每次重新生成 domain 文件,不仅浪费 token,还引入了「同一个动作在不同轮次定义不一致」的严重风险。下面是转换代码:

def json_to_pddl(parsed: dict) -> str: objects = " ".join(parsed["objects"]) init_facts = "\n ".join(parsed["init"]) goal_facts = "\n ".join(parsed["goal"]) problem = f""" (define (problem planning-task) (:domain blocksworld) (:objects {objects}) (:init {init_facts}) (:goal (and {goal_facts})) ) """ return problem problem_text = json_to_pddl(parsed) with open("problem.pddl", "w") as f: f.write(problem_text)

这里最关键的是:init:goal的格式对齐——谓词之间的空格、括号的数量必须精确匹配 PDDL 规范。(:init ...)内部是初始成立的事实集合,(:goal (and ...))内部是目标条件的合取,目标里每个事实都必须最终成立,规划器才会判定任务完成。

3.4 预置的 blocksworld Domain 文件:动作模型的最小定义

配套的 domain 文件如下,它定义了三个动作:pick-up(从桌面拿起积木)、stack(把积木堆到另一块上)、unstack(从另一块积木上取下)。这是 STRIPS 经典动作模型,每个动作都有参数、前置条件和效果。

(define (domain blocksworld) (:predicates (on ?x ?y) ; x 在 y 上面 (ontable ?x) ; x 在桌面上 (clear ?x) ; x 顶部没有其他积木 (handempty) ; 机械手为空 (holding ?x)) ; 机械手正拿着 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 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)) (not (handempty)) (clear ?y) (holding ?x))))

把两个文件放到同一目录,执行pyperplan domain.pddl problem.pddl,你会在生成的.soln文件里看到规划出的动作序列。这个序列的特征是:每一步的前置条件在前一个状态下都被满足,而且最终状态精确达成目标条件。这一性质是纯 LLM 方案无法保证的。

4. 复杂任务的关键一步:用规则引擎或代码修正扩充领域描述

4.1 领域描述为何卡住「复杂任务」:缺谓词和缺动作

积木堆叠能跑通不代表复杂任务能跑通。真实复杂任务(例如机器人路径规划,或者「智能工厂规划总师」语义下的产线调度)的领域模型通常包含条件效应(conditional effect)、数值变量(numeric fluents)、时间窗约束等远超 STRIPS 表达能力的元素。经典规划器的求解质量高度依赖领域建模的精度——如果领域模型里少定义了一个动作,或者某个动作的前置条件写宽了,规划器会错误地「成功」:输出一个看起来每一步都合法、但整个目标并没有真正达成的序列。

这个坑在 LLM 参与建模时尤其致命。LLM 生成 domain 文件时倾向于「把前置条件写少一点」——它不理解一个动作的前置条件写窄了会导致状态空间爆炸,写宽了会导致语义错误。所以工程实践中,高可靠性的做法是:LLM 生成初始 domain,然后人工用规则引擎或代码做合法性与完备性校验,校验通过后才进入规划求解阶段。

4.2 一个可用的校验思路:重放与状态回溯

规划器输出/soln文件后,用脚本重放整个规划,逐步验证状态迁移的合法性,是一个成本极低但效果极好的校验手段。以下代码实现了这个重放逻辑:

def validate_plan(domain_path, problem_path, plan_file): """重放规划结果,逐布验证合法性""" from pyperplan import grounding from pyperplan.search import breadth_first_search # 实际项目中,更常见的方式是解析 plan 文件后用 PDDL 的 effect 规则 # 逐步更新状态字段。这里做结构性的关键判断: plan = open(plan_file).read().strip().splitlines() state = parse_init_state(problem_path) # 从 problem 文件解析初始状态 for action_line in plan: action = parse_action(action_line) # 解析动作名与参数 if not check_preconditions(state, action): print(f"STEP ERROR: 前置条件不满足 - {action_line}") return False state = apply_effects(state, action) # 应用效果更新状态 print(f"STEP OK: {action_line} -> 当前状态更新") return check_goal(state, problem_path)

核心逻辑是:从初始状态出发,按序读取计划中的每个动作,检查其前置条件是否在当前状态中成立,成立则用效果更新状态,不成立则直接报错中断。这个程序虽然简单,但能捕获 90% 以上的「假性正确」规划——尤其是 LLM 生成的 domain 里前置条件不完整导致的非法状态迁移。

4.3 领域资源的写法:从无限状态空间到可搜索状态空间

复杂任务的另一个难点是资源建模。以路径规划为例,如果直接把地图栅格全部作为对象,状态空间会爆炸到规划器根本无法求解。常见做法是把「地图坐标」抽象为「节点」,把「可通行路径」抽象为节点之间的边——这本质上是在做状态空间压缩,压缩的规则写在 domain 文件里。

一个折中的工程方案是:保留经典规划器的完整动作模型,但把路径决策拆成两层——上层用规划器做任务级决策(先拿哪个托盘、放到哪个出料口),下层用 A* 或 RRT 做路径级规划。规划器不再直接面对坐标节点,而是面对「pickup_at(zone_a, item_1)」这类高层动作。这个「分层规划」的思路在机器人路径规划和「3x核心网ip规划」这类带资源约束的场景里都成立,核心逻辑是让搜索发生在正确的抽象层级上。

5. 最后一步是「可解释的正确性」:回读校验与规划结果的可视化验证

5.1 让 LLM 回读规划结果:自然语言与 PDDL 状态之间的闭环

规划器输出的动作序列是形式化的,不直接可读。用 LLM 把它翻译回自然语言之前,先要做一次「回读校验」:把动作序列应用后的最终状态与目标状态做结构化比对。这个比对不是让 LLM 看,而是用代码做。

def validate_goal(state, goal_facts): """检查最终状态是否满足所有目标谓词""" for fact in goal_facts: # fact 形如 (on blue-block red-block) if fact not in state: print(f"GOAL NOT SATISFIED: {fact}") return False print("GOAL SATISFIED: 所有目标条件均已成立") return True

这段代码逐条检查目标谓词是否都出现在最终状态里。注意这里的state是字符串形式的谓词集合,goal_facts是从原始 problem 文件里解析的目标谓词列表。两者格式完全一致是因为同源——都来自 PDDL 文本。这保证了校验不依赖 LLM 的语义理解,是纯逻辑判定。

5.2 代价权重与规划最优性的取舍

经典规划器的目标函数默认是「步数最少」,但在真实任务里,步数最少不等于代价最低。比如移动机器人场景,move(zone_a, zone_b)move(zone_a, zone_c)步数相同但能耗不同,需要在动作效果里引入代价。Fast Downward 支持(:action move ... :cost 2)这种写法,把代价建模进动作定义,规划器会搜索「总代价最低」的解而非「步数最少」的解。这个参数在工业场景里几乎必定要调——只按步数优化会让规划结果在物理世界里难以执行。

5.3 三种融合模式的选用原则

最后给出一个实践层面的总结性经验。当前「LLM + 经典规划器」的融合有三档做法:

轻量融合:LLM 只做状态翻译(自然语言到 JSON 到 PDDL),规划器全权求解,LLM 再把结果解释回自然语言。适合领域模型清晰、动作定义稳定的任务(如积木堆叠、托盘搬运、泊车路径规划算法的任务层规划)。开发成本最低,正确性由规划器保证,推荐大多数场景优先采用。

重量融合:LLM 不仅做翻译,还参与 domain 文件的迭代修正。规划器求解失败时,把失败原因(不可满足的谓词、冲突的约束)返回给 LLM,让 LLM 修正动作定义和谓词集合。适合领域模型未知或需要人机共创的新场景,但要注意加一轮人工审查——LLM 修 PDDL 时可能越修越乱,需要设置最大迭代次数。

分层融合:规划和路径分离,高层任务用规划器做决策,低层路径规划用专门算法(A*、RRT)。适合移动机器人、「动态避障小车路径规划」这类既有离散任务约束又有连续空间约束的场景。这是目前工业落地最稳的方案,因为每一层都用到了该层最强的工具。

实践中最推荐的落地路径是从轻量融合开始——先跑通一个健全的 JSON 到 PDDL 转换闭环,再逐步把校验规则、代价优化、领域迭代加进去。

本文还有配套的精品资源,点击获取

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

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

立即咨询