【大模型调用编排】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 / 某某模型”

posted @ 2026-02-01 18:21  X1OO  阅读(0)  评论(0)    收藏  举报