news 2026/10/9 7:52:45

长时间 AI 任务怎么让用户放心?队列、进度、取消与状态反馈

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
长时间 AI 任务怎么让用户放心?队列、进度、取消与状态反馈

用户上传一个视频、文档或一批图片后,最容易焦虑的不是等待本身,而是等待期间系统只显示一个转圈图标。页面刷新后看不到任务,进度停在 99%,点击取消没有反应,失败后又只剩“处理异常”。这些体验问题背后通常是同一个工程问题:系统没有把异步任务的真实状态和用户能理解的反馈分开设计。

本文使用脱敏任务PT-20261008-001:一份已通过校验的输入资产进入处理队列,Worker 依次完成准备、执行、结果验证和交付。示例中的时间、进度和文件均为教学数据;不公开真实视频、内部队列地址、模型服务或产品编排。

环境边界:Java 17、Spring Boot 风格服务层、Python 3.11 Worker、MySQL 8.x。本文讨论任务队列、进度语义与取消协作,不依赖某一种消息中间件;具体并发量和超时必须按实际资源评估。

目录

  1. 为什么“处理中”不能作为唯一状态
  2. 固定案例:用户到底应该看到什么
  3. 队列与领取:谁有资格开始执行任务
  4. 进度设计:阶段事实优先于虚假百分比
  5. 取消设计:请求取消不等于立即停止
  6. 服务层与 Worker:状态转换如何协作
  7. 预期输出与自动测试
  8. SQL 验证:如何发现卡死、重复领取和进度倒退
  9. 异常边界与上线验收
  10. 小结和延伸阅读

一、为什么“处理中”不能作为唯一状态

把QUEUED和RUNNING都翻译为“处理中”,会掩盖两个完全不同的问题:前者可能还在等待资源,后者已经被某个 Worker 领取。把READY、FAILED和CANCELLED也混进同一个模糊状态,用户不知道是否应继续等、重新提交还是下载结果,运维也无法判断瓶颈在队列、执行器还是产物验收。

对用户展示的状态可以保持简洁,但后台状态必须可验证。本文采用:QUEUED、RUNNING、CANCEL_REQUESTED、READY、FAILED、CANCELLED。其中QUEUED有等待位置,RUNNING有领取者和心跳,READY有正式结果,FAILED有错误类别,CANCEL_REQUESTED表示取消已被受理但尚未到达安全停止点。

图1:状态的价值不是增加术语,而是让用户和系统对“现在发生了什么”达成一致。

二、固定案例:用户到底应该看到什么

任务PT-20261008-001在用户侧只需要展示与下一步有关的信息:当前阶段、阶段说明、可否取消、可否下载、最近更新时间和失败后的可执行建议。它不应展示真实机器名、命令行、内部目录或堆栈。

后台状态用户展示必须具备的事实用户可执行操作
QUEUED等待处理已受理时间、队列位置或等待说明查看、取消
RUNNING正在处理当前阶段、最近心跳、阶段进度查看、申请取消
CANCEL_REQUESTED正在停止请求时间、安全点说明查看,避免重复点击
READY处理完成正式产物和验收摘要预览、下载
FAILED未完成脱敏错误类别、失败阶段查看原因、按条件重试
CANCELLED已取消停止时间、临时产物处置结果重新提交

例如任务进入RUNNING后,不显示“67% 完成”这种没有来源的数字,而显示“正在验证输出文件,第 3/4 阶段”。如果 Worker 的第三阶段确实有可测的子单元,如已处理帧数或页数,才可以同时显示其确定进度和总数。

图2:状态反馈要帮助用户判断下一步,而不是暴露 Worker 的运行细节。

三、队列与领取:谁有资格开始执行任务

任务创建成功只代表可以进入队列,不能代表已经开始。无论使用数据库轮询、消息队列还是调度框架,都必须有一个原子“领取”动作:只有将状态从QUEUED条件更新为RUNNING的 Worker,才取得执行权。否则两台 Worker 恰好同时读取同一任务时,可能生成两份结果或竞争覆盖临时目录。

下面用 MySQL 的条件更新表达领取语义;影响行数为1才表示成功。生产系统还需要限制每个 Worker 的并发槽位,并记录worker_token和心跳时间,而不是长期依赖一个进程名称。

UPDATEtool_jobSETstatus='RUNNING',worker_token=:workerToken,started_at=NOW(),heartbeat_at=NOW(),stage_code='PREPARING'WHEREid=:jobIdANDstatus='QUEUED';

若更新为 0 行,Worker 必须放弃执行:任务可能已经被其他执行者领取、已被取消,或者状态已被人工处理。这里不能采用“再查一下仍然执行”的补救逻辑,否则原子领取就失去意义。

图3:队列负责等待顺序,领取动作负责执行唯一性,心跳负责识别失联执行者。

四、进度设计:阶段事实优先于虚假百分比

进度必须能回答“数字从哪里来”。一个稳妥的做法是将任务拆成稳定阶段,并为每个阶段定义可验证的完成条件:

阶段代码用户文案进入条件完成条件是否适合展示百分比
PREPARING正在准备输入已成功领取输入资产和快照可读取否
PROCESSING正在执行处理Worker 已启动处理单元已处理单元数达到总数仅总数已知时
VERIFYING正在验证结果临时产物出现摘要可读且验收通过否
DELIVERING正在提交结果验收通过正式对象和下载记录已建立否

对于视频逐帧处理,可以记录processedFrames / totalFrames;对于大模型生成或外部调用,常常只有“请求已发出、响应已收到”两个可靠节点,此时只显示阶段和最近更新时间。把未知耗时伪装成 88%、92%、99%,一旦发生排队、重试或外部限流,就会迅速损害信任。

defprogress_for_frames(processed:int,total:int|None)->tuple[str,int|None]:iftotalisNoneortotal<=0:return"PROCESSING",Nonereturn"PROCESSING",min(100,processed*100//total)defcan_publish_progress(stage:str,percent:int|None)->bool:returnstage=="PROCESSING"andpercentisnotNoneand0<=percent<=100

阶段进度应连同updated_at一起保存。页面轮询、SSE 或 WebSocket 只是传输方式;没有可靠状态来源时,换成实时推送也只会更快地推送错误信息。

图4:用户宁可看到“正在验证结果”,也不需要一个没有计算依据的 99%。

五、取消设计:请求取消不等于立即停止

浏览器点击“取消”时,服务层通常无法安全地立刻杀死一段正在写文件或提交对象的工作。正确的语义是先把可取消任务改为CANCEL_REQUESTED,Worker 在阶段边界或安全检查点读取该标记:未开始执行则直接转CANCELLED;正在处理则停止后清理临时产物;已经进入正式提交阶段则根据事务结果继续完成或明确拒绝取消。

@TransactionalpublicvoidrequestCancel(LongjobId){intchanged=jobRepository.requestCancel(jobId);if(changed==0){thrownewIllegalStateException("JOB_NOT_CANCELLABLE");}}// UPDATE tool_job SET status = 'CANCEL_REQUESTED'// WHERE id = :jobId AND status IN ('QUEUED', 'RUNNING');

Worker 每完成一个可中断单元,就查询或接收取消信号。不能在“写一半的正式结果”时直接删除对象并声称取消完成;临时目录与正式产物分离,正是为了让取消和交付边界清楚。

六、服务层与 Worker:状态转换如何协作

状态转换集中在少数入口,能减少“任意代码随手更新 status”的问题。服务层负责受理、查询和取消请求;Worker 负责领取、阶段推进、心跳、失败和交付完成。所有转换都携带前置状态,避免迟到的 Worker 把一个已取消任务重新写成READY。

publicvoidreportStage(LongjobId,Stringtoken,Stringstage,Integerpercent){intchanged=jobRepository.updateProgress(jobId,token,stage,percent);if(changed!=1){thrownewIllegalStateException("WORKER_LEASE_LOST");}}publicvoidcomplete(LongjobId,Stringtoken,ResultArtifactresult){validationService.requireDeliverable(result);intchanged=jobRepository.completeRunningJob(jobId,token);if(changed!=1){thrownewIllegalStateException("JOB_STATE_CHANGED");}artifactRepository.insert(result);}

实际实现中,登记产物与任务转READY要在同一个短事务内完成;运行 Worker 的长时间过程不应持有这个事务。worker_token相当于一次短租约:如果系统确认 Worker 已失联,恢复流程会在下一篇讨论,不能简单把所有超时任务立刻重跑。

七、预期输出与自动测试

任务查询的输出应让用户和运营人员得到一致结论:

{"jobNo":"PT-20261008-001","status":"RUNNING","stage":"PROCESSING","progress":{"processedUnits":450,"totalUnits":900,"percent":50},"cancellable":true,"updatedAt":"2026-10-08T10:35:20"}

测试既要检查重复领取被拒绝,也要检查取消后的 Worker 不能完成交付:

@TestvoidonlyOneWorkerCanClaimAQueuedJob(){ToolJobjob=jobRepository.save(queuedJob());assertThat(jobRepository.claim(job.getId(),"worker-a")).isEqualTo(1);assertThat(jobRepository.claim(job.getId(),"worker-b")).isEqualTo(0);assertThat(jobRepository.find(job.getId()).getWorkerToken()).isEqualTo("worker-a");}@TestvoidcancelledJobCannotBecomeReadyFromLateWorkerReport(){ToolJobjob=runningJob("worker-a");jobRepository.requestCancel(job.getId());jobRepository.markCancelled(job.getId(),"worker-a");assertThat(jobRepository.completeRunningJob(job.getId(),"worker-a")).isEqualTo(0);assertThat(jobRepository.find(job.getId()).getStatus()).isEqualTo("CANCELLED");}

还应测试进度边界:未知总数不得返回伪百分比;已完成任务不得继续上报PROCESSING;updated_at长时间不变的RUNNING任务必须进入巡检列表。

八、SQL 验证:如何发现卡死、重复领取和进度倒退

上线后最需要主动发现的是“看起来在运行,实际上已经没人处理”的任务。以下 SQL 使用示例阈值,实际分钟数应按最长正常任务时长确定:

-- 预期结果:0 行。RUNNING 任务必须有领取者和最近心跳。SELECTjob_noFROMtool_jobWHEREstatus='RUNNING'AND(worker_tokenISNULLORheartbeat_atISNULL);-- 预期结果:巡检列表,而不是直接自动重跑。SELECTjob_no,stage_code,heartbeat_atFROMtool_jobWHEREstatusIN('RUNNING','CANCEL_REQUESTED')ANDheartbeat_at<NOW()-INTERVAL15MINUTE;-- 预期结果:0 行。非执行状态不应保留可继续更新的 Worker 租约。SELECTjob_no,status,worker_tokenFROMtool_jobWHEREstatusIN('READY','FAILED','CANCELLED')ANDworker_tokenISNOTNULL;

第二条只生成待核实清单。网络抖动、长时间外部调用或重启恢复都可能造成心跳延迟;未经检查就直接把任务标为失败或重新投递,反而可能制造并发执行和重复结果。

九、异常边界与上线验收

场景系统应做什么不应做什么
队列积压保持QUEUED,展示等待说明和提交时间伪装成已在执行
Worker 失联通过心跳生成巡检或恢复候选立即启动第二个 Worker 同时执行
未知总工作量展示阶段和更新时间虚构百分比或预计剩余时间
取消到达先置为CANCEL_REQUESTED,在安全点停止强行删除可能正在提交的正式结果
迟到状态上报以状态和worker_token条件更新拒绝覆盖已取消或已失败的最终状态

上线验收建议准备一个短任务和一个故意延迟的任务:确认第二个 Worker 无法重复领取;在执行中申请取消并检查临时产物未进入下载列表;暂停 Worker 模拟心跳中断,确认巡检只报警不盲目重跑;再完成一个正常任务,确认只有通过验收的结果才显示下载入口。

图5:用户看到的进度来自后端事实;任务完成必须经过结果验证,而不是停在“运行结束”。

十、小结和延伸阅读

长任务体验的关键不是把刷新频率调高,而是让每一个状态都有清晰含义:排队说明尚未领取,运行有阶段和心跳,进度有可验证依据,取消是协作过程,完成则必须有可下载产物。这样用户知道应该等待、取消、重试还是下载,系统也有证据定位卡在哪里。

下一篇将继续处理更棘手的一层:当 Worker 真正失败、进程中断或临时产物遗留时,怎样通过重试、幂等和检查点恢复,而不把同一任务重复交付。

参考资料:

  1. Spring Framework:事务管理参考
  2. MySQL 8.4:UPDATE 语句
  3. Python asyncio:任务取消
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/9 7:48:58

LHE7909肌电信号采集实战:从硬件配置到Python数据分析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/9 7:48:01

Spring Boot短信模块高可用设计:可靠性、可观测性与可运维性

1. 为什么“发个短信”在Spring Boot里反而成了高频故障点&#xff1f;“Java短信接口开发对接全流程”——这个标题听起来平平无奇&#xff0c;甚至有点过时。毕竟&#xff0c;短信早不是什么新技术&#xff0c;连我带的实习生第一周就能用RestTemplate调通一个HTTP接口。但过…

作者头像 李华
网站建设 2026/10/9 7:46:37

高并发论坛系统全链路测试:从单元测试到CI/CD发布门禁

最近一直在折腾一个高并发论坛系统的全链路测试&#xff0c;从单元测试到接口自动化&#xff0c;再叠上性能测试和 CI/CD 流水线&#xff0c;前后跑了将近一个月。很多测试同学这三项都单独做过&#xff0c;但真要让它们像齿轮一样咬合成一条自动触发的验证链路&#xff0c;并且…

作者头像 李华
网站建设 2026/10/9 7:46:33

AI调用的限流机制

引言 在 AI 应用开发中&#xff0c;调用大模型 API 时经常会遇到 429 Too Many Requests 错误。这是因为模型厂商对每个账号的请求频率&#xff08;RPM&#xff09;和 Token 消耗&#xff08;TPM&#xff09;都有限制。当你的应用用户量增长、并发请求增多时&#xff0c;如何优…

作者头像 李华