系列第七篇,实践篇开篇。理论讲完了,该教怎么用了。本文将系统介绍HTN领域设计的核心方法、常见模式和最佳实践。
一、开篇:为什么领域设计是"拦路虎"?
在前面六篇文章中,我们系统学习了HTN规划的理论基础:
- 什么是HTN?——形式化定义与核心机制
- HTN与经典规划的区别——表达能力更强,但不可判定
- TFD算法详解——如何分解任务、搜索解
然而,当你真正动手设计一个HTN领域时,可能会遇到这些困惑:
❓ 我的任务层次结构怎么划分才合理?
❓ 一个compound task应该有多少个method?
❓ method的precondition写太细会怎样?写太粗又怎样?
❓ 为什么我的规划器跑不出来?是领域设计问题还是算法问题?
这些问题指向一个核心事实:HTN领域设计是一门"艺术"。
本文将从实践角度,系统回答这些问题,帮助你从"懂HTN理论"进阶到"会设计HTN领域"。
二、识别任务的自然层次结构
2.1 什么是"自然层次"?
HTN的核心是任务分解,而任务分解的前提是任务本身具有层次结构。一个好的层次结构应该:
- 符合人类认知习惯:专家是如何思考这个问题的?
- 反映问题本身的结构:任务之间的逻辑关系是什么?
- 便于复用和扩展:子任务能否被多个上层任务共享?
反例:强行构造的层次结构
任务:去北京
分解为:[买机票, 预订酒店, 打包行李, 去机场, ...]
这个分解看似合理,但实际上混合了多个层次:买机票是"计划"层次,打包行李是"准备"层次,去机场是"执行"层次。
正例:自然的层次结构
任务:出差北京
分解为:[规划行程, 准备出发, 执行行程]
- 规划行程 → [订机票, 订酒店, 安排会议]
- 准备出发 → [打包行李, 处理工作交接]
- 执行行程 → [去机场, 登机, 到达后活动]
2.2 识别层次结构的三步法
Step 1:枚举原子动作
先不考虑层次,列出所有可能的原子动作:
航天器领域:slew, takePhoto, download, charge, ...
物流领域:load, unload, drive, fly, ...
Step 2:按"目的"分组
原子动作很少单独出现,通常是为了完成某个"小目标":
[定位目标, 调整姿态, 拍照, 存储数据] → 观测任务
[加载数据, 建立链路, 传输数据, 确认接收] → 下传任务
Step 3:构建任务树
从底层向上归纳,形成任务树:
执行观测任务
├── 观测准备
│ ├── 定位目标
│ └── 调整姿态
├── 执行观测
│ └── 拍照
└── 数据处理
├── 存储
└── 下传
2.3 判断层次结构好坏的标准
| 标准 | 好的层次结构 | 差的层次结构 | 示例 |
|---|---|---|---|
| 粒度一致性 | 同层任务粒度相近 | 有的很粗,有的很细 | ✅ [观测、下传] vs ❌ [观测、买咖啡] |
| 逻辑清晰性 | 分解理由显而易见 | 需要大量解释才能理解 | ✅ [规划→准备→执行] vs ❌ 随意混合 |
| 复用性 | 子任务可被多处调用 | 每个任务只在一个地方用 | ✅ "充电"可被多任务调用 |
| 可扩展性 | 新增任务容易插入 | 新增任务需要大改结构 | ✅ 新增目标类型只需加method |
三、设计有效的方法(Method)
3.1 Method的核心要素
一个method由三部分组成:
(:method 方法名
:precondition (前提条件)
:tasks (子任务序列)
)
设计原则:
- Precondition:精确描述何时适用这个方法
- Tasks:清晰定义分解后的执行顺序
3.2 Precondition设计的两难
问题:precondition应该写多细?
太粗的后果:
(:method 观测目标
:precondition () ; 无条件适用
:tasks (定位 调整姿态 拍照)
)
→ 可能生成不可行的计划(如目标不可见时仍尝试观测)
太细的后果:
(:method 观测目标
:precondition (and
(可见 ?target)
(姿态正确 ?target)
(电量充足)
(存储空间充足)
(无更高优先级任务)
...
)
:tasks (拍照)
)
→ 限制了规划器的灵活性,可能错过可行解
平衡策略:
核心约束 → 写进precondition
辅助约束 → 在子任务中处理
示例:
(:method 观测目标
:precondition (可见 ?target) ; 核心约束
:tasks (调整姿态 拍照) ; 调整姿态会检查姿态正确性
)
3.3 多Method设计的"覆盖"问题
一个compound task通常有多个method,它们共同构成"解决方案空间"。
设计要点:
1. 互斥性:不同method的适用条件应尽量不重叠
2. 完备性:所有可能的情况都应该有对应的method
3. 偏好性:如果希望优先选择某个method,可以在precondition中设置"偏好条件"
示例:到达目的地的多method设计
(:method 到达目的地
:precondition (距离近 ?dest)
:tasks (步行 ?dest)
)
(:method 到达目的地
:precondition (and (距离远 ?dest) (有车))
:tasks (开车 ?dest)
)
(:method 到达目的地
:precondition (and (距离远 ?dest) (无车) (有公交站 ?dest))
:tasks (坐公交 ?dest)
)
这三个method几乎覆盖了所有情况,但边界情况可能需要额外处理。
⚠️ 关于互斥性的重要说明:
上述示例的method并非严格互斥。如果 (距离近 ?dest) 和 (有车) 同时成立,第一个和第二个method都可能被选择。
HTN规划器通常按定义顺序尝试——先定义的method优先级更高。如果第一个method适用,规划器会优先使用它,不会再尝试后面的method。
严格互斥的写法:
(:method 到达目的地 ; Method 1
:precondition (距离近 ?dest)
:tasks (步行 ?dest))
(:method 到达目的地 ; Method 2
:precondition (and (距离远 ?dest) (有车))
:tasks (开车 ?dest))
(:method 到达目的地 ; Method 3 - 兜底
:precondition (and (距离远 ?dest) (not (有车)) (有公交站 ?dest))
:tasks (坐公交 ?dest))
这样三个method的条件互斥,确保每种情况只有一个method适用。严格互斥的好处是行为更可预测,坏处是条件写起来更复杂。实践中,根据需求选择:如果顺序优先已满足需求,不必强求严格互斥。
3.4 Task排序的学问
Task的排序影响:
1. 执行效率:并行任务可以无序
2. 可行性:某些任务必须先执行(如先开机再操作)
3. 规划器性能:有序任务减少搜索空间
排序规则:
- 因果依赖:A的effect是B的precondition → A必须在B前
- 资源竞争:共享资源 → 根据资源状态决定顺序
- 逻辑顺序:业务逻辑要求 → 按业务规则排序
四、常见设计模式与反模式
4.1 设计模式(推荐)
Pattern 1:顺序分解模式
适用场景:任务有明确的执行顺序
(:method 执行观测
:tasks (准备 → 执行 → 清理)
)
优点:简单直观,规划器负担小
Pattern 2:选择分解模式
适用场景:同一任务有多种实现方式
(:method 数据传输
:precondition (有地面站)
:tasks (通过地面站传输)
)
(:method 数据传输
:precondition (无地面站)
:tasks (通过中继卫星传输)
)
优点:增强鲁棒性,处理多种场景
Pattern 3:迭代分解模式
适用场景:需要多次执行相似任务
(:method 观测多个目标
:precondition (目标列表非空)
:tasks (观测第一个目标 观测剩余目标)
)
(:method 观测多个目标
:precondition (目标列表为空)
:tasks () ; 空任务,结束递归
)
优点:处理动态数量的任务
⚠️ 关键注意:迭代分解必须有明确的终止条件。
上面的例子中,(目标列表为空) 是终止条件,返回空任务 ()。没有终止条件,规划器会陷入无限递归。
常见错误:忘记写终止method
;; 错误!只有递归,没有终止
(:method 观测多个目标
:tasks (观测第一个目标 观测剩余目标)) ; 永远不会停止
Pattern 4:条件执行模式
适用场景:根据状态选择是否执行某步骤
(:method 完成任务
:tasks (检查条件 (条件分支))
)
(:method 条件分支
:precondition (条件满足)
:tasks (执行动作)
)
(:method 条件分支
:precondition (not (条件满足))
:tasks () ; 跳过
)
优点:灵活处理不确定性
4.2 反模式(避免)
Anti-Pattern 1:过度分解
问题:把原子动作也当作compound task分解
(:method 拍照
:tasks (打开相机 对准目标 按下快门 关闭相机)
)
后果:
- 增加不必要的搜索空间
- 违反原子动作的语义
正确做法:拍照是原子动作,直接在method中引用
Anti-Pattern 2:循环依赖
问题:任务A分解出任务B,任务B又分解出任务A
(:method 任务A
:tasks (任务B)
)
(:method 任务B
:tasks (任务A)
)
后果:规划器陷入无限循环
正确做法:确保分解是有向无环的
Anti-Pattern 3:Precondition遗漏
问题:method的前提条件不完整
(:method 开车去
:tasks (启动汽车 行驶)
)
遗漏:没检查有车、有驾照、油量充足...
后果:生成不可行计划
正确做法:系统性检查所有必要条件
Anti-Pattern 4:任务粒度不一致
问题:同一层级的任务粒度差异巨大
(:method 完成项目
:tasks (写代码 买咖啡) ; 前者是核心任务,后者是细节
)
后果:
- 逻辑混乱
- 难以理解和维护
正确做法:买咖啡应该在"休息"或"日常"层次
五、HTN领域设计的最佳实践
5.1 从简单到复杂
推荐流程:
1. 先实现一个最小可行领域
- 只包含最核心的任务和方法
- 确保能跑通一个简单场景
- 逐步扩展
- 添加新的method处理更多情况
-
添加新的compound task处理更复杂的任务
-
持续测试
- 每次扩展后测试是否破坏已有功能
- 使用测试用例覆盖各种场景
5.2 领域文档化
推荐格式:
# HTN领域说明文档
## 1. 领域概述
- 应用场景:航天器任务规划
- 主要任务:观测、通信、维护
## 2. 任务层次图
(可视化任务树)
## 3. Method说明表
| Compound Task | Method | Precondition | Subtasks |
|--------------|--------|--------------|----------|
| 观测 | 标准观测 | 目标可见 | [定位, 调姿, 拍照] |
## 4. 原子动作说明
| 动作 | 参数 | Precondition | Effect |
|------|------|--------------|--------|
| takePhoto | target | 对准目标 | 存储图像 |
好处:
- 自己回顾时能快速理解
- 与他人协作时降低沟通成本
- 调试时快速定位问题
5.3 调试技巧
问题:规划器跑不出来,怎么定位?
调试步骤:
- 检查任务网络
lisp ;; SHOP2中查看分解过程 (shop2:find-plans 'domain 'problem :verbose 2) - 分解是否到达原子动作?
- 是否存在循环依赖?
-
使用
(trace-methods)追踪method调用 -
检查约束冲突
- 前提条件是否矛盾?
- 时间约束是否可满足?
-
失败时检查当前状态与precondition的差异
-
简化问题——最小复现
lisp ;; 创建最小测试用例 (defproblem minimal-test (:init (visible target1)) (:task (observe target1))) - 减少任务数量
- 简化前提条件
-
找到最小复现案例
-
使用规划器日志
- SHOP2等规划器有详细日志
- 查看分解路径、失败原因
常见失败模式速查表:
| 现象 | 可能原因 | 检查方法 |
|---|---|---|
| 无限循环/超时 | 递归method无终止条件 | 检查递归出口 |
| 找不到解 | precondition过严 | 逐步放宽条件测试 |
| 解太慢 | method数量过多 | 合并相似method |
| 生成的计划不可执行 | precondition遗漏关键约束 | 检查原子动作前提 |
| 分解过早终止 | 缺少某个method | 检查是否覆盖所有情况 |
5.4 性能优化
影响性能的因素:
- Method数量:越多搜索空间越大
- Precondition复杂度:复杂的条件判断耗时
- 任务网络深度:越深递归层数越多
优化策略:
1. 合并相似method:用参数区分而非多个method
2. 简化precondition:移除冗余条件
3. 调整分解策略:优先分解约束最紧的任务
六、实例演练:航天器观测领域
6.1 问题定义
场景:卫星对多个地面目标进行观测并下传数据
约束:
- 目标有可见性窗口
- 电量有限
- 存储空间有限
- 地面站有通信窗口
6.2 任务层次设计
主任务:执行观测计划
├── 观测任务
│ ├── 定位目标
│ ├── 调整姿态
│ └── 拍照
├── 数据管理
│ ├── 存储数据
│ └── 下传数据
└── 资源管理
├── 充电
└── 清理存储
6.3 Method设计
;; 主任务分解
(:method 执行观测计划
:precondition (有目标任务)
:tasks (观测任务 数据管理)
)
;; 观测任务分解
(:method 观测任务
:precondition (可见 ?target)
:tasks (定位 ?target 调姿 ?target 拍照 ?target)
)
;; 数据下传分解
(:method 下传数据
:precondition (and (有地面站窗口) (存储非空))
:tasks (建立链路 传输数据 确认接收)
)
;; 无地面站时的备选方案
(:method 下传数据
:precondition (and (无地面站窗口) (有中继卫星))
:tasks (通过中继传输)
)
6.4 完整性检查
检查清单:
- [ ] 所有compound task都有对应的method
- [ ] 所有method的precondition覆盖了可能的场景
- [ ] 分解最终都能到达原子动作
- [ ] 没有循环依赖
- [ ] 关键约束(电量、存储、窗口)都有处理
6.5 常见错误示例
学习正确的设计很重要,但避免错误同样关键。以下是一些典型的踩坑案例:
错误1:Precondition遗漏关键约束
;; 错误!没有检查目标可见性
(:method 观测任务
:tasks (定位 ?target 调姿 ?target 拍照 ?target))
;; 后果:目标不可见时仍会尝试规划,生成不可执行的计划
;; 正确做法
(:method 观测任务
:precondition (可见 ?target) ; 关键约束
:tasks (定位 ?target 调姿 ?target 拍照 ?target))
错误2:任务粒度混乱
;; 错误!"充电"是资源管理任务,不应与"观测任务"同级
(:method 执行观测计划
:tasks (观测任务 充电))
;; 正确做法:分层处理
(:method 执行观测计划
:tasks (观测任务 资源管理))
(:method 资源管理
:precondition (电量不足)
:tasks (充电))
错误3:Method条件不完整
;; 错误!缺少"无车且无公交站"的情况
(:method 到达目的地
:precondition (距离近 ?dest)
:tasks (步行 ?dest))
(:method 到达目的地
:precondition (距离远 ?dest)
:tasks (开车 ?dest)) ; 如果没车怎么办?
;; 后果:当"距离远且无车"时,规划器无解
;; 正确做法:覆盖所有情况,或提供兜底method
(:method 到达目的地 ; 兜底
:precondition (and (距离远 ?dest) (not (有车)) (not (有公交站 ?dest)))
:tasks (打车 ?dest))
错误4:递归无终止条件
;; 错误!无限递归
(:method 处理所有任务
:tasks (处理第一个任务 处理所有任务))
;; 正确做法:添加终止条件
(:method 处理所有任务
:precondition (任务列表非空)
:tasks (处理第一个任务 处理所有任务))
(:method 处理所有任务
:precondition (任务列表为空)
:tasks ()) ; 终止
经验总结:每写完一个compound task的所有method,都应该问自己:
- 是否覆盖了所有可能的情况?
- 是否有遗漏的关键约束?
- 递归是否有出口?
- 任务粒度是否一致?
七、总结与展望
7.1 核心要点回顾
| 主题 | 核心要点 |
|---|---|
| 层次结构 | 自然、清晰、一致、可复用 |
| Method设计 | Precondition平衡、多method覆盖、合理排序 |
| 设计模式 | 顺序、选择、迭代、条件执行 |
| 反模式 | 过度分解、循环依赖、条件遗漏、粒度不一致 |
| 最佳实践 | 从简到繁、文档化、系统调试、性能优化 |
7.2 下一步
设计好领域后,需要用规划器来执行。下一篇我们将介绍:
- 主流HTN规划器对比:SHOP2、PyHOP、SIADEX
- 如何选择合适的规划器
- 规划器配置与调优
参考文献
- Nau, D. S., Au, T. C., Ilghami, O., Kuter, U., Murdock, J. W., Wu, D., & Yaman, F. (2003). SHOP2: An HTN planning system. Journal of Artificial Intelligence Research, 20, 379-404.
- Erol, K., Hendler, J., & Nau, D. S. (1994). UMCP: A Sound and Complete Procedure for Hierarchical Task-Network Planning. AIPS.
- Ghallab, M., Nau, D., & Traverso, P. (2004). Automated Planning: Theory and Practice. Morgan Kaufmann. (Chapter 11)
- Bercher, P., Alford, R., & Höller, D. (2019). A survey on hierarchical planning. Artificial Intelligence, 277, 103-165.