当前位置:首页 > 文章列表 > 科技周边 > 人工智能 > Go 调用 OpenAI Responses API 后台模式:从超时请求迁移到可轮询任务

Go 调用 OpenAI Responses API 后台模式:从超时请求迁移到可轮询任务

来源:17golang原创 2026-07-24 16:03:15 0浏览 收藏

接入长推理模型或带工具调用的 Responses API 时,走同步请求最先碰到的往往不是模型能力上限,而是HTTP请求的生命周期限制:客户端等不到结果就主动断开,服务端却可能还在后台跑任务。Go 服务端更稳妥的迁移方案,就是把单次调用拆成「提交后台响应任务」和「按响应ID轮询结果」两步,把任务状态直接落到自己的业务数据表中。

要点速览
  • background: true 提交长任务,首个接口返回只负责回传响应ID。
  • 轮询接口只认 queuedin_progresscompleted 等状态值,不用靠猜判断结果是否可读。
  • 轮询间隔建议从2秒起步,同时配置总运行时限、最大查询次数和退避策略,避免把超时问题转化为请求风暴。
  • OpenAI后台响应数据最多保留10分钟左右,不适合作为长期任务仓库,敏感项目还要重新核对Zero Data Retention的合规约束。

先看清同步调用为什么会失效

原来的常规写法基本是一次 POST /v1/responses,Go 客户端等着把全量结果读完才给前端返回。短文本场景下完全没问题,但长推理、文件分析或者多工具联动的链路,会把等待时间拉得很长。网关的30秒超时、客户端的60秒超时,和模型真正跑完的耗时,上限根本对不齐。

这时候别直接把HTTP超时一口气调到十分钟。长连接持续占用会挤压连接池配额,用户侧反复重试又容易生成大量重复任务。更合理的边界是:提交请求阶段只等「任务已成功创建」的回执,后续结果拉取全交给后台worker或者定时任务处理。

Go 提交 OpenAI Responses API 后台响应后获得 response ID 的任务创建流程

迁移时真正要改的三个核心字段

从同步模式切换到后台模式,业务代码至少要重新定义三个字段:外部响应ID、内部任务状态、最后查询时间。外部ID用来向OpenAI侧查询结果,内部状态直接服务于用户界面展示、重试逻辑和审计流程,绝对不能直接把OpenAI返回的外部状态当成自己数据库里的任务状态。

同步写法后台写法迁移后的检查点
等待完整响应返回提交任务后立刻存好response ID提交成功但任务未完成也算可恢复状态
一次读取返回的output_text任务标记completed之后再读取output字段未完成状态不能直接解析最终文本内容
HTTP超时直接判定任务失败区分查询失败、任务本身失败和本地超时三种场景判定失败的场景支持重试且不会重复生成新任务

Go 最小实现:提交一次,再按 ID 查询

下面的示例直接用标准库发送两个请求,能清晰看到协议层的边界。API密钥从环境变量读取,生产环境下应该放到密钥服务或者运行环境的保密配置里,绝对不能硬写进代码配置和输出到日志中。

package main

import (
    "context"
    "encoding/json"
    "fmt"
    "io"
    "net/http"
    "os"
    "time"
)

type responseEnvelope struct {
    ID     string `json:"id"`
    Status string `json:"status"`
    Output []struct {
        Type    string `json:"type"`
        Content []struct {
            Text string `json:"text"`
        } `json:"content"`
    } `json:"output"`
}

func submit(ctx context.Context, prompt string) (string, error) {
    body := map[string]any{
        "model": "gpt-5.4-pro",
        "input": prompt,
        "background": true,
    }
    raw, err := json.Marshal(body)
    if err != nil { return "", err }
    req, err := http.NewRequestWithContext(ctx, http.MethodPost,
        "https://api.openai.com/v1/responses", bytesReader(raw))
    if err != nil { return "", err }
    req.Header.Set("Authorization", "Bearer "+os.Getenv("OPENAI_API_KEY"))
    req.Header.Set("Content-Type", "application/json")
    res, err := http.DefaultClient.Do(req)
    if err != nil { return "", err }
    defer res.Body.Close()
    if res.StatusCode/100 != 2 { data, _ := io.ReadAll(res.Body); return "", fmt.Errorf("submit status %s: %s", res.Status, data) }
    var out responseEnvelope
    if err := json.NewDecoder(res.Body).Decode(&out); err != nil { return "", err }
    if out.ID == "" { return "", fmt.Errorf("missing response id") }
    return out.ID, nil
}

// bytesReader 省略了业务无关的 reader 封装,实际项目可直接使用 bytes.NewReader。
func bytesReader(raw []byte) io.Reader { return &sliceReader{data: raw} }
type sliceReader struct { data []byte; pos int }
func (r *sliceReader) Read(p []byte) (int, error) { if r.pos >= len(r.data) { return 0, io.EOF }; n := copy(p, r.data[r.pos:]); r.pos += n; return n, nil }

func query(ctx context.Context, id string) (responseEnvelope, error) {
    req, err := http.NewRequestWithContext(ctx, http.MethodGet,
        "https://api.openai.com/v1/responses/"+id, nil)
    if err != nil { return responseEnvelope{}, err }
    req.Header.Set("Authorization", "Bearer "+os.Getenv("OPENAI_API_KEY"))
    res, err := http.DefaultClient.Do(req)
    if err != nil { return responseEnvelope{}, err }
    defer res.Body.Close()
    if res.StatusCode/100 != 2 { return responseEnvelope{}, fmt.Errorf("query status %s", res.Status) }
    var out responseEnvelope
    err = json.NewDecoder(res.Body).Decode(&out)
    return out, err
}

func wait(ctx context.Context, id string) (responseEnvelope, error) {
    ticker := time.NewTicker(2 * time.Second)
    defer ticker.Stop()
    for {
        out, err := query(ctx, id)
        if err != nil { return responseEnvelope{}, err }
        switch out.Status {
        case "completed": return out, nil
        case "failed", "cancelled", "incomplete": return out, fmt.Errorf("background response ended with %s", out.Status)
        case "queued", "in_progress":
        default: return out, fmt.Errorf("unknown response status %q", out.Status)
        }
        select { case 

示例故意把提交和查询两个逻辑拆开。真实项目里可以换成官方维护的 openai-go SDK,但是核心逻辑要保留同样的状态机:New 只负责创建响应任务,查询阶段负责等待任务跑完并统一收口。需要把轮询结果映射到自己的 ai_tasks 表时,至少要存下 provider_idstatusattemptsnext_check_atlast_error

轮询不是死循环:给任务加上合理边界

最小示例里的固定2秒间隔只适合演示场景。线上跑的worker应该设置总时限,比如8分钟;每次查询失败就累加一次attempts计数,短暂网络错误可以做有限次数重试,连续多次失败后直接把内部任务标记为 provider_check_failed,等人工介入或者补偿任务后续处理。

拿到成功返回也不要只判断HTTP状态码200。只有响应里的任务状态为 completed 时,才可以读取返回文本;如果业务需要结构化结果,还要在本地额外做JSON解码和字段合法性校验。模型侧返回调用成功,不等于你的订单、工单或者摘要结果已经通过业务层校验。

Go 轮询 OpenAI Responses API 响应状态并在完成、失败和超时之间收口
const maxWait = 8 * time.Minute
ctx, cancel := context.WithTimeout(context.Background(), maxWait)
defer cancel()

result, err := wait(ctx, responseID)
if err != nil {
    // 保留 responseID 和错误,不要在这里无条件重新提交一份任务。
    return markTaskRetryable(responseID, err)
}
return saveOutput(responseID, result.Output)

保留窗口、数据策略和回归检查

后台模式适合把长耗时响应从前端长连接里转移出来,但是不能当成长期任务队列用。官方公开说明里后台响应数据大约只保留10分钟供查询,这个时间窗口远小于很多业务「隔天重试」的周期。需要长期留存任务记录的场景,要等响应完全跑完后,把经过脱敏和校验的业务结果写入自己的存储服务。

另一个容易被忽略的边界是数据策略:后台模式和Zero Data Retention规则不兼容。涉及用户个人资料、内部文档或者合规要求较高的场景,先和安全同事确认数据留存要求,再决定是否采用该模式,不要觉得请求变成异步的就默认数据风险完全消失。

  • 提交超时:先确认没有生成有效的response ID,再决定是否重试,避免生成大量重复任务。
  • 查询返回404:优先检查ID是否正确、项目权限是否正常、是否超出数据保留窗口,不要立刻新建任务覆盖原有记录。
  • 状态标记失败:保存完整错误信息和最后一次返回的响应内容,给业务侧留一个可解释的失败结果。
  • 结果校验:文本内容、JSON结构、业务自定义字段分别做校验,不能只靠服务商返回的成功状态判断结果可用。

常见问题

后台模式能不能替代消息队列?

不能。它解决的是模型侧长耗时响应的连接生命周期问题,不负责你的重试、幂等、优先级调度和长期存储。生产系统还是要用数据库任务表或者消息队列来管理业务侧的全流程任务。

轮询间隔固定为 2 秒可以吗?

小流量场景下可以作为起点。任务量上来之后要做退避和随机抖动,同时设置并发查询上限,不然查询请求本身会成为新的限流触发来源。

为什么不能拿到响应 ID 就读取 output?

提交阶段返回的只是响应对象基础信息和初始状态,不代表最终内容已经生成完成。只有状态变为 completed 之后,输出字段的内容才适合作为业务流程的输入。

使用 Go SDK 还是标准库?

官方SDK能减少请求结构和类型定义的重复工作;标准库更适合先把协议逻辑和状态机跑通验证。不管选哪一个实现方案,都要留存好response ID、内部状态和失败原因这三类数据。

迁移清单

把同步调用改成后台任务,核心不是把某个布尔字段设为true,而是把「等待结果」这个动作改造成可恢复的业务流程:先提交任务落库,再限时轮询结果,跑完之后校验输出,出问题时保留完整现场。这样即使前端网关连接中途断开,任务也不会跟着用户页面一起消失。

版本声明
本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
Go io/fs.ValidPath 为什么拒绝 ./config.yaml:FS 路径规则与迁移边界Go io/fs.ValidPath 为什么拒绝 ./config.yaml:FS 路径规则与迁移边界
上一篇
Go io/fs.ValidPath 为什么拒绝 ./config.yaml:FS 路径规则与迁移边界
Go slog 日志字段怎么安全流转:Attr、Handler 与敏感信息边界
下一篇
Go slog 日志字段怎么安全流转:Attr、Handler 与敏感信息边界
查看更多
最新文章
查看更多
课程推荐
  • 前端进阶之JavaScript设计模式
    前端进阶之JavaScript设计模式
    设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
    543次学习
  • GO语言核心编程课程
    GO语言核心编程课程
    本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
    516次学习
  • 简单聊聊mysql8与网络通信
    简单聊聊mysql8与网络通信
    如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
    500次学习
  • JavaScript正则表达式基础与实战
    JavaScript正则表达式基础与实战
    在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
    487次学习
  • 从零制作响应式网站—Grid布局
    从零制作响应式网站—Grid布局
    本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
    485次学习
查看更多
AI推荐
  • ljg-skills -
    ljg-skills
    ljg-skills 是李继刚开源的 AI 技能与提示词集合,面向大模型使用者整理了一批可复用的 prompt、角色设定和任务技能模板,适合用于学习提示词设计、搭建个人 AI 工作流和沉淀团队常用智能体能力。
    4665次使用
  • MELO音乐 - AI 音乐生成平台,支持多模态创作能力
    MELO音乐
    MELO音乐是一站式AI视频与音乐制作助手,对标suno, udio的高品质体验。提供伴奏生成、原创写词、无损导出、哼唱识曲、混音变声等全套音频与短视频编辑工具。无论是流行Kpop、电音说唱、民谣古风、摇滚儿歌还是商用轻音乐,MELO为你免费谱曲,轻松做同款!
    4274次使用
  • UniScribe - AI 免费在线音视频转文字平台
    UniScribe
    UniScribe 是一款 AI 音视频转文字与内容整理工具,支持上传音频、视频文件或粘贴 YouTube 链接,自动生成转写文本、摘要、思维导图和关键问题,并支持多格式导出,适合会议记录、课程学习、访谈整理和内容创作复盘。
    4231次使用
  • 剧云 - 免费 AI 智能中文剧本创作平台
    剧云
    剧云是专业中文剧本创作平台,安全稳定运行十余年,集成AI编剧、剧本医生审核、人物小传、剧情关系图、大纲编写、多人协作、Word导入导出、版权管控功能,数据安全防护,轻松高效创作剧本。
    4451次使用
  • 万象有声 - AI 一站式有声内容创作平台
    万象有声
    万象有声,一个专为有声创作者打造的新一代智能有声内容创作平台。平台提供专业的智能拆章、智能画本编辑、AI配音、AI生成音效、后期制作、智能对轨、智能审听等有声创作全流程工具,可以帮助创作者高效、低成本创作出引人入胜的有声作品。立即体验,让有声书制作更简单!
    4411次使用
微信登录更方便
  • 密码登录
  • 注册账号
登录即同意 用户协议隐私政策
返回登录
  • 重置密码