4. 决策树与行为树(BT)

好,咱们今天聊聊行为树。说实话,我刚开始做自动驾驶决策时,用的还是传统的有限状态机(FSM)。那时候项目赶得紧,状态一多,代码就乱成一锅粥。后来接触到行为树,才感觉打开了新世界的大门。

行为树这东西,说白了就是一棵树。树的每个节点代表一个行为或一个判断。机器人从根节点开始,按照树的逻辑往下走,最终执行某个动作。听起来简单,但用起来是真香。

4.1 行为树节点类型

行为树的节点类型不多,但每个都有它的脾气。我一个个说。

4.1.1 Sequence(序列节点)

Sequence 节点,顾名思义,就是按顺序执行子节点。它从左到右依次执行,如果某个子节点返回 Success,就继续下一个;如果返回 Failure,整个 Sequence 就失败,不再继续。

我在项目中遇到过这样一个场景:车辆要完成一个“靠边停车”的动作。这个动作需要先打右转向灯,然后减速,再向右打方向盘,最后回正。这四个步骤必须按顺序来,缺一不可。用 Sequence 节点就特别合适。

关键点:Sequence 节点要求所有子节点都成功,才算成功。任何一个失败,整个序列就中断。

4.1.2 Selector(选择节点)

Selector 节点,也叫 Fallback 节点。它也是从左到右执行子节点,但逻辑正好相反:只要有一个子节点返回 Success,整个 Selector 就成功,不再继续执行后面的节点。

你想想看,这像不像一个“备选方案”?比如车辆要避障,它先尝试向左变道。如果左边有车,那就尝试向右变道。如果右边也有车,那就减速。Selector 节点就是干这个的——从多个方案中选一个能用的。

我的经验:我曾经在写避障逻辑时,把 Selector 和 Sequence 搞混了,结果车辆总是同时执行多个动作,差点撞上护栏。后来我养成了一个习惯:在写代码前,先在纸上画出行为树的结构,标清楚每个节点的类型。

4.1.3 Decorator(装饰节点)

Decorator 节点,说白了就是给子节点加一层“包装”。它可以改变子节点的行为,比如限制执行次数、设置超时时间、反转结果等。

举个例子,车辆在等待红绿灯时,不能无限等下去。如果红灯超过 30 秒,就应该考虑绕行。这时候就可以用 Decorator 节点,给“等待”这个行为加一个超时限制。

常用的 Decorator 有:

  • Inverter:反转子节点的结果(Success 变 Failure,Failure 变 Success)
  • Repeater:重复执行子节点 N 次
  • Timeout:给子节点设置超时时间
  • Force Success:不管子节点返回什么,都返回 Success

注意:Decorator 节点虽然好用,但别滥用。我见过有人把整个行为树都包在 Decorator 里,结果调试时根本看不出问题出在哪。Decorator 应该用在刀刃上,而不是当万能胶。

4.1.4 Condition(条件节点)

Condition 节点,就是做判断的。它不执行任何动作,只检查某个条件是否成立。比如“前方是否有障碍物”、“当前速度是否超过限速”、“电池电量是否充足”等。

Condition 节点通常放在 Sequence 或 Selector 的最前面,用来决定是否继续执行后续节点。嗯,这里要注意:Condition 节点不应该有副作用,它只是“检查”,不应该“改变”任何状态。

4.2 行为树与 FSM 的对比

说到 FSM(有限状态机),很多老工程师对它感情复杂。FSM 简单直观,但状态一多,就变成了“面条式代码”。我当年维护过一个有 50 多个状态的 FSM,每次改一个状态,都要担心会不会影响其他状态。

行为树和 FSM 的区别,我用一个表格来说明:

对比维度 FSM(有限状态机) BT(行为树)
状态管理 状态之间需要显式定义转移 节点之间通过树结构隐式管理
可扩展性 状态增多时,转移关系呈指数级增长 新增节点不影响已有节点
可复用性 状态逻辑通常耦合在一起 节点可以独立复用
调试难度 需要追踪状态转移路径 可以通过树结构直观定位问题
适用场景 状态少、逻辑简单的系统 状态多、逻辑复杂的系统

我个人习惯是:如果系统状态不超过 5 个,用 FSM 就够了。但如果状态超过 10 个,或者需要频繁增删状态,我会毫不犹豫地选择行为树。

4.3 使用 PyTrees 库构建一个避障行为树

PyTrees 是一个轻量级的 Python 行为树库。它简单易用,特别适合做原型验证。下面我带你手写一个避障行为树。

先安装 PyTrees:

pip install py_trees

然后,我们定义几个行为节点:

import py_trees

class CheckObstacle(py_trees.behaviour.Behaviour):
    """检查前方是否有障碍物"""
    def __init__(self, name="CheckObstacle"):
        super(CheckObstacle, self).__init__(name)
        self.has_obstacle = False

    def update(self):
        # 这里模拟传感器检测
        # 实际项目中会调用激光雷达或视觉模块
        if self.has_obstacle:
            return py_trees.common.Status.FAILURE
        else:
            return py_trees.common.Status.SUCCESS

class MoveForward(py_trees.behaviour.Behaviour):
    """直行"""
    def update(self):
        print("正在直行...")
        return py_trees.common.Status.SUCCESS

class TurnLeft(py_trees.behaviour.Behaviour):
    """左转避障"""
    def update(self):
        print("左转避障...")
        return py_trees.common.Status.SUCCESS

class TurnRight(py_trees.behaviour.Behaviour):
    """右转避障"""
    def update(self):
        print("右转避障...")
        return py_trees.common.Status.SUCCESS

接下来,构建行为树:

def create_obstacle_avoidance_tree():
    # 根节点:Selector,优先选择左转,其次右转,最后直行
    root = py_trees.composites.Selector("避障策略")

    # 左转分支:先检查障碍物,再左转
    left_turn = py_trees.composites.Sequence("左转避障")
    check_left = CheckObstacle("检查左侧")
    turn_left = TurnLeft("左转")
    left_turn.add_children([check_left, turn_left])

    # 右转分支:先检查障碍物,再右转
    right_turn = py_trees.composites.Sequence("右转避障")
    check_right = CheckObstacle("检查右侧")
    turn_right = TurnRight("右转")
    right_turn.add_children([check_right, turn_right])

    # 直行分支:直接直行
    move_forward = MoveForward("直行")

    # 组装树
    root.add_children([left_turn, right_turn, move_forward])

    return root

最后,运行行为树:

if __name__ == "__main__":
    tree = create_obstacle_avoidance_tree()
    for i in range(10):
        tree.tick_once()
        print(f"第 {i+1} 次执行完成")
        # 模拟障碍物变化
        if i == 3:
            tree.children[0].children[0].has_obstacle = True

避坑指南:我曾经在写行为树时,忘记给 Condition 节点设置初始状态,结果第一次执行时,Condition 节点返回了 Undefined,导致整个树卡住了。后来我养成了习惯:所有 Condition 节点在初始化时,都要显式设置一个默认状态。

这个例子虽然简单,但已经涵盖了行为树的核心思想:用树结构组织行为,用节点类型控制执行逻辑。实际项目中,你可能会需要更复杂的节点,比如并行节点、随机节点等,但万变不离其宗。

好了,行为树的内容就讲到这里。下一章我们会聊聊更高级的决策方法——蒙特卡洛树搜索(MCTS)。那个东西更有意思,咱们到时候见。