Day 06 / 共 20 天 · 第 2 周 Block 系统

Block 基类与执行生命周期

深入 Block 基类。今天读 blocks/_base.py:Block 基类的构造参数、run 被注入什么上下文、以及 run() 外面那层 _execute() 执行生命周期管理。

📍 你在整门课的位置 · 第 2 周 Block 深入
D5 执行旅程 D6 Block 基类/生命周期 D7 真实 Block 精读 D8 Block 凭证
💡 今天的类比世界观:Block 生命周期 = 一名员工"上一个班" __init__ = 入职登记(写下工号 id、能力清单 input/output schema);_execute = 一整个"打卡→干活→交报告"的班次外壳(框架负责考勤、校验、计费);run = 班次里你真正动手干活那一段BlockType / BlockCategory = 工牌上的岗位 / 部门标签同步到数据库 = 把这名员工登记进公司 HR 系统,别人才找得到、派得动。今天都用"员工上班"来想。

👶 小白:run 和 _execute 有啥区别?我写块时只写了 run 啊。

👨‍🏫 老师:run 是你写的"干活逻辑",_execute 是框架包在 run 外面的"上班外壳"。你只管埋头干活(run);打卡、检查你交的东西合不合格式(校验 schema)、算这趟该扣多少费(计费)、出错了怎么走 error 引脚——这些琐事都由 _execute 这层外壳统一处理。这样每个块只写核心逻辑,通用流程不必重复写,正是"基类"的价值所在。

L01

Block 基类本体

🤔 痛点:300+ 个五花八门的块,怎么保证"长得像、能被统一调度"? 有的块只做纯计算,有的块要调 LLM、有的要发邮件——如果每个块作者各写各的入口、各自定义"怎么被执行、怎么校验输入输出",那执行引擎就没法统一对待它们,平台也乱成一锅粥。
💡 本质:基类 = 给所有块定一份"必须遵守的合同" 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 分钟
读法:Block 是抽象类(ABC)+ 泛型(Generic)泛型参数是"输入 Schema 类型、输出 Schema 类型"——让类型检查器知道这个 Block 吃什么、吐什么。抽象类意味着不能直接实例化 Block,必须写子类实现 run()。有个默认 30 分钟的执行超时(防止某个块卡死)。
泛型 Block 有什么好处? Block[MyInput, MyOutput] 让 IDE 和类型检查器知道"这个块的 run 接收 MyInput、产出符合 MyOutput 的东西"。写错类型会飘红。类型安全——和 eino 的泛型 Runnable、CrewAI 的 Pydantic 一样,都是"让错误在写代码时就暴露"。
🏭 生活类比:Block 基类 = 工厂里的"标准化工位作业规程" 想象一条流水线,每个工位都必须贴一张统一格式的作业卡:【进料口要什么】【出料口出什么】【本工位怎么操作】。不管这个工位是拧螺丝还是喷漆,卡片格式都一样——于是流水线调度员(执行引擎)根本不用关心工位内部干嘛,只按卡片"送料、收货"即可。Block 基类就是这张强制格式的作业卡input_schema=进料规格、output_schema=出料规格、run()=本工位操作。今天这一整天,我们都用"标准化工位"这个画面来理解 Block。
L02

__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_configwebhook 触发配置
为什么 id 永不可变? 因为 Graph 里的 Node 用 block_id 引用 Block(Day 03)。如果 id 变了,所有引用这个 Block 的 Graph 就断了。id 是 Block 的永久身份证——写死一个 UUID,之后无论怎么改代码,id 不能动。
L03

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
📝 具体例子:run() 实际收到什么 用户 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 完全独立,不会串。
读法:Block 执行时,框架把"我在哪个图、哪个节点、哪次执行、哪个用户、用什么凭证"这些上下文通过 kwargs 注入。Block 需要时可以取用——比如上传文件的块需要 execution_context 知道存哪、调 API 的块需要 credentials
为什么用 kwargs 注入而不是全局变量? 因为同一个 Block 类可能同时被多个执行并发调用(不同用户、不同图)。如果用全局变量存"当前用户",并发时就串了。通过参数注入,每次执行的上下文各自独立、互不干扰。这是并发安全的正确做法(和 eino 用 ctx、OpenHands 每请求独立上下文一致)。
L04

_execute:run 外面的执行生命周期

外界不直接调 run(),而是走 execute()_execute()_base.py:819)。它在 run 前后做一堆事:

1敏感操作拦截(is_sensitive_action + 安全模式 → 暂停等人工审核)
2输入校验(按 input_schema 校验用户填的输入)
3调 run(),对每条输出:error 口 → 抛异常;否则按 output_schema 校验
4yield 校验通过的输出给下游
_execute() —— run() 的外壳(框架统一管理) ① 敏感操作 拦截/等人审 ② 输入校验 按 input_schema ③ run() 你的业务逻辑 ④ 输出校验 按 output_schema yield →下游
模板方法模式:外壳(①②④)固定不变,每个块只需专注中间那格紫色的 run()(③)。校验、人审、输出把关全由框架代办。
🚶 第一人称之旅:我,一个"数字乘 2"的执行,走一遍 _execute 「我被执行引擎点名了。用户在画布上给我填了 {"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
读法:⚠️ 常见误解:小白常以为"外界直接调 run()、run 里自己写校验"。其实 run() 只管业务,校验/人审/输出把关全在外层 _execute() 里,run 永远收到的是已校验的干净输入。一句话记:run 只做菜,_execute 负责进货验货和出餐盖章。
读法:_execute() 是 run 的"外壳"——负责"执行前校验、敏感操作人工审核、执行后输出校验"这些通用管理,让每个 Block 的 run() 只需专注自己的业务逻辑。这是模板方法模式:外壳固定流程,run 是可变的具体逻辑。
敏感操作人工审核(Human-in-the-loop) 标了 is_sensitive_action 的 Block(比如"删除文件""发推文""转账"),在安全模式下执行前会暂停、等人点头——又见 HITL(eino Day14、OpenHands Day17、CrewAI Day13 都有)。本系列第 N 次强调:能自主执行危险操作的 AI,关键动作必须能停下等人。AutoGPT 把它做成 Block 的一个标志位,执行引擎在 _execute 里统一处理。
L05

BlockType:类型枚举

BlockType_base.py:58)决定 Block 的 UI 表现和特殊处理:

类型含义
STANDARD普通功能块
INPUT/OUTPUT图的输入/输出口(Day 03 的 I/O 推导靠它)
AGENT子 Agent(Agent-as-Block,Day 03)
WEBHOOK外部 webhook 触发
AIAI 块(调 LLM)
HUMAN_IN_THE_LOOP需人工介入
MCP_TOOLMCP 工具
类型影响执行引擎和前端的特殊处理——比如 INPUT/OUTPUT 块用于推导 Agent 的对外接口(Day 03)、AGENT 块触发子图执行、WEBHOOK 块由外部事件触发而非上游连线。大部分块是 STANDARD,特殊类型各有专门用途。
L06

BlockCategory:分类

BlockCategory_base.py:72)纯粹用于归组(前端左侧积木清单按类别展示):AISOCIAL(社交媒体)、TEXTBASICINPUT/OUTPUTLOGIC(逻辑/条件)、DATA……

Category vs Type 别混 BlockType 影响"执行/UI 的行为"(这块怎么被处理)。BlockCategory 只影响"归到哪一组显示"(用户在画布左侧哪个分类里找到它)。一个块可以属于多个 category(比如既是 AI 又是 TEXT)。Type 管行为、Category 管展示——一个功能性、一个组织性。
L07

同步到数据库

initialize_blocks()data/block.py:21)把内存里的每个 Block 同步到数据库 AgentBlock 表——比对 id/name/schema/description,有变化就 upsert(block.py:28)。还会加载 LLM 生成的"优化版描述" optimizedDescriptionblock.py:87)。

为什么 Block 要在 DB 里有一份?还有"优化描述"?前端要列出所有可用 Block(画布左侧清单),从 DB 读比每次扫代码快。② Graph 里的 Node 用 block_id 引用,DB 里有记录才能校验引用有效。③ optimizedDescription:Block 作者写的描述可能不够 LLM 友好,平台用 LLM 把它"优化"成更利于(copilot、搜索)理解的版本,存在 DB。"代码定义 → 同步到 DB → 前端/引擎使用"这条链让 Block 既是代码又是数据。
L08

今日小结 + 动手

🧠 今天你应该能回答

  • 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
明天预告 · Day 07精读几个真实 Block——从最简纯计算块,到 I/O 边界块、状态复用块、带凭证的 AI 块,看真实的 Block 长什么样、各种写法。
← Day 05 旅程 Day 07 · 真实 Block →