玩转LlamaIndex:分支循环

12 阅读3分钟

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

上一篇我们写的 Workflow 是个"直筒子":A → B → C,一条道走到黑,

这一篇就把 Workflow 从"静态流程图"变成"动态业务引擎",

img

一、Workflow 的"状态机"本质

先纠正一个误区:Workflow 不是流程图,是状态机,

普通流程图描述的是"控制流"(先做 A 再做 B),而 Workflow 描述的是"事件流"(接收什么事件 → 产生什么事件),

理解了这一点,分支和循环就只是"事件路由"的不同表现而已,

img

你看,这就是个典型的"分支 + 循环"的状态机,,

二、第一个分支:让 Workflow 会"判断"

2.1 需求场景

假设我们有个"数据校验"Workflow:

  • 输入一个数字
  • 如果是偶数 → 走"处理"分支
  • 如果是奇数 → 走"重试"分支

2.2 完整代码(太长,不想贴)

,..
class BranchWorkflow(Workflow):
    @step
    async def start(self,ev:StartEvent)->NumberEvent:
       """接收初始输入"""
       return NumberEvent(number = ev.input_number)

    @step
    async def classify(self,ev:NumberEvent)->EvenEvent|OddEvent:
       """关键:返回类型用 Union,框架根据类型自动路由"""
       if(ev.number) %2 ==0:
          return EvenEvent(number = ev.number)
       else:
          return OddEvent(number = ev.number)

    @step
    async def handle_even(self, ev: EvenEvent) -> StopEvent:
       """处理偶数分支"""
       return StopEvent(result = f"处理偶数: {ev.number} / 2 = {ev.number // 2}")

    @step
    async def handle_odd(self, ev: OddEvent) -> StopEvent:
       """处理奇数分支"""
       return StopEvent(result = f"奇数 {ev.number} 需要换个数,重试中...")
...

2.3 运行效果

img

2.4 关键代码解读

第 1 个关键:Union 返回类型

async def classify(self, ev: NumberEvent) -> EvenEvent | OddEvent:

-> EvenEvent | OddEvent 告诉框架:"我可能返回这两种事件中的任何一种,你看着办"。

框架会自动分析类型注解,然后把 EvenEvent 路由到 handle_even,把 OddEvent 路由到 handle_odd,

这才是workflow的优雅之处!!!

img

三、第一个循环:让 Workflow 会"重试"

3.1 需求场景

接着上面的例子,如果用户输入的是奇数,我们想让他重新输入,直到他给个偶数为止。

这就是"循环"——在 Workflow 里,循环本质上是:"事件路由回自己"。

3.2 完整代码

class LoopWorkflow(Workflow):
    @step
    async def start(self,ev:StartEvent)->NumberEvent:
       """接收初始输入"""
       return NumberEvent(number = ev.input_number)

    @step
    async def classify(self,ev:NumberEvent)->EvenEvent|RetryEvent:
       """关键:返回类型用 Union,框架根据类型自动路由"""
       if(ev.number) %2 ==0:
          return EvenEvent(number = ev.number)
       else:
          return RetryEvent(number = ev.number, err="必须是偶数!")

    @step
    async def handle_even(self, ev: EvenEvent) -> StopEvent:
       """处理偶数分支"""
       return StopEvent(result = f"处理偶数: {ev.number} / 2 = {ev.number // 2}")

    @step
    async def ask_for_even(self, ev: RetryEvent) -> NumberEvent:
       """循环点:返回 NumberEvent 又回到 classify"""
       print(f"{ev.err},当前数字是{ev.number}")
        # 模拟用户重新输入(实际场景可以用 input())
       new_number = ev.number + 1  # 让奇数+1 变成偶数
       print(f"重新输入: {new_number}")
       return NumberEvent(number=new_number)

核心:ask_for_even 的返回类型是 NumberEvent,而 classify 接收 NumberEvent,于是就形成了一个回路:classify → ask_for_even → classify → ask_for_even → ...

img

暂时无法在飞书文档外展示此内容

3.3 加个保险:防止死循环

循环写不好就是死循环。

LlamaIndex 提供两个保险:

# 1. 全局超时
w = LoopWorkflow(timeout=10)  # 10 秒还没结束就抛异常

# 2. 循环计数器
class LoopWorkflow(Workflow):
    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        self.loop_count = 0

    @step
    async def ask_for_even(self, ev: RetryEvent) -> NumberEvent | StopEvent:
        self.loop_count += 1
        if self.loop_count > 5:
            return StopEvent(result="重试太多次,放弃")
        # ...正常逻辑

最佳实践:循环一定要有退出条件 + 超时保护,缺一不可。

四、新手最常踩坑

坑 1:循环没退出条件

# ❌ 错误:永远满足不了的条件
@step
async def loop_forever(self, ev: RetryEvent) -> RetryEvent:
    if False:  # 这个条件永远不满足
        return StopEvent(...)
    return RetryEvent(...)
# 流程会一直转,直到 timeout

# ✅ 正确:明确的退出条件
@step
async def loop_with_limit(self, ev: RetryEvent) -> RetryEvent | StopEvent:
    if ev.attempts >= 5:  # 明确限制
        return StopEvent(result="超时")
    return RetryEvent(...)

五、本篇小结

这一篇我们把 Workflow 从"静态流程图"升级成了"动态业务引擎":

  • 分支:用 Union 返回类型,框架自动路由(-> A | B)
  • 循环:让 step 返回"上一步的事件类型",形成事件回路

下一篇我们讲状态共享