跳转至

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 行为树执行内核的静态架构——节点基类、依赖注入和端口绑定。要理解运行时如何驱动这些节点进入执行,请继续阅读: