1.2 规划模块基础任务分解与子目标级联的单路径推理
阅读指南:本文假设你已经熟悉 OpenCV 的
Mat内存模型,并且对强化学习或层级规划(Hierarchical Planning)有初步概念。我们将从宏观的任务分解结构开始,层层剥开,直到最底层的原始状态指针访问。全程使用"我们",像朋友聊天一样探索。如果某个概念听起来抽象,别急,我们先用一个生活中的例子建立直觉,再推广到真实规模。
1.2.1 多级任务分解的拓扑结构
1.2.1.1 单路径级联存储与层级步进公式推导
想象你走进一家巨大的超市,手里拿着一张购物清单。如果清单上的商品随机排列——“牛奶、螺丝刀、香蕉、猫粮”——你会在货架之间来回折返,疲惫不堪,甚至遗漏物品。但如果清单按 “区域 → 货架 → 商品” 层层排好,并且你决定 “一次只推一条购物车通道走到底”,这就是单路径级联(Single-Path Cascade)的核心直觉。
在规划模块(Planning Module)中,我们面对的不是图像像素,而是 “目标 → 子目标 → 原子动作” 的多级结构。根据 STEP Planner(Shi et al., 2024)的研究,长程任务失败主要源于两个鸿沟:上下文鸿沟(Contextual Gap)和 逻辑鸿沟(Logical Gap)。单路径级联的核心思想,就是把一棵复杂的子目标树**"拍平"成一条深度优先的线性序列,就像行优先**(Row-Major)存储多维数组一样,让规划器一次只聚焦一个逻辑连续的片段。
别急,听起来抽象对吧?我们从一个 toy example 开始。
Toy Example:做一杯手冲咖啡
- Goal0(根):MakeCoffee
- Subgoal1:PrepareTools(准备器具)
- Action1.1:GetCup
- Action1.2:GetKettle
- Subgoal2:Brew(冲泡)
- Action2.1:BoilWater
- Action2.2:PourOver
- Subgoal1:PrepareTools(准备器具)
如果画成图,它像一棵深度为 3 的树。但在单路径推理中,我们不维护多条分支并行探索,而是像行优先遍历一样,把它拍平成一个全序序列:
[PrepareTools → GetCup → GetKettle → Brew → BoilWater → PourOver]
现在,层级步进怎么算?假设我们在第 l 层,每个节点平均分解出 n<sub>l+1</sub> 个子节点。那么,要在拍平后的线性存储中跳到下一个同级子目标,步长 step[l] 等于该节点下方所有后代节点的总数乘以原子动作的大小。
公式上,这类似于多维数组的步进推导:
step[deepest] = sizeof(AtomAction)step[l] = children_count(l+1) × step[l+1]
这保证了即使逻辑上是"树",物理存储和推理过程却是一条连续的磁带。规划器只需要一个指针,沿着磁带顺序读取,就能完成整个任务。
代码示例:级联拍平的实现
class SubgoalNode:
def __init__(self, name, is_primitive=False):
self.name = name
self.children = []
self.is_primitive = is_primitive # 是否为原子动作
def flatten_cascade(self):
"""单路径级联拍平:深度优先,全序展开"""
sequence = []
if not self.is_primitive:
sequence.append(self) # 复合目标保留为导航节点
for child in self.children:
sequence.extend(child.flatten_cascade())
return sequence
# Toy Example: 做咖啡
root = SubgoalNode("MakeCoffee")
prep = SubgoalNode("PrepareTools")
brew = SubgoalNode("Brew")
prep.children = [SubgoalNode("GetCup", True), SubgoalNode("GetKettle", True)]
brew.children = [SubgoalNode("BoilWater", True), SubgoalNode("PourOver", True)]
root.children = [prep, brew]
cascade = root.flatten_cascade()
print([n.name for n in cascade])
# 输出: ['MakeCoffee', 'PrepareTools', 'GetCup', 'GetKettle',
# 'Brew', 'BoilWater', 'PourOver']
1.2.1.2 goal/subgoal/action 属性的联合语义解析
类比 OpenCV 中 dims / channels / depth 三个属性共同决定了一个 Mat 对象的内存布局,规划模块中的每个节点也由三个核心属性联合定义其语义:
depth:抽象层级(0= 根目标,1= 子目标,...,∞= 原子动作)arity:该节点分解出的直接子节点数(类似channels,表示"并行度"或"分支数")state_dim:该节点依赖的环境状态维度(类似dims,表示状态空间大小)
这三个属性不是孤立的。一个 depth=1、arity=3、state_dim=256 的节点,告诉规划器:“我是中间层子目标,需要被进一步拆成 3 个串行步骤,并且我的所有子节点共享同一个 256 维状态空间。”
而 depth=2、arity=0、state_dim=64 的原子动作节点则说:“我不能再拆了,直接执行,只需要 64 维的局部控制状态。”
这种联合语义让分解器能快速判断:遇到哪些节点该调用 LLM 分解(复合目标),遇到哪些节点该送入低层策略网络(原子动作)。根据 STEP Planner 的框架,这种区分直接对应了可映射性(Mappability)和一致性(Consistency)的判断——如果子目标无法被低层策略可靠执行,分解器就应该拒绝这个分解方案。
代码示例:联合语义解析
class PlanNode:
def __init__(self, name, depth, arity, state_dim, is_primitive=False):
self.name = name
self.depth = depth # 抽象层级,类似 OpenCV 的 cvMat.depth
self.arity = arity # 子目标数,类似 channels
self.state_dim = state_dim # 状态维度,类似 dims
self.is_primitive = is_primitive
def describe(self):
if self.is_primitive:
return f"{self.name}: 原子动作 | state_dim={self.state_dim}"
return (f"{self.name}: depth={self.depth}, "
f"arity={self.arity}, state_dim={self.state_dim}")
# 联合语义示例
root = PlanNode("CleanRoom", depth=0, arity=2, state_dim=256)
sub1 = PlanNode("VacuumFloor", depth=1, arity=3, state_dim=256)
sub2 = PlanNode("MopFloor", depth=1, arity=2, state_dim=256)
action = PlanNode("MoveArm", depth=2, arity=0, state_dim=64, is_primitive=True)
print(root.describe()) # 复合目标,需继续分解
print(action.describe()) # 原子动作,可直接执行
1.2.1.3 子规划(Sub-plan)的头部共享机制与父规划依赖
现在我们已经了解了单路径级联的存储拓扑,接下来看看一个关键问题:当我们从父规划中"截取"一段子任务(比如只执行"冲泡"部分),需要复制整个状态吗?
听起来没必要,对吧?就像 OpenCV 中提取 ROI(Region of Interest)时,子矩阵只创建一个头部(Header),共享父矩阵的数据指针。在规划模块中,子规划(Sub-plan)也完全类似:它持有一个指向父规划状态缓存的引用,加上自己的起始索引和长度,而不做深拷贝。
如果画成图,它像是一本书的目录卡片:卡片本身很薄(Header),但指向了书架上厚重的实体书(Parent State)。
这种头部共享机制极大降低了长程规划的内存开销。根据 Shivashankar et al.(2012)的 Hierarchical Goal Networks 研究,目标分解的优势之一就是允许子目标在推理时复用父级状态表示,避免状态空间的指数级膨胀。
但这里有一个关键约束:子规划的生命周期严格依赖于父规划。父规划必须在所有子规划执行完毕后才能释放。如果父规划提前销毁,子规划就会像 OpenCV 中父 Mat 被释放后的 ROI 一样,变成悬空指针。
代码示例:子规划头部共享
class PlanHeader:
"""子规划头部:共享父状态,不拥有数据"""
def __init__(self, parent_state, start_idx, length, name):
self.parent_state = parent_state # 引用父规划状态
self.start_idx = start_idx # 在级联序列中的起始位置
self.length = length # 子规划长度
self.name = name
def __getitem__(self, idx):
if idx >= self.length:
raise IndexError("子规划索引越界")
return self.parent_state[self.start_idx + idx]
def is_orphaned(self):
"""检查父状态是否已被释放"""
return self.parent_state is None
# 从父规划提取 ROI 子规划
parent_plan = {"state_vector": [0.0]*256, "released": False}
roi = PlanHeader(parent_plan, start_idx=4, length=2, name="BrewOnly")
print(roi[0]) # 访问父规划的第 4 个元素
1.2.1.4 引用计数原子操作与生命周期自动管理
在继续之前,我们必须解决一个悬而未决的问题:如果父规划被多个子规划共享,谁来负责释放它?
答案就是引用计数(Reference Counting)。这类似于 OpenCV Mat 内部的 refcount 指针:每次创建子规划头部,计数器原子递增;每次子规划完成或被销毁,计数器递减。当计数归零,父规划的状态缓存自动释放。
这种机制让规划器可以像使用智能指针一样使用子规划,无需手动跟踪谁还在依赖谁。在多线程场景下,原子操作(如 std::atomic 或 Python 的 threading.Lock)保证了计数器的线程安全。
代码示例:引用计数与自动释放
import threading
class SharedPlanState:
def __init__(self, data):
self.data = data
self.refcount = 1
self.lock = threading.Lock()
self.released = False
def add_ref(self):
with self.lock:
self.refcount += 1
def release(self):
with self.lock:
self.refcount -= 1
if self.refcount == 0:
self.released = True
self.data = None
print("父规划状态已自动释放")
return True
return False
# 父规划被两个子规划共享
state = SharedPlanState([0.0] * 256)
state.add_ref() # 子规划 A 引用
state.add_ref() # 子规划 B 引用
state.release() # A 完成
state.release() # B 完成,触发自动释放
1.2.2 深状态复制与浅状态复制的判定
现在我们已经了解了子目标分解的拓扑结构和生命周期管理,接下来看看规划器在执行过程中最容易踩的坑:状态的复制语义。
如果你熟悉 RNN 中的隐藏状态传递,你可能已经习惯了"同一个状态张量在不同时间步被读取和更新"。但在层级规划模块中,状态的共享与复制策略远比 RNN 复杂,因为我们不仅要传递状态,还要在分解、回滚、并行探索(即使是单路径,也可能有临时分支预测)时决定"该不该复制一份"。
1.2.2.1 clone() 与 copyTo() 的显式状态分配策略
最安全的策略是深拷贝(clone):当你需要保存一个检查点(checkpoint)以备回滚时,必须完整复制整个环境状态。这对应 OpenCV 的 Mat::clone(),它总是分配新内存并复制数据。
但深拷贝昂贵。如果你只是想把当前状态"覆盖"到一个已存在的缓冲区(例如,预分配的推理张量),copyTo() 更高效:它复用目标内存,只做内容拷贝。
在规划模块中,clone() 用于异常恢复前的备份;copyTo() 用于每步迭代中把"当前状态"刷入"下一步状态"的固定槽位。
代码示例:深拷贝与覆盖拷贝
import copy
class PlanState:
def __init__(self, vector):
self.vector = vector
def clone(self):
"""深拷贝:完全独立的状态副本"""
return PlanState(copy.deepcopy(self.vector))
def copy_to(self, target):
"""拷贝到目标状态,复用目标内存"""
target.vector[:] = self.vector[:] # 原地替换
return target
# 场景:进入不确定动作前做备份
current = PlanState([1.0, 2.0, 3.0])
backup = current.clone() # 显式深拷贝,安全但昂贵
# 场景:复用预分配缓冲区
buffer = PlanState([0.0, 0.0, 0.0])
current.copy_to(buffer) # 零分配拷贝,覆盖写入
1.2.2.2 赋值运算符的引用传递陷阱与调试识别
这是规划模块中最隐蔽的 bug 源。如果你写 plan_b = plan_a,你以为复制了一份计划,实际上只是多了一个引用。修改 plan_b 会连带修改 plan_a。
听起来很熟悉对吧?这就像 Python 列表的浅拷贝,或者 C++ 中忘了写拷贝构造函数。在规划器中,这种错误会导致"子规划修改了父规划的原始状态,回滚时无法恢复"的诡异行为。
调试技巧:在调试模式下为每个 PlanState 分配唯一 id,赋值时检查 id 是否相同。
代码示例:识别引用传递陷阱
def debug_identity(a, b, label=""):
same = (a is b)
print(f"[{label}] 同一对象? {same} | id(a)={id(a)}, id(b)={id(b)}")
if same:
print("警告:发生浅拷贝引用传递,修改将互相影响!")
# 陷阱演示
plan_a = PlanState([1, 2, 3])
plan_b = plan_a # 赋值运算符:仅仅是引用传递
debug_identity(plan_a, plan_b, "赋值后")
plan_b.vector[0] = 999
print(plan_a.vector) # [999, 2, 3] —— plan_a 也被改了!
1.2.2.3 ROI 子规划的状态共享安全性与父规划释放风险
如果父规划在一个子规划还在执行时就因为异常被释放(例如,上层决策认为整个任务已失败),子规划访问父状态就会悬空。这类似于 OpenCV 中父 Mat 被释放后 ROI 变成"野指针"。
Strict Subgoal Execution(SSE)框架特别强调:高层策略必须确保低层策略严格完成当前子目标后,才允许状态转移。这种"严格完成"语义,本质上就是在保护 ROI 子规划的生命周期——父状态的释放必须等待所有子目标达成。
安全策略:子规划执行前检查父状态有效性;使用弱引用(weakref)而非强引用,让父状态能被垃圾回收,同时子规划可以检测到悬空。
代码示例:弱引用安全保护
import weakref
class SafePlanHeader:
def __init__(self, parent_state, start_idx, length):
# 使用弱引用,不阻止父状态释放
self._parent = weakref.ref(parent_state)
self.start_idx = start_idx
self.length = length
def __getitem__(self, idx):
parent = self._parent()
if parent is None or getattr(parent, 'released', False):
raise RuntimeError("父规划已释放,子规划悬空!")
return parent.data[idx]
# 安全演示
state = SharedPlanState([10, 20, 30, 40])
safe_roi = SafePlanHeader(state, 1, 2)
print(safe_roi[0]) # 20
state.release() # 强制释放
try:
print(safe_roi[0])
except RuntimeError as e:
print(f"安全捕获: {e}")
1.2.2.4 C++11 移动语义在规划传递中的零拷贝优化
在规划器内部,大型状态向量(如 256 维激光雷达点云或 100 万维的图像特征)如果在函数返回时被拷贝,开销巨大。C++11 的移动语义(Move Semantics)允许我们"偷走"资源而非复制。
类比全连接层的前向传播:你不需要把权重矩阵复制到每一层,只需要把张量的"所有权"沿着计算图传递。移动语义在规划模块中同样关键——它让子规划可以把状态缓存移交给下一个子规划,实现零拷贝级联。
代码示例:C++ 移动语义
#include <vector>
#include <iostream>
#include <utility>
class PlanState {
public:
std::vector<float> data;
// 移动构造函数:零拷贝转移所有权
PlanState(std::vector<float>&& d) : data(std::move(d)) {
std::cout << "移动构造: 偷走资源,零拷贝\\n";
}
// 移动赋值
PlanState& operator=(PlanState&& other) noexcept {
if (this != &other) {
data = std::move(other.data);
}
return *this;
}
};
// 工厂函数返回大状态,自动触发移动语义
PlanState create_sensor_state() {
std::vector<float> big(1000000, 0.5f);
return PlanState(std::move(big));
}
int main() {
PlanState s = create_sensor_state(); // 零拷贝传递
return 0;
}
1.2.3 执行步长计算与状态对齐
现在我们已经了解了状态复制的显式与隐式语义,接下来进入更微观的层面:当规划器沿着单路径级联序列高速执行时,如何像 SIMD 遍历图像像素那样,高效地"遍历"子目标?
1.2.3.1 step[] 数组与 stateSize / atomSize 的推导关系
在 OpenCV 中,step[] 数组告诉你"跳到下一行需要跨越多少字节"。在单路径级联规划中,我们也有类似的步进结构:如果当前在第 l 层,step[l] 表示跳到该层下一个同级子目标所需跨越的线性存储单元数。
推导关系:
atomSize:一个原子动作的状态单元大小(如一个float占 4 字节)stateSize:一个复合目标的状态块大小(包含其所有子目标)step[l]= 从层级l的某节点出发,到下一个同级节点的偏移量
具体地,如果层级 l+1 的每个父节点平均包含 n 个子节点,那么:
step[l] = n × step[l+1]
这让你可以用一个简单的指针加法 ptr += step[l] 来跳跃到下一个同级子目标,而不需要递归遍历树。
代码示例:步长推导
class CascadeLayout:
def __init__(self, level_shapes, atom_bytes=4):
"""
level_shapes: 每层节点数,如 [1, 2, 6]
表示根1个,子目标层2个,原子动作层6个
"""
self.levels = len(level_shapes)
self.shapes = level_shapes
self.step = [0] * self.levels
self.atom_bytes = atom_bytes
# 从底层向上推导步长
self.step[-1] = atom_bytes
for l in range(self.levels - 2, -1, -1):
children_per_node = self.shapes[l + 1] // max(self.shapes[l], 1)
self.step[l] = children_per_node * self.step[l + 1]
def offset(self, level, index):
"""计算层级 l 第 index 个节点的字节偏移"""
return index * self.step[level]
# 示例:3层级联 [1, 2, 6]
layout = CascadeLayout([1, 2, 6])
print(f"step[0]={layout.step[0]}B, step[1]={layout.step[1]}B, step[2]={layout.step[2]}B")
# 输出: step[0]=24B, step[1]=12B, step[2]=4B
1.2.3.2 行尾填充(padding)的 SIMD 对齐要求(16/32/64 字节)
现代规划器常常需要在 GPU 或 AVX-512 上批量评估多个子目标的状态转移函数。为了使用 SIMD 指令,状态向量的内存布局必须对齐到 16、32 或 64 字节边界。
如果状态维度不是对齐的倍数(比如一个状态占 70 个 float,即 280 字节),我们需要在"行尾"填充(padding)到最近的边界。这类似于 OpenCV 为了 cv::Mat 运算效率而做的内存对齐。
对齐后的状态布局意味着:即使逻辑上状态只有 70 维,物理缓存可能是 80 维,最后几个 float 是"哑元"。规划器在遍历时必须区分"逻辑尺寸"和"物理步长"。
代码示例:SIMD 对齐状态
import numpy as np
class AlignedState:
ALIGNMENT = 64 # 64字节对齐,适配 AVX-512
def __init__(self, dim):
self.raw_dim = dim
bytes_needed = dim * 4 # float32 = 4 bytes
remainder = bytes_needed % self.ALIGNMENT
padding = (self.ALIGNMENT - remainder) % self.ALIGNMENT
self.padded_bytes = bytes_needed + padding
self.padded_dim = self.padded_bytes // 4
self.buffer = np.zeros(self.padded_dim, dtype=np.float32)
def logical_view(self):
return self.buffer[:self.raw_dim]
def physical_buffer(self):
"""返回对齐后的完整缓冲区,可直接送 SIMD"""
return self.buffer
# 示例:原始维度为 70,对齐到 64 字节
st = AlignedState(70)
print(f"原始: {st.raw_dim}, 填充后维度: {st.padded_dim}, 总字节: {st.buffer.nbytes}")
# 70*4=280, remainder=24, padding=40, padded_bytes=320, padded_dim=80
# 320 / 64 = 5,完美对齐
1.2.3.3 isContinuous() 判断与连续状态块的遍历优化
如果整个级联规划的状态存储在一块连续内存中,并且执行顺序与存储顺序完全一致(没有跳转、没有条件分支),我们就可以启用"快速遍历"模式。这类似于 OpenCV 的 Mat::isContinuous():当数据没有填充、没有 ROI 间隙时,你可以把整个二维矩阵当成一维数组来遍历。
在规划中,"连续"意味着所有子目标按执行顺序紧密排列,状态缓存是一整块线性内存。此时,规划器可以用一个简单循环从头到尾执行,无需每步重新计算偏移。
代码示例:连续执行优化
class CascadeExecutor:
def __init__(self, state_buffer, plan_sequence):
self.state_buffer = state_buffer
self.plan_sequence = plan_sequence
def is_continuous(self):
"""检查状态缓存是否逻辑连续(无跳转、无分支)"""
# 简化判断:如果所有子目标按顺序执行,中间无回溯,则为连续
return getattr(self.state_buffer, 'continuous', True)
def fast_execute(self):
"""连续内存块的批量遍历优化"""
if not self.is_continuous():
raise ValueError("非连续状态,不能使用快速遍历")
# 像遍历一维数组一样顺序执行所有子目标
for i, atom in enumerate(self.plan_sequence):
print(f"快速执行第 {i} 个原子动作: {atom}")
# 连续路径
continuous_plan = ["GetCup", "GetPowder", "AddWater", "Stir"]
exe = CascadeExecutor(None, continuous_plan)
exe.fast_execute()
1.2.3.4 非连续子规划(如 ROI 提取)的逐行安全遍历策略
当子规划是从父规划中"挖"出来的一段非连续片段(例如,只执行第 1 步和第 3 步,跳过第 2 步),或者因为对齐填充导致物理存储不连续时,我们不能假设连续性。必须逐个子目标访问,每次重新计算偏移。
这就像在 OpenCV 中遍历一个带有间隙的 ROI:你不能用 ptr += step 无脑推进,而必须用 ptr += step[row] 逐行定位。
代码示例:非连续安全遍历
class SafeROIIterator:
def __init__(self, parent_sequence, roi_indices):
self.parent = parent_sequence
self.indices = roi_indices # 例如 [0, 2, 4],非连续
def execute(self):
"""逐行安全遍历:每次重新定位,不假设连续性"""
for logical_idx in self.indices:
# 重新计算偏移,安全访问
action = self.parent[logical_idx]
print(f"ROI 执行 [{logical_idx}]: {action}")
# 执行后检查父序列是否仍然有效
if logical_idx >= len(self.parent):
raise RuntimeError("父规划在遍历过程中被修改!")
# 非连续 ROI:只执行第 0、2、4 步
parent = ["Step0", "Step1", "Step2", "Step3", "Step4", "Step5"]
roi = SafeROIIterator(parent, [0, 2, 4])
roi.execute()
1.2.4 数据指针直接访问规范
现在我们已经了解了步长计算与对齐,最后来到规划模块的最底层:当所有抽象层都被剥离,我们如何直接操作原始状态缓存?
1.2.4.1 data / uchar* 指针与具体类型的强制转换约定
在 OpenCV 中,Mat.data 是一个 uchar* 指针,你需要根据 elemSize 把它转换为 float*、int* 或 Vec3b*。在规划模块中,最底层的"环境状态缓存"同样是一块无类型字节流(uint8_t*),高层根据不同的状态语义将其强制转换为结构化类型。
这种"无类型字节流 + 显式转换"的模式非常强大,因为它允许同一个物理缓冲区在不同层级被解释为不同的状态类型(例如,底层是字节数组,中层是 float32 向量,顶层是 RobotState 结构体)。
但强制转换是危险的。必须遵守对齐约定:转换后的类型大小必须整除偏移量,否则会导致未定义行为(UB)。
代码示例:C++ 安全强制转换
#include <cstdint>
#include <iostream>
#include <stdexcept>
struct RobotState {
float joint_angles[6];
int32_t gripper_status;
};
class RawStateAccess {
public:
uint8_t* data; // 原始字节指针,类似 OpenCV 的 uchar*
size_t total_bytes;
RawStateAccess(uint8_t* d, size_t sz) : data(d), total_bytes(sz) {}
template<typename T>
T* ptr(size_t byte_offset = 0) {
if (byte_offset + sizeof(T) > total_bytes) {
throw std::out_of_range("指针越界");
}
if (byte_offset % alignof(T) != 0) {
throw std::runtime_error("对齐错误:偏移量必须是类型对齐大小的倍数");
}
return reinterpret_cast<T*>(data + byte_offset);
}
};
int main() {
alignas(64) uint8_t buffer[256]; // 64字节对齐的原始缓存
RawStateAccess raw(buffer, 256);
// 强制转换为 RobotState*,直接操作结构化状态
RobotState* state = raw.ptr<RobotState>(0);
state->joint_angles[0] = 1.57f;
state->gripper_status = 1;
std::cout << "Joint[0] = " << state->joint_angles[0] << "\\n";
return 0;
}
闭环结尾
好了,到这里我们已经从最上层的"为什么需要分解"一路走到了最底层的"如何强制转换原始状态指针"。让我们回顾一下这趟旅程:
单路径级联规划的本质,是用行优先拍平的策略把复杂的子目标树变成一条可顺序执行的磁带;用头部共享和引用计数来管理父子规划的内存依赖;用 clone / copyTo / 移动语义来精确控制状态的复制与传递;用 step 数组和对齐规则来加速底层遍历;最终落到无类型的原始指针转换。
这在实际训练和部署中意味着什么呢?
-
训练稳定性:在层级强化学习中,Strict Subgoal Execution(SSE)利用这种显式的生命周期管理,确保高层策略只在低层策略严格完成子目标后才获得正向奖励,避免了"虚假成功"导致的策略崩溃。
-
推理效率:移动语义和头部共享让长程任务规划(如 STEP Planner 中的机器人任务)能够在消费级硬件上实时运行,而不需要在每一步都复制庞大的环境状态。
-
调试安全性:
isContinuous()检查、弱引用保护和显式的深拷贝标记,让规划器在发生状态污染或悬空引用时能够快速定位问题,而不是默默产生错误的行为。
延伸阅读
如果你想深入这个领域,建议按以下顺序阅读:
- 入门:Shivashankar et al. (2012) 的 Hierarchical Goal Networks,理解目标分解与任务分解的统一形式化。
- 进阶:Shi et al. (2024) 的 STEP Planner,看 LLM 如何与子目标树结合解决长程具身规划。
- 理论:Strict Subgoal Execution (SSE),理解严格子目标执行在层级 RL 中的收敛性保证。
- 框架:Hierarchical Task Networks (HTN) 的综述,了解 HTN、HGN 与概率规划的交叉。
单路径级联不是唯一的规划范式,但它是最适合资源受限、需要严格可解释性的场景的"一根筋"美学——像一条笔直的栈道,每一步都踏在实地上,直到终点。
407



被折叠的 条评论
为什么被折叠?



