OpenAI 后台响应被取消后客户端怎么保存最终状态
来源:17golang原创
时间:2026-11-11 05:49:31 165浏览 收藏
我在接入后台响应时最容易踩的坑,是把“用户发起了取消”当成“本地任务已经取消”。真正可靠的做法是保存 OpenAI 返回的 response_id,把 response.cancelled 作为独立终态事件写入任务表,并用条件更新保证重复 webhook 和迟到事件不会把终态改回运行中。
官方地址:https://developers.openai.com/api/docs/guides/background
结论:本地任务至少要区分 queued、running、completed、failed、cancelled 和 incomplete;收到取消事件后只允许幂等地落为 cancelled,后续事件不能覆盖已经确认的终态。
先把取消当成一种独立终态
后台模式会异步创建响应,客户端可以轮询响应对象,也可以接收 webhook。官方文档把 queued、in_progress 和离开这两个状态后的最终状态区分开;只有设置了 background=true 的响应才支持取消。webhook 事件中,response.cancelled 表示后台响应已经被取消,它不应该和普通失败共用一个模糊的 error 状态。
我会在本地任务表中保存外部响应和业务状态的双重信息:response_id 用于关联 OpenAI 对象,status 用于驱动客户端展示,version 或更新时间用于挡住旧事件。下面这张图只表达状态关系,是静态说明图,不是实际运行结果。

任务表不要只存一个布尔值
如果只有 is_done,取消、失败和正常完成都会变成同一种结果,客户端无法决定是否重试,也无法向用户解释任务为什么停止。最小表结构可以这样设计:
CREATE TABLE ai_tasks (
id BIGINT PRIMARY KEY,
response_id VARCHAR(128) NOT NULL UNIQUE,
status VARCHAR(24) NOT NULL,
version INT NOT NULL DEFAULT 0,
last_event_id VARCHAR(128),
updated_at TIMESTAMP NOT NULL
);
-- response_id 建唯一约束,防止同一个后台响应被重复创建成本地任务。
-- status 保存业务终态,last_event_id 用于记录最近一次已处理的事件。
UPDATE ai_tasks
SET status = 'cancelled', version = version + 1,
last_event_id = :event_id, updated_at = CURRENT_TIMESTAMP
WHERE response_id = :response_id
AND status IN ('queued', 'running');
关键不是字段多,而是更新条件。只有 queued 或 running 允许转成 cancelled;如果同一个事件被投递两次,第二次更新不到行即可视为幂等成功。如果 completed 已经先落库,迟到的 cancelled 不应覆盖它;如果业务明确规定取消优先级,也应该把这条优先级写进状态机,而不是靠最后到达的网络包决定。
Webhook 接收层要验签并快速返回
OpenAI 的 webhook 指南建议在服务端验证签名,且验签应使用尚未解析的原始请求体。接收函数只做验签、记录事件和投递内部任务,然后尽快返回 2xx;耗时的数据库联动或通知放到后台 worker。官方文档还说明,失败响应会触发重试,webhook-id 可以作为去重键。
下面的 Python 示例展示处理思路,具体 SDK 版本以项目实际依赖为准。代码没有把密钥写入文件,unwrap 处理的是原始字节,事件业务处理则交给一个幂等函数。
import os
from openai import OpenAI, InvalidWebhookSignatureError
client = OpenAI(webhook_secret=os.environ["OPENAI_WEBHOOK_SECRET"])
def receive_webhook(raw_body: bytes, headers: dict) -> int:
# 先用原始请求体验签,不能先 json.loads 再把重排后的内容拿去验证。
try:
event = client.webhooks.unwrap(raw_body, headers)
except InvalidWebhookSignatureError:
# 签名不可信时直接拒绝,不把事件写入任务队列。
return 400
event_key = headers.get("webhook-id")
# 先按 event_key 去重,再把业务更新交给可重试的 worker。
if not event_key or already_seen(event_key):
return 200
save_event_once(event_key, event.type, event.data.id)
enqueue_state_update(event_key, event.type, event.data.id)
# 快速确认收到,避免非必要的 webhook 重试。
return 200
def apply_state_update(event_key: str, event_type: str, response_id: str) -> None:
# 只有未结束任务允许被取消、完成或失败事件推进。
target = {
"response.cancelled": "cancelled",
"response.completed": "completed",
"response.failed": "failed",
"response.incomplete": "incomplete",
}.get(event_type)
if target is not None:
conditional_update(response_id, target, event_key)
这里的 already_seen 和 save_event_once 应该共享唯一约束,不能只放在进程内存里;服务重启或多副本部署后,内存集合会失效。事件类型不在映射表时可以记录并返回成功,避免未知事件阻塞整个投递链,同时保留告警供后续补充。

把恢复和清理策略也写进状态模型
取消不是异常重试的同义词。客户端收到 cancelled 后,可以把任务展示为“已取消”,允许用户新建一次任务;failed 才适合根据错误类型决定自动重试;incomplete 则应单独保留原因,避免把不完整结果误当成成功。查询详情时仍可带上 response_id,方便把本地记录和远端对象对应起来。
清理时也不要马上删除所有行。至少保留事件去重键、最终状态和更新时间一段时间,让重试到达时仍能得到幂等结果;超过保留期再归档或删除事件明细。对外返回状态时只暴露业务需要的字段,不要把 webhook 签名、内部队列名或密钥相关信息放进客户端响应。
常见问题
收到 response.cancelled 后还要继续轮询吗?
通常不必把取消当成继续等待的中间态。保存为 cancelled 后结束本地任务;如果需要补充审计信息,再异步读取或记录,不要让用户界面一直显示处理中。
重复 webhook 会不会把 version 加很多次?
用 webhook-id 唯一约束做事件级去重,再用状态条件做任务级幂等。重复事件第一次更新成功,后续请求应直接返回成功但不重复推进业务状态。
为什么不能只在前端点击取消时改状态?
前端可能断网、刷新或重复点击,真正的远端状态仍由 API 和 webhook 传递。服务端收到 response.cancelled 后再落库,才能让多个客户端看到同一个最终状态。
我现在会把这类任务拆成两层:外部用 response_id 追踪 OpenAI 响应,内部用带条件的状态机追踪业务任务。这样取消、失败、完成和不完整结果各自有明确含义,网络重试也不会把已经确定的 cancelled 改回处理中。
-
501 收藏
-
501 收藏
-
501 收藏
-
501 收藏
-
501 收藏
-
383 收藏
-
科技周边 · 人工智能 | 19分钟前 | 人工智能 · openai · Webhook · WebHook OpenAI Responses API background response response.completed499 收藏
-
305 收藏
-
356 收藏
-
165 收藏
-
468 收藏
-
科技周边 · 人工智能 | 16小时前 | python · 人工智能 · Hugging Face Transformers tokenizer max_length truncation 句子对分词 only_second471 收藏
-
378 收藏
-
367 收藏
-
403 收藏
-
241 收藏
-
123 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 485次学习