Home
avatar

.𝙃𝙖𝙣

AI 生图任务调度、取消、重试和进度查询

这篇接着上一篇 vchoo 的全链路复盘往下拆,专门记生图这一层最容易把系统做乱的几件事:任务调度、取消、重试和进度查询。

前面从故事到分镜那一段,更多是在把内容链接起来。到了生图这一步,系统开始真正面对耗时任务、批量执行、局部失败和用户等待体验。这里处理得顺不顺,直接决定前端看到的是“可用的产品”,还是一堆偶尔能跑通的功能。

1. 先把问题说清楚:生图一旦进业务,难点就不在“会不会出图”了

刚开始接这块的时候,最容易把注意力放在工作流本身:

  • 模型能不能跑
  • 分镜提示词能不能接进去
  • 图最后能不能出

这些当然都重要,但系统真跑起来以后,最先暴露出来的问题很快就变了。

因为 vchoo 里的生图不是一张一张手工点着跑,而是:

  • 一个故事会拆成多个分镜
  • 一个分镜会对应一张图或者多张图
  • 多个用户会同时发任务
  • 某一张图可能成功,某一张图可能失败
  • 用户会在前端盯着看进度
  • 用户还可能中途点取消

后端面对的已经不是“调用一次 ComfyUI”,而是一组批量、异步、可中断、可恢复的执行任务。

所以这篇主要记我当时怎么理解并处理这几件事:

  1. 任务怎么排
  2. 任务怎么取消
  3. 失败以后怎么重试
  4. 前端怎么查进度

2. 我最开始踩的第一个坑,是把生图任务想得太“平”了

一开始最容易冒出来的思路,其实是很直线的。

比如:

  • 前端传一个请求过来
  • 后端把分镜循环一遍
  • 一张张调用 ComfyUI
  • 等全跑完再把结果返回

这种想法在 demo 阶段看起来很顺,但真进业务以后问题非常多。

第一,接口会卡死

生图耗时摆在那里,不可能让前端一直等。

第二,局部失败不好处理

十张图里失败一张,不能因为这一张失败就把前面九张也当作没做。

第三,用户体验很差

前端除了“还在转”之外什么都看不到,用户不知道系统现在是不是卡住了。

第四,取消和重试几乎没法做

因为一旦你把整个过程写成一段线性逻辑,后面很难在中间插入“暂停、取消、继续”这种能力。

所以我后面很快就把这件事换了个理解方式:

生图不是一次接口调用,而是一批带状态的子任务。

这个认知一旦定下来,后面的设计才开始顺。

3. 任务调度这件事,我当时先做的不是“智能”,而是先让任务别撞在一起

一提到调度,很容易把事情想得很大。

但对我当时那个阶段来说,最现实的问题不是调度算法有多高级,而是:

  • 同时来几批任务时别乱
  • 每个分镜至少有独立状态
  • 一张图失败不要拖垮整次请求
  • 后面查进度时能知道每张图当前在哪

所以我当时先抓的,是任务粒度和状态落点。

我先把任务拆成两层

第一层:创作任务

这一层对应用户发起的一次完整创作。

第二层:分镜生图任务

这一层对应某一个具体分镜的执行单元。

这样拆开以后,很多事情一下就有地方放了。

比如:

  • 故事级别状态放主任务
  • 分镜级别状态放子任务
  • 重试可以只重试失败的分镜
  • 进度可以按子任务聚合出来

我当时觉得这一步特别关键。因为如果一开始不拆层,后面取消、重试、查询全会混在一起。

4. 状态设计是这篇里最重要的一块,不先定状态,后面所有动作都会打架

这部分是我后来印象最深的地方之一。

任务调度表面上看是“怎么排”,实际上先要回答的是:

一个任务在生命周期里会经过哪些状态。

至少在我当时做的这套东西里,子任务这层通常会有这些状态:

  • 待执行
  • 执行中
  • 成功
  • 失败
  • 已取消

如果再细一点,还会继续区分:

  • 已入队
  • 已下发工作流
  • 结果回传中
  • 结果已落库

状态一旦定义不清楚,后面几乎每个功能都会乱:

  • 前端查进度不知道该显示什么
  • 取消时不知道哪些任务还能取消
  • 重试时不知道哪些失败能重试、哪些不能
  • 回调回来以后也不知道该不该覆盖旧状态

所以我当时对状态这件事有个很强的感受:

状态字段不是记录结果用的,它其实是在约束系统接下来允许做什么。

这个理解挺重要。

因为一旦接受这一点,很多判断就会自然很多。

比如:

  • 只有待执行、执行中的任务才谈得上取消
  • 只有失败的任务才需要重试
  • 已成功的任务不能被重复覆盖
  • 已取消的任务回调回来时,后端要决定是丢弃、忽略还是特殊处理

5. 取消任务看起来像一个按钮,实际处理起来是状态和时机问题

从前端视角看,取消可能就是点一下按钮。

但从后端角度看,取消一点都不轻。

因为它至少包含两层意思:

第一层:业务侧不想继续了

用户或者系统决定这次任务不用再往下跑。

第二层:执行侧到底停在哪一步

这时候就得看任务当前在哪个状态:

  • 还没开始执行
  • 已经在队列里
  • 已经下发到 ComfyUI
  • 正在生成
  • 已经生成完成,只是结果还没回写

这些阶段下,取消的处理方式并不一样。

所以我当时对“取消”的处理,重点不是追求所有情况下都立刻停,而是先保证:

  • 系统状态别乱
  • 后续别继续派生新动作
  • 前端看到的状态能说得通

换句话说,取消至少要做到一件事:

哪怕底层执行已经很难完全中断,业务层也得先把这次任务标成不再继续推进。

这样后面即便还有迟到回调,系统也知道该怎么处理,而不是又把任务状态写回去,把整条链搅乱。

6. 重试不是“再调一次接口”这么简单,它首先是一次重新进入状态流转

失败以后做重试,表面上像是把按钮再点一次。

但真放到系统里,它更像一次新的控制动作。

因为你得先回答几个问题:

  • 重试的是整次创作任务,还是单个分镜
  • 已经成功的分镜要不要重跑
  • 失败原因是不是暂时性的
  • 原来的中间数据要不要复用
  • 新一轮执行和上一轮怎么区分

我当时最后更倾向于:

尽量支持局部重试,而不是整批全重来。

原因很现实。

如果一个故事拆了很多分镜,其中大部分已经成功,只有个别失败,这时候整批重跑代价太高,而且对用户来说也没有必要。

所以重试更适合落在子任务层。

这样处理的好处很明显:

  • 节省时间
  • 节省资源
  • 不会把已经成功的结果重新洗掉
  • 前端也更容易理解“哪张图重新跑了”

当然,这里也会带出新的问题,比如:

  • 重试次数要不要限制
  • 重试前要不要清理旧状态
  • 新结果回来以后,旧失败记录怎么留

但这些问题至少是在“任务已拆细”的前提下才有机会处理。

7. 进度查询是前端最直接能感知到的一层,也是最容易被低估的一层

这块一开始也很容易被当成附属能力。

因为从后端视角看,核心好像还是把图跑出来。

但真进项目以后,进度查询很快就变成一层非常关键的能力。

原因很简单:

  • 生图本来就慢
  • 用户不会一直无条件等
  • 前端需要给出反馈
  • 运营和测试也要判断系统是不是卡住了

所以后端得能回答这些问题:

  • 当前总任务完成了多少
  • 哪几个分镜还在跑
  • 哪几个已经成功
  • 哪几个失败了
  • 现在是不是还能继续等

我后来会把进度查询理解成:

把后端内部状态翻译成前端能展示、用户能理解的结果。

所以进度查询不能只是简单返回一个百分比。

很多时候前端真正想知道的是结构化信息,比如:

  • 总数
  • 成功数
  • 失败数
  • 执行中数量
  • 当前整体状态

有了这些,前端才能决定是:

  • 继续转圈
  • 提示部分完成
  • 提示可以重试
  • 提示任务已取消

8. 回调和进度查询放在一起看时,我第一次感觉到“最终一致”这件事有多真实

这一块是我当时挺有体感的一件事。

因为系统状态不是靠一个地方自然长出来的,而是几个动作一起拼出来的:

  • 后端先创建任务
  • 子任务进队列
  • ComfyUI 跑工作流
  • 结果通过回调回来
  • 系统落库
  • 前端轮询查进度

这几个动作之间本来就有时间差。

所以你很难要求系统每一瞬间都绝对同步。

更现实的目标其实是:

状态允许短暂有延迟,但最后要能收敛到一致。

比如:

  • 子任务刚下发时,前端查到的是“执行中”
  • 回调回来后,再更新成“成功”
  • 某个任务被取消后,迟到回调回来也不能把它改回成功

我当时第一次比较明显地体会到,任务型系统和简单增删改查很不一样的一点就在这里。

很多逻辑不只是“查一次、改一次”,而是多方在不同时间点往同一个状态上写东西。

如果没有前面的状态约束,这里会非常容易乱。

9. 回头看,这一层真正让我补上的,不是调度技巧,而是任务系统视角

只看表面功能,这篇讲的是:

  • 调度
  • 取消
  • 重试
  • 进度查询

但我现在回头看,真正补上的东西其实更深一层。

就是我开始有了一个比较明确的任务系统视角。

以前我更容易把问题看成接口问题:

  • 这个接口收什么参数
  • 这个接口返回什么结果

到这里以后,我开始更常想的是:

  • 一个任务从开始到结束会过哪些状态
  • 哪些动作会改变它的状态
  • 哪些状态下允许哪些操作
  • 一个任务拆成多个子任务以后,整体进度怎么聚合
  • 外部回调回来时,系统怎么保证状态不被写乱

这个视角变化,对我后面做别的东西也很重要。

因为它让我不再只盯着“接口有没有通”,而是开始关心:

一套会跑一段时间、会失败、会被取消、会被重试的任务系统,后端到底该怎么接。

10. 这篇先记一个阶段结论

这段生图任务层先下一个阶段性结论:

  1. 生图进业务以后,后端面对的核心问题是任务管理,不只是模型调用
  2. 任务必须拆层,至少要区分主任务和分镜级子任务
  3. 状态设计是调度、取消、重试、进度查询的基础,不先定清楚后面一定乱
  4. 取消和重试本质上都是状态控制动作,不只是按钮行为
  5. 进度查询不是附属功能,它是前端感知系统是否可用的关键能力
  6. 回调、轮询和状态更新放在一起时,系统真正考验的是一致性和边界控制

这篇先记到这里。

.NET AI 任务调度 ComfyUI 后端