玩转LlamaIndex:Agent 编排

25 阅读5分钟

阅读时长:约 12 分钟 难度:⭐⭐

先看结果吧

img

一、为什么需要 Workflow?

如果你写过 Agent, 啃腚经历过这种崩溃:

# 这种"函数套函数"的写法,跑 3 步还行,跑 30 步直接看花眼
def main():
    step1()
    if condition:
        step2()
        step3()
    else:
        step4()
        if another_condition:
            step5()
    # ...200 行后,你自己都不知道在干啥

这种面条式代码有三个问题:

  1. 状态到处飞 — 局部变量、返回值、副作用混在一起
  2. 分支难调试 — 一个 if 嵌套 5 层,出错根本不知道卡哪
  3. 没法可视化 — 画个流程图?不好意思,代码里没有"流程"这个概念

LlamaIndex Workflow 就是为这个而生的。

它把"流程"变成了一等公民:每一步都是独立的方法,数据靠"事件"传递,整个流程可以画成图、调试、复用。

img

二、Workflow 的核心:5 个零件

img

img

一句话总结:事件是接力棒,step 是跑步的人,Workflow 是赛道。

三、第一个 Workflow:3 步流程

写一个最经典的"三步走"流程,每一步处理一件事:

img

四、代码逐段拆解

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:

这行代码告诉框架:

  1. step_one 是一个步骤
  2. 它接收 StartEvent 类型的事件 — 流程启动时自动被触发
  3. 它返回 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 都巨好用。

img

六、子类化: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 实现来对比效果
  • 插件化架构:核心流程稳定,周边步骤可插拔

img

七、新手最常踩的 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 会"判断"和"记住东西",从静态流程变成动态业务引擎。