4、目标驱动与规划算法:目标表示方法、前向搜索规划、分层任务网络(HTN)、规划域定义语言(PDDL)基础
好,咱们今天聊点硬核的——目标驱动与规划算法。
你想想看,一个AI智能体,光有感知和反应是不够的。它得知道自己要干什么,怎么干。这就涉及到「规划」了。说白了,就是让AI学会「先做A,再做B,最后达成C」这套逻辑。
我在做自动化流程项目时,踩过不少坑。最典型的就是:智能体拿到目标后,完全不知道从哪下手。嗯,这就是规划能力缺失的表现。今天咱们就把这块补上。
4.1 目标表示方法
先聊目标怎么表示。你不能让AI去猜「我要变得更好」这种模糊指令。目标必须是结构化的。
我个人习惯把目标分成三类:
- 原子目标:不可再分的最小目标。比如「拿起杯子」。
- 复合目标:由多个原子目标组成。比如「倒水」=「拿起杯子」+「走到饮水机」+「按下出水键」。
- 状态目标:描述世界应该变成什么样。比如「杯子里有水」。
在实际项目中,我常用的是状态目标。为什么?因为智能体不需要关心过程,它只关心结果。你告诉它「最终状态是杯子满水」,它自己想办法。
核心要点:目标表示要满足三个条件——可观测、可验证、可分解。缺一个,规划就容易跑偏。
举个例子。我曾经做一个仓储机器人项目,目标写的是「把货物从A移到B」。听起来没问题对吧?结果机器人把货物扔到B点就走了,完全不管货物有没有损坏。后来我改成「货物在B点且状态完好」,这才解决问题。
4.2 前向搜索规划
前向搜索,说白了就是「从当前状态出发,一步步试,直到找到目标」。
它的逻辑很简单:
- 当前状态是S0
- 看看有哪些动作可以执行
- 选一个动作,执行,得到新状态S1
- 检查S1是不是目标状态
- 不是?继续。是?结束。
听起来像暴力搜索?没错,它就是。但你别小看它,在很多场景下它够用了。
我的经验:前向搜索适合状态空间小、动作数量少的情况。如果动作太多,搜索树会爆炸。我曾经试过在20个动作的场景下跑前向搜索,结果等了5分钟没出结果。后来果断换了算法。
代码实现其实不复杂:
def forward_search(initial_state, goal_state, actions):
frontier = [(initial_state, [])]
visited = set()
while frontier:
state, plan = frontier.pop(0)
if state == goal_state:
return plan
if state in visited:
continue
visited.add(state)
for action in actions:
if action.is_applicable(state):
new_state = action.apply(state)
frontier.append((new_state, plan + [action]))
return None # 没找到规划
你看,核心就这几行。但实际项目中,你得考虑状态怎么比较、动作怎么定义、搜索顺序怎么优化。嗯,这些细节才是坑。
4.3 分层任务网络(HTN)
前向搜索有个硬伤——它不知道「先做什么后做什么」的常识。比如做菜,正常人知道先洗菜再切菜再炒菜。但前向搜索可能会先炒菜再洗菜,然后发现锅糊了。
HTN就是来解决这个问题的。它的思路是:把大任务拆成小任务,小任务再拆成更小的任务,直到拆成可以直接执行的动作。
我特别喜欢HTN的一点是:它符合人类的思维方式。你想想看,你做一个项目,是不是先定大目标,再拆成几个阶段,每个阶段再拆成具体任务?HTN就是干这个的。
HTN的核心概念有两个:
- 原始任务:可以直接执行的动作。比如「拿起螺丝刀」。
- 复合任务:需要拆解的任务。比如「组装桌子」。
每个复合任务都对应一个方法,方法告诉你怎么拆。比如「组装桌子」的方法可能是:
- 先装桌腿
- 再装桌面
- 最后拧螺丝
注意:HTN的难点在于方法的设计。方法写得太死,智能体遇到意外情况就卡住。方法写得太灵活,搜索空间又太大。我建议刚开始做的时候,先写死,跑通了再优化。
我曾经做过一个手术机器人项目,用HTN来规划手术步骤。一开始方法写得特别细,结果机器人遇到血管位置偏移就停了。后来我改成「如果遇到异常,先暂停,重新评估」,这才跑通。
4.4 规划域定义语言(PDDL)基础
聊了这么多理论,总得有个工具来落地。PDDL就是干这个的。
PDDL是一种描述规划问题的语言。它把问题分成两部分:
- 域文件(domain):描述动作、前提、效果。相当于「规则书」。
- 问题文件(problem):描述初始状态、目标状态。相当于「具体任务」。
我刚开始学PDDL时,觉得它语法有点啰嗦。但用久了发现,这种分离设计其实很科学。域文件可以复用,问题文件可以换着来。
看个简单的例子。这是「搬箱子」的域文件:
(define (domain move-box)
(:requirements :strips)
(:predicates
(at ?obj ?loc)
(box ?obj)
(robot ?obj)
)
(:action move
:parameters (?robot ?from ?to)
:precondition (and (robot ?robot) (at ?robot ?from))
:effect (and (not (at ?robot ?from)) (at ?robot ?to))
)
(:action push
:parameters (?robot ?box ?from ?to)
:precondition (and (robot ?robot) (box ?box)
(at ?robot ?from) (at ?box ?from))
:effect (and (not (at ?box ?from)) (at ?box ?to)
(not (at ?robot ?from)) (at ?robot ?to))
)
)
问题文件长这样:
(define (problem move-box-problem)
(:domain move-box)
(:objects
robot1 - robot
box1 - box
roomA roomB - location
)
(:init
(at robot1 roomA)
(at box1 roomA)
)
(:goal
(at box1 roomB)
)
)
关键点:PDDL的难点不在于语法,而在于如何把现实问题抽象成谓词和动作。我见过很多新手,写出来的PDDL要么太细(每个关节角度都写进去),要么太粗(忽略关键约束)。我的建议是:先写一个能跑的版本,再逐步细化。
PDDL的规划器有很多,比如FastDownward、FF、LAMA。我个人常用FastDownward,因为它支持多种搜索策略,而且社区活跃。
嗯,说到这我想起一个事。有次我用PDDL描述一个物流调度问题,写了200多行,结果规划器跑了半小时没出结果。后来发现是动作的前提条件写得太宽松,导致搜索空间爆炸。改成更严格的前提后,3秒就出结果了。所以,前提条件越精确,搜索效率越高。
小结
今天咱们聊了四个东西:
- 目标表示:别让AI猜,给它清晰的状态描述
- 前向搜索:简单粗暴,适合小场景
- HTN:像人一样拆任务,适合复杂场景
- PDDL:把规划问题形式化,方便机器处理
下一章咱们会聊「执行监控与异常处理」——毕竟规划得再好,执行时总会出幺蛾子。到时候我会分享一些实战中的避坑经验。
个人建议:如果你刚开始学规划,先别急着上HTN。从前向搜索和PDDL入手,跑通几个小例子,再逐步升级。我当年就是这么过来的,稳扎稳打比什么都重要。