状态机与行为树:从有限状态机到现代 AI 决策架构
当我们让一台机器人、一架无人机、一个游戏 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 q0∈Q 是初始状态;
- F ⊆ Q F \subseteq Q F⊆Q 是终止状态集合(在控制系统中常常为空,因为智能体持续运行)。
转移函数 δ \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 ∣Q∣∼i=1∏kni
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(取反):把
SUCCESS变FAILURE,反之亦然; - 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 的主流选择。 - 二者并非对立:理解它们各自的抽象边界,在合适的层次选择合适的工具(甚至混合使用),才是构建可靠自主系统的关键。
从巡逻无人机到多机协同集群,决策架构的选型直接决定了系统能否优雅地扩展。希望这篇文章能帮你在下一次设计智能体时,做出更从容的选择。
如果这篇文章对你有帮助,欢迎点赞、收藏、关注。后续会继续更新机器人自主系统、多智能体协同相关的深度技术内容。
DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。
更多推荐

所有评论(0)