本节摘要:规划的第一步不是找路,是把任务翻译成机器能核验的形式。本节定义满足型与效用型目标的机器表示,给出递归任务分解的终止条件与粒度标准,交付结构化任务单的数据骨架——这份任务单是本章后续搜索、程序展开与不确定性处理的统一输入契约。
调度单上只有一句话:"把冷库的托盘送到装柜口。"人读了会心领神会,导航计算机读到的只是一串无法执行的字符——它不知道托盘在哪算"在冷库"、送到哪个位置算"到装柜口"、路上要不要避让叉车。上一章的目标栈里存着这个目标的名与优先级,本节要给它一个可执行的躯体:目标谓词化、任务分解化、步骤可验收。这份翻译质量决定了后面一切算法的下限——垃圾目标进,垃圾航线出。
第 1.3 节分过型:满足型目标与效用型目标。机器表示上,满足型目标写成谓词——对状态板信念的可判定提问。谓词的设计纪律比写法重要:只引用信念里真实存在的字段(2.4 节航图的节点、标签、位姿),判定必须确定性返回真或假,拒绝模糊副词。"到达装柜口"写成"位姿与节点'装柜口'中心的距离小于两米且朝向误差小于直角的一半"——每个参数都是可配置、可测试的常数,而不是"大概到了"。
效用型目标写成评价函数:输入信念,输出标量分数。路径越短越好、电量消耗越少越好、完成越早越好,多目标合成时加权求和——权重是策略的显式声明,写进任务单让上游可审计。工程红线:评价函数不许引用规划舱不可知的量。如果打分依赖"客户满意度"而系统里没有任何字段承载它,这个函数就是装饰品,跑起来只会产生自欺的分数。
分解的机械定义是递归的:把复合任务按某种方法展开成子任务序列,直到子任务全部是原语动作——行动舱接口表上直接列着的那类动作(移动、夹持、释放、等待,见第 4.1 节)。分解质量的三条标准:终止(展开必然到达原语,不存在自我引用的循环展开)、粒度均匀(子任务的工作量在同一量级,避免"移动到冷库"与"抬起机械臂几厘米"并列)、失败可定位(每步带验收谓词,出错能指认具体步骤而不是整体报废)。
粒度是最容易被低估的设计维度。拆得过粗,单步失败无法定位也难以重试;拆得过细,规划舱被琐碎步骤淹没,重规划的代价失控。对照表给出体感:
| 粒度 | 例子 | 失效模式 |
|---|---|---|
| 过粗 | "完成转运" | 失败不知道坏在哪步,无法部分重试 |
| 过细 | "抬起臂、转动手腕、再抬一毫米" | 任务单膨胀,重规划代价失控 |
| 合适 | "移动到冷库 → 装载托盘 → 移动到装柜口 → 卸载" | 每步可验收、可重试、可指派 |
分解还隐含依赖排序:有先后依赖的子任务串行排列(先装载后移动),无依赖的可以并行(移动路径规划与装柜口状态确认可同时进行)。串并行标注写进任务单,给 3.3 节的程序展开与第 4 章的执行调度使用。
结构化任务单是本节的交付物,也是全章的数据枢纽:
from dataclasses import dataclass, field from typing import Callable @dataclass class Step: """原子步骤:原语动作 + 验收谓词 + 依赖。""" action: str # 原语名,对应行动舱接口表 params: dict # 动作参数(目标点、速度档) accept: Callable # 验收谓词:读信念,返回真/假 depends: list = field(default_factory=list) # 前置步骤号 parallel_group: str = "" # 同组步骤可并行 @dataclass class TaskOrder: """结构化任务单:本章所有流水线的统一输入。""" goal_pred: Callable # 总目标谓词 utility: Callable = None # 效用函数(可选) deadline: float = None # 时间窗(秒),过期触发重规划 steps: list = field(default_factory=list) def decompose(goal, knowledge, depth=0): """递归分解:复合任务按方法表展开,抵达原语为止。""" if depth > 8: raise RecursionError("分解过深:检查是否存在循环方法") if goal in PRIMITIVES: # 终止:已是行动舱原语 return [goal] method = pick_method(goal, knowledge) # 选适用方法(3.3 节展开) if method is None: raise ValueError(f"无方法可用:{goal}") plan = [] for sub in method.subtasks: plan.extend(decompose(sub, knowledge, depth + 1)) return plan PRIMITIVES = {"move_to", "grasp", "release", "wait", "dock"}
代码里 decompose 与 3.3 节的 HTN 展开是同一个思想的两个成熟度:本节的版本只演示递归骨架,方法表 pick_method 的完整形态(前置条件匹配、多方法择优)留到 3.3 节。任务单的字段在后续章节各归其位——验收谓词给第 4 章的执行监控当判据,deadline 给 3.4 节的重规划当触发器,依赖与并行组给执行调度当依据。
静态校验器是任务单的第一道质检岗,骨架如下:
def validate_task_order(order, cabin_map, primitives): """任务单静态校验:三查不过不开工。""" errors = [] # 一查:验收谓词引用的字段在信念里真实存在 for step in order.steps: for field in step.accept.fields: if cabin_map.field_missing(field): errors.append(f"谓词引用未知字段:{field}") # 二查:原语全部在行动舱接口表白名单内 for step in order.steps: if step.action not in primitives: errors.append(f"私造原语:{step.action}") # 三查:依赖图无环且并行组自洽 if has_cycle(order.steps): errors.append("步骤依赖成环") return errors def has_cycle(steps): state = {id(s): 0 for s in steps} # 0 未访 1 在栈 2 完成 def visit(s): if state[id(s)] == 1: return True if state[id(s)] == 2: return False state[id(s)] = 1 if any(state[id(dep)] != 2 and visit(dep) for dep in s.depends): return True state[id(s)] = 2 return False return any(visit(s) for s in steps if state[id(s)] == 0)
任务单的联调不需要跑执行,静态校验就能拦下大半问题:谓词引用的字段在航图与状态板里是否都存在(防"引用不存在的世界");分解树是否全部抵达原语且无环(跑一遍深度限额展开);验收谓词在录制的成功案例回放上是否全部转真(防"永远验收失败"的死任务)。
按症状排查:分解循环爆栈是方法表里出现了互相引用的复合任务,深度限额只是保险丝,根治要靠方法表的静态审查;验收永远失败先查谓词引用的字段名拼写与量纲,再查阈值是否定得过紧——两米与半米的差别常在动态环境里定生死;重规划代价失控通常是粒度过细的晚期症状,回到粒度对照表重新划线;计划与执行对不上(行动舱抱怨收到没见过的动作名)是任务单越过接口表私造原语,接口表必须作为唯一白名单。
⚠️ 常见坑:把自然语言的模糊直接带进谓词。"尽快到达"不是谓词,"deadline 内到达"才是;"小心叉车"不是约束,"距叉车路径三米外通行"才是。任务单里不许有需要"领会"的词。
任务单有了航点表,但航点之间的路还没连起来。下一节展开规划舱最经典的算法底座:状态空间搜索与 A 星寻路。