阅读时长:约 12 分钟 难度:⭐⭐
先看结果吧
一、为什么需要 Workflow?
如果你写过 Agent, 啃腚经历过这种崩溃:
# 这种"函数套函数"的写法,跑 3 步还行,跑 30 步直接看花眼
def main():
step1()
if condition:
step2()
step3()
else:
step4()
if another_condition:
step5()
# ...200 行后,你自己都不知道在干啥
这种面条式代码有三个问题:
- 状态到处飞 — 局部变量、返回值、副作用混在一起
- 分支难调试 — 一个 if 嵌套 5 层,出错根本不知道卡哪
- 没法可视化 — 画个流程图?不好意思,代码里没有"流程"这个概念
LlamaIndex Workflow 就是为这个而生的。
它把"流程"变成了一等公民:每一步都是独立的方法,数据靠"事件"传递,整个流程可以画成图、调试、复用。
二、Workflow 的核心:5 个零件
一句话总结:事件是接力棒,step 是跑步的人,Workflow 是赛道。
三、第一个 Workflow:3 步流程
写一个最经典的"三步走"流程,每一步处理一件事:
四、代码逐段拆解
4.1 自定义事件:为啥要搞个类?
class FirstEvent(Event):
first_output: str
每个事件就是一个数据结构,在步骤之间传数据用。Pydantic 风格,字段有类型注解:
first_output: str— 必填字符串字段- 你可以加默认值:
first_output: str = "default" - 可以加 List、Dict、甚至嵌套其他 BaseModel
为啥不直接用 dict****? 因为 Workflow 是类型驱动的,后面会看到,框架根据事件的"类型"来决定哪个 step 被触发。dict 没法做类型分发。
4.2 @step 装饰器:它是"触发器"
@step
async def step_one(self, ev: StartEvent) -> FirstEvent:
这行代码告诉框架:
step_one是一个步骤- 它接收
StartEvent类型的事件 — 流程启动时自动被触发 - 它返回
FirstEvent类型的事件 — 触发的下一步会收到这个事件
关键点:类型注解就是 Workflow 的"流程图"。框架通过分析 ev: XXX 和 -> YYY 自动建图,你不用手写任何连线代码。
4.3 StopEvent:流程的"出口"
return StopEvent(result="整个流程跑完了!")
返回 StopEvent 表示"流程结束",result 字段就是整个 Workflow 的最终输出。
任何一步返回了 StopEvent,流程立即终止。
4.4 运行方式
w = MyWorkflow(timeout=10, verbose=False)
result = await w.run(first_input="开始!")
timeout=10:超时抛异常(防卡死)verbose=False:不打印内部日志,生产环境关掉w.run(first_input="开始!"):first_input会传给StartEvent的字段await:因为整个 Workflow 是异步的
五、流程图可视化:框架自带的福利
LlamaIndex 提供了一个超好用的可视化工具,一行代码生成流程图:
# pip install llama-index-utils-workflow
from llama_index.utils.workflow import draw_all_possible_flows
draw_all_possible_flows(MyWorkflow, filename="workflow.html")
跑完会在当前目录生成一个 workflow.html,浏览器打开就能看到带颜色标注的流程图,所有可能的分支都会画出来。这玩意儿写文档、做汇报、debug 都巨好用。
六、子类化:Workflow 的"继承"模式
如果你想复用已有 Workflow 的一部分步骤,直接继承 + 重写就行:
class CustomWorkflow(MyWorkflow):
"""继承 MyWorkflow,重写 step_two,多加一步 step_two_b"""
@step
async def step_two(self, ev: FirstEvent) -> SecondEvent:
# 重写父类的 step_two,加个"发送邮件"逻辑
print(f"自定义 Step 2: 发送邮件 + {ev.first_output}")
return SecondEvent(second_output="邮件已发送")
@step
async def step_two_b(self, ev: SecondEvent) -> ThirdEvent:
# 新增的步骤:发完邮件再发短信
print(f"自定义 Step 2B: 发短信 + {ev.second_output}")
return ThirdEvent(third_output="短信也发了")
@step
async def step_three_b(self, ev: ThirdEvent) -> StopEvent:
print(f"Step 3 收到: {ev.second_output}")
return StopEvent(result="整个流程跑完了!")
效果:step_one 和 step_three 还是用父类的,但 step_two 被换成了发邮件版,中间多了个 step_two_b 发短信。
这个模式特别适合:
- 不同业务线共用一套基础流程,只在某些节点做定制
- A/B 测试:同一套流程,换不同的 step 实现来对比效果
- 插件化架构:核心流程稳定,周边步骤可插拔
七、新手最常踩的 3 个坑
坑 1:忘了把方法标记为 async
# ❌ 错误:不是 async
@step
def step_one(self, ev: StartEvent) -> FirstEvent:
return FirstEvent(first_output="x")
# ✅ 正确:必须是 async
@step
async def step_one(self, ev: StartEvent) -> FirstEvent:
return FirstEvent(first_output="x")
坑 2:事件"没人接"导致流程卡死
class MyWorkflow(Workflow):
@step
async def step_one(self, ev: StartEvent) -> FirstEvent:
return FirstEvent(first_output="x")
# ❌ 漏了处理 FirstEvent 的步骤!
# 流程会卡在 FirstEvent,直到 timeout
解决:要么补一个处理 FirstEvent 的 step,要么让 step_one 直接返回 StopEvent。
坑 3:类型注解"指向自己"造成死循环
# ❌ 错误:start 又返回 StartEvent,会无限循环
@step
async def start(self, ev: StartEvent) -> StartEvent:
return StartEvent(...)
解决:返回别的事件类型,或者加个终止条件。
八、本篇小结
这一篇我们搞清楚了:
- Workflow 是什么:把"流程"变成代码一等公民,每步独立、事件驱动
- 5 个核心零件:
Workflow、@step、StartEvent、StopEvent、Event - 怎么跑通第一个 3 步流程:定义事件 → 写步骤 →
await w.run() - 子类化复用:继承 + 重写 step,核心流程稳定,周边步骤可定制
- 可视化神器:
draw_all_possible_flows,一行代码生成流程图
下一篇我们讲 分支、循环与状态管理 — 让 Workflow 会"判断"和"记住东西",从静态流程变成动态业务引擎。