数据截至 (上游 commit 6a6b6383eb6a)
工作流引擎:用 Block 编排多步骤
30 秒导读: 前面几章讲的是"一个任务怎么跑"(02-agent-loop-planning.md 讲单步 Agent 循环)。但真实业务很少是一个任务——通常是"登录 → 循环处理每一行 → 抽取数据 → 发邮件"这种多步骤流程。工作流引擎就是把这些步骤拼成一条可复用、可参数化的流水线的东西。它的最小积木叫 Block;block 与 block 之间用参数传数据。本章讲清楚:block 是编排单元,参数是数据管道。
1. 这是什么(零基础也能懂)
一句话定义: 工作流(workflow)= 一串按顺序执行的 Block;每个 Block 是一个自带输入输出的"步骤积木"。
它解决什么问题: 假设你要每天从一个供应 商门户下载账单。这件事是:登录 → 进报表页 → 对每个月循环下载 → 把下载的文件解析成表格 → 邮件发给财务。你不想每次都写一遍代码,也不想让一个巨型 Agent"一口气自由发挥"(不可控、不可复用)。工作流让你把它拆成命名好的步骤,存成一份定义,以后换参数就能重跑。
一个 Block 长什么样(概念示意,非源码):
# 示意:一个"导航到登录页并登录"的 block
- block_type: login
label: do_login # 块的唯一名字,别的块靠它引用输出
url: "https://portal.example.com"
navigation_goal: "用给定账号登录"
parameters: [username, password]
一句话直觉: 把工作流想成一条工厂流水线——每个工位(Block)干一件事,上一个工位的产出(OutputParameter)顺着传送带流到下一个工位。有的工位是"派个工人去网页上干活"(浏览器任务类 block),有的工位是"纯机器计算"(代码/HTTP/解析类 block),还有的是"控制传送带走向"(循环、分支)。
它和"单步 Agent"的分工:
| 关切 | 谁负责 | 本章讲不讲 |
|---|---|---|
| 网页里的一步动作(点哪、填什么) | 单步 Agent 循环 | 不讲,见 02 |
| 一句话目标自主拆解成整条流程 | Skyvern 2.0 规划器 | 不讲,见 06 |
| 把多个任务显式编排成可复用流水线 | 工作流引擎 / Block | 本章 |
2. 顶层全景(它大概怎么转)
怎么读下面这张图: 从上到下是"一次工作流运行"的生命周期;左边是用户给的东西,右边是引擎内部。
用户 引擎入口 编排层 积木层
┌──────┐ run_workflow ┌──────────────┐ execute_workflow ┌──────────────────┐
│ 参数 │ ───────────────> │ prepare_ │ ─────────────────> │ 逐个 block: │
│ 值 │ library/ │ workflow │ service.py │ block.execute_ │
└──────┘ skyvern.py │ 建 WorkflowRun│ │ safe() │
└──────────────┘ └────────┬─────────┘
│ │
│ 初始化 ┌────────▼─────────┐
▼ │ 浏览器任务类 │
┌───────────────────┐ 取/写参数值 │ → 落到单步 Agent │
│ WorkflowRunContext │ <──────────────>│ 控制流类 → 循环/ │
│ (参数 + 值的仓库) │ │ 分支 │
└───────────────────┘ │ 副作用类 → 直接干│
└──────────────────┘
部件一句话职责:
| 部件 | 干什么 | 在哪个文件 |
|---|---|---|
run_workflow | 公共入口:接参数、建运行、丢给执行器 | skyvern/services/workflow_service.py:149;库封装 skyvern/library/skyvern.py:369 |
WorkflowService.execute_workflow | 编排:按顺序取出 block 挨个跑,处理失败/终止/取消 | skyvern/forge/sdk/workflow/service.py:4759 |
Block / 各子类 | 积木本身:每类 block 一件事 | skyvern/forge/sdk/workflow/models/block.py:594 |
WorkflowRunContext | 参数与值的中央仓库,block 间传数据靠它 | skyvern/forge/sdk/workflow/context_manager.py:118 |
block_yaml_to_block | 把 no-code 的 YAML 定义翻译成运行时 Block 对象 | skyvern/forge/sdk/workflow/workflow_definition_converter.py:467 |
主线走一遍(高层,不进代码):
- 用户调
run_workflow,给一份 workflow 的 id + 一个参数字典。 - 引擎建一个
WorkflowRun,初始化WorkflowRunContext(把用户参数值灌进去)。 - 编排层从 workflow 定义里拿到 block 列表,
for block in blocks挨个调block.execute_safe(...)。 - 每个 block 跑完把产出写回 context(键叫
<label>_output),下一个 block 用 Jinja 模板{{ do_login.output }}就能引用到。 - 某个 block 失败/终止 → 看它的
continue_on_failure,决定是整条流程停,还是跳过继续。
3. 核心概念一:Block 是编排单元
3.1 Block 家族全景
Skyvern 的 block 类型由一个枚举 BlockType 定义(skyvern/schemas/workflows.py:474)。按"干什么"可以分成三大类——这是理解整章的骨架:
| 大类 | 干什么 | 代表 block |
|---|---|---|
| 浏览器任务类 | 派一个"单步 Agent"去网页上完成一个子目标 | TaskBlock NavigationBlock ExtractionBlock ValidationBlock LoginBlock FileDownloadBlock ActionBlock UrlBlock |
| 控制流类 | 不干活,只决定传送带怎么走 | ForLoopBlock WhileLoopBlock ConditionalBlock |
| 副作用 / 纯计算类 | 不碰浏览器 Agent,直接执行确定性逻辑 | CodeBlock TextPromptBlock SendEmailBlock FileParserBlock HttpRequestBlock UploadToS3Block FileUploadBlock WaitBlock PDFParserBlock PrintPageBlock HumanInteractionBlock TaskV2Block WorkflowTriggerBlock |
关键区分(全章最重要的一条): 浏览器任务类 block 自己不执行网页动作——它把自己"翻译"成一个 Task + Step,交给单步 Agent 循环去跑(§4.2) 。其余两类是引擎自己就地处理。
3.2 继承结构
Block (抽象基类, block.py:594)
├── BaseTaskBlock (block.py:1300) ← 浏览器任务类的公共父类
│ ├── TaskBlock / NavigationBlock / ExtractionBlock
│ ├── ValidationBlock / LoginBlock / FileDownloadBlock
│ ├── ActionBlock / UrlBlock / HumanInteractionBlock
├── ForLoopBlock / WhileLoopBlock / ConditionalBlock ← 控制流
└── CodeBlock / TextPromptBlock / SendEmailBlock / ... ← 副作用/纯计算
具体的浏览器 block 大多是空壳——只声明 block_type,行为全在 BaseTaskBlock 里。例如整个 NavigationBlock 就一行有效代码:
class NavigationBlock(BaseTaskBlock):
block_type: Literal[BlockType.NAVIGATION] = BlockType.NAVIGATION
—— block.py:11493。区别只在语义标签(engine/goal 的默认解读),执行路径共享。
3.3 Block 抽象基类:公共骨架
每个 block 至少带这几样字段(block.py:594 class Block):
| 字段 | 作用 |
|---|---|
label | 块的唯一名字,别的块靠它引用输出 |
block_type | 判别类型(Pydantic 靠它做 discriminated union) |
output_parameter | 这个块的产出插槽,是数据管道的接口(§5) |
continue_on_failure | 失败时整条流程停还是跳过继续 |
next_block_label | v2 图式定义里指向下一个块;省略则按顺序 |
真正的执行契约是两个方法:
execute(...)—— 抽象方法,每类 block 自己实现"我这一步干什么"(block.py:1025)。execute_safe(...)—— 统一包装,所有 block 共用:建数据库记录、截图、调execute、兜底 catch 异常(block.py:1137)。
execute_safe 是编排层唯一调用的入口。它做了三件不显眼但重要的事(block.py:1158-1247):
- 建
workflow_run_block记录——每个 block 每次执行在库里有一行,带label/block_type/status,前端靠它显示进度。 - 执行前截图——存一张
SCREENSHOT_LLM工件,方便回放调试(block.py:1190)。 - 兜底容错——
execute抛任何异常都被 catch,转成一个success=False的BlockResult而不是让整个进程崩(block.py:1227-1247)。