第 09 步:编写自定义节点¶
这一节学什么¶
- 什么时候应该写自定义 Node,而不是 Tool 或 Skill
- 自定义同步 / 异步节点的最小实现
.bind(inputs=..., outputs=...)在节点编排中的作用
你会运行什么¶
示例代码:
examples/getting_started/step_09_write_custom_node.py
运行:
核心概念¶
这一节的重点是边界感:
- Tool 适合暴露给模型主动调用
- Skill 适合打包一组能力或子流程
- Node 适合显式编排运行时控制流
如果你开始需要稳定的树结构、明确的状态端口和可预测的调度语义,通常就该写 Node,而不是继续把逻辑塞进 prompt。
为什么这里继承 AsyncBehaviour¶
这个示例里的两个节点都继承了:
这里选 AsyncBehaviour,是因为它很适合作为 Getting Started 里的统一节点基类。
你可以先这样理解:
AsyncBehaviour- 适合通过
update_async()写节点逻辑 - 即使当前逻辑只是本地字符串处理
- 也可以先用异步节点写法
- 这样以后接文件、网络、数据库、外部服务时不用再整体改风格
所以这一步不是在强调“这个例子必须异步”,而是在建立一个更稳的节点编写心智。
一个自定义节点的最小契约¶
一个自定义节点最少需要做三件事:
- 读取输入
- 产生输出
- 返回
Status
也就是说,update_async() 不是普通 helper 函数。
它是行为树节点真正被 tick 时执行的入口。
最小结构通常像这样:
async def update_async(self) -> Status:
value = self.read_port("input_name", default="")
result = ...
self.write_port("output_name", result)
return Status.SUCCESS
这里的 Status 很重要:
Status.SUCCESS- 这一步完成了,而且结果满足预期
Status.FAILURE- 这一步完成了,但条件不满足,或者这条路径不成立
Status.RUNNING- 这一步还没做完,后续 tick 还要继续推进
Getting Started 里的这个例子都是同步可完成的小逻辑,所以直接返回 SUCCESS。
read_state/write_state 和 read_port/write_port 的区别¶
你前一节已经见过直接读写 state。
这一节开始进入更推荐的节点写法:
read_state/write_state- 直接依赖具体 state key
- 节点内部知道外部字段名
read_port/write_port- 先面向逻辑端口读写
- 再通过
.bind(...)映射到真实 state key
可以先把区别理解成:
state- 更像共享存储本身
port- 更像节点对外暴露的输入输出接口
如果一个节点只是一次性 demo,直接写 state 也能跑。 但如果你希望节点能复用,port 通常是更好的边界。
.bind(...) 到底解决了什么¶
这一步最关键的其实不是 read_port() 或 write_port() 本身,而是:
NormalizeTextNode(name="NormalizeText").bind(
inputs={"source_text": "text"},
outputs={"normalized_text": "normalized_text"},
)
.bind(...) 解决的是“节点内部名字”和“外部 state 字段名”之间的解耦。
也就是说:
- 节点内部只知道自己需要一个
source_text输入端口 - 但它不需要知道外部 state 里这个字段是不是叫
text
同理:
- 节点内部只知道自己输出一个
normalized_text - 但这个输出最终也可以映射到别的 state key
这带来的好处是:
- 同一个节点可以复用到不同 state schema
- 节点实现不必跟某个具体业务字段名绑死
- 组合节点时,胶水逻辑集中在树装配处,而不是塞进节点内部
这就是为什么:
- Step 08 先讲“怎么组一棵树”
- Step 09 再讲“节点怎么通过 port 接进这棵树”
为什么这个例子拆成两个节点¶
这个例子没有把逻辑塞进一个大节点,而是拆成:
NormalizeTextNodeSummarizeNode
这是故意的。
它想表达的是节点设计里的一个很实用原则:
- 一个节点尽量只负责一步清晰的工作
这里两步分别是:
- 先规范化输入文本
- 再基于规范化结果构造摘要
拆开之后更容易得到这些收益:
- 每个节点职责更单一
- 中间结果可以被观察、复用或替换
- 后续更容易把其中一步替换成别的实现
什么时候不该写 Node¶
这一节也要反过来提醒一次:
- 如果你只是想给模型暴露一个能力
- 更像
Tool - 如果你只是想打包一段技能说明或子流程
- 更像
Skill - 只有当你需要显式控制流、状态推进和调度语义时
- 才更适合写
Node
Checkpoint 与恢复¶
对普通自定义节点来说,恢复机制通常应该是无感的:
- 纯计算不需要恢复代码。
- 需要跨 resume 保留的业务进度,应通过端口或 state helper 写入 Jianmu state。
- Tool / LLM / Agent 相关外部任务优先使用内置 API,它们会记录已完成任务,供 checkpoint replay。
- 如果自定义节点直接执行外部副作用,需要通过
self.recovery.task(...)/self.recovery.tasks登记,或者让该操作显式具备幂等和 reconcile 策略。
普通应用节点不应该实现 dump_resume_state() 或 restore_resume_state()。这些 hook 是内置节点迁移旧 node-local 恢复路径时使用的内部兼容机制。
运行后观察什么¶
- 节点如何从输入端口读取数据
- 节点如何把结果写回输出端口
.bind(...)如何减少状态胶水代码- 一个较大的流程为什么值得拆成多个小节点