实时生图服务应拆开响应与生成链路
文章以游戏客服生图为例,建议先快速确认请求,超出响应时限时返回任务句柄,再异步交付图片。延迟评估应覆盖请求接入、生成、交付及本地处理,并用窄接口隔离供应商。
AI 深度解读
选图像生成 API,最快返回图片的服务,未必能让客服操作台最快恢复可用。游戏客服处理玩家描述时,真正要控制的是从点击生成,到确认受理、拿到可展示图片的整条链路。
原文没有给出供应商排名,也没有延迟跑分。它提出的是一套工程取舍:先快速返回任务标识,让图片在后台生成,再用幂等提交、明确状态和独立图片处理保护客服流程。对准备把 AI 生图接入业务系统的开发者,这比比较一次请求谁快更有参考价值。
实时体验先保证受理,再等待图片
游戏客服可能根据玩家文字生成地图标记、道具概念图,或帮助判断问题的示意图。图片有用,但生成过程不能拖住工单处理。
作者把一次操作拆成两个延迟预算:多久确认请求已受理,以及多久拿到可用图片。前一个响应负责返回任务标识,后一个结果通过状态查询或实时通道送达。客服可以继续处理工单,不必守着一个没有截止时间的加载动画。
这里至少有四段耗时:请求受理、图片生成、结果交付、本地处理。供应商返回字节很快,浏览器仍可能卡在大图下载、代理超时或格式转换上。只测生成阶段,无法回答操作台是否流畅。
队列的价值,是把客服操作与生成耗时之间的依赖切开。
操作台还要有确定的状态:accepted、ready、failed、expired,也就是已受理、已就绪、失败和过期。图片迟迟不来时,界面需要表达真实进度,并让当前操作能够结束。
但作者没有主张所有请求都进队列。如果实测生成和交付始终满足交互预算,同步请求更简单。决定依据应当是延迟分位数和失败处理能力。原文没有给出具体预算,也没有提供 P95、P99 数据,不能据此推导出通用超时阈值。
重试不能多生一张图,也不能丢掉已受理任务
作者提到,自己曾因任务遗漏和重复交付收到告警。这些是其经历陈述,文章没有提供事故规模或修复前后的数据。
由此提出的约束很明确:一次客服请求最多产生一个被接受的生成意图;图片仍在等待时,客服必须能继续工作。请求处理器负责校验工单引用、确定幂等键、持久化任务并入队,worker 再调用生成服务。
下面这段代码解决的是客户端重试时复用已有任务,避免再次创建生成意图:
func AcceptImageJob(ctx context.Context, req ImageRequest, store JobStore, queue Queue) (string, error) {
if req.TicketID == "" || req.Prompt == "" || req.Idempotency == "" {
return "", errors.New("missing required request field")
}
job, created, err := store.CreateOrGet(ctx, req.Idempotency, req)
if err != nil {
return "", err
}
if created {
if err := queue.Publish(ctx, job.ID); err != nil {
return "", err
}
}
return job.ID, nil
}
要看的是 CreateOrGet 和 if created 两处:相同幂等键应查到同一个任务,只有新任务才尝试入队。这个保证需要存储层真正执行去重,函数名本身不提供可靠性。
代码也留下了一个关键缺口:任务写入成功,但 queue.Publish 失败,怎么办?客户端再次提交时,已有任务的 created 为 false,便不会重新发布。如果没有恢复机制,任务可能一直留在存储中。
作者因此建议,在不能丢失已受理工作的场景使用 outbox 或等价的持久交接机制。可以理解为把「任务已创建」和「还需要投递」一起可靠记录,再补齐投递。
这仍不等于供应商只会执行一次。worker 重复收到任务,以及供应商可能已受理却返回超时,都需要额外去重。适配器支持的幂等键,或应用维护的去重记录,要覆盖后续执行路径。
换供应商之前,先固定接口和比较条件
可迁移性不是换一个 URL。供应商特有参数如果进入工单、存储结构和 UI 状态,替换服务就会牵动整条业务链。
原文用 ImageGenerator.Generate(ctx context.Context, req ImageRequest) (ImageResult, error) 隔离调用。示例请求包含 TicketID、Prompt、Idempotency、Width、Height,结果返回 ObjectURL、Provider、ElapsedMs。正文还要求契约覆盖内容策略结果、取消状态和错误类别,供应商及模型元数据另行记录,用于诊断。
适配器需要区分三种结果:成功返回图片、可重试失败、永久拒绝。超时也要分类,否则 worker 无法执行有边界的重试策略。
统一接口有代价:它可能装不下供应商最特殊的控制能力。作者选择让核心流程保持小范围契约,把高级功能放进可选能力记录,并单独测试。
图片处理也应独立于适配器。Node 项目可以用 sharp 承担缩放和格式处理,但原文没有给出具体配置或性能测试。明确的要求是:发布对象引用前校验内容类型和尺寸;保留策略允许时存原图,给操作台提供尺寸受限的派生图,并独立记录处理耗时和失败。
通知则选择满足预算的简单机制:带退避的轮询、server-sent events,或已有实时通道。WebRTC 适合已经需要低延迟点对点数据或媒体传输的应用,但会增加连接与生命周期管理。很多场景只需通知「任务已就绪」,图片再从受控对象存储获取。
比较供应商时,应使用脱敏后的真实客服提示词集合,同时记录受理延迟、首个可用结果时间、完成延迟、错误类别、重试次数、输出大小和取消率。切换期间还要保持尺寸与提示词归一化规则一致,否则比较的就是两套工作流。
我的判断
我赞同它把比较对象从生成接口扩展到完整交互链路。前端最终需要的是能展示的图片、可信的状态,以及失败后还能继续工作的操作路径。
不过,标题里的「最低延迟」并没有得到跑分证明。作者没有给出供应商、模型、负载规模或延迟分布,也没有对照实验。这是一篇架构经验文章,不能据此认定异步方案一定更快。
异步化能控制等待方式,不能自动缩短图片生成时间。
它更适合生成耗时波动大、客服需要持续处理工单的场景。一次性内部实验如果允许丢请求,持久队列、outbox 和完整恢复链路可能超过实际需要。同步调用加明确超时,也可以是合理起点。
原文还有两处需要补齐。首先,状态机只列出了四种状态,取消以及工单关闭后的处理没有展开。其次,示例返回单个 ElapsedMs,却要求拆分测量多段耗时,落地时需要更细的埋点。
测试建议则很实用:除适配器契约测试外,还应覆盖确认丢失后重试、worker 重复收件、工单关闭后生成完成、输出校验失败、UI 重连前收到通知,以及供应商可能已受理后的超时。告警也不能只盯 HTTP 5xx,还要看队列年龄、失败比例和通知延迟。
能拿来做什么
- 给客服操作台分别定义受理预算与图片可用预算,并围绕 accepted、ready、failed、expired 设计界面。图片等待期间,工单操作应能继续。
- 在后端实现按幂等键创建或查询任务,并检查持久化到入队之间的恢复路径。验收时主动模拟「写入成功、发布失败」,确认任务不会停在那里。
- 如果使用 Node 处理图片,把 sharp 缩放、格式策略和输出校验设成独立阶段,分别测量生成、处理与交付耗时,再决定是否需要更复杂的通知通道。
- 替换供应商前,用相同的脱敏提示词和输出要求跑完整链路测试,先导入少量有代表性的任务,并保留回退能力。选择依据应是延迟分布与可靠性表现。