玩转LlamaIndex:流式输出与并发执行

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

本文是 LlamaIndex Workflow 系列第 3 篇

前两篇我们写的 Workflow 都是"跑完才返回":

Python 复制代码
result = await w.run(input="xxx")
print(result)  # 等所有 step 跑完才看到结果

但真实业务里,等那么久,谁TMD用。

LLM 一个 token 一个 token 地吐多个分支可以同时跑------这两种"快"我们都要。

一、为什么需要流式输出?

想象一个 RAG 场景:用户问"介绍一下 LlamaIndex",Agent 要,

  1. 先去知识库检索(2 秒)
  2. 再调 LLM 生成回答(5 秒)

总共 7 秒。

如果用 w.run(),用户得盯着空白屏幕等 7 秒。

用流式输出,LLM 每生成一个 token 就能立刻推给前端,用户 2 秒后就开始看到内容,体验天差地别。

二、流式输出:3 种粒度

LlamaIndex Workflow 提供 3 个层次的流式能力,从粗到细:

粒度API适用场景Step 级handler.stream_events()想知道"现在跑到哪一步了"Event 级ctx.write_event_to_stream()想在 step 中间吐自定义数据Token 级LLM streaming + 事件想知道 LLM 每个 token

2.1 Step 级流式:看进度

最简单------await w.run() 改成 handler = w.run(),然后迭代事件:

Python 复制代码
class ProgressWorkflow(Workflow):
    @step
    async def step1(self, ctx: Context, ev: StartEvent) -> Step1DoneEvent:
       await asyncio.sleep(1)
       # ✅ 显式写入流,stream_events 才能收到
       ctx.write_event_to_stream(ProgressEvent(msg = "第 1 步完成"))
       # ✅ 返回不同类型的事件,路由到 step2
       return Step1DoneEvent(msg = "第 1 步完成")

    @step
    async def step2(self, ctx: Context, ev: Step1DoneEvent) -> Step2DoneEvent:
       await asyncio.sleep(1)
       ctx.write_event_to_stream(ProgressEvent(msg = "第 2 步完成"))
       return Step2DoneEvent(msg = "第 2 步完成")

    @step
    async def step3(self, ctx: Context, ev: Step2DoneEvent) -> StopEvent:
       await asyncio.sleep(1)
       ctx.write_event_to_stream(ProgressEvent(msg = "第 3 步完成"))
       return StopEvent(result = "全部完成!")


# ========== 3. 运行 ==========
async def main():
    w = ProgressWorkflow()
    handler = w.run()  # 注意:返回 handler,不 await

    # 异步迭代事件流
    async for event in handler.stream_events():
       if isinstance(event, ProgressEvent):
          print(f"📍 进度: {event.msg}")

    # 最后别忘了取结果
    result = await handler
    print(f"🎯 最终: {result}")

运行你会看到:

2.2 关键:handler vs await w.run()

Python 复制代码
# 普通模式:一锤定音
result = await w.run()  # 阻塞,跑完才返回

# 流式模式:边跑边看
handler = w.run()  # 立刻返回 handler
async for event in handler.stream_events():
    print(event)
result = await handler  # 最后 await 拿结果

两个 API 都能跑 Workflow,区别只是"你想不想看中间过程"。

2.3 Event 级流式:在 step 中间吐数据

有时候你想在一个 step 内部 就吐数据,而不是等 step 跑完。这种时候用 ctx.write_event_to_stream():

Python 复制代码
@step
async def long_running_step(self, ctx: Context, ev: StartEvent) -> StopEvent:
    """模拟一个跑得很慢的 step,每 0.5 秒吐一次进度"""
    for i in range(10):
        await asyncio.sleep(0.5)
        # 关键:用 ctx 直接往流里写事件
        await ctx.write_event_to_stream(
            ProgressEvent(msg=f"处理中... {i+1}/10")
        )
    return StopEvent(result="跑完了")

使用场景:

  • 长任务的实时进度
  • 工具调用过程中吐中间结果(比如文件上传到 50% 时报告一下)
  • LLM 流式生成的每个 token(下面会详细说)

三、LLM Token 级流式:让大模型逐字输出

这是最有用的场景。

LlamaIndex 的 LLM 都支持 streaming,把 LLM 的每个 token 通过 ctx.write_event_to_stream 吐到事件流里。

3.1 完整代码

Python 复制代码
class StreamLLMWorkflow(Workflow):
    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        self.llm = DeepSeek(model="deepseek-chat", api_key=os.getenv("DEEPSEEK_API_KEY"))

    @step
    async def generate(self, ctx: Context, ev: StartEvent) -> StopEvent:
        # 关键:stream=True 让 LLM 流式返回
        response = await self.llm.astream_complete(ev.prompt)

        full_text = ""
        async for chunk in response:
            delta = chunk.delta  # 本次新增的 token
            full_text += delta
            # 立刻把 delta 推到事件流
            ctx.write_event_to_stream(TokenEvent(delta=delta))

        return StopEvent(result=full_text)


async def main():
    w = StreamLLMWorkflow()
    handler = w.run(prompt="用 50 字介绍一下 Python")

    async for event in handler.stream_events():
        if isinstance(event, TokenEvent):
            # 实时打印每个 token
            print(event.delta+'\n', end="", flush=True)

    result = await handler
    print(f"\n\n完整结果: {result}")

效果:你会看到文字一个 token 一个 token 地"蹦"出来,而不是等 5 秒后突然出现一大段。

3.2 关键代码解读

Python 复制代码
response = await self.llm.astream_complete(ev.prompt)
#                                ^^^^^^^ 多了个 astream
  • astream_complete:异步 + 流式
  • chunk.delta:本次相对上一次新增的内容
  • write_event_to_stream:写到事件流,不阻塞 step 继续执行

四、并发执行:让多个 step 同时跑

流式解决了"边跑边看"的问题,现在解决"跑得更快"。

4.1 串行 vs 并发

串行 9 秒,并发 3 秒------3 倍提速。

4.2 关键 API:send_event + collect_events

LlamaIndex 用事件广播 + 收集模式实现并发:

  • ctx.send_event(event):发一个事件,不阻塞
  • @step(num_workers=N):这个 step 同时跑 N 个实例
  • ctx.collect_events(ev, expected=3):等 3 个事件都到了再继续

4.3 完整代码:并发调用 3 个 LLM

Python 复制代码
@step(num_workers = 3)  # 关键:开 3 个 worker 同时跑
async def query_llm(self, ctx: Context, ev: QueryEvent) -> AnswerEvent:
    response = await self.llm.acomplete(ev.question)
    return AnswerEvent(answer = str(response), source = ev.question[:20])

@step
async def collect(self, ctx: Context, ev: AnswerEvent) -> StopEvent:
    """收集:等 3 个回答都到了,合并结果"""
    results = ctx.collect_events(ev,  expected=[AnswerEvent] * 3)
    if results is None:
       return None  # 没收集完,继续等
    # 合并 3 个回答
    combined = "\n".join([f"• {r.source}: {r.answer}" for r in results])
    return StopEvent(result = combined)

效果 :3 个 LLM 调用同时进行,总耗时 = 最慢的那个(约 3 秒),而不是 3 × 3 = 9 秒。

4.4 关键代码逐行拆解

(1) 扇出用 send_event****,不是 return

Python 复制代码
ctx.send_event(QueryEvent(question=q))  # 不阻塞,塞进事件队列
return  # 关键:不要 return 任何东西,否则就变成串行了

(2) num_workers 控制并发度

Python 复制代码
@step(num_workers=3)  # 最多同时跑 3 个
async def query_llm(self, ctx: Context, ev: QueryEvent) -> AnswerEvent:
    ...

如果你的 LLM API 有 rate limit,可以把 num_workers 调小,比如 5 或 10。

(3) collect_events 是同步点

Python 复制代码
results = ctx.collect_events(ev, expected=3)
if results is None:
    return None  # ← 没收集完,这一步先结束,框架会自动重入
# 收集完了,继续往下走

collect_events阻塞 直到拿到 expected 个事件,或者返回 None 表示"还没完,稍后再来"。

五、并发 + 流式:边并发边吐结果

最有用的组合------多个 LLM 并发调用,每个 LLM 又流式输出 token。

Python 复制代码
class StreamParallelWorkflow(Workflow):
    @step
    async def fan_out(self, ctx: Context, ev: StartEvent) -> QueryEvent:
        for q in ev.queries:
            ctx.send_event(QueryEvent(question=q))

    @step(num_workers=3)
    async def query_llm_stream(self, ctx: Context, ev: QueryEvent) -> AnswerEvent:
        # 这次用流式
        response = await self.llm.astream_complete(ev.question)
        full = ""
        async for chunk in response:
            full += chunk.delta
            # 每个 token 都吐到事件流
            await ctx.write_event_to_stream(TokenEvent(delta=chunk.delta))
        return AnswerEvent(answer=full, source=ev.question[:20])

效果:用户能在 1 秒内看到第一个 token,然后陆续看到 3 个 LLM 都在"同时打字"。

六、新手最常踩的 3 个坑

坑 1:send_event 后还 return,变成串行

Python 复制代码
# ❌ 错误:return 让框架以为"这一步完了,只能产生 1 个事件"
@step
async def fan_out(self, ctx: Context, ev: StartEvent) -> QueryEvent:
    ctx.send_event(QueryEvent(question="q1"))
    return QueryEvent(question="q2")  # 串行了!

# ✅ 正确:要么纯 send_event,要么纯 return
@step
async def fan_out(self, ctx: Context, ev: StartEvent):
    for q in questions:
        ctx.send_event(QueryEvent(question=q))
    # 没有 return

坑 2:collect_events 忘了判 None

SQL 复制代码
# ❌ 错误:没判 None,可能拿到 None 还往下走
@step
async def collect(self, ctx: Context, ev: AnswerEvent) -> StopEvent:
    results = ctx.collect_events(ev, expected=3)
    return StopEvent(result="\n".join(results))  # results 可能是 None!

# ✅ 正确:判 None 后再 return
@step
async def collect(self, ctx: Context, ev: AnswerEvent) -> StopEvent | None:
    results = ctx.collect_events(ev, expected=[AnswerEvent] * 3)
    if results is None:
        return None  # 让框架知道"还没完"
    return StopEvent(result=...)

坑 3:流式事件没在主流程消费

Python 复制代码
# ❌ 错误:用 w.run() 而不是 handler
result = await w.run()  # 流式事件被框架"吞掉",看不到!

# ✅ 正确:用 handler 才能拿到 stream
handler = w.run()
async for event in handler.stream_events():
    ...
result = await handler

七、性能对比:并发效果

八、本篇小结

这一篇我们把 Workflow 的"快"拉到了极限:

  • 流式输出 3 种粒度 :
    • stream_events() 看 step 进度
    • write_event_to_stream() 在 step 中间吐数据
    • LLM astream_complete() 逐 token 输出
  • 并发执行 :send_event 扇出 + num_workers 并行 + collect_events 汇合
  • 组合使用:并发 + 流式,边跑边看多个 LLM 同时打字
  • 三大坑:send_event 后别 return、collect 要判 None、流式必须用 handler

核心心法 :流式管"看",并发管"快"------流式让用户不再干等,并发让程序不再空转。

相关推荐
哈__42 分钟前
Windows部署Next AI Draw.io:用大模型生成并修改draw.io图表
人工智能·draw.io
2301_7803567042 分钟前
【技术解码】全视通“物联数据中台”如何打通医养结合全链路?
大数据·人工智能·物联网
乔氪智造43 分钟前
21 个免费大模型入口,实测只有 9 个能用
人工智能
正经教主43 分钟前
AI提示词工程(高阶)第15课:ReAct模式与工具调用
人工智能
SixHateSeven1 小时前
用 TRAE 自动生成股票分析报告,从敲命令到一句话搞定
人工智能
安逸Ai1 小时前
多模态模型是什么?文本、图像、音频如何统一理解?
人工智能
张张123y1 小时前
Hermes Agent 是什么?它能做什么,以及如何部署到飞书
人工智能·python·飞书
架构小何1 小时前
从检索到可靠执行:Context System 赋能灵巧手Agent工程落地实践
人工智能
H_Peak1 小时前
Tines 3B 模型本地部署与调用实战指南
人工智能