上一篇:对照 Dify 搭学习仓:先让后端能跑起来
示例仓库:flow-forge
本篇讲:在已能探活、能连库的后端上,第一次把Workflow跑通——用一份图描述步骤、触发一次运行、事后按run_id回看逐步事件。
先说结论
上一篇只证明「壳子活着」。本篇补的是工作流产品里最短的一条可演示路径:
| 能力 | 你能感知到什么 |
|---|---|
| 持久化图定义 | 用POST /workflows交一份「步骤图」,之后能按 id 读回来 |
| 同步执行 | 用POST .../runs发一次请求,服务在同一次请求里跑完 Start → Template → End |
| 运行可回看 | 拿回一个run_id,再查终态和逐步事件,不必开推送长连接 |
一句话:本篇证明「一张最小图能被创建、跑完、事后复盘」;不做画布、LLM 节点,也不做 SSE/队列。
跟跑时你主要和两种「文字格式」打交道,都不是 Python 专属:
| 格式 | 像什么 | 本篇哪里会用到 |
|---|---|---|
| JSON | 花括号包起来的键值对,前后端传数据的常用包装 | 创建图、启动运行时的请求体;接口返回 |
| HTTP | 对某个网址发 GET/POST,拿回状态码和正文 | curl跟跑全程 |
Python 只是本仓用来实现这些能力的语言;你先会调接口,再回头看源码,进度更顺。
1. 上一篇停在哪,本篇补哪块?
壳子阶段有探活、分层空位、SQLite 连通,但还没有:
- 什么叫「一份工作流定义」
- 怎么触发「跑一次」
- 跑的过程如何留下可查询记录
对照 Dify 一类产品,用户真正关心的正是这三块。本仓用 OpenSpec changegraph-runner切出最小可跑切片:节点只留三种,执行在请求内做完,事件先写入数据库,给以后「反复查询进度」留句柄。
| 术语 | 是什么 | 本仓怎么用 |
|---|---|---|
| 图(graph) | 工作流定义:有哪些步骤、谁连谁 | 一份 JSON,存进数据库 |
| 节点(node) | 图上的一步 | 本阶段只有:开始、模板拼接、结束 |
| 边(edge) | 谁连到谁 | 每条边写清「从哪来 / 到哪去」 |
| Run | 某一次执行 | 有稳定编号run_id;结果是成功或失败 |
| Event | 这一次运行里的逐步记录 | 例如某节点开始了、成功了、失败了 |
| Runner(执行器) | 负责按图把节点跑完的程序入口 | 今天在 HTTP 请求里直接跑;以后可改成排队,事件结构尽量不动 |
2. 功能一:用图描述「开始 → 模板 → 结束」
2.1 图长什么样(先看 JSON,不看 Python)
一份合法图至少有两块:
| 字段 | 含义 |
|---|---|
nodes | 步骤列表;每个步骤有自己的id,类型写在data.type |
edges | 连线列表;用source/target指向节点的id |
三种节点:
data.type | 干什么 |
|---|---|
start | 入口;你这次运行提交的输入从这里进入 |
template | 把输入填进一句模板(例如Hello, {name}!),得到一段文字 |
end | 收尾,把最终文字当作本次运行的输出 |
非法图(未知节点类型、边指到不存在的节点、两个节点共用同一个 id 等)在创建时就会被拒绝,不会变成「跑到一半才炸」。
最小示例(逻辑上就是:开始 → 拼一句 Hello → 结束):
{"nodes":[{"id":"start_1","data":{"type":"start"}},{"id":"tpl_1","data":{"type":"template","template":"Hello, {name}!"}},{"id":"end_1","data":{"type":"end"}}],"edges":[{"id":"e1","source":"start_1","target":"tpl_1"},{"id":"e2","source":"tpl_1","target":"end_1"}]}{name}不是神秘语法:它表示「这里以后要换成名叫name的输入」。你启动运行时若传"name": "Forge",拼出来就是Hello, Forge!。
2.2 接口怎么用
| 动作 | 方法与路径 | 你交出什么 | 你拿到什么 |
|---|---|---|---|
| 创建工作流 | POST /workflows | { "graph": <上面那份图> } | 工作流id |
| 读取工作流 | GET /workflows/<id> | 无 body | 同一份图 |
2.3 仓库里校验在干什么(可选读)
实现上,校验写在graph.py。你暂时只需知道它在做三件事:
- 检查每个节点有没有合法的
type - 检查节点
id是否重复 - 检查每条边的两端是不是都指向已有节点
顺带认一个 Python 词:源码里常见的class ...可以先当成「一份带规则的表格模板」——声明图里允许出现哪些字段、不合法时抛出错误。不必先背class语法;看到「校验图」就定位到这个文件即可。
3. 功能二:触发一次运行,同请求内跑完整张图
创建只是「把菜谱存起来」。真正干活是启动一次Run:
| 动作 | 方法与路径 | 请求体示例 |
|---|---|---|
| 启动运行 | POST /workflows/<workflow_id>/runs | { "inputs": { "name": "Forge" } } |
inputs就是一张「名字 → 值」的对照表。传给模板里的{name}用。
服务端按固定顺序做事(你无需读代码也能跟):
- 新建一条 Run,状态进入「执行中」
- 从唯一的
start出发,沿边走到下一个节点(本阶段不支持同时走两条岔路) - 遇到
template:用inputs(以及上游写出的变量)填模板;缺了需要的名字 → 这次 Run 记为失败,但服务进程继续活着 - 遇到
end:收齐最终输出 - 这次 HTTP 请求返回时,运行已经结束;正文里带
id(即run_id)、status,成功时还有outputs
成功时outputs大致是:
{"text":"Hello, Forge!"}执行器在仓库里怎么走(白话版)
对应文件:runner.py。逻辑可以记成一张流程图:
拿到图和 inputs → 记下「当前节点 = start」 → 循环: 写事件:节点开始 按类型做事(start 几乎空转 / template 填空 / end 收输出) 成功则写「节点成功」;失败则写「节点失败」并结束本次 Run 沿唯一一条出边走到下一节点;没有出边就停 → 把 Run 标成 succeeded 或 failed,写回数据库顺带认两个 Python 词(仍不必会写):
| 你在源码里可能瞥到 | 先怎么理解 |
|---|---|
dict/{ "name": "Forge" } | 「键值对照表」,和 JSON 对象很像;运行时的变量就放在这类结构里 |
while ... | 「条件还成立就重复做」;这里用来沿着边一节点一节点往下走 |
今天 HTTP 层直接调用执行器;以后若改成「先入队、后台再跑」,优先换的是谁去调用,而不是推倒事件该怎么记——这是本切片故意留下的升级缝。
4. 功能三:用run_id查终态与逐步事件
启动接口返回时你往往已经看到结果了,但产品约定仍是:以run_id为稳定编号,以后查询都认它。
| 接口 | 用途 |
|---|---|
GET /runs/<run_id> | 看终态:成功还是失败、最终输出、错误信息 |
GET /runs/<run_id>/events | 看逐步事件列表(按顺序):每个节点何时开始、成败如何 |
为什么要先落库、而不是「必须开一条推送流才能看见过程」?
因为先有可查询的事件记录,以后无论是「隔几秒再问一次」(轮询)还是推送,读的都是同一套数据。
数据库里对应三张表(名字即职责):
| 表名 | 存什么 |
|---|---|
workflows | 图定义(菜谱) |
workflow_runs | 某一次运行的输入、输出、状态 |
workflow_run_events | 该次运行的逐步事件(带序号sequence) |
表结构入口:models.py——可以先当「三张表的说明书」,不必先学 ORM。
5. 跟跑:create → run → events
- 按上一篇在
api/启动服务。 - 按
api/README.md的 curl 示例走三步(Windows / macOS 续行符不同,README 里有说明):
| 步 | 做什么 | 你要记下来的 |
|---|---|---|
| 1 | POST /workflows提交最小图 | 返回的工作流id |
| 2 | POST /workflows/<id>/runs,带上inputs | 返回的 runid,以及outputs |
| 3 | GET /runs/<run_id>与.../events | 终态与逐步事件是否对得上 |
想确认仓库自测也绿:
cdapi uv run pytestpytest是自动跑测试的工具:它替你扮演客户端,把「创建 → 运行 → 查事件」走一遍。你暂时只需知道「全绿 ≈ 这条主路径没坏」。
6. 分层空位怎么被填上?
上一篇留下的职责地图,本篇开始有实活:
| 层 | 本篇长出的内容 | 若你想点开文件 |
|---|---|---|
| controllers | 对外的 HTTP 门口:创建图、启动 run、查询 | workflows.py、runs.py |
| services | 创建/读取时的编排(先校验再存盘) | workflow_service.py |
| core/workflow | 图校验规则、真正按边执行、写事件 | graph.py、runner.py |
| models / db | 三张表;启动时建好 | models.py |
读仓库的建议顺序(仍然可以几乎不读语法):
- 先会用 curl 打通三步(本节第 5 节)
- 再打开
controllers,对照「哪个网址对应哪段门口代码」 - 最后才进
core/workflow,对照「填模板 / 写事件」发生在哪
不要在路由文件里找「模板字符串怎么替换」——那是core的事。
本篇顺带认识的 Python(一张表就够)
| 词 | 先怎么记 | 和本篇功能的关系 |
|---|---|---|
.py文件 | Python 源码文件 | 业务都在api/src/flow_forge/下 |
class | 「一类带行为的数据结构」的声明 | 图校验、执行器都是 class |
dict | 键值对照表 | inputs、运行中的变量 |
while | 条件成立就重复 | 执行器沿着边往下走 |
抛错 /raise | 主动报告「这里不行了」 | 非法图、缺变量时失败并记入 Run |
系列后续仍按「功能先、语法附注」写:每出现绕不开的词,就地用一句话钉住,不单独开语法长课。
和前作怎么接
| 篇 | 补哪一段 |
|---|---|
| 01 后端壳子 | 依赖、探活、分层空位、库能连 |
| 本篇 | 最小图、同步执行、run/event 可查询 |
下一篇更可能落在「谁来消费这些 API」(例如最小 Web 联调),而不是先把节点类型堆满。
你可以从这里带走什么?
- 工作流最短演示路径是:存图 → 跑一次 → 用 run_id 回看,不是先做画布。
- 图是定义,Run是某一次执行,Event是那一次里的时间线;三者分开,以后才好做轮询或异步。
- 跟跑优先认JSON + HTTP;Python 是实现语言,可以后看。
- Template 本阶段只做「填空成句」,不执行任意代码。
- 读源码按门口 → 编排 → 核心规则的顺序,比从上到下背语法更快建立地图。
仓库与相关文档
- GitHub:https://github.com/jimchou-h/flow-forge
- 跟跑与 curl:api/README.md
- 图校验:core/workflow/graph.py
- 执行器:core/workflow/runner.py
- HTTP(工作流):controllers/workflows.py
- HTTP(运行):controllers/runs.py
欢迎 Star、Issue 和 PR。
本文基于 Flow Forgegraph-runner:覆盖最小图定义、同步执行与按 run_id 查询事件;假定读者不必先会 Python。不包含画布、LLM 节点与 SSE/队列。