阅读时长:约 10 分钟 难度:⭐⭐⭐
本文是 LlamaIndex Workflow 系列第 3 篇
前两篇我们写的 Workflow 都是"跑完才返回":
Python
result = await w.run(input="xxx")
print(result) # 等所有 step 跑完才看到结果
但真实业务里,等那么久,谁TMD用。
LLM 一个 token 一个 token 地吐 、多个分支可以同时跑------这两种"快"我们都要。
一、为什么需要流式输出?
想象一个 RAG 场景:用户问"介绍一下 LlamaIndex",Agent 要,
- 先去知识库检索(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
核心心法 :流式管"看",并发管"快"------流式让用户不再干等,并发让程序不再空转。