Behavior tree cn
本文档系统阐述 Jianmu 行为树执行内核的架构设计:以 py_trees 为同步骨架、通过一套异步扩展层实现 I/O 密集节点的非阻塞执行,并在此基础上构建具备依赖注入、端口绑定与状态通信能力的节点模型。本文将覆盖从 py_trees 原语重新导出、异步行为基类的任务生命周期、依赖注入机制、自定义组合节点(如 LoopUntilSuccess)、到预设模式(ReAct、Plan-Execute)的完整层次结构。
整体架构分层¶
Jianmu 的行为树执行内核由四个逻辑层组成。py_trees 原语层定义了行为树的语法——Behaviour、Composite、Selector、Sequence、Status 等——并由 jianmu/tree/ 模块统一重新导出,保证项目无需直接依赖 py_trees。扩展基类层(jianmu/engine/behaviour.py)通过 JianmuNodeMixin 将运行时依赖注入、状态读写和端口绑定能力注入每一个 Jianmu 节点,然后派生出同步 Behaviour、异步 AsyncBehaviour 和 Decorator 三种核心基类。运行时驱动层(jianmu/engine/runtime.py 中的 ReactiveRunner)负责将根行为树绑定到 StateManager 和 RunContext,通过事件驱动的 _event_loop() 或交互式 step() 模式执行树节拍。节点实现层(jianmu/node/ 下的内置节点与预设)在扩展基类之上实现 LLM 调用、工具执行、流程控制和多 Agent 通信等具体能力。
graph TD
subgraph py_trees 原语层
PT_B[Behaviour]
PT_C[Composite]
PT_S[Sequence]
PT_SEL[Selector]
PT_D[Decorator]
PT_BT[BehaviourTree]
end
subgraph 扩展基类层
JNM[JianmuNodeMixin]
JB[Behaviour]
JAB[AsyncBehaviour]
JD[Decorator]
JNM -->|注入| JB
JNM -->|注入| JAB
JNM -->|注入| JD
JB -->|继承| PT_B
JAB -->|继承| JB
JD -->|继承| PT_D
end
subgraph 运行时驱动层
RR[ReactiveRunner]
SM[StateManager]
RC[RunContext]
RR -->|绑定| PT_BT
RR -->|注入| SM
RR -->|注入| RC
end
subgraph 节点实现层
LLM[AgentLLMNode<br/>SimpleLLMNode]
TOOL[ToolExecutor<br/>ToolNode]
CTRL[StateCondition<br/>LoopUntilSuccess]
SWARM[SpawnAgent<br/>SendMessage]
PRESET[create_react_node<br/>create_plan_execute_node]
LLM -->|继承| JAB
TOOL -->|继承| JAB
CTRL -->|继承| JD
SWARM -->|继承| JAB
end
py_trees 原语层:统一导入面¶
jianmu/tree/__init__.py 是 Jianmu 中所有 py_trees 级构建块的官方导入面。它将 py_trees 的核心类型——Behaviour(叶子节点)、Composite(组合节点)、Selector(优先级选择)、Sequence(顺序执行)、Parallel(并行执行)、Decorator(装饰器)、BehaviourTree(树容器)以及 Status(SUCCESS / FAILURE / RUNNING / INVALID)统一重新导出。项目中所有使用行为树的地方都应从此模块导入,而非直接从 py_trees 导入。
这一设计有两个关键意图:封装层隔离——当未来需要替换或升级 py_trees 版本时,只需修改此单一模块的映射关系;语义明确——jianmu.tree 作为项目内行为树的专属命名空间,在 import 跟踪和代码审查中更容易定位行为树依赖。
# 这是 Jianmu 推荐的导入方式
from jianmu.tree import Behaviour, Selector, Sequence, Status, BehaviourTree, Decorator
扩展基类层:JianmuNodeMixin 与三类行为基类¶
JianmuNodeMixin:运行时注入的统一入口¶
JianmuNodeMixin 是所有 Jianmu 节点共享的能力混入——它不是 py_trees 节点,而是为行为树节点提供运行时基础设施的横向关注点。其核心职责包括:
| 职责 | 方法 | 说明 |
|---|---|---|
| 依赖注入 | inject(payload) |
接收 ReactiveRunner 产出的 InjectPayload,绑定 RunContext、StateManager 和 wake_up 回调 |
| 端口绑定 | bind(inputs=..., outputs=...) |
将逻辑端口名映射到共享状态字段,支持方法链式调用 |
| 状态读取 | read_state(key), read_port(port_name) |
在节点的命名空间内读取状态字段值 |
| 状态写入 | write_state(updates), write_port(port_name, value), write_ports(values) |
将多个端口输出合并写入状态,可选通知监听器 |
| 消息追加 | append_port_messages(port_name, messages) |
向绑定的消息端口批量追加对话消息 |
端口绑定的 key 解析规则由 _normalize_binding_key() 统一处理:支持 "state.field_name" 前缀格式(自动去除 state. 前缀),空值回退到端口名本身,保证配置灵活性与默认行为的一致性。
Behaviour(同步基类)= jianmu.node.Node¶
Behaviour 通过多重继承组合 JianmuNodeMixin 与 py_trees.behaviour.Behaviour,是 Jianmu 中最基础的行为树节点。它适用于纯同步的判断逻辑节点(如 StateCondition),不需要异步 I/O 支持。
class Behaviour(JianmuNodeMixin, py_trees.behaviour.Behaviour):
def __init__(self, name: str, namespace: Optional[str] = None):
py_trees.behaviour.Behaviour.__init__(self, name)
self._init_jianmu_fields(namespace)
在 jianmu/node/base.py 中,Behaviour 被赋予语义别名 Node,提示用户"这是 Jianmu 世界中语法最通用的行为树节点基类"。
AsyncBehaviour(异步基类)= jianmu.node.AsyncNode¶
AsyncBehaviour 是 Jianmu 行为树内核中最关键的扩展——它将 py_trees 原生的同步 tick() 模型桥接到 Python asyncio 的协程世界。其核心机制分为四个生命周期阶段:
1. 启动(initialise()):py_trees 在节点首次进入执行或从 INVALID 重新进入时调用此方法。AsyncBehaviour 在此处取消任何未完成的旧任务,通过 asyncio.get_running_loop().create_task(self.update_async()) 创建新协程任务,并通过 add_done_callback(lambda _: wake_up()) 注册完成回调——当协程结束时自动唤醒 ReactiveRunner 的事件循环,消除轮询开销。
2. 推进(tick()):这是 py_trees 要求每个 Behaviour 实现的生成器方法。对于异步节点,tick() 的核心工作不是执行业务逻辑,而是检测已完成的异步任务并在必要时重新初始化——如果 async_task.done() 为 True 且上一次结果为 RUNNING,则调用 initialise() 启动下一轮执行。
3. 状态映射(update()):将 asyncio Task 的运行时状态映射为 py_trees 的 Status 枚举。任务未完成 → RUNNING;任务返回无效类型 → FAILURE;任务被取消 → INVALID;正常完成 → 返回 update_async() 产出的 Status 值。
4. 终止(terminate()):当父节点或树中断此节点时,取消飞行中的异步任务。
stateDiagram-v2
[*] --> INITIALISING: tree.tick() 首次进入
INITIALISING --> RUNNING: create_task(update_async())
RUNNING --> COMPLETED: task.done() & result=SUCCESS/FAILURE
RUNNING --> REINIT: task.done() & result=RUNNING
REINIT --> RUNNING: create_task(new_update_async)
RUNNING --> CANCELLED: terminate(new_status)
CANCELLED --> [*]
COMPLETED --> [*]
子类必须实现 async def update_async() -> Status,在其中编写 I/O 密集型逻辑(LLM 调用、工具执行、网络请求),返回 py_trees 状态值。这一层抽象使得所有 Jianmu 内置节点(AgentLLMNode、ToolExecutor、SkillNode 等)只需关注 update_async() 中的业务逻辑,异步任务生命周期管理由基类统一处理。
Decorator(装饰器基类)¶
Decorator 同时继承 py_trees.decorators.Decorator 和 JianmuNodeMixin,使装饰器节点(如 Timeout、LoopUntilSuccess、FlattenedAgentNode)也能享受依赖注入和端口绑定能力。其构造函数接收 child 参数——被装饰的子行为,并在初始化时同步注入 Jianmu 字段。
依赖注入系统:RunContext、InjectPayload 与注入链路¶
RunContext:运行时资源容器¶
RunContext 是一个 @dataclass,承载不应序列化入 Agent 状态的运行时资源。它与 StateManager 形成职责分离——状态中持久化的数据进入 StateManager,而连接级、会话级的服务对象进入 RunContext:
| 字段 | 类型 | 用途 |
|---|---|---|
model_client |
ModelClient |
LLM 调用门面,被 LLM 节点通过 self.model_client 属性解析 |
approval_manager |
ApprovalManager |
审批流程管理器,被 Guard 和挂起恢复机制使用 |
sandbox |
执行沙箱句柄 | 工具执行隔离环境(LocalSandbox / DockerSandbox) |
constraints |
Constraints |
全局约束(如预算、频率限制)供 Guard 组件消费 |
runtime |
任意扩展对象 | APP 专属运行时扩展槽位 |
runtime_event_bus |
RuntimeEventBus |
运行时语义事件总线,供节点、Guard 和 Runtime 发布执行事实 |
prompt_runtime |
PromptRuntimeContext |
当前运行的 Prompt 构造覆盖(技能目录、工作空间等) |
InjectPayload 与注入时序¶
InjectPayload 是一个 frozen dataclass,聚合了 RunContext、StateManager 和 wake_up 回调。ReactiveRunner 在初始化时通过 inject_runtime_deps() 遍历树中所有节点,对每个 JianmuNodeMixin 实例调用 node.inject(payload)。注入发生在 tree.setup() 之后、首次 tick 之前,确保所有节点在进入 initialise() 时已具备完整的运行时上下文。
在 run() 方法每次被调用时,runner 还会重新注入 wake_up 回调——因为 _event_loop() 退出时会清理回调引用以防止陈旧的闭包引用。
sequenceDiagram
participant App
participant RR as ReactiveRunner
participant Tree as BehaviourTree
participant Node as JianmuNode
App->>RR: __init__(root, state_manager, ctx)
RR->>RR: 创建 InjectPayload(ctx, sm, wake_up)
RR->>Tree: iterate() 遍历所有节点
Tree-->>RR: 每个节点
RR->>Node: inject_runtime_deps(node, payload)
Node->>Node: inject(payload) 绑定 ctx/sm/wake_up
RR->>Tree: setup(timeout)
App->>RR: run(input_data)
RR->>Tree: 重新注入 wake_up
RR->>Tree: tick() → _event_loop()
ReactiveRunner:事件驱动的树执行¶
ReactiveRunner 是行为树与运行时基础设施之间的桥梁。它在构造时接收根行为节点、StateManager 和可选的 RunContext,内部创建 py_trees.trees.BehaviourTree 实例,提供三种执行入口:
| 入口 | 模式 | 适用场景 |
|---|---|---|
tick_once() |
同步单次节拍 | 最低层 API,单元测试和调试 |
step() |
异步单步 + Ephemeral 重置 | 交互式 UI(如 TUI Chat)的逐回合控制 |
run() |
事件驱动全自动运行 | 端到端 Agent 执行的常规模式 |
run() 的核心是 _event_loop()——一个基于 asyncio.Event 的事件驱动循环。它不是忙等待轮询树状态,而是通过 _wait_for_tick() 阻塞在 tick_signal.wait() 上,直到以下事件之一触发信号:
- 状态变更:
StateManager写入后通知订阅者,_on_wake_signal()被调用 - 异步任务完成:
AsyncBehaviour.initialise()中注册的add_done_callback(wake_up)触发 - 审批恢复:外部审批管理器解析挂起的审批请求后,桥接回调调用
_signal_tick()
这个设计消除了对 asyncio.sleep() 轮询的依赖,使 Runner 在没有工作时完全空闲,在有事件时立即响应。为防止信号过载,Runner 采用 _pending_tick_count 计数器和 tick_signal.clear() 实现无损合并——在消费信号前不会重复唤醒。
Runner 还内建了热循环检测——通过 hot_loop_warn_factor 参数监控单秒内的 tick 频率,当节点在无实质进展的情况下高频重试时发出警告日志,帮助开发者诊断循环逻辑错误。
内置组合节点:LoopUntilSuccess¶
LoopUntilSuccess 是 Jianmu 提供的核心组合节点——也是对 py_trees Decorator 最直接的扩展示范。它包装一个子节点,当子节点返回 FAILURE 时将其重置为 INVALID 并重新进入,直至子节点返回 SUCCESS、达到最大迭代次数、或 abort_condition 回调返回 True。
flowchart TD
INIT[initialise: 重置计数器] --> TICK[update]
TICK --> CHECK_ABORT{abort_condition?}
CHECK_ABORT -->|是| FAIL[返回 FAILURE]
CHECK_ABORT -->|否| CHECK_CHILD{子节点状态?}
CHECK_CHILD -->|SUCCESS| SUCCESS[返回 SUCCESS]
CHECK_CHILD -->|RUNNING| RUNNING[返回 RUNNING]
CHECK_CHILD -->|FAILURE| INC[iteration_count += 1]
INC --> BUDGET{超过 max_iterations?}
BUDGET -->|是| TERM[记录 TerminationRecord → FAILURE]
BUDGET -->|否| RESET[子节点 stop(INVALID) + 信号唤醒]
RESET --> RUNNING
这个节点是 ReAct 和 Plan-Execute 预设模式的循环外壳——它将"重试直到成功"的控制流语义封装为一个可配置的装饰器节点,使预设工厂函数只需关心线性步骤的组装。
预设模式:组合节点的工厂化组装¶
ReAct 模式¶
create_react_node() 是最常用的 Agent 工厂函数。它将以下线性节点组装进 LoopUntilSuccess 的循环体中:
LoopUntilSuccess (max_iterations, abort_condition=token_budget)
└── Sequence (memory=True) ← "ReActLoop"
├── AgentLLMNode ← 调用 LLM,产出 tool_calls 或 final_answer
├── ToolExecutor ← 执行 LLM 产出的 tool_calls,追加观察消息
└── StateCondition ← 检查 done 标志,决定循环是否终止
Sequence 的 memory=True 参数是关键——它确保在子节点返回 RUNNING 后,下一次 tick 从上次中断的位置恢复,而非从序列头部重新开始。工厂函数同时注入了 abort_condition(基于共享 token 预算的超限检查),使循环在模型调用量超出配置时主动终止。
Plan-Execute 模式¶
create_plan_execute_node() 将任务分解为规划→执行→审查三阶段。与 ReAct 不同,规划器和执行器使用不同的状态键——规划输出写入中间 plan 字段,执行器输出写入 final_answer,避免覆盖。每个阶段使用独立的 ContextBuilder(规划器使用静态规划 Prompt + 历史,执行器使用执行 Prompt + 工具描述 + 历史),且可选启用在每轮执行后进行评分审查的 EvaluationNode。
节点系统的完整类型谱系¶
下表总结了 Jianmu 行为树中所有节点基类与其 py_trees 原型的对应关系:
| Jianmu 类 | 语义别名 | py_trees 原型 | 异步支持 | 适用场景 |
|---|---|---|---|---|
Behaviour |
Node |
py_trees.behaviour.Behaviour |
否 | 同步条件判断、日志输出 |
AsyncBehaviour |
AsyncNode |
py_trees.behaviour.Behaviour |
是 | LLM 调用、工具执行、网络 I/O |
Decorator |
— | py_trees.decorators.Decorator |
取决于子节点 | 超时包装、循环重试、子树编译 |
FunctionNode |
— | AsyncBehaviour |
是 | @node 装饰器快速定义 |
FlattenedAgentNode |
— | Decorator |
取决于编译子树 | SkillNode、SwarmNode 的延迟编译 |
所有 Jianmu 节点都通过 JianmuNodeMixin 获得依赖注入和端口绑定能力。继承链如下:
py_trees.behaviour.Behaviour
└── JianmuNodeMixin + py_trees.behaviour.Behaviour → Behaviour (=Node)
├── AsyncBehaviour (=AsyncNode)
│ ├── SimpleLLMNode → AgentLLMNode
│ ├── ToolNode → ToolExecutor
│ ├── SkillNode (via FlattenedAgentNode)
│ ├── SwarmNode (via FlattenedAgentNode)
│ ├── Wait
│ ├── EvaluationNode
│ └── FunctionNode (@node 装饰器)
└── StateCondition
py_trees.decorators.Decorator
└── JianmuNodeMixin + py_trees.decorators.Decorator → Decorator
├── Timeout
├── LoopUntilSuccess
└── FlattenedAgentNode → SkillNode, SwarmNode
端口绑定与状态通信模式¶
Jianmu 的行为树节点通过逻辑端口与共享状态交互,而非直接访问全局变量。JianmuNodeMixin 提供了完整的端口绑定生命周期:
绑定阶段:调用 node.bind(inputs={"messages": "state.messages"}, outputs={"final_answer": "state.answer"}) 建立端口名与状态键的映射。该调用返回 self,支持链式风格。
读取阶段:node.read_port("messages") 内部调用 resolve_input_port("messages") → 从绑定映射中找到 "messages" → 获取规范化键名 → 通过 state_manager.get(key, namespace=...) 读取值。
写入阶段:node.write_port("final_answer", content) 执行反向映射。write_ports() 支持批量写入,append_port_messages() 专门处理对话消息的追加操作(使用 StateMessageStore 处理消息规范化)。
这一设计使得同一个节点类可以被不同工作流复用——只需改变绑定的状态键名,无需修改节点内部逻辑。例如,同一个 SimpleLLMNode 实例可以绑定到 "plan" 端口产出规划文本,也可以绑定到 "final_answer" 端口产出最终回答。
@node 装饰器:从函数到行为树节点¶
@node 装饰器是 Jianmu 提供的最低门槛节点创建方式。它将一个接受 state 参数并返回 dict 或 None 的同步/异步函数包装成 FunctionNode 子类:
@node(name="my_step", description="Process the current state")
async def my_step(state):
result = await some_async_work(state.task)
return {"output": result}
装饰器内部机制:FunctionNode 继承自 AsyncBehaviour,其 update_async() 自动检测函数是否为协程(inspect.iscoroutinefunction),同步函数通过 asyncio.to_thread() 在线程池中执行避免阻塞事件循环;函数返回的 dict 被自动合并到状态管理器。这为快速原型和简单的状态转换逻辑提供了极其便捷的入口,同时保持了与完整 AsyncBehaviour 子类相同的运行时注入和端口绑定能力。
阅读建议¶
本文档覆盖了 Jianmu 行为树执行内核的静态架构——节点基类、依赖注入和端口绑定。要理解运行时如何驱动这些节点进入执行,请继续阅读:
- ReactiveRunner:事件驱动的异步 tick 调度与挂起恢复机制 — 深入
_event_loop()的唤醒合并、审批挂起和检查点恢复机制 - 节点体系全景:AsyncNode 基类、端口绑定与依赖注入 — 各内置节点的具体实现细节
- 类型化状态管理:Pydantic Schema、Reducer 合并与 Ephemeral 字段 — 理解节点读写状态的
StateManager内部机制