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 带凭证 AIAIStructuredResponseGeneratorBlock——调 LLM,最有代表性。
A 纯计算 输入→算→输出 无凭证·无副作用 数单词块 B I/O 边界 图的出入口 对外的"插座" AgentInput/Output C 状态复用 存一个值 静态输出·反复取 StoreValue D 带凭证 AI 调外部付费 API 凭证注入·最典型 AIStructuredResponse ← 由简到繁:越往右,依赖越多、越接近"真实生产块" →
四类范例地图:从左(无依赖的纯计算)到右(调外部付费服务的 AI 块)。看懂这四格,blocks/ 里的块都能对号入座。
这四个覆盖了"计算/接口/状态/外部调用"四类典型 Block。看懂它们,你就能读懂 blocks/ 目录里绝大多数块,也能自己写块(Day 10)。
L02

A · 纯计算块(Day 02 复习 + 深化)

WordCharacterCountBlockblocks/count_words_and_char_block.py:11):内嵌 Input/Output → __init__ 声明元数据+测试 → run() yield 多个输出。无凭证、无副作用。

📝 具体例子:一次纯计算块的输入→输出 输入 {"text": "hello world foo"} → run() 内部数一数 → 依次 yield "word_count", 3yield "character_count", 15同样的输入永远得到同样的输出(确定性),所以它的 test_input/test_output 能直接写死断言,测试起来最省心。
纯计算块的价值 很多 Block 就是"接收数据、算一下、输出"——数单词、格式化文本、算数学、转换数据格式。这类块无副作用(不改外部世界)、无凭证(不调外部 API)、确定性强(同输入必同输出)——最容易写、最容易测。可视化编排里,这类"胶水块"用来在其他块之间转换/处理数据,非常常用。
L03

B · I/O 边界块

AgentInputBlockblocks/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 · 状态复用块

StoreValueBlockblocks/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:97inspect.cleandoc)——写块时把说明写在 docstring 里即可。
静态输出块的用途 比如你有个"配置值"(API 端点、阈值)要在图里多个地方用。用 StoreValueBlock 存一次,它的静态输出连到多个下游——每个下游都能取到这个值,不会"用一次就没了"。相当于图里的一个"常量/变量"。同文件的 FileStoreBlockbasic.py:19)则演示需要 execution_context 的块(下载/存文件——Day 06 讲的运行时上下文注入在这派上用场)。
L05

D · 带凭证的 AI 块(最有代表性)

AIStructuredResponseGeneratorBlockblocks/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_mockllm.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 的套路都一样:

  1. 内嵌 class Input(BlockSchemaInput)class Output(BlockSchemaOutput),字段用 SchemaField
  2. 需要调外部服务?加一个 credentials 字段。
  3. __init__super().__init__(id=唯一UUID, description, categories, input_schema, output_schema, test_input, test_output)
  4. async def run(self, input_data, **kwargs),用 yield "口名", 值 产出(多个输出多次 yield)。
  5. 调外部 API 的块加 test_mock
  6. 放进 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 08Block 凭证系统——Block 怎么声明和获取 API Key/OAuth。引用 vs 实体两层结构、CredentialsField、discriminator 智能推断、命名强约束。
← Day 06 Block 基类 Day 08 · 凭证系统 →