Block 基类与执行生命周期
深入 Block 基类。今天读 blocks/_base.py:Block 基类的构造参数、run 被注入什么上下文、以及 run() 外面那层 _execute() 执行生命周期管理。
👶 小白:run 和 _execute 有啥区别?我写块时只写了 run 啊。
👨🏫 老师:run 是你写的"干活逻辑",_execute 是框架包在 run 外面的"上班外壳"。你只管埋头干活(run);打卡、检查你交的东西合不合格式(校验 schema)、算这趟该扣多少费(计费)、出错了怎么走 error 引脚——这些琐事都由 _execute 这层外壳统一处理。这样每个块只写核心逻辑,通用流程不必重复写,正是"基类"的价值所在。
Block 基类本体
Block 用抽象类(ABC)强制"你必须实现 run()",用泛型(Generic)钉死"你吃什么 Schema、吐什么 Schema"。于是无论块内部干什么,对外都是同一副面孔:有 id、有 input/output schema、有一个 run()。执行引擎只认这份合同,就能调度任意块。# blocks/_base.py:550
class Block(ABC, Generic[BlockSchemaInputType, BlockSchemaOutputType]):
execution_timeout_seconds = DEFAULT_BLOCK_EXECUTION_TIMEOUT_SECONDS # 默认 30 分钟
run()。有个默认 30 分钟的执行超时(防止某个块卡死)。Block[MyInput, MyOutput] 让 IDE 和类型检查器知道"这个块的 run 接收 MyInput、产出符合 MyOutput 的东西"。写错类型会飘红。类型安全——和 eino 的泛型 Runnable、CrewAI 的 Pydantic 一样,都是"让错误在写代码时就暴露"。Block 基类就是这张强制格式的作业卡:input_schema=进料规格、output_schema=出料规格、run()=本工位操作。今天这一整天,我们都用"标准化工位"这个画面来理解 Block。__init__ 的关键参数
子类在自己的 __init__ 里 super().__init__(...) 传入(_base.py:554):
| 参数 | 含义 |
|---|---|
id | 唯一 UUID,持久化到 DB,永不可变 |
description/categories | 描述、分类 |
input_schema/output_schema | 指向内嵌 Input/Output 类 |
test_input/test_output/test_mock | 内建测试样例(Day 02) |
block_type | 类型(L05) |
disabled | 是否停用 |
is_sensitive_action | 是否敏感操作(需人工审核) |
webhook_config | webhook 触发配置 |
block_id 引用 Block(Day 03)。如果 id 变了,所有引用这个 Block 的 Graph 就断了。id 是 Block 的永久身份证——写死一个 UUID,之后无论怎么改代码,id 不能动。run 被注入的运行时上下文
run()(_base.py:653)签名里,input_data 是校验好的输入对象,**kwargs 注入运行时上下文(_base.py:660):
async def run(self, input_data, **kwargs):
# kwargs 里有:graph_id / node_id / graph_exec_id / node_exec_id / user_id
# credentials(凭证,Day 08)/ execution_context
u_42 在图 g_9 里跑一个"发邮件"块,框架调用时大致等于:block.run(input_data=EmailInput(to="a@x.com", body="hi"), user_id="u_42", graph_id="g_9", node_id="n_3", graph_exec_id="ge_88", credentials=<真实SMTP凭证>)→ 块内部用
kwargs["credentials"] 拿到密钥去发信,用 kwargs["user_id"] 记录是谁发的。同一个块类被别的用户同时调用时,各自的 kwargs 完全独立,不会串。execution_context 知道存哪、调 API 的块需要 credentials。_execute:run 外面的执行生命周期
外界不直接调 run(),而是走 execute() → _execute()(_base.py:819)。它在 run 前后做一堆事:
{"number": "21"}。① 我先被摸了摸口袋——
is_sensitive_action?没有,我只是算个乘法,放行。② 门口安检:按
input_schema 校验,字符串 "21" 被规整成整数 21,类型对,过。③ 轮到我干活了(run):我
yield "result", 21*2,吐出 ("result", 42)。④ 出门再安检:按
output_schema 校验 42 是不是合法输出,是,盖章。⑤ 我被 yield 给下游节点,功成身退。」——注意:我(run)自始至终只操心第③步,前后的安检都是 _execute 外壳替我做的。
| 步骤 | 发生什么 | 此刻关键变量 |
|---|---|---|
| ① 敏感拦截 | 查 is_sensitive_action,false 直接放行 | input_data=用户原始输入(未校验) |
| ② 输入校验 | 按 input_schema 解析/规整 | "21" → number=21(已校验对象) |
| ③ run() | 你的业务逻辑,yield 输出 | 产出 ("result", 42) |
| ④ 输出校验 | 按 output_schema 逐条校验;error 口则抛异常 | 42 校验通过 |
| ⑤ yield 下游 | 把合法输出交给执行引擎 | 下游节点收到 result=42 |
_execute() 里,run 永远收到的是已校验的干净输入。一句话记:run 只做菜,_execute 负责进货验货和出餐盖章。_execute() 是 run 的"外壳"——负责"执行前校验、敏感操作人工审核、执行后输出校验"这些通用管理,让每个 Block 的 run() 只需专注自己的业务逻辑。这是模板方法模式:外壳固定流程,run 是可变的具体逻辑。is_sensitive_action 的 Block(比如"删除文件""发推文""转账"),在安全模式下执行前会暂停、等人点头——又见 HITL(eino Day14、OpenHands Day17、CrewAI Day13 都有)。本系列第 N 次强调:能自主执行危险操作的 AI,关键动作必须能停下等人。AutoGPT 把它做成 Block 的一个标志位,执行引擎在 _execute 里统一处理。BlockType:类型枚举
BlockType(_base.py:58)决定 Block 的 UI 表现和特殊处理:
| 类型 | 含义 |
|---|---|
STANDARD | 普通功能块 |
INPUT/OUTPUT | 图的输入/输出口(Day 03 的 I/O 推导靠它) |
AGENT | 子 Agent(Agent-as-Block,Day 03) |
WEBHOOK | 外部 webhook 触发 |
AI | AI 块(调 LLM) |
HUMAN_IN_THE_LOOP | 需人工介入 |
MCP_TOOL | MCP 工具 |
BlockCategory:分类
BlockCategory(_base.py:72)纯粹用于归组(前端左侧积木清单按类别展示):AI、SOCIAL(社交媒体)、TEXT、BASIC、INPUT/OUTPUT、LOGIC(逻辑/条件)、DATA……
同步到数据库
initialize_blocks()(data/block.py:21)把内存里的每个 Block 同步到数据库 AgentBlock 表——比对 id/name/schema/description,有变化就 upsert(block.py:28)。还会加载 LLM 生成的"优化版描述" optimizedDescription(block.py:87)。
今日小结 + 动手
🧠 今天你应该能回答
- Block 基类为什么是 ABC + Generic?id 为什么永不可变?
- run 被注入哪些运行时上下文?为什么用 kwargs 注入?
- _execute() 在 run 外面做了哪些事?(校验/人审/输出校验)
- BlockType 和 BlockCategory 的区别?
- Block 为什么要同步到数据库?optimizedDescription 是什么?
✋ 动手
P=autogpt_platform/backend/backend
sed -n '550,670p' $P/blocks/_base.py | head -60 # Block 基类 + run
sed -n '819,940p' $P/blocks/_base.py | head -50 # _execute 生命周期
sed -n '58,95p' $P/blocks/_base.py # BlockType/BlockCategory
sed -n '21,64p' $P/data/block.py # initialize_blocks