游戏生成异步链路技术解析:从服务端到扣子,再回到前端

游戏生成异步链路技术解析:从服务端到扣子,再回到前端

前言

最近把项目里的"生成游戏"功能完整收了一遍,表面上看,这只是一个异步任务场景:前端提交参数,后端调用 AI 工作流,最后把结果返回给用户。

但真正做下来会发现,难点并不在"怎么调 AI",而在于这条链路怎么做稳:

  • 前端提交之后,不能一直傻等接口返回
  • 用户想实时看到状态变化,而不是只看到"提交成功"
  • 服务重启后,执行到一半的任务不能直接丢
  • WebSocket 断线后,前端不能彻底失去状态
  • 失败任务不能全靠人工重试
  • 多个任务同时提交时,服务端还要能并发推进

我最后落地的方案,是一套比较典型但又比较贴业务的组合:

  • MySQL 存任务状态和事件
  • Redis Stream 做异步任务调度
  • worker 负责消费任务和推进状态
  • WebSocket 负责实时把状态推给前端
  • Coze 工作流 负责真正生成游戏内容

这篇文章不只讲"用了什么技术",更想把这条链路从头到尾讲清楚:

  • 状态是怎么变化的
  • S 端怎么把任务发给扣子
  • 扣子怎么把结果再回给 S 端
  • S 端怎么把状态同步给 C 端
  • 服务端怎么实现多个任务并行生成

一、先把几个角色分清楚

要理解这套链路,先把几个角色摆正。

1. C 端

这里的 C 端就是前端页面或者小程序,职责很明确:

  • 发起"生成游戏"请求
  • 接收任务状态变化
  • 在断线或重连后恢复状态

2. S 端

S 端是你的后端服务,负责整条链路的编排:

  • 接收生成请求
  • 创建任务记录
  • 入 Redis 队列
  • 启动 worker 消费
  • 调扣子工作流
  • 接扣子回调
  • 把状态推给前端

3. Redis Stream

Redis Stream 在这里不是拿来存业务真相的,它更像一个"任务调度通道"。

前端请求进来后,服务端会先把任务放进 Stream,后面的 worker 再从 Stream 里把任务取出来执行。

4. worker

worker 本质上就是后台干活的人。

它不负责接用户请求,它只负责一件事:

从 Redis Stream 里取出游戏生成任务,然后真正执行它。

5. 扣子端

扣子工作流是外部 AI 生成能力的提供方。

服务端不会自己凭空生成游戏,而是会在任务进入执行阶段后,调用扣子的工作流接口,让扣子去真正产出游戏内容。


二、为什么这块一定要做成异步

如果不用异步,最直接的写法其实很简单:

text 复制代码
前端请求 -> 服务端同步调用扣子 -> 等几分钟 -> 返回结果

但这种模式在实际业务里很快就会暴露问题:

  • 接口耗时太长
  • 前端一直 loading,体验很差
  • 请求超时风险大
  • 服务端容易被长请求拖住
  • 中间状态没法实时告诉前端
  • 一旦服务重启,执行到一半的任务不好恢复

所以后面我把它拆成了两段:

text 复制代码
1. 接口负责快速接单
2. worker 负责后台执行

这就是这条链路的出发点。


三、整体链路长什么样

先看一版简化后的整体流程:

text 复制代码
C端发起生成
   ->
S端创建任务记录
   ->
S端写首条 queued 事件
   ->
S端把任务写入 Redis Stream
   ->
worker 消费任务
   ->
任务进入 generating
   ->
S端异步调用扣子工作流
   ->
扣子执行完成后回调 S端
   ->
S端收口为 success / failed
   ->
S端通过 WebSocket 把状态推给 C端

如果把这条链路按方向拆开,其实就是四件事:

  • C端 -> S端:提交任务
  • S端 -> 扣子端:异步提交工作流
  • 扣子端 -> S端:回调结果
  • S端 -> C端:同步状态和结果

接下来就按这四条线分别讲。


四、第一条线:从 C 端到 S 端

1. 前端发起生成请求

用户在页面上点击"生成游戏"后,前端会把生成参数发给服务端。

这一步服务端做的不是直接生成,而是先接住这笔任务。

2. 服务端创建任务记录

服务端接到请求后,会先在数据库里创建一条任务记录,也就是 game_record

这个时候任务的初始状态就是:

  • queued

也就是"已受理,等待执行"。

3. 服务端写首条状态事件

除了任务表本身,服务端还会写一条首个状态事件,表示:

  • 任务已入队

这条事件后面会给两个地方用:

  • WebSocket 实时推送
  • 断线后的事件补拉

4. 服务端写入 Redis Stream

任务记录落库后,服务端再把这条任务写入 Redis Stream。

到这一步,接口层的事情就基本结束了。接口不会继续等任务执行完成,而是快速返回给前端:

  • 任务提交成功
  • 当前状态是 queued

所以从 C 端视角来看,第一跳只是"提交成功 + 排队中"。


五、第二条线:从 S 端到 C 端

很多时候大家一提状态同步,第一反应就是 WebSocket。这个方向没错,但如果只靠 WebSocket,真实环境里一定会漏。

所以我后面把 S 到 C 端拆成了三条通道。

1. WebSocket:负责实时推送

每次关键状态变化时,服务端都会通过 WebSocket 推送给前端:

  • queued
  • generating
  • success
  • failed

这样前端能第一时间看到:

  • 任务已排队
  • 任务开始生成
  • 任务生成成功
  • 任务生成失败

2. /game/pending:负责当前态快照

WebSocket 有一个天然问题:如果用户断线、刷新页面,实时消息就断了。

所以我补了 /game/pending,它只做一件事:

  • 返回当前还在进行中的任务快照

也就是只关心:

  • queued
  • generating

前端在页面进入、断线重连、回前台时,可以先调这个接口,把当前态先对齐。

3. /game/status/events:负责补拉漏消息

只拿当前态还不够,因为前端有时还想知道自己漏掉了哪些状态变化。

所以又补了一张事件表和一个事件补拉接口:

  • game_status_event
  • /game/status/events

前端带着 last_event_id 过来,就可以把断线期间漏掉的状态事件补回来。

最后 S 到 C 端的完整模型就是:

text 复制代码
WebSocket 实时推送
   +
/game/pending 当前态快照
   +
/game/status/events 事件补拉

这也是为什么我后来一直强调:前端不能只信 WebSocket。


六、第三条线:从 S 端到扣子端

1. worker 真正开始接手任务

任务一旦写进 Redis Stream,后面就是 worker 持续监听并消费。

worker 会做两件核心的事:

  • 把任务状态从 queued 推进到 generating
  • 真正调用扣子工作流

也就是说,generating 这个状态的含义不是"接口已经接到了任务",而是:

后台 worker 已经正式开始执行这条任务了。

2. 服务端调用扣子工作流

状态切到 generating 之后,服务端会去调用扣子的工作流接口。

这里有一个很关键的点:

  • 我用的是异步提交,不是同步等待最终结果

也就是说,服务端发给扣子的不是"请你马上返回生成好的游戏",而是:

  • "请你开始执行这个工作流"

扣子会先返回一个 execute_id,表示这次工作流执行已经被受理。

3. 为什么这里要异步提交

如果这里同步等扣子完整跑完,会有两个明显问题:

  • worker 会长时间阻塞
  • 并发能力会变差

而异步提交后,服务端只负责把任务启动起来,真正的结果等扣子后续再回调回来。

这一步其实很像:

text 复制代码
worker 不是自己做游戏
worker 只是把任务交给扣子开始做

七、第四条线:从扣子端回到 S 端

1. 扣子执行完成后回调服务端

扣子工作流执行完成后,不是让服务端一直去轮询结果,而是由扣子主动回调服务端内部接口。

这样整条链路会更自然:

  • S 端发起异步执行
  • 扣子执行完成
  • 扣子把结果主动回给 S 端

2. 服务端根据回调结果收口任务

服务端收到回调后,会根据回调状态做收口:

  • 成功:收口为 success
  • 失败:收口为 failed

同时还会做三件事:

  1. 更新 game_record
  2. 写入对应的状态事件
  3. 推送 WebSocket 给前端

也就是说,扣子端回来的不是"只是一个结果",而是会进一步驱动服务端完成最终状态闭环。

3. 这一步为什么重要

因为前面 S -> 扣子 那一步只是"把任务提交出去",真正让任务结束的,是这里的回调收口。

没有这一步,任务就会一直停留在 generating


八、状态变化是怎么推进的

把整条链路抽象完之后,状态变化其实并不复杂。

1. 正常链路

正常情况下,状态流转是:

text 复制代码
queued -> generating -> success

2. 失败链路

如果扣子执行失败,或者服务端收口失败,状态就会走到:

text 复制代码
queued -> generating -> failed

3. 可重试链路

如果失败是可重试的,任务不会立刻结束,而是会重新回到队列:

text 复制代码
queued -> generating -> queued -> generating -> success / failed

4. 为什么我只保留这几个状态

异步系统不是状态越多越好。

这块我最后坚持只保留:

  • queued
  • generating
  • success
  • failed

原因很简单:

  • 前后端都容易理解
  • 状态边界清晰
  • 补偿和恢复也更容易收口

真正复杂的部分,不在状态种类,而在状态变化怎么保证可靠。


九、服务端是怎么实现并行生成的

这一块很多人第一反应会问:

你说支持多个任务同时生成,那到底是哪里在并行?

这个问题要分开看。

1. 不是一个任务内部并行

先说清楚,你现在这套"并行生成",不是把一个游戏任务拆成很多线程同时生成不同片段。

它的含义是:

  • 多个游戏生成任务可以同时推进

也就是任务级并行,不是单任务内部并行。

2. 第一层并行:多个 worker 并发消费

服务启动后,会按配置启动多个 worker。

这意味着:

  • worker1 可以消费任务 A
  • worker2 可以消费任务 B
  • worker3 可以消费任务 C

于是多个任务可以并行被服务端处理。

3. 第二层并行:任务提交给扣子后,worker 不再阻塞等待

每个 worker 把任务推进到 generating 后,会把任务异步提交给扣子工作流。

这里有一个很容易被说模糊的点:worker 并不是在本地一直等扣子把游戏生成完。

真正发生的事情其实是:

  1. worker 取到任务
  2. 把任务推进到 generating
  3. 调用扣子工作流接口
  4. 扣子先返回一个 execute_id
  5. worker 当前这次处理结束
  6. worker 继续去消费下一条任务

也就是说,worker 在把任务成功提交给扣子之后就被释放出来了,后续真正继续执行这条任务的是扣子侧的工作流实例,而不是这个 worker 一直卡在那里。

所以从效果上看,会更像这样:

text 复制代码
任务A -> worker1 提交给扣子 -> worker1 继续处理下一条
任务B -> worker2 提交给扣子 -> worker2 继续处理下一条
任务C -> worker3 提交给扣子 -> worker3 继续处理下一条

如果只有一个 worker,本地也是"快速连续提交多个任务给扣子";如果有多个 worker,就是"多个 worker 并发提交多个任务给扣子"。真正的任务执行并行,主要还是发生在扣子工作流侧。

4. 并行能力真正来自哪里

所以你这套服务端并行生成能力,实际上来自两个组合:

  • Redis Stream 多消费者
  • 扣子异步工作流

前者负责让多个任务可以并发被取出来,后者负责让每个任务提交出去后不阻塞服务端主流程。


十、为什么这套链路比"同步调用扣子"更适合上线

如果只是为了把功能做出来,同步调用扣子当然也不是不能写。

但一旦放到真实业务里,异步链路的价值就很明显了。

1. 接口更轻

前端提交后,接口可以很快返回,不需要等几分钟。

2. 状态更透明

前端不是只看到"成功"或者"失败",而是能看到任务从排队到执行再到完成的全过程。

3. 服务更稳

任务执行和接口处理拆开后,服务端不会因为长耗时生成被同步拖住。

4. 更容易补偿

异步链路一旦拆清楚,就比较容易在每一层加恢复能力,比如:

  • pending 恢复
  • 失败重试
  • 超时恢复
  • 事件补拉

这也是为什么这类场景越做越像"异步系统",而不只是"一个 AI 调用接口"。


十一、我在这条链路里最看重的几个点

如果让我把这套方案压缩成几个关键词,我会重点强调下面这些。

1. Redis Stream 不是状态真相,而是调度通道

很多人一看到 Redis 就容易把所有事情都往里面放,但这块我一直比较克制。

  • Redis Stream 负责调度任务
  • MySQL 负责保存状态真相

这个边界一旦清楚,后面很多问题都更容易定位。

2. worker 才是真正推进任务的人

接口层只是接单,真正推进状态的是 worker。

没有 worker,任务就只会停留在 queued,永远不会真正执行。

3. 扣子回调是整条链路的收口点

S -> 扣子 那一步,只是把任务提交出去。

真正让任务结束的,是:

  • 扣子执行完成
  • 回调到服务端
  • 服务端再更新状态并通知前端

4. 前端不能只依赖 WebSocket

前端真正可靠的状态同步,不是"只要 WS 连着就行",而是:

  • WebSocket 负责实时
  • /game/pending 负责当前态恢复
  • /game/status/events 负责漏消息补拉

十二、总结

回过头看,这条"生成游戏"的链路,真正难的地方其实不是接 Redis,也不是调扣子,而是把角色边界和数据流向理顺。

我最后比较认同的一种理解是:

  • C 端负责发起和展示
  • S 端负责编排和收口
  • Redis Stream 负责调度
  • worker 负责执行
  • 扣子负责产出结果

只要这几个角色边界够清楚,状态机就不会乱,补偿机制也更容易往里加。

所以这套方案最后不只是"实现了异步生成游戏",更准确地说,是把一条从:

  • C端 -> S端 -> 扣子端 -> S端 -> C端

的完整闭环链路真正跑顺了。

这也是我觉得这次实现最有价值的地方。