Block:积木的概念
第一个核心概念。Block 是有"输入口/输出口"和 run() 逻辑的可复用功能积木。今天建立心智模型、读一个最简 Block 的真实代码。核心代码在 blocks/_base.py。
👶 小白:run() 为什么要写成 async def 还带 yield(异步生成器),不直接 return 一个结果?
👨🏫 老师:因为一台设备可能陆续吐出好几件产品,而不是一次给一个。return 像自动贩卖机"咣当"掉一件就结束;yield 像流水线设备加工好一件就送出一件、边算边出——下游能立刻接着处理,不用干等全部算完。而 async 让设备"联网等外部响应时不霸占整条线",好让别的块并行干活。
Block 心智模型
run() 干活。有了统一的"口子规范",任意两个 Block 就能对接、复用、可视化拖拽——这就是"积木"心智,也是整个平台的原子。一个 Block 就像电路里的一个元件——有输入引脚、输出引脚,中间一段处理逻辑:
输入口:text | 输出口:word_count、character_count
data/block.py 里没有 Block 基类!它只放类型别名和"同步到数据库"的逻辑。真正的 Block 基类在 blocks/_base.py:550。data/block.py"——名字太像了。其实那里只放类型别名和"同步到数据库"逻辑,真正的 Block 抽象基类在 blocks/_base.py:550。记住:data/ 放数据模型,blocks/_base.py 放能力基类。run() 的实现),只要凸凹接口对得上就能拼。一句话:Block = 一块规定好接口的乐高,"选块 + 拼接"就能搭出任意造型。run 是异步生成器(关键设计)
看类型别名(data/block.py:15)——它揭示了 Block 的核心:
BlockInput = dict[str, Any] # 输入:口名 → 数据
BlockOutputEntry = tuple[str, Any] # 输出:(口名, 值)
BlockOutput = AsyncGenerator[BlockOutputEntry, None] # 输出:一个异步生成器!
run() 的返回类型是异步生成器——它用 yield ("口名", 值) 可以产出多次。所以一个 Block 天然支持"一个输入 → 多个输出",甚至流式地陆续产出。return 一次就结束、只能返回一个值。生成器能 yield 很多次——这让 Block 能:① 有多个输出口(yield "word_count", 4 然后 yield "character_count", 19);② 流式产出(比如一个"逐行读文件"块,每读一行 yield 一次,下游能边收边处理);③ 一个输出口产出多个值(列表展开)。异步(async)则让 Block 里能 await 网络请求等 I/O 而不阻塞。这个设计让 Block 既灵活又高效。最简 Block 精读
看一个最纯粹的 Block(blocks/count_words_and_char_block.py:11)——理解结构的最佳入门:
class WordCharacterCountBlock(Block):
class Input(BlockSchemaInput): # ← 内嵌输入 schema
text: str = SchemaField(description="要统计的文本", advanced=False)
class Output(BlockSchemaOutput): # ← 内嵌输出 schema
word_count: int = SchemaField(description="单词数")
character_count: int = SchemaField(description="字符数")
error: str = SchemaField(...) # error 口(L06)
def __init__(self):
super().__init__(id="ab2a782d-...", categories={BlockCategory.TEXT},
input_schema=..., output_schema=...,
test_input={"text": "Hello, how are you?"},
test_output=[("word_count", 4), ("character_count", 19)])
async def run(self, input_data: Input, **kwargs) -> BlockOutput:
yield "word_count", len(input_data.text.split())
yield "character_count", len(input_data.text)
Input/Output 两个类(定义有哪些输入/输出口);② __init__ 里声明元数据(唯一 id、分类、测试样例);③ run() 里 yield 输出。这个块无副作用、无凭据,最纯粹。input_data.text = "Hello, how are you?":①
run() 执行 "Hello, how are you?".split() = ["Hello,","how","are","you?"] → 长度 4 → yield "word_count", 4;② 执行
len("Hello, how are you?") = 19(含空格标点)→ yield "character_count", 19。这正好等于 Block 自带的
test_output=[("word_count", 4), ("character_count", 19)]——测试用例就是"标准答案"。test_input 应该产出 test_output。这是很好的设计:Block 自带"我该怎么用、正确输出长啥样"的示例,既能自动测试、又是文档。把测试数据做成 Block 定义的一部分——保证每个积木都可验证。Input/Output Schema
BlockSchema(blocks/_base.py:194)继承 Pydantic BaseModel,是输入/输出的"类型契约"。关键能力:
jsonschema()(:197):转成前端能用的 JSON Schema——前端画布就靠它画出"输入表单/输出引脚"。validate_data()(:227):执行前校验用户填的输入。- 派生出
BlockSchemaInput(输入基类)、BlockSchemaOutput(输出基类,自带 error 字段)。
_base.py:337 的 __pydantic_init_subclass__):每当有人定义新 Schema 子类,Pydantic 自动跑校验,强制命名规范——名为 credentials 或 *_credentials 的字段必须是凭证类型,反之亦然。这保证整个平台凭证字段命名统一,前端能据此识别"哪些字段要弹连接账号的 UI"(Day 08)。SchemaField:带 UI 元数据的字段
Block 不用 Pydantic 原生 Field,而用封装的 SchemaField(data/model.py:263)——它把 UI/行为元数据塞进字段:placeholder、secret(密码型,前端打码)、advanced(收进高级折叠区)、hidden、depends_on(字段依赖)等。
model.py:283:字段没有默认值 → 强制 advanced=False(因为是必填,得让用户显眼地看到);有默认值且没显式指定 → 默认 advanced=True(收进"高级"折叠区,不打扰新手)。框架用这个小逻辑自动决定"哪些输入口显眼、哪些藏起来"——让画布界面对新手友好(只显示必填的),对高级用户也够用(展开高级)。这种对 UI 体验的用心,是"面向非程序员的可视化平台"该有的细致。error 引脚约定
BlockSchemaOutput 默认带一个 error: str 字段(_base.py:471)。这是全局的错误处理约定——看执行包装 _execute()(_base.py:922):
async for output_name, output_data in self.run(...):
if output_name == "error":
raise BlockExecutionError(...) # ★ yield "error" = 失败信号,转成异常
# 每条输出还按 schema 校验
yield output_name, output_data
yield "error", "出错了...",框架就把它转成异常、中断执行。这就是为什么每个输出 schema 默认带 error 字段——它是"Block 报告失败"的统一渠道。_execute() 还做了输入校验、敏感操作人工审核、输出 schema 校验——它是 run() 外面的一层"执行生命周期管理"。自动发现注册
怎么让 300+ 个 Block 被系统认识?load_all_blocks()(blocks/__init__.py:17)——放进 blocks/ 目录就自动加载,无需手动注册:
# 递归扫描 blocks/ 下所有 .py,逐个 import,
# 再用 Block.__subclasses__() 找出所有 Block 子类
# 结果 @cached(ttl=3600) 缓存一小时
Block 结尾、id 必须是唯一 UUID、字段必须是 SchemaField……不合规立刻报错。initialize_blocks()(data/block.py:21)再把内存里的 Block 同步到数据库(AgentBlock 表)——比对 id/schema 有变化就 upsert。为什么要同步到 DB?因为前端要列出所有可用 Block(画布左侧的积木清单)、Graph 里的 Node 用 block_id 引用 Block——这些都需要 Block 在数据库里有记录。"代码里定义 Block → 自动发现 → 同步到 DB → 前端可拖拽"这条链让加新积木极其简单。今日小结 + 动手
🧠 今天你应该能回答
- Block 是什么?和 Node 的区别?(能力/类 vs 摆放/数据)
- run 为什么是异步生成器?带来什么能力?
- 一个 Block 的标准三段式结构?test_input 有什么用?
- error 引脚约定是什么?为什么把错误做成输出口?
- Block 怎么被自动发现和注册?
✋ 动手
P=autogpt_platform/backend/backend
sed -n '15,20p' $P/data/block.py # 类型别名
sed -n '11,40p' $P/blocks/count_words_and_char_block.py # 最简 Block
sed -n '456,490p' $P/blocks/_base.py # BlockSchemaInput/Output
sed -n '17,60p' $P/blocks/__init__.py # 自动发现
data/graph.py:Node(节点)+ Link(连线)怎么把 Block 连成一个智能体、图怎么校验、起始节点怎么确定、agent-as-block 子图。