更新于

OpenAI Background Mode 异步任务设计


很多团队第一次接 Responses API 时,默认心智还是传统的同步请求:发起调用,等结果,超时就重试。但一旦任务变成长摘要、深度研究、多工具链推理,单次执行时间会明显拉长,同步阻塞就开始拖垮用户体验和服务端资源。OpenAI 官方把这类场景放到 background=true 的异步模式里处理:请求先进入后台,客户端后续再通过查询状态或 webhook 拿结果。

这篇文章不重复讲 Responses API 的基础参数,而是只回答一个更具体的问题:如果你要把 Background Mode 当成生产能力来用,任务状态、回调、取消、幂等和合规边界应该怎么设计?

一、Background Mode 解决的不是“更快”,而是“更稳”

很多人看到“后台模式”会误以为它是性能优化开关。不是。它解决的是长任务执行期间,同步连接不可靠、前端等待成本高、服务端难以扩展这三个工程问题。

用同步模式时,你往往要一直占着一个 HTTP 请求,前端要么傻等,要么自己做超时和重试;而 Background Mode 的思路是先把任务变成一个可跟踪的 response 对象,再围绕这个对象做状态机。官方文档当前给出的典型流程也很明确:创建时带上 background=true,之后用 Responses 的 GET 接口轮询;如果任务最终完成,再取输出;如果业务不想等,还可以调用取消接口终止执行。

对于“结果晚几秒甚至几分钟到达也没关系”的链路,这种模式通常比硬撑同步连接更合理。它和 OpenAI Responses API 完整使用指南 的区别在于:后者讲协议能力,这里讲的是如何把协议能力变成可运维的后台作业系统

二、先把 response 当成 job,而不是一次普通请求

落地时最关键的心态转换是:不要把 background response 当成“延迟返回的 HTTP 响应”,而要把它当成一条 job 记录。

OpenAI 当前公开的状态集合里,至少包括这些终态和中间态:

状态含义你的系统应该做什么
queued已创建,尚未开始处理标记为排队中,允许前端离开页面
in_progress正在执行展示处理中;不要重复提交相同任务
completed成功完成拉取结果并落库,触发后续业务
failed执行失败记录错误原因,进入重试或人工排查
cancelled已取消结束任务,不再继续轮询
incomplete结果不完整结合业务规则判断补跑、降级或放弃

这张表的重要性在于,它直接决定你数据库里该怎么建模。最实用的做法通常是本地建一张 llm_jobs 表,至少存:

id, provider, response_id, business_key, status,
submitted_at, completed_at, error_code, payload_hash

其中 business_key 用来表达“这条 job 对应哪条业务记录”,payload_hash 用来做幂等去重。否则前端用户多点一次按钮、队列消费者重放一次消息,你就可能向 OpenAI 提交两份完全一样的长任务,最后不仅重复计费,还会让下游结果互相覆盖。

三、轮询和 webhook 不是二选一,而是主备组合

官方文档给了两种思路:主动轮询 response 状态,或者在后台任务完成时接收 webhook。很多团队会问到底该选哪一个。更稳的答案通常是:webhook 做主路径,轮询做兜底。

原因很简单。纯轮询实现最直观,但如果任务量上来,轮询会制造大量“没有新信息的查询”;纯 webhook 看似优雅,但只要你的回调链路偶发失败、签名校验写错、消费者短时不可用,就会出现“OpenAI 已完成,但你没收到”的漏数问题。

比较务实的组合是:

场景推荐方式
管理后台单人低频操作轮询即可,逻辑简单
大规模离线作业webhook 主路径,定时轮询兜底
面向用户的异步生成前端短轮询展示状态,服务端仍保存 webhook 结果

这样设计的好处是,webhook 负责把完成事件尽快推到你系统里,轮询负责发现“漏回调”“重放失败”“状态长期卡住”这类异常。不要把 webhook 当成唯一真相源;response 对象本身才是最终真相源

四、代码实现里最容易漏掉的是取消和超时升级

很多异步任务系统上线后能“提交”和“完成”,但不能优雅地“终止”。这会带来两个后果:一是用户取消页面后,后台任务还在继续烧 token;二是故障任务卡住时,运维没有一键止损手段。

OpenAI 官方提供了取消 response 的接口,因此你自己的任务系统也应该暴露取消动作,而不是让后台任务只能自然结束。一个可落地的调用骨架大概长这样:

from openai import OpenAI
import time

client = OpenAI()

job = client.responses.create(
    model="gpt-5",
    input="请完成一份较长的研究摘要",
    background=True,
)

while True:
    current = client.responses.retrieve(job.id)
    if current.status in {"queued", "in_progress"}:
        time.sleep(5)
        continue
    if current.status == "completed":
        print(current.output_text)
        break
    if current.status in {"failed", "cancelled", "incomplete"}:
        raise RuntimeError(f"job ended with status={current.status}")

生产环境里,不建议无限轮询。给每个 job 设一个业务超时阈值会更稳,比如“30 分钟没出终态就升级为人工排查”或“超过 10 分钟自动取消并重建”。这里的重点不是精确秒数,而是你必须在系统里有“什么时候继续等、什么时候止损”的规则。

五、ZDR 和数据留存要求要在设计阶段说清楚

Background Mode 最容易被忽略的不是代码,而是合规边界。OpenAI 官方当前文档明确提到:后台请求为了支持异步执行和轮询,会临时把 response 数据落盘大约 10 分钟;这也是为什么它与严格的 Zero Data Retention(ZDR)要求不兼容。官方同时说明,ZDR 项目的后台请求会以 store=false 运行,但如果你的业务明确要求 ZDR,就不应该把 background=true 当成默认方案。

这意味着你在架构评审时就要把任务分成两类:

  • 允许短期应用态存储的长任务:可以走 Background Mode
  • 必须严格无状态处理的任务:优先考虑同步、store=false 或其他兼容 ZDR 的执行方式

这类判断不能等到上线后再补。很多团队的坑正是“先按最方便的异步方案上线,后面才发现客户采购条款要求 ZDR”,最后不得不返工整条任务链。如果你本来就在做合规敏感场景,先看 OpenAI Structured Outputs vs tool_choice 选型指南 这类偏约束性输出的能力,很多任务其实不需要跑成长时间后台作业。

六、把 Background Mode 接进现有系统时,优先补齐这四层

真正决定系统是否可维护的,不是那一行 background=True,而是外围配套是否补齐:

  1. 任务入库层:提交前先写本地 job 记录,把业务主键、请求摘要、操作者信息都存下来。
  2. 状态同步层:轮询或 webhook 到达后,统一走状态机更新,不要在多个服务里各改各的。
  3. 结果消费层:只有在 completed 后才把输出推进下游,比如写数据库、发通知、触发二次分析。
  4. 异常治理层:针对 failedcancelledincomplete 设计不同处理策略,而不是一律“再试一次”。

如果你已经在做多模型统一网关,这一层抽象尤其重要。因为今天是 OpenAI Background Mode,明天可能是 Anthropic batch、Gemini 长任务、或者自家队列任务。把“外部长任务”统一建模成内部 job 状态机会更省事,这和 LLM 异步任务队列设计:从原型到生产 的思路是一致的。

七、结论:先定义 job 语义,再接 OpenAI 接口

Background Mode 真正的价值,不在于让一个 API 看起来更“高级”,而在于它给了你构建长任务系统所需的最小原语:可跟踪的 response ID、明确的状态集合、轮询接口、取消接口,以及可选 webhook。只要你把这些原语接到自己的 job 模型里,它就会变成稳定能力;如果只是把它当成“同步接口的延迟版本”,后面一定会在幂等、取消、漏回调和合规审计上补课。

最稳的落地顺序通常是:先定义你的业务任务状态机,再决定哪些任务值得走 Background Mode,最后再把 OpenAI 的 response 状态映射进去。这样一来,就算未来你要切模型、切供应商,内部系统也不用重写。

八、相关阅读

如果你要把 OpenAI 长任务、同步请求和其他模型接口统一接到同一套业务系统里,YoTradeApi 可以作为统一入口,帮助你减少多家供应商在鉴权、路由和调用方式上的重复接入工作。