当我们让一台机器人、一架无人机、一个游戏 NPC "自己做决定"时,背后一定有一套决策架构在支撑。本文从最经典的有限状态机(FSM)讲起,一路推进到今天机器人和游戏 AI 领域的主流方案——行为树(Behavior Tree)。全文包含形式化定义、完整可运行的 Python 实现,以及一个可以实时观察两者运行的交互式可视化。


一、为什么智能体需要"决策架构"

想象一架巡逻无人机。它需要:平时按航线巡飞;发现目标就靠近侦察;电量低于阈值就返航;返航途中如果又发现更紧急的目标,还得能重新权衡。

这些行为不是一段线性的脚本,而是一张**“什么情况下该做什么"的决策网络**。如果我们用一堆 if-else 硬写,很快就会变成没人敢改的"意大利面条代码”:一个新条件加进来,要在十几个分支里同时修改,漏掉一处就是一个隐蔽的 bug。

决策架构(Decision Architecture)要解决的核心问题是:

  • 可读:决策逻辑要能被人一眼看懂,最好能画成图。
  • 可扩展:加一个新行为,不应该牵动整个系统。
  • 可复用:一段"靠近目标"的逻辑,应该能在多个场景里重用。
  • 可响应:环境突变时,智能体要能立刻切换到更合适的行为。

有限状态机和行为树,就是工业界给出的两代主流答案。


二、有限状态机(Finite State Machine)

2.1 直观理解

有限状态机的核心思想极其朴素:任何时刻,系统只处于有限个"状态"中的一个;在满足特定条件时,从一个状态"转移"到另一个状态。

还是那架巡逻无人机,用状态机描述就是:

        发现目标                电量 < 20%
巡逻 ─────────────► 侦察          巡逻 ───────────► 返航
巡逻 ◄───────────── 侦察          侦察 ───────────► 返航
        目标消失

四个要素跃然纸上:

  • 状态(State):巡逻、侦察、返航……
  • 事件/条件(Event / Condition):发现目标、电量过低……
  • 转移(Transition):从"巡逻"到"侦察"这条带箭头的线。
  • 动作(Action):进入某个状态时要执行的行为。

2.2 形式化定义

有限状态机可以用一个五元组严格定义:

M = ( Q ,    Σ ,    δ ,    q 0 ,    F ) M = (Q, \; \Sigma, \; \delta, \; q_0, \; F) M=(Q,Σ,δ,q0,F)

其中:

  • Q Q Q有限状态集合,例如 Q = { 巡逻 , 侦察 , 返航 } Q = \{\text{巡逻}, \text{侦察}, \text{返航}\} Q={巡逻,侦察,返航};
  • Σ \Sigma Σ输入字母表(触发转移的事件集合),例如 Σ = { 发现目标 , 目标消失 , 低电量 } \Sigma = \{\text{发现目标}, \text{目标消失}, \text{低电量}\} Σ={发现目标,目标消失,低电量};
  • δ : Q × Σ → Q \delta: Q \times \Sigma \to Q δ:Q×ΣQ状态转移函数,它接收"当前状态 + 输入事件",返回"下一个状态";
  • q 0 ∈ Q q_0 \in Q q0Q初始状态;
  • F ⊆ Q F \subseteq Q FQ终止状态集合(在控制系统中常常为空,因为智能体持续运行)。

转移函数 δ \delta δ 是整台机器的灵魂。例如:

δ ( 巡逻 , 发现目标 ) = 侦察 \delta(\text{巡逻}, \text{发现目标}) = \text{侦察} δ(巡逻,发现目标)=侦察

δ ( 侦察 , 低电量 ) = 返航 \delta(\text{侦察}, \text{低电量}) = \text{返航} δ(侦察,低电量)=返航

在数学上,如果 δ \delta δ 对每个 ( q , σ ) (q, \sigma) (q,σ) 都给出唯一确定的下一状态,称为确定性有限状态机(DFA);若一个输入可能导向多个状态,则是非确定性有限状态机(NFA)。工程实践中我们几乎总是使用 DFA——机器人的行为必须是可预测、可复现的。

2.3 Moore 机与 Mealy 机

按"输出何时产生"划分,状态机有两种经典模型:

类型 输出决定于 数学形式 直观含义
Moore 机 当前状态 output = λ ( q ) \text{output} = \lambda(q) output=λ(q) 输出绑定在状态上,“进入侦察就开启摄像头”
Mealy 机 当前状态 + 输入 output = λ ( q , σ ) \text{output} = \lambda(q, \sigma) output=λ(q,σ) 输出绑定在转移上,“从巡逻切到侦察的瞬间播报一次”

Moore 机的输出更稳定、更容易理解,机器人控制中用得更多;Mealy 机反应更快(在转移边上就能产生输出),状态数往往更少。二者在表达能力上等价,可以相互转换。

2.4 用代码实现:巡逻无人机的状态机

下面是一个清晰、可运行的 FSM 框架,并用它实现巡逻无人机的决策逻辑。这里采用状态模式(State Pattern)——每个状态是一个独立的类,把"状态爆炸"造成的 switch-case 拆散成一个个可维护的小模块。

from abc import ABC, abstractmethod
from enum import Enum


class Event(Enum):
    """触发状态转移的事件(输入字母表 Σ)"""
    FOUND_TARGET = "发现目标"
    LOST_TARGET = "目标消失"
    LOW_BATTERY = "低电量"
    ARRIVED_HOME = "到家"


class State(ABC):
    """状态基类:每个状态封装自己的进入动作与转移规则"""

    def __init__(self, drone):
        self.drone = drone

    @abstractmethod
    def on_enter(self):
        """进入该状态时执行的动作(Moore 机的输出)"""
        ...

    @abstractmethod
    def handle(self, event: Event):
        """转移函数 δ 在本状态下的局部实现:
        返回下一个状态实例,或 None 表示保持不变"""
        ...


class Patrol(State):
    def on_enter(self):
        print("[巡逻] 按预设航线巡飞,传感器全开扫描")

    def handle(self, event):
        if event == Event.FOUND_TARGET:
            return Scout(self.drone)
        if event == Event.LOW_BATTERY:
            return ReturnHome(self.drone)
        return None


class Scout(State):
    def on_enter(self):
        print("[侦察] 靠近目标,开启高清摄像头持续跟踪")

    def handle(self, event):
        if event == Event.LOST_TARGET:
            return Patrol(self.drone)
        if event == Event.LOW_BATTERY:      # 低电量的优先级高于继续侦察
            return ReturnHome(self.drone)
        return None


class ReturnHome(State):
    def on_enter(self):
        print("[返航] 规划最短路径返回起降点")

    def handle(self, event):
        if event == Event.ARRIVED_HOME:
            return Patrol(self.drone)
        return None                          # 返航途中忽略其它事件


class Drone:
    """状态机上下文:持有当前状态,负责派发事件"""

    def __init__(self):
        self.state = Patrol(self)
        self.state.on_enter()

    def dispatch(self, event: Event):
        print(f"  ↳ 收到事件: {event.value}")
        next_state = self.state.handle(event)
        if next_state is not None:
            self.state = next_state
            self.state.on_enter()
        else:
            print(f"    (当前状态不响应该事件,保持不变)")


if __name__ == "__main__":
    drone = Drone()
    for e in [Event.FOUND_TARGET, Event.LOW_BATTERY,
              Event.ARRIVED_HOME, Event.LOST_TARGET]:
        drone.dispatch(e)

运行输出:

[巡逻] 按预设航线巡飞,传感器全开扫描
  ↳ 收到事件: 发现目标
[侦察] 靠近目标,开启高清摄像头持续跟踪
  ↳ 收到事件: 低电量
[返航] 规划最短路径返回起降点
  ↳ 收到事件: 到家
[巡逻] 按预设航线巡飞,传感器全开扫描
  ↳ 收到事件: 目标消失
    (当前状态不响应该事件,保持不变)

注意最后一条:在"巡逻"状态下收到"目标消失"是无意义的,状态机自然地忽略它。这正是状态机的优雅之处——每个状态只关心与自己相关的转移

2.5 分层状态机(HSM):缓解状态爆炸

普通 FSM 有一个致命弱点。假设无人机除了上述状态,还有一个正交(orthogonal)的维度:“是否处于夜间模式”。如果夜间和白天的每个状态行为都略有不同,状态数会从 n n n 膨胀到 2 n 2n 2n;再加一个正交条件,就是 4 n 4n 4n……这就是状态爆炸(State Explosion):

∣ Q ∣ ∼ ∏ i = 1 k n i |Q| \sim \prod_{i=1}^{k} n_i Qi=1kni

k k k 个相互独立的维度,状态总数是各维度取值数的乘积

分层状态机(Hierarchical State Machine, HSM) 通过引入"父状态"来缓解这个问题:多个子状态可以共享父状态的转移规则。例如把"巡逻"“侦察"都放进一个父状态"作业中”,而"低电量→返航"这条转移只需在父状态上定义一次,所有子状态自动继承:

┌─────────── 作业中(父状态)───────────┐
│                                        │   低电量
│   巡逻 ⇄ 侦察                          │ ─────────► 返航
│                                        │
└────────────────────────────────────────┘

HSM 大大减少了重复的转移边,但当行为逻辑进一步复杂化时,它依然会走向臃肿。于是,人们发明了行为树。


三、行为树(Behavior Tree)

3.1 起源

行为树最早在 2000 年代初的游戏工业中成型,《Halo 2》(2004)是最著名的早期案例,随后成为游戏 AI 的事实标准。近十年,它被大量引入机器人领域——ROS 生态中的 BehaviorTree.CPP 已成为移动机器人、机械臂任务编排的主流框架。

行为树之所以能取代状态机的很多场景,靠的是一个看似微小、实则革命性的设计。

3.2 核心机制:tick 与三种返回状态

行为树是一棵树。系统以固定频率(比如每帧、每 100ms)从根节点发出一个信号,称为 tick。tick 沿着树向下传播,每个被 tick 到的节点执行自己的逻辑,并向父节点返回三种状态之一:

返回状态 含义
SUCCESS 该节点的任务成功完成
FAILURE 该节点的任务失败
RUNNING 任务仍在进行中,尚未结束

RUNNING 状态是行为树相对状态机的关键创新。 传统状态机在一次决策中必须立刻给出结果,而行为树允许一个动作"横跨多个 tick 持续执行"——例如"飞往目标点"这个动作会在到达之前一直返回 RUNNING,让整棵树在下一个 tick 时从上到下重新评估一遍环境。这带来了天然的响应性(reactivity):每个 tick 都会重新检查条件,一旦更高优先级的情况出现,立刻就能切换。

3.3 节点类型

行为树的节点分为三大类。

(1)组合节点(Composite):控制流

组合节点有多个子节点,决定子节点的执行顺序和逻辑关系。

Sequence(顺序节点,逻辑与 AND):从左到右依次 tick 子节点。

  • 只要有一个子节点返回 FAILURE,立即返回 FAILURE;
  • 遇到 RUNNING,返回 RUNNING(下个 tick 从这里继续);
  • 全部 SUCCESS,才返回 SUCCESS

语义:“做完 A,再做 B,再做 C,中途任何一步失败就整体失败。”

Selector / Fallback(选择节点,逻辑或 OR):从左到右依次 tick 子节点。

  • 只要有一个子节点返回 SUCCESS,立即返回 SUCCESS;
  • 遇到 RUNNING,返回 RUNNING;
  • 全部 FAILURE,才返回 FAILURE

语义:“试试 A,不行就试 B,再不行试 C,只要有一个成功就算成功。”

Parallel(并行节点):同时 tick 所有子节点,按预设的成功阈值(如"M/N 个成功即成功")汇总结果。

(2)装饰节点(Decorator):修饰单个子节点

装饰节点只有一个子节点,用于改写它的返回值或控制其执行。常见的有:

  • Inverter(取反):把 SUCCESSFAILURE,反之亦然;
  • Repeater(重复):让子节点重复执行 N 次;
  • Succeeder / Failer:无论子节点结果如何都返回固定值;
  • Cooldown / Timeout:加入时间约束。
(3)叶节点(Leaf):真正干活的地方
  • Action(动作节点):执行具体行为,如"飞往坐标"“拍照”“返航”,返回 SUCCESS/FAILURE/RUNNING;
  • Condition(条件节点):检查某个条件,如"电量是否充足"“是否发现目标”,返回 SUCCESS(真)或 FAILURE(假)。

3.4 黑板(Blackboard):数据共享

行为树的节点之间需要共享数据(目标坐标、当前电量……)。这通过一个称为黑板(Blackboard) 的键值存储实现:任何节点都能读写黑板,从而解耦了"决策结构"与"数据流"。这类似你在多机协同系统里用的共享状态思路——决策逻辑只管读写黑板上的字段,不关心数据从哪来。

3.5 用巡逻无人机重画一遍

把 2.4 节那台无人机用行为树重新描述,它长这样:

                    [Selector 根节点]
                          │
        ┌─────────────────┼──────────────────┐
        │                 │                  │
  [Sequence 返航]    [Sequence 侦察]     [Action 巡逻]
        │                 │
   ┌────┴────┐       ┌────┴─────┐
[条件:      [动作:  [条件:      [动作:
 电量低?]    返航]   发现目标?]   靠近侦察]

根节点是一个 Selector,它按优先级从左到右尝试:先看"要不要返航"(最高优先级),不满足再看"要不要侦察",都不满足就默认"巡逻"。由于每个 tick 都会从根重新评估,低电量这个高优先级条件一旦满足,无论无人机当前在做什么,下个 tick 就会自动切到返航分支——响应性天然内置,完全不需要像状态机那样为每个状态手动写一条"→返航"的转移边。

3.6 完整实现:一个可运行的迷你行为树框架

下面是一套自包含的行为树框架(约 120 行),包含组合节点、装饰节点、叶节点和黑板,并用它驱动巡逻无人机。

from enum import Enum
from abc import ABC, abstractmethod


class Status(Enum):
    SUCCESS = "SUCCESS"
    FAILURE = "FAILURE"
    RUNNING = "RUNNING"


class Blackboard(dict):
    """黑板:全局共享的键值存储,所有节点可读写"""
    pass


# ─────────────────────── 节点基类 ───────────────────────
class Node(ABC):
    def __init__(self, name=""):
        self.name = name

    @abstractmethod
    def tick(self, bb: Blackboard) -> Status:
        ...


# ─────────────────────── 组合节点 ───────────────────────
class Sequence(Node):
    """顺序节点(AND):全部成功才成功,一个失败即失败"""
    def __init__(self, name, children):
        super().__init__(name)
        self.children = children

    def tick(self, bb):
        for child in self.children:
            status = child.tick(bb)
            if status != Status.SUCCESS:      # FAILURE 或 RUNNING 直接冒泡
                return status
        return Status.SUCCESS


class Selector(Node):
    """选择节点(OR):一个成功即成功,全部失败才失败"""
    def __init__(self, name, children):
        super().__init__(name)
        self.children = children

    def tick(self, bb):
        for child in self.children:
            status = child.tick(bb)
            if status != Status.FAILURE:      # SUCCESS 或 RUNNING 直接冒泡
                return status
        return Status.FAILURE


# ─────────────────────── 装饰节点 ───────────────────────
class Inverter(Node):
    """取反:SUCCESS <-> FAILURE"""
    def __init__(self, name, child):
        super().__init__(name)
        self.child = child

    def tick(self, bb):
        status = self.child.tick(bb)
        if status == Status.SUCCESS:
            return Status.FAILURE
        if status == Status.FAILURE:
            return Status.SUCCESS
        return status                         # RUNNING 原样返回


# ─────────────────────── 叶节点 ───────────────────────
class Condition(Node):
    """条件节点:检查黑板上的谓词,返回 SUCCESS / FAILURE"""
    def __init__(self, name, predicate):
        super().__init__(name)
        self.predicate = predicate

    def tick(self, bb):
        return Status.SUCCESS if self.predicate(bb) else Status.FAILURE


class Action(Node):
    """动作节点:执行一个可能横跨多个 tick 的行为"""
    def __init__(self, name, fn):
        super().__init__(name)
        self.fn = fn

    def tick(self, bb):
        return self.fn(bb)


# ═════════════════════ 构建无人机行为树 ═════════════════════
def fly_home(bb):
    """返航动作:每 tick 让 distance_home 递减,到 0 才 SUCCESS"""
    bb["distance_home"] = max(0, bb["distance_home"] - 30)
    if bb["distance_home"] == 0:
        print("    ✓ 已抵达起降点,电量回充")
        bb["battery"] = 100
        return Status.SUCCESS
    print(f"    → 返航中,距家 {bb['distance_home']} m")
    return Status.RUNNING


def approach_target(bb):
    print("    → 靠近目标并开启摄像头侦察")
    return Status.RUNNING            # 侦察是持续性动作,一直 RUNNING


def patrol(bb):
    print("    → 按航线巡逻扫描")
    return Status.RUNNING


# 树的结构 = 决策逻辑(优先级从左到右)
tree = Selector("root", [
    Sequence("返航分支", [
        Condition("电量低?", lambda bb: bb["battery"] < 20),
        Action("返航", fly_home),
    ]),
    Sequence("侦察分支", [
        Condition("发现目标?", lambda bb: bb["target_found"]),
        Action("侦察", approach_target),
    ]),
    Action("默认巡逻", patrol),
])


if __name__ == "__main__":
    bb = Blackboard(battery=80, target_found=False, distance_home=90)
    # 模拟一段时间线:每个 tick 环境略有变化
    timeline = [
        {},                                   # tick1: 正常巡逻
        {"target_found": True},               # tick2: 发现目标 → 侦察
        {"battery": 15},                      # tick3: 电量骤降 → 立即返航(即使在侦察)
        {}, {}, {},                           # tick4-6: 持续返航直到到家
    ]
    for i, change in enumerate(timeline, 1):
        bb.update(change)
        print(f"[tick {i}] battery={bb['battery']}, target={bb['target_found']}")
        result = tree.tick(bb)
        print(f"    根节点返回: {result.value}\n")

运行输出(节选):

[tick 1] battery=80, target=False
    → 按航线巡逻扫描
    根节点返回: RUNNING

[tick 2] battery=80, target=True
    → 靠近目标并开启摄像头侦察
    根节点返回: RUNNING

[tick 3] battery=15, target=True
    → 返航中,距家 60 m
    根节点返回: RUNNING

请重点看 tick 3:上一刻无人机还在侦察,这一刻电量掉到 15%,由于根 Selector 每次都从最高优先级的"返航分支"重新评估,它立即切到了返航——我们没有写任何"从侦察到返航"的显式转移。行为的切换是决策结构自然涌现的结果,而不是手工连线的产物。这就是行为树相对状态机在可维护性上的根本优势。


四、FSM 与 BT:该用哪一个?

两者不是取代关系,而是各有适用场景。

维度 有限状态机(FSM) 行为树(BT)
核心抽象 状态 + 转移 树 + tick
决策粒度 一次转移一步到位 每 tick 从根重评估
响应性 需为每个状态手写转移边 天然内置(优先级重评估)
可扩展性 加状态 → 转移边呈平方增长 加节点 → 局部插入,不影响其它分支
状态爆炸 严重,需 HSM 缓解 基本免疫
持续性动作 需额外机制表达 RUNNING 状态原生支持
可读性 简单逻辑非常直观 复杂逻辑更清晰,可模块化
模块复用 状态耦合度高,复用难 子树可整体复用
学习/调试成本 略高(需理解 tick 语义)

经验法则:

  • 逻辑简单、状态少(3~5 个)、转移清晰 → 用 FSM,简单直接不过度设计。典型如:充电桩状态、通信协议握手、UI 界面流转。
  • 行为复杂、需要频繁响应环境变化、要求高度模块化和复用 → 用 BT。典型如:游戏 NPC AI、移动机器人任务编排、无人机集群的单体决策。
  • 实践中二者常混合使用:用 BT 组织顶层任务逻辑,而在某个 Action 节点内部用一个小 FSM 管理底层的时序状态。

五、交互式可视化

为了直观感受两者的区别,文末附带了一个交互式演示。你可以:

  • 手动切换环境条件(发现目标 / 电量高低),同时观察同一个决策问题在 FSM 和 BT 两种架构下的运行状态;
  • 在 BT 视图中,实时看到 tick 信号如何沿树传播、哪个分支被激活、每个节点返回什么状态;
  • 感受 RUNNING 状态与"每 tick 重评估"带来的响应性。

(交互式组件已作为独立 widget 集成到本文,发布到 CSDN 时嵌入在此处。)


六、总结

  • 有限状态机用"状态 + 转移"这一朴素而强大的抽象,以五元组 M = ( Q , Σ , δ , q 0 , F ) M=(Q,\Sigma,\delta,q_0,F) M=(Q,Σ,δ,q0,F) 严格描述,是一切决策架构的基石。它在简单场景下无可替代,但面对正交维度会遭遇状态爆炸,分层状态机(HSM)只能部分缓解。
  • 行为树用"树 + tick"重新组织决策,其核心创新——RUNNING 状态与每 tick 从根重评估——带来了天然的响应性、模块化和可扩展性,成为现代游戏与机器人 AI 的主流选择。
  • 二者并非对立:理解它们各自的抽象边界,在合适的层次选择合适的工具(甚至混合使用),才是构建可靠自主系统的关键。

从巡逻无人机到多机协同集群,决策架构的选型直接决定了系统能否优雅地扩展。希望这篇文章能帮你在下一次设计智能体时,做出更从容的选择。


如果这篇文章对你有帮助,欢迎点赞、收藏、关注。后续会继续更新机器人自主系统、多智能体协同相关的深度技术内容。

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐