1. SHOP2规划器入门:用Python实现HTN任务分解
在人工智能规划领域,HTN(Hierarchical Task Network)规划是一种强大的任务分解方法。而SHOP2(Simple Hierarchical Ordered Planner 2)作为最经典的HTN规划器之一,以其清晰的算法逻辑和实用的状态感知特性,成为学习HTN规划的理想起点。
我第一次接触SHOP2是在一个卫星任务规划项目中,当时需要让卫星自主决定如何完成观测和数据回传任务。传统规划器难以处理这种需要根据电量等实时状态调整策略的场景,而SHOP2的状态感知特性完美解决了这个问题。下面我就用Python带大家实现一个简化版的SHOP2规划器,核心代码仅150行左右。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. HTN规划与SHOP2核心概念
2.1 什么是HTN规划?
HTN规划的核心思想是将复杂任务逐步分解为可执行的原子动作。与经典规划不同,HTN规划强调:
- 层次化分解:任务可以不断分解为子任务,直到全部变为原子动作
- 方法导向:通过预定义的"方法"(Method)指导如何分解任务
- 状态感知:分解过程中可以感知和利用当前世界状态
2.2 SHOP2的核心组件
SHOP2作为HTN规划器,包含几个关键概念:
| 概念 | 代码表示 | 说明 |
|---|---|---|
| 原子动作 | Action |
可直接执行的基本操作,包含前置条件和执行效果 |
| 复合任务 | Task |
需要进一步分解的高层任务,不能直接执行 |
| 方法 | Method |
定义如何将复合任务分解为子任务的"配方" |
| 领域 | Domain |
包含所有可用动作和方法的集合 |
| 问题 | Problem |
包含初始状态和目标任务定义 |
2.3 为什么选择SHOP2?
- 算法清晰:基于TFD(全序向前分解)算法,逻辑简单直接
- 状态感知:任务分解时能考虑当前世界状态
- 数值支持:天然支持处理电量、时间等数值型状态变量
- 学术认可:论文引用超过4000次,是HTN领域的标杆实现
3. Python实现SHOP2规划器
3.1 核心数据结构
首先定义规划器需要的基础数据结构:
python复制from typing import List, Dict, Callable, Optional, Any
from dataclasses import dataclass, field
from copy import deepcopy
import json
@dataclass
class State:
"""世界状态表示"""
facts: Dict[str, Any] = field(default_factory=dict)
def clone(self) -> 'State':
return State(deepcopy(self.facts))
def get(self, key: str, default=None):
return self.facts.get(key, default)
def set(self, key: str, value: Any):
self.facts[key] = value
@dataclass
class Action:
"""原子动作定义"""
name: str
params: List[str]
precond: Callable[[State, List[str]], bool] # 前置条件
effect: Callable[[State, List[str]], None] # 执行效果
@dataclass
class Method:
"""任务分解方法"""
name: str
task_name: str # 能分解的任务名
precond: Callable[[State, List[str]], bool] # 方法适用条件
subtasks: Callable[[State, List[str]], List[tuple]] # 子任务列表
@dataclass
class Task:
"""任务表示"""
name: str
args: List[str]
3.2 TFD算法实现
SHOP2的核心是TFD(Total-Order Forward Decomposition)算法,其Python实现如下:
python复制class SHOP2Planner:
def __init__(self):
self.actions: Dict[str, Action] = {}
self.methods: Dict[str, List[Method]] = {}
def plan(self, state: State, tasks: List[Task], depth: int = 0) -> Optional[List[Task]]:
if not tasks: # 无任务表示规划成功
return []
current_task = tasks[0]
remaining_tasks = tasks[1:]
# 处理原子动作
if current_task.name in self.actions:
action = self.actions[current_task.name]
if action.precond(state, current_task.args):
new_state = state.clone()
action.effect(new_state, current_task.args)
result = self.plan(new_state, remaining_tasks, depth + 1)
if result is not None:
return [current_task] + result
return None
# 处理复合任务
elif current_task.name in self.methods:
for method in self.methods[current_task.name]:
if method.applicable(state, current_task.args):
subtasks = [Task(name, args) for name, args in
method.decompose(state, current_task.args)]
result = self.plan(state, subtasks + remaining_tasks, depth + 1)
if result is not None:
return result
return None
else:
return None # 未知任务类型
3.3 卫星观测领域示例
让我们用一个卫星观测的实例来演示SHOP2的应用:
python复制def create_satellite_domain() -> SHOP2Planner:
planner = SHOP2Planner()
# 定义原子动作
def slew_precond(state: State, args: List[str]) -> bool:
sat, target = args[0], args[1]
return state.get(f"{sat}.battery", 0) > 10
def slew_effect(state: State, args: List[str]) -> None:
sat, target = args[0], args[1]
state.set(f"{sat}.pointing", target)
battery = state.get(f"{sat}.battery", 100)
state.set(f"{sat}.battery", battery - 5)
planner.add_action(Action("slew", ["sat", "target"], slew_precond, slew_effect))
# 定义观测任务的两种分解方法
def observe_standard_precond(state: State, args: List[str]) -> bool:
return state.get(f"{args[0]}.battery", 0) > 30
def observe_standard_subtasks(state: State, args: List[str]) -> List[tuple]:
sat, target = args[0], args[1]
return [
("slew", [sat, target]),
("power_on", ["camera1"]),
("calibrate", ["camera1"]),
("capture", [sat, target]),
]
planner.add_method(Method(
"observe-standard", "observe",
observe_standard_precond,
observe_standard_subtasks
))
# 电量不足时的快速观测方法
def observe_quick_precond(state: State, args: List[str]) -> bool:
return state.get(f"{args[0]}.battery", 0) <= 30
def observe_quick_subtasks(state: State, args: List[str]) -> List[tuple]:
return [
("slew", [args[0], args[1]]),
("capture", args),
]
planner.add_method(Method(
"observe-quick", "observe",
observe_quick_precond,
observe_quick_subtasks
))
return planner
3.4 规划执行示例
初始化状态和目标任务:
python复制# 初始状态
state = State({
"sat1.battery": 80,
"sat1.pointing": "ground",
"sat1.data_stored": 0,
"camera1.on": False,
"camera1.calibrated": False,
})
# 目标任务:观测TargetA并下传数据
goal_tasks = [Task("mission", ["sat1", "TargetA", "GroundStation"])]
# 执行规划
planner = create_satellite_domain()
plan = planner.plan(state, goal_tasks)
执行结果将输出类似如下的规划序列:
code复制1. slew(sat1, TargetA)
2. power_on(camera1)
3. calibrate(camera1)
4. capture(sat1, TargetA)
5. slew(sat1, GroundStation)
6. transmit(sat1, GroundStation)
4. SHOP2关键技术解析
4.1 状态感知的任务分解
SHOP2最强大的特性是在任务分解过程中能够感知当前状态。在我们的卫星示例中:
- 当电量>30%时,采用标准观测流程(转向→开机→校准→拍摄)
- 当电量≤30%时,采用快速流程(转向→直接拍摄)
这种动态方法选择能力使得SHOP2特别适合实时系统。
4.2 前向分解策略
TFD算法采用前向分解策略:
- 从初始任务列表开始
- 总是处理第一个任务
- 如果是原子动作且条件满足,则执行
- 如果是复合任务,则选择适用方法分解
- 递归处理剩余任务
这种策略保证了规划过程始终基于当前最新状态。
4.3 回溯机制
当某个方法分解失败时,SHOP2会自动尝试其他适用方法。这种隐式回溯机制通过递归调用自然实现,无需额外编码。
5. SHOP2与其他规划器对比
| 特性 | SHOP2 | 经典规划器 |
|---|---|---|
| 算法 | TFD(前向分解) | 状态空间搜索 |
| 状态感知 | ✅ 分解时考虑状态 | ❌ 规划时状态未知 |
| 数值计算 | ✅ 原生支持 | ❌ 需要特殊扩展 |
| 任务抽象 | ✅ 层次化任务网络 | ❌ 扁平动作序列 |
| 适用场景 | 复杂任务分解 | 简单动作序列规划 |
6. 实际应用中的经验技巧
6.1 方法设计原则
- 正交性:确保不同方法处理不同条件,避免重叠
- 完备性:为每个复合任务提供覆盖所有可能情况的方法
- 效率优先:将高效方法放在方法列表前面
6.2 调试技巧
- 打印递归深度:如示例中的depth参数,帮助理解分解过程
- 状态快照:在关键步骤保存状态副本,便于回溯分析
- 方法追踪:记录尝试过的方法及其结果
6.3 性能优化
- 状态哈希:对频繁访问的状态值进行缓存
- 方法预过滤:根据简单条件快速排除不适用方法
- 任务批处理:将连续原子动作合并为宏动作
7. 扩展与进阶
7.1 添加时间约束
可以通过扩展State类来支持时间感知:
python复制@dataclass
class TimedState(State):
time: float = 0
def clone(self):
return TimedState(deepcopy(self.facts), self.time)
def timed_action_effect(state: TimedState, args: List[str]):
state.time += 5 # 假设动作耗时5单位时间
# ...其他效果...
7.2 资源管理
扩展状态表示来支持资源约束:
python复制state = State({
"sat1.fuel": 100,
# ...其他状态...
})
def action_precond(state: State, args: List[str]):
return state.get("sat1.fuel", 0) > args.required_fuel
7.3 并发任务支持
修改规划器以支持并行任务分解:
python复制def parallel_decompose(tasks: List[Task]) -> List[List[Task]]:
# 返回可以并行执行的任务组
return [[task] for task in tasks] # 简单实现:全顺序
8. 常见问题与解决方案
8.1 规划效率低下
问题:任务分解耗时过长
解决:
- 优化方法选择策略
- 添加方法适用性缓存
- 限制最大递归深度
8.2 方法冲突
问题:多个方法同时适用导致非预期结果
解决:
- 明确方法优先级
- 添加更精确的前置条件
- 引入代价函数选择最优方法
8.3 状态爆炸
问题:状态变量过多导致性能下降
解决:
- 只跟踪相关状态变量
- 使用分层状态表示
- 对状态进行抽象和聚合
9. 学习资源与下一步
9.1 推荐资源
- 官方论文:Nau et al. "SHOP2: An HTN Planning System" (2003)
- Python实现:PyHOP(更完整的SHOP2实现)
- 应用案例:机器人任务规划、游戏AI、工作流调度
9.2 实践建议
- 从简单领域开始(如积木世界)
- 逐步添加复杂特性(时间、资源)
- 尝试与其他AI技术结合(如学习最优方法)
我在实际项目中发现,SHOP2最适合那些需要根据实时状态调整策略的场景。比如在无人机配送系统中,我们使用SHOP2根据电量、天气和交通状况动态调整配送路线。当电量低于阈值时,它会自动切换到最近的充电站,而不是继续执行原定任务。这种灵活性是传统规划器难以实现的。
