GPT-Realtime-2.1 只管语音回合、业务状态和工具调用自己收口

文章导读
把业务状态塞进语音会话里,最先出问题的往往不是模型质量,而是重连和阻塞:连接一断,新会话里没有工单号、没有当前流程节点,模型只能重新问一遍用户;工具调用如果在音频回调线程里同步等结果,上行采集和下行播放都会被卡住。可行的划分是让 GPT-Realtime-2.1 只负责“这一轮语音回合”,业务状态和工具结果留在自己的服务里,两层之间用显式字段对接。
📋 目录
  1. Ⅰ 画出一次语音回合的生命周期,标出哪些事件由语音侧产生
  2. Ⅱ 把业务状态放到语音会话之外,约定两层之间的消息字段
  3. Ⅲ 让工具调用的结果异步回到语音回合
  4. Ⅳ 用一轮带工具调用的对话验证音频不等待业务
A A

把业务状态塞进语音会话里,最先出问题的往往不是模型质量,而是重连和阻塞:连接一断,新会话里没有工单号、没有当前流程节点,模型只能重新问一遍用户;工具调用如果在音频回调线程里同步等结果,上行采集和下行播放都会被卡住。可行的划分是让 GPT-Realtime-2.1 只负责“这一轮语音回合”,业务状态和工具结果留在自己的服务里,两层之间用显式字段对接。

适用场景:用实时语音模型做外呼、坐席辅助或语音助手,且一轮对话里需要查订单、写工单、调外部接口。处理方向:语音侧只保留会话与回合状态,业务状态以 session_id / turn_id 为纽带放在业务库;工具调用走异步,先回执再回填结果。验证方式:在日志里按 source 字段区分事件来源,用一轮带工具调用的对话观察音频分片是否连续。风险边界:异步回填会改变多轮顺序,需要业务侧自己保证幂等与顺序。

画出一次语音回合的生命周期,标出哪些事件由语音侧产生

一次语音回合通常经历这几个阶段:会话建立、上行音频开始、语音活动检测(说话开始 / 说话结束)、转写文本生成、模型生成回复、下行音频分片播放、回合结束。其中前六个阶段的事件基本都由语音侧产生,只有“回合结束”之后是否还要继续动作,需要业务侧确认。

属于会话的状态包括:连接标识、音频流是否在推、当前是否处于说话中、未播完的音频缓冲、上下文窗口。属于业务的状态包括:用户身份、订单号或工单号、流程当前节点、已经执行过哪些工具、工具返回了什么。把这两类状态混在一张表里,重连时就必然要重建一次业务数据。

事件来源标注方法很朴素:接入层写日志时给每条事件加一个 source 字段,voice 表示语音侧回调,biz 表示业务服务自己产生;两类事件都带同一个 turn_id,排查时按 turn_id 排序就能还原一次回合的完整顺序。下面这张草图用于和团队对齐边界,不是接口定义。

语音侧(会话 / 回合)                 业务侧(状态 / 工具)
---------------------------------    ---------------------------
session.open
  audio.in
  vad.start            voice 事件
  vad.stop        ─────────────────►  只记录,不写业务状态
  transcript.delta
  response.audio.delta
  turn.end        ─────────────────►  业务侧决定是否开新一轮
                                      biz: 订单 / 工单 / 节点
                    tool.call ──────►  入队
                    ◄──── tool.result(异步回填,带 reply_to)

把业务状态放到语音会话之外,约定两层之间的消息字段

重连丢状态的根因是业务数据存在了会话上下文里。把业务状态挪到自己的存储后,语音会话重建时只要把业务键重新带进去就能恢复;业务服务重启同理,只要业务库还在,语音侧不需要知道这些细节。

GPT-Realtime-2.1 只管语音回合、业务状态和工具调用自己收口
  • session_id:语音会话标识,两边必须一致;若由语音侧生成,业务侧要建一张映射表
  • turn_id:单次语音回合标识,用于把一次回合内的所有事件串起来
  • biz_key:业务主键,用工单号或订单号这类业务唯一键,不要拿随机 UUID 当业务主键
  • event_type:如 transcript.delta、tool.call、tool.result
  • reply_to:回填时指向原始 tool_call 的 id
  • deadline_ms:该工具调用允许等待的毫秒数,超时由业务侧负责回填
  • idempotency_key:幂等键,重连重放时避免同一工具被执行两次

会话标识的传递方式:建立会话时通过请求 metadata 或首帧 hello 消息把 session_id 和 biz_key 传进去,语音侧每个回调原样回带。不要在每一个音频帧里重复携带业务对象,音频链路不该承担这个职责。

{
  "session_id": "<语音会话标识>",
  "turn_id": "<本回合标识>",
  "biz_key": "<工单号或订单号>",
  "event_type": "tool.result",
  "reply_to": "<tool_call_id>",
  "status": "ok | timeout | error",
  "result": { "fields": "按各工具自己约定" },
  "idempotency_key": "<tool_call_id>"
}

替换项:session_id 若由业务侧指定,需要在建立会话的请求里显式声明;biz_key 的取值要和业务库主键一致,避免多一层翻译。

让工具调用的结果异步回到语音回合

时序上分四步:模型输出 tool.call,语音侧把事件抛给业务侧;业务侧立即返回 accepted 并带上 tool_call_id,不返回结果;语音侧不等待,继续收发音频;业务侧处理完成后调用语音侧的“注入事件”入口,带上 reply_to 把结果回填,语音侧据此生成后续回复。

  1. tool.call 到达业务侧,写入任务队列并立刻回执
  2. 音频链路不受影响,用户可以继续说话或听到停顿提示
  3. 工具执行完成,业务侧发起一次注入事件调用
  4. 语音侧消费注入事件,产生新的语音回合

超时处理写法:业务侧为每个任务设一个 deadline,超时也要回填一条 tool.result,status 填 timeout,宁可让模型说“正在处理”,也不要让语音侧无限等待。语音侧自身再设一个最大等待时长,超过就先播一句占位话术,避免静音。

GPT-Realtime-2.1 只管语音回合、业务状态和工具调用自己收口
def on_tool_call(call):
    # call: {tool_call_id, session_id, turn_id, name, args, deadline_ms}
    biz_key = store.lookup(call.session_id)            # 业务侧维护的映射
    enqueue(worker, {**call, "biz_key": biz_key})
    return {"status": "accepted", "tool_call_id": call.tool_call_id}  # 立即返回

def worker(job):
    try:
        result, status = run_tool(job.name, job.args, timeout=job.deadline_ms / 1000), "ok"
    except TimeoutError:
        result, status = None, "timeout"
    except Exception as exc:
        result, status = str(exc), "error"
    voice.inject_event({
        "session_id": job.session_id,
        "turn_id": job.turn_id,
        "reply_to": job.tool_call_id,
        "event_type": "tool.result",
        "status": status,
        "result": result,
        "idempotency_key": job.tool_call_id,
    })

需要留意的边界:异步回填意味着结果到达顺序不一定等于调用顺序,业务侧要么给工具调用编号,要么在回填前做一次顺序校验。

用一轮带工具调用的对话验证音频不等待业务

验证场景:准备一轮会触发工具调用的对话,业务侧的执行函数里人为加一段延迟(例如固定等待若干秒),其余逻辑保持不变。这样做的目的是把“业务慢”放大到能被日志观察到,而不是为了测性能。

观察指标主要有五个:上行音频分片的发送间隔是否稳定;下行音频分片的播放间隔是否稳定;tool.call 到业务侧回执之间的耗时;业务侧回填结果到语音侧开始生成的间隔;日志中 voice 与 biz 两类事件是否按 turn_id 正常交错,而不是出现一长段 voice 事件空白。

记录点方向sourceevent_typeturn_id备注
T0—voicesession.open—记录 session_id 与 biz_key
T1上行voicevad.start / vad.stopturn-1确认音频在推
T2出voicetool.callturn-1记录 tool_call_id 与 deadline_ms
T3回biztool.acceptedturn-1回执时间,此时音频不应中断
T4入biztool.resultturn-1带 reply_to,注明 status
T5下行voiceresponse.audio.deltaturn-1确认回填后才有新回复

填写方式:每条记录只填实际观察到的时间点或相对顺序,不补齐推测值;如果某个 event_type 在日志里没出现,就在备注里写“未观察到”,而不是留空。判定标准也很直接——T2 到 T3 之间以及业务执行期间,上行和下行音频的分片记录应当是连续的;若这段时间里 voice 事件出现整段空白,说明工具调用仍然卡在音频线程里,需要回到第二节的字段约定和第三节的异步回填重新对齐。