Day 07 / 共 20 天 · 第 2 周 Block 系统
精读几个真实 Block
从最简纯计算块,到 I/O 边界块、状态复用块、带凭证的 AI 块——看真实的 Block 长什么样,掌握各种写法。
📍 你在整门课的位置 · 第 2 周 Block 深入
D6 Block 基类/生命周期→
D7 真实 Block 精读→
D8 Block 凭证→
D9 Block 成本
💡 今天的类比世界观:四种 Block = 厨房里四类"厨具"
纯计算块 = 打蛋器(只加工手里的料,不联网);I/O 边界块 = 水龙头(通向外部世界,取水 / 放水);状态复用块 = 冰箱(把料存起来下次复用,不用每次现买);带凭证的 AI 块 = 要刷门禁卡的高级设备(用之前得先验证身份)。test_mock = 试菜时用仿真食材排练,不浪费真材料(不花钱调真模型)。今天都用"厨房"来想。
👶 小白:测一个 AI 块,每次都要真的花钱调一次大模型吗?跑几十遍单测不烧钱吗?
👨🏫 老师:不用。AI 块可以配 test_mock——测试时框架把"调模型"这一步换成事先写好的假返回(替身演员),逻辑照跑,但不联网、不花钱。就像试菜用仿真食材排练流程,等真开业才用真料。这样单测既快又免费,还稳定(不受模型随机输出影响)。
L01
四种范例概览
🤔 痛点:blocks/ 目录里躺着 300+ 个文件,从哪读起?
挨个读到天荒地老也读不完,而且会迷失在细节里。真正该问的是:这些块虽然功能天差地别,结构上是不是都长一个样?
💡 本质:所有块都是"同一副骨架 + 不同血肉"
任何块都逃不出四类:纯算(无外部依赖)、I/O 边界(图的出入口)、状态复用(存值)、带凭证(调外部付费服务)。吃透这四个代表,剩下 296 个你扫一眼
class Input/run() 就懂了。学范例,而非背清单。我们挑四个由简到繁的真实 Block,覆盖主要模式:
- A 纯计算:
WordCharacterCountBlock——无副作用无凭证,最纯粹。 - B I/O 边界:
AgentInputBlock/AgentOutputBlock——图的对外接口。 - C 状态复用:
StoreValueBlock——static_output 缓存值。 - D 带凭证 AI:
AIStructuredResponseGeneratorBlock——调 LLM,最有代表性。
四类范例地图:从左(无依赖的纯计算)到右(调外部付费服务的 AI 块)。看懂这四格,blocks/ 里的块都能对号入座。
这四个覆盖了"计算/接口/状态/外部调用"四类典型 Block。看懂它们,你就能读懂 blocks/ 目录里绝大多数块,也能自己写块(Day 10)。
L02
A · 纯计算块(Day 02 复习 + 深化)
WordCharacterCountBlock(blocks/count_words_and_char_block.py:11):内嵌 Input/Output → __init__ 声明元数据+测试 → run() yield 多个输出。无凭证、无副作用。
📝 具体例子:一次纯计算块的输入→输出
输入
{"text": "hello world foo"} → run() 内部数一数 → 依次 yield "word_count", 3 和 yield "character_count", 15。同样的输入永远得到同样的输出(确定性),所以它的 test_input/test_output 能直接写死断言,测试起来最省心。纯计算块的价值
很多 Block 就是"接收数据、算一下、输出"——数单词、格式化文本、算数学、转换数据格式。这类块无副作用(不改外部世界)、无凭证(不调外部 API)、确定性强(同输入必同输出)——最容易写、最容易测。可视化编排里,这类"胶水块"用来在其他块之间转换/处理数据,非常常用。
L03
B · I/O 边界块
AgentInputBlock(blocks/io.py:29)——block_type=BlockType.INPUT,让整张 Agent 图对外暴露一个命名参数:
class Input(BlockSchemaInput):
name: str = SchemaField(description="输入参数的名字")
value: Any = SchemaField(description="传入的值", default=None)
async def run(self, input_data, *args, **kwargs) -> BlockOutput:
if input_data.value is not None:
yield "result", input_data.value
读法:INPUT 块是"图的入口"——它声明"这个 Agent 有一个叫 name 的输入参数"。Day 03 讲的"扫描 INPUT 节点自动推导 Agent 输入 schema"就靠它。注意它的
Output 直接继承 BlockSchema(不是 BlockSchemaOutput),为了避免自动加 error 字段——因为它只是接口定义,不该有 error 口。为什么需要专门的 I/O 块?
一个 Agent 要能被外部调用(传参数、拿结果)。INPUT 块标记"外部数据从这进"、OUTPUT 块标记"结果从这出"。这样 Agent 就有了清晰的"函数签名"(输入参数、返回值)。没有 I/O 块,Agent 就是个封闭的图,没法接收外部输入——I/O 块是 Agent 对外的"插座"。
L04
C · 状态复用块
StoreValueBlock(blocks/basic.py:70)——用 static_output=True 把一个值"存住"供图内多次消费:
async def run(self, input_data, **kwargs) -> BlockOutput:
yield "output", input_data.data or input_data.input
读法:它就是个"值的中转站"——存一个值,输出口是静态的(Day 03 的 is_static),所以下游能反复取这个值。类的 docstring 直接被拿来当 description(
basic.py:97 的 inspect.cleandoc)——写块时把说明写在 docstring 里即可。静态输出块的用途
比如你有个"配置值"(API 端点、阈值)要在图里多个地方用。用 StoreValueBlock 存一次,它的静态输出连到多个下游——每个下游都能取到这个值,不会"用一次就没了"。相当于图里的一个"常量/变量"。同文件的
FileStoreBlock(basic.py:19)则演示需要 execution_context 的块(下载/存文件——Day 06 讲的运行时上下文注入在这派上用场)。L05
D · 带凭证的 AI 块(最有代表性)
AIStructuredResponseGeneratorBlock(blocks/llm.py:1016)——调 LLM,是"调外部付费 API"的典范:
class Input(BlockSchemaInput):
prompt: str = SchemaField(...)
model: LlmModel = SchemaField(default=DEFAULT_LLM_MODEL, advanced=False)
credentials: AICredentials = AICredentialsField() # ★ 凭证字段
async def run(self, input_data, *, credentials: APIKeyCredentials, **kwargs):
... # credentials 是框架解析注入的真实凭证
读法:关键点:
run 签名里 credentials 是框架解析后注入的真实凭证实体(Day 05 讲的执行时注入),不是用户填的引用。Block 里用 credentials.api_key.get_secret_value() 拿到真实 key 去调 API。它属于 BlockCategory.AI。这个模式覆盖了所有"调外部服务"的块
无论调 OpenAI、Google 搜索、发 Discord 消息——套路都一样:① 声明一个
credentials 字段;② run 签名带 credentials 参数;③ 用注入的凭证调 API。凭证字段和运行时注入,是"让 Block 安全地调外部服务"的标准模式(Day 08 深入)。Block 代码里从不出现明文密钥。L06
test_mock:不花钱测 AI 块
AI 块调真实 API 要花钱,怎么自动测试?test_mock(llm.py:1111)把真实的 llm_call 换成假响应:
# test_mock 里把 llm_call 替换成返回预设假数据的函数
# 测试时不真的调 API、不花钱,但能验证 Block 的逻辑对不对
mock(模拟)是什么?为什么必要?
如果测试真的调 OpenAI,① 花钱、② 慢、③ 结果不确定(模型每次回答不同,没法断言)、④ 需要真 API key(CI 环境可能没有)。mock = 用一个"假的"替换真实调用——测试时
llm_call 直接返回预设的假响应,于是能快速、免费、确定地验证"Block 拿到这个响应后处理得对不对"。这和 crewAI 的 fakeModel、eino 的 mock 一个道理——测编排/逻辑时,把慢/贵/不确定的外部依赖换成可控的假货。L07
写块的通用套路
看完四个范例,写任何 Block 的套路都一样:
- 内嵌
class Input(BlockSchemaInput)和class Output(BlockSchemaOutput),字段用SchemaField。 - 需要调外部服务?加一个
credentials字段。 __init__里super().__init__(id=唯一UUID, description, categories, input_schema, output_schema, test_input, test_output)。- 写
async def run(self, input_data, **kwargs),用yield "口名", 值产出(多个输出多次 yield)。 - 调外部 API 的块加
test_mock。 - 放进
blocks/目录——自动被发现注册(Day 02)。
就这么固定。正因为套路统一,300+ 个 Block 才能保持一致的质量和可维护性,社区也能轻松贡献新块。Day 10 讲 SDK 会让写块更简单。
L08
今日小结 + 动手
🧠 今天你应该能回答
- 四种范例块各代表什么模式?
- I/O 边界块为什么 Output 不继承 BlockSchemaOutput?
- static_output 块(StoreValue)有什么用?
- 带凭证 AI 块的 run 签名有什么特点?
- test_mock 为什么必要?写块的通用套路?
✋ 动手
P=autogpt_platform/backend/backend/blocks
sed -n '11,40p' $P/count_words_and_char_block.py # A 纯计算
sed -n '29,110p' $P/io.py | head -50 # B I/O 边界
sed -n '70,115p' $P/basic.py # C 状态复用
sed -n '1016,1120p' $P/llm.py | head -50 # D 带凭证 AI + test_mock
明天预告 · Day 08:Block 凭证系统——Block 怎么声明和获取 API Key/OAuth。引用 vs 实体两层结构、CredentialsField、discriminator 智能推断、命名强约束。