BehaviorTree.CPP 经常被介绍成“比一堆 if-else 更适合机器人任务”。这句话太容易让人误解:如果问题只是判断一次就调用一次函数,行为树不会凭空减少代码,反而会增加 XML、节点注册、tick 循环和 Action 胶水代码。
先把当前实现的规模说清楚。workflows.xml 整个定义大约 8 行,只有两棵树:single_task 是 RuntimeReady -> ExecuteTask,ready_then_task 是 RetryReady -> ExecuteTask。两棵树各约两个节点,最长流程两步;每个任务当前只下发一条设备命令,也就是一个 ExecuteDeviceCommand goal。设备桥可以围绕 ACK 做重试,但那不是任务层再编排出第二条应用命令。
先和 if-else 比一遍
同样的 single_task,直接写成 C++ 可能只是:
1 | if (ready()) { |
ready_then_task 也可以在一个循环里检查两次就结束。就当前业务逻辑而言,if-else 更短、更直接,BT 反而更长。不能把“用了 BT”写成“代码更少”或“已经更实时”。这里真正要比较的不是行数,而是任务在取消、观察和变更控制上的契约是否被明确写出来。
1. 把取消变成节点契约
任务编排器在 goal 被取消、超过 deadline 或进程停止时调用 tree.haltTree()。这一步本身只是沿树传播 halt,真正有价值的是活动 Action 节点实现了 onHalted():它设置 halt_requested,等待子 goal 的响应或结果,必要时发起 cancel,并在取消没有得到确认时记录超时。
这比在一段普通 if-else 里“顺手检查一个取消标志”更容易审查。审查者可以沿着 haltTree -> onHalted -> async_cancel_goal -> terminal result 检查出口:子任务还没拿到 handle 怎么办,取消请求在飞行中怎么办,设备结果迟到怎么办,超时后上层返回什么。当前代码还把 CANCELED、SAFE_STOP 等终态区分开了。
但这不是 BT 自动提供的安全性。RetryReady 的 onHalted() 为空,是因为它没有外部副作用;将来新增一个会发送命令的节点,若忘了实现清理契约,树形结构仍然漂亮,取消仍然可能不完整。BT 的收益是让责任点有名字、有生命周期钩子,而不是替开发者完成停止动作。
2. 把活动状态外化
普通函数可以知道自己执行到哪一行,却不一定把这个状态交给调用者。当前 WorkflowState::mark() 保存 active_node 和 progress,ExecuteTask 的反馈也会更新它;编排器每次 tick 后通过 ExecuteWorkflow feedback 发布这两个字段。外部客户端因此能看到当前是 RuntimeReady 还是 ExecuteTask,以及一个任务进度值,而不必猜测线程正在等待什么。
这解决的是可观察性和审查入口:日志、Action 客户端和历史记录可以把“请求了什么”和“当时走到哪一步”联系起来。边界也很具体:当前代码主要标记 RuntimeReady 和 ExecuteTask,RetryReady 没有单独更新 active_node,所以反馈不是完整的树状态镜像。它也不证明设备已经运动,更不证明实时确定性。当前实现有固定的 tick 间隔,但项目没有给出实时确定性测量,不能把这个反馈接口包装成实时保证。
3. 把策略和控制流分开
XML 描述的是固定控制流:哪些节点先后执行、哪些节点带重试参数。C++ 则注册 RuntimeReady、RetryReady、ExecuteTask,并在 goal 入口只接受 single_task 和 ready_then_task。网关也会先检查 workflow allowlist,再提交 ExecuteWorkflow。因此规则或模型适配器只能申请一个已有工作流,不能把一段新 XML 或一组临时设备动作直接变成执行路径。
这就是 if-else 里容易混在一起的边界:策略决定“允许哪种工作流”,树决定“这个工作流怎样走”,Action/设备桥决定“一个节点怎样和设备交互”。代码与 README 都把可控输入、allowlist 和固定 BehaviorTree 放在这条边界上。控制流仍可像配置一样被审查,但执行能力仍由 C++ 节点注册表约束。
代价和当前缺陷
审查性不是免费的。当前每个 goal 都会重新创建 BehaviorTreeFactory,重新注册节点,并调用 registerBehaviorTreeFromFile 重读 XML,然后再创建对应树。这样做让每次执行都暴露在文件读取和 XML 解析错误下,也增加了每个 goal 的准备工作;它是当前工程实现的缺陷,不是 BT 的理论优势。更合理的工程方向是进程启动时校验或缓存固定定义,再按 goal 创建轻量树实例,但这篇文章不把尚未实现的优化算作现状。
更重要的是,两步流程的收益有明确上限:如果没有长时间运行的子 Action、取消语义、对外反馈或需要由非 C++ 人员审查的工作流,BT 只会增加抽象层。一次同步调用、一个简单条件、没有外部状态的工具函数,直接 if-else 更合适;高频控制环也不该因为“看起来像树”就交给这个任务编排层。只有当步骤会等待、取消、重试、对外报告状态,或者希望把固定控制流与输入策略隔离时,BT 才开始支付得起它的复杂度。
结论
对当前项目,BehaviorTree.CPP 解决的不是“两步任务写不出 if-else”,而是把三类边界显式化:活动节点如何被 halt、状态如何反馈给外部、允许的策略如何与固定控制流分离。它让审查对象从散落的线程标志和回调,变成有名称的节点与契约;代价是更多代码、更多测试和当前每 goal 重读 XML 的工程债务。
所以结论应该保持克制:这两棵树还不足以证明 BT 比 if-else 更快、更可靠或更实时。它们只说明,在一个准备向可取消、可观察、受 allowlist 约束的工作流扩展的系统里,BT 提供了一个值得审查的结构;至于真实硬件、实时确定性和更长流程,当前 README 没有提供本文可以代替的 benchmark、硬件或模型数据。
参考资料
证据边界:本文只讨论固定工作流的可审查性结构,不涉及真实硬件、实时确定性或更长流程。两棵约两节点的树不足以证明 BT 比 if-else 更快、更可靠;文中的代码规模和取消契约来自当前实现,性能与硬件结论需要单独的 benchmark 和现场数据。