第271篇 行为树基础——序列/选择/装饰/并行节点
局部规划器选型聊完了,DWA、TEB、MPC三种方案的特点和适用场景应该清楚了。从这篇开始,我们进入行为树系列。
Nav2用行为树来编排整个导航流程,行为树在机器人任务管理领域已经成了标配方案。面试问到Nav2架构,行为树是绕不开的话题。
行为树(Behavior Tree,简称BT)听起来很高大上,但核心概念其实很简单。它就是用一棵树来组织机器人的行为逻辑,树的每个节点负责一小块任务,节点之间通过固定的组合方式协作。搞清楚四种基本节点类型,行为树就入门了。
一、为什么需要行为树
在行为树之前,机器人任务管理的主流方案是有限状态机(FSM)。状态机用状态和转移来组织逻辑——"如果在充电状态且电量低于20%,转移到充电行为"。
状态机的问题在于:状态一多就变成蜘蛛网。十几个状态之间的转移关系可能上百条,改一个状态可能影响一堆转移。调试和维护都很痛苦。
行为树换了一种完全不同的思路。它不用"状态+转移",而是用"节点组合"来组织逻辑。核心优势是模块化——每个节点只关心自己的任务,节点之间的交互规则是固定的(由节点类型决定)。加一个新行为只需要在树上挂一个新节点,不影响其他部分。
游戏行业最早大规模使用行为树(《光环2》之后几乎成了游戏AI的标配),后来机器人领域也跟进采用。
二、四种基本节点类型
行为树的节点分两大类:控制节点(compositor)和执行节点(leaf)。
控制节点负责"怎么组织子节点",有四种基本类型:
1. 序列节点(Sequence)
子节点从左到右依次执行。只要有一个子节点返回Failure,整个序列就返回Failure。只有所有子节点都返回Success,序列才返回Success。
类似于编程里的"逻辑与"(AND)。
Sequence ├── 检查电量充足? ├── 计算路径 └── 执行导航
"检查电量"失败(电量不足),后面的"计算路径"和"执行导航"都不执行。
2. 选择节点(Fallback/Selector)
子节点从左到右依次执行。只要有一个子节点返回Success,整个选择节点就返回Success。只有所有子节点都返回Failure,选择节点才返回Failure。
类似于"逻辑或"(OR)。
Fallback ├── 直接到达目标? ├── 重新规划路径 └── 执行恢复行为
"直接到达"成功就不需要后面的步骤。失败了就尝试"重新规划"。规划也失败了就执行"恢复行为"。
3. 装饰节点(Decorator)
只有一个子节点,对子节点的结果做修饰。常见的装饰器有:
- Inverter:反转结果。Success变Failure,Failure变Success。
- Repeat:重复执行子节点N次。
- RetryUntilSuccessful:反复重试直到成功(或达到最大次数)。
- Timeout:给子节点设一个超时时间。
RetryUntilSuccessful (max_tries=3) └── 请求机械臂抓取
抓取失败就重试,最多试3次。
4. 并行节点(Parallel)
同时执行所有子节点。返回结果的策略可以配置——比如"所有子节点都Success才Success"或者"有一个Success就Success"。
Parallel (success_threshold=2) ├── 导航到目标位置 ├── 持续检测障碍物 └── 上报当前位置
三个任务同时跑,只要有2个成功就算成功。
三、执行节点:叶子节点
控制节点下面的叶子节点才是真正干活的。叶子节点分两种:
Condition(条件节点)——检查某个条件是否满足。比如"电量是否充足""目标是否可见"。不满足返回Failure,满足返回Success。执行是瞬时的,不耗时。
Action(动作节点)——执行一个具体动作。比如"发送速度指令""调用规划服务""发出语音提示"。动作节点通常需要时间执行,返回Running直到完成。
Sequence
├── [Condition] 电量 > 20%
├── [Action] ComputePath → 调用全局规划器
└── [Action] FollowPath → 调用局部规划器
这棵小树的意思是:先检查电量,够了再算路径,算出来再执行。
四、行为树的执行机制
行为树从根节点开始,按深度优先的方式遍历。每个tick(通常10-20ms一次)执行一遍整棵树。
执行规则:
- 序列节点:从第一个子节点开始执行。如果子节点返回Running,下次tick还从这个子节点继续。
- 选择节点:从第一个子节点开始。如果返回Failure,尝试下一个。返回Running就暂停在这里。
- 装饰节点:先执行唯一的子节点,再根据装饰规则处理返回值。
- 叶子节点:条件节点瞬时返回Success/Failure,动作节点可能返回Running。
# 序列节点的tick逻辑简化
class Sequence(Node):
def tick(self):
for child in self.children:
status = child.tick()
if status == FAILURE:
return FAILURE
if status == RUNNING:
return RUNNING # 下次从这里继续
return SUCCESS
这种执行机制的关键特性是:行为树是"反应式"的。每个tick都从根节点重新评估,条件变了行为立刻跟着变。不像状态机需要显式的转移条件。
来看一个完整的例子。Nav2的导航行为树简化版:
<root main_tree_to_execute="MainTree">
<BehaviorTree ID="MainTree">
<Sequence name="NavigateWithReplanning">
<RetryUntilSuccessful num_attempts="3">
<Sequence>
<ComputePath goal="{goal}" path="{path}"/>
<FollowPath path="{path}"/>
</Sequence>
</RetryUntilSuccessful>
<ClearCostmap/>
</Sequence>
</BehaviorTree>
</root>
执行流程:先尝试"算路径+走路径"的组合。如果任一步骤失败,重试最多3次。3次都失败就清除代价地图(恢复行为),然后整棵树的tick结束,等下一个tick重新来。
注意黑板变量{goal}和{path}——ComputePath把算好的路径写到黑板的path字段,FollowPath从黑板读取路径。两个动作节点通过黑板通信,互不直接依赖。
五、面试高频追问
Q:行为树和状态机的核心区别是什么? A:状态机用"状态+转移"组织逻辑,行为树用"节点组合"组织逻辑。行为树的优势是模块化(加新行为不影响旧逻辑)和反应式执行(条件变了行为自动变)。
Q:行为树的tick频率一般设多少? A:Nav2里默认是10-20Hz(50-100ms一次)。频率太高浪费CPU,太低响应不够快。具体取决于任务需求——避障要求高的可以跑快一些。
Q:行为树怎么处理并发任务? A:用并行节点(Parallel)。但并行节点的子节点共享同一个执行线程,不是真正的多线程。如果需要真正的并行执行,得用多个行为树或者在动作节点内部实现异步。
Q:行为树的"黑板"(Blackboard)是什么? A:黑板是行为树的全局数据共享机制。节点之间通过黑板读写数据。比如规划器把算好的路径写到黑板上,控制器从黑板上读路径。黑板避免了节点之间的直接耦合。Nav2用的是BehaviorTree.CPP库的黑板实现,支持类型检查和端口映射。
行为树的四种基本节点类型就是这些基础。看起来简单,但组合起来能表达非常复杂的逻辑。下一篇我们来聊行为树的设计模式——怎么把这些基本节点拼成实用的机器人任务管理方案。
行为树系列开篇,把序列、选择、装饰、并行四种基本节点讲清楚了。理解了这些基础,后面的设计模式和实战应用就好懂了。行为树是Nav2的核心调度机制,面试必问。
上一篇:第270篇 局部规划器选型
下一篇聊行为树的设计模式。
如果这篇文章对你有帮助,欢迎点赞支持一下,你的鼓励是我持续更新的动力!
DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。
更多推荐

所有评论(0)