【大模型调用编排】Task A流式输出,Task B/C/D并发执行完毕后流式输出
继文献之后,希望将Task B/C/D并发执行完毕后流式输出,该如何做?
好,这个问题问得非常到位 👍
一旦 B / C / D 也改成流式,本质就变成了一个「多路流式并发 → 顺序回放」的问题。
我直接给你工程上正确、稳定、可扩展的做法,并解释为什么一定要这么做。
一、结论先行(一句话版)
B / C / D 可以并发流式跑,但绝不能直接 yield
👉 正确做法是:
「每个任务一个 Queue 缓存 token,先并发跑完 / 或边跑边存,等 A 完成后再按顺序回放这些 token」
二、为什么「不能直接 yield B / C / D」
如果你这样写(❌ 错误示范):
async for chunk in task_b():
yield chunk
问题会立刻出现:
| 问题 | 原因 |
|---|---|
| token 乱序 | A / B / C / D 都在 yield |
| SSE 事件交叉 | 客户端无法区分阶段 |
| 难以控制顺序 | generator 天然抢占 |
👉 流式并发 ≠ 流式输出
三、正确架构(B / C / D 流式,但可控)
核心思想(非常重要)
Task A → 直接 yield(前台)
Task B/C/D → 流式生成 → Queue 缓存(后台)
↓
Task A 完成后
↓
依次从 Queue 取 token → yield
四、整体结构图
┌──────────────┐
│ Task A │───► SSE 实时输出
└──────────────┘
┌──────────────┐
│ Task B │───► Queue[B] (token)
└──────────────┘
┌──────────────┐
│ Task C │───► Queue[C] (token)
└──────────────┘
┌──────────────┐
│ Task D │───► Queue[D] (token)
└──────────────┘
Task A 结束
↓
按 B → C → D 顺序回放 Queue
五、关键实现(完整可落地)
1️⃣ 流式大模型调用(复用之前)
async def llm_stream_call(name: str):
for i in range(5):
await asyncio.sleep(0.5)
yield f"{name} token {i}"
2️⃣ Task A(前台流式)
async def task_a():
async for chunk in llm_stream_call("TaskA"):
yield chunk
3️⃣ Task B / C / D(后台流式 → Queue)
⚠️ 这里是核心
async def stream_to_queue(task_name: str, queue: asyncio.Queue):
try:
async for chunk in llm_stream_call(task_name):
await queue.put(chunk)
finally:
await queue.put(None) # 结束标志
4️⃣ 流式调度器(重点中的重点)
import asyncio
async def stream_controller(payload: dict):
# 为每个任务准备独立 Queue
queues = {
"task_b": asyncio.Queue(),
"task_c": asyncio.Queue(),
"task_d": asyncio.Queue(),
}
# 1️⃣ 并发启动 B / C / D(流式但后台)
asyncio.create_task(stream_to_queue("TaskB", queues["task_b"]))
asyncio.create_task(stream_to_queue("TaskC", queues["task_c"]))
asyncio.create_task(stream_to_queue("TaskD", queues["task_d"]))
# 2️⃣ 先流式输出 Task A
async for chunk in task_a():
yield {
"event": "task_a",
"data": chunk
}
# 3️⃣ Task A 完成后,依次回放 B / C / D
for task_name in ["task_b", "task_c", "task_d"]:
queue = queues[task_name]
while True:
chunk = await queue.get()
if chunk is None:
break
yield {
"event": task_name,
"data": chunk
}
5️⃣ FastAPI + SSE(不变)
from fastapi import FastAPI, Request
from sse_starlette.sse import EventSourceResponse
app = FastAPI()
@app.post("/stream")
async def stream_endpoint(request: Request):
payload = await request.json()
return EventSourceResponse(
stream_controller(payload),
media_type="text/event-stream"
)
六、客户端看到的效果(理想)
event: task_a
data: TaskA token 0
event: task_a
data: TaskA token 1
...
event: task_b
data: TaskB token 0
event: task_b
data: TaskB token 1
...
event: task_c
data: TaskC token 0
...
event: task_d
data: TaskD token 0
...
✔ 顺序严格可控
✔ 但 B / C / D 实际上早就在并发跑
七、性能 & 工程说明(很重要)
✅ 并发效率
- B / C / D 在 A 输出期间已经生成了大量 token
- 回放几乎是瞬时的
✅ 内存风险
- Queue 默认无界
👉 可改成:
asyncio.Queue(maxsize=200)
用于 backpressure
✅ 可扩展性
-
你现在已经具备:
- 任意 DAG
- 任意阶段控制
- 混合流式 / 非流式
八、常见错误总结(你可以对照排雷)
| 错误 | 后果 |
|---|---|
| B/C/D 直接 yield | SSE 乱序 |
| 用 list 存 token | 内存暴涨 |
| 没有 None 结束标志 | 死等 |
| 共用一个 Queue | token 串台 |
九、升级方向(下一步你一定会用到)
如果你继续往生产走,我可以直接帮你:
- ✅ Task DAG 编排器(拓扑排序)
- ✅ 流式任务取消(客户端断开即 cancel)
- ✅ SSE 心跳 / keep-alive
- ✅ Token 级 tracing(哪个任务吐的)
- ✅ OpenAI / 通义 / Claude 真 SDK 版本
你可以直接说一句:
👉 “下一步我想做生产级取消 / DAG / 某某模型”

浙公网安备 33010602011771号