上一代语音 Agent 的写法几乎是固定的:先跑一遍语音识别,把文本丢给大模型,等模型吐完再跑一遍语音合成。三段流水线串起来,架构清晰,Demo 也能跑通,但只要进了真实通话场景就会暴露三个老问题——每段交接都加一次延迟、用户一打断就得重置轮次、以及「听着像在思考」的停顿无法被自然表达。
OpenAI 在 2026 年 9 月把 GPT-Live-1 开进了 API,给出的答案是把这个级联拆掉:听和说由同一个模型联合处理,重推理则被显式外包出去。这篇工坊就按它的实际接口形态,从零搭一个能接真实通话的全双工语音 Agent。
一、架构:为什么要把推理「委托」出去
GPT-Live-1 的设计有一个很硬的前提:它本身不负责深度推理。它只做语音交互层——听、说、判断轮次边界、处理打断和反向信道。真正的工具调用和长链思考,交给一个后端文本模型,官方示例里配的是 GPT-6 Astra(指定 medium 推理强度),轻任务也可以挂 Terra 之类的低强度模型。
这样切分的好处很直接。语音延迟要求是几百毫秒量级,而深度推理动辄几秒到几十秒,两者的时间尺度差了两个数量级。如果强行让一个模型同时满足两边的约束,结果只能是语音变卡或者推理变浅。拆开之后,每一边都能按自己的节拍工作。
代价是:你必须自己维护前后端的状态一致性。这也是后面绝大多数坑的来源。
| 层面 | 职责 | 时间尺度 |
|---|---|---|
| 前端语音层(GPT-Live-1) | 音频理解与生成、轮次判断、打断、反向信道 | 百毫秒级 |
| 后端推理层(你的 harness) | 工具调用、检索、长链推理、代码执行 | 秒到数十秒 |
| 你的编排代码 | 用 delegation_id 关联两者、处理过期结果 | —— |
二、第一步:建立会话并选好后端
语音层的另一个实用特性是原生支持轮次检测但并非基于轮次。也就是说模型本身是流式的,但你可以要求它在显式轮次边界上工作。对于迁移传统的 IVR 状态机,这个开关很重要,它让你不必一次性重写全部对话逻辑。
语音库这次也扩了,Quartz、Ripple、Vesper、Willow、Stone、Gleam、Meridian、Bossa、Tempo、Beacon、Delta、Cinder 覆盖不同口音、方言与语种。选语音时不要只看音色,还要实测它在你的业务词汇上的发音稳定性。
// 会话配置:前端语音 + 后端推理两段式
const session = await live.createSession({
voice: "Meridian",
systemPrompt: `
你是一名餐厅预订助理。语气自然、节奏偏慢。
遇到需要查询库存、改期、计算费用的请求时,
调用 delegate 而不是自己猜答案。
如果用户打断你,先承认打断,再回答新问题。
`,
backend: {
model: "gpt-6-astra",
reasoningEffort: "medium", // 深度推理任务:medium 起步
tools: ["check_availability", "create_booking", "modify_booking"]
},
turnDetection: "native", // 可切换为显式轮次,便于迁移状态机
keywordBiasing: ["包间", "靠窗", "素食", "无障碍通道"]
});
keywordBiasing 值得单独说一句。语音模型对字母数字和专有名词的识别天然不稳,把门店名、SKU、房型名预置进去,比事后做文本纠错便宜得多。
三、第二步:事件协议与 delegation
前后端之间靠 delegation_id 串起来。前端发起一次委托时拿到这个 ID,后端跑完之后通过事件把结果贴回会话。官方示例里用的是 session.commentary.append——「往会话里追加一段旁白」,也就是让后端产出的文本以中间结果的形式进入对话流,而不是抢过话筒。
async function answer(live, delegationId, context) {
// 后端长跑:检索 + 推理 + 工具调用
const result = await backendThread.run({
input: context.transcriptTail,
tools: context.allowedTools
});
// 结果回灌到前端会话,仍然挂在同一条 delegation 上
live.send({
type: "session.commentary.append",
delegation_id: delegationId,
content: finalResponse(result)
});
}
// 前端触发委托,立刻拿到 ID,不等后端
live.on("delegation.requested", (evt) => {
const id = evt.delegation_id;
answer(live, id, buildContext(evt)).catch((err) => {
live.send({
type: "session.commentary.append",
delegation_id: id,
content: "这个我需要再确认一下,稍等半秒。"
});
});
});
注意上面那段 catch。委托失败必须有一条语音兜底,否则用户体验到的就是一段莫名其妙的沉默——比说一句「我需要确认一下」糟糕得多。
四、第三步:处理打断与过期结果
这是全双工真正区别于半双工的地方,也是我认为最容易写错的地方。
用户中途插话时,前端会立刻转向。但此时后端很可能还在跑——它不知道话题已经变了。如果你按「后端结果回来就播报」的朴素写法实现,就会出现经典事故:用户已经问到第三件事,Agent 突然念出第一件事的答案。
正确做法是给每个委托结果打上一个语义版本号,回灌前做一次相关性判断:
const inflight = new Map(); // delegation_id -> { topicSeq, inserted }
live.on("user.turn.detected", (evt) => {
topicSeq += 1; // 话题序号前进
for (const [id, meta] of inflight) {
if (meta.topicSeq !== topicSeq) {
meta.inserted = false; // 标记为可能过期
}
}
});
function onBackendResult(id, text) {
const meta = inflight.get(id);
if (meta.topicSeq !== topicSeq && !stillRelevant(text)) {
return; // 丢弃,或降级写入纪要
}
live.send({ type: "session.commentary.append", delegation_id: id, content: text });
}
另外两个实测有效的小技巧:
- 反向信道要显式打开。「嗯」「明白」「好的」这类短反馈以及笑声,应当被允许在模型说话期间发生而不打断它。关掉之后对话会变得很机械。
- 背景噪声与静音要做区分。官方明确改进了这块行为,模型不会因为环境音就中断叙述,也不会把每一步操作都大声念出来。如果你的提示词里写了「每步都汇报」,反而会把这个优化抵消掉。
五、基准与成本:值不值得换
官方给的几个数字可以作为选型参考:
| 指标 | 结果 |
|---|---|
| Full Duplex Bench | 较 GPT-Realtime-2.1 提升约 30 个百分点 |
| Tau3 端到端语音智能 | 搭配 GPT-6 Astra(medium)排名第一 |
| 前端语音层定价 | 每分钟 0.05 美元 |
| 客户实测(Speak 语言陪练) | 思考停顿期间的中断率下降近 80% |
| 客户实测(某工程团队) | 相较级联实现移除约 2.3 万行代码 |
成本这块要算清楚结构。语音层按会话分钟计费,只要会话开着就在计费,不论谁说。客服场景里用户思考、犹豫、找资料的时间全都算钱。所以会话超时回收策略必须做,否则一个挂着不动的通话会持续烧预算。
每小时语音成本 = 60 × 0.05 = 3.00 美元(每并发会话)
总成本 = 语音时长费 + 后端推理费 + 电话接入费
按这个结构,10 路并发的客服坐席,光语音层一小时就是 30 美元。这解释了为什么官方示例里的很多设计都在强调「早结束会话」。
六、上线前的检查清单
- 前端语音层,后端推理层,各自超时时间是否分开配置?
- 委托失败是否有语音兜底,而不是静默?
- 打断后,过期结果是否有丢弃逻辑?
- 反向信道是否开启,模型说话时用户能否自然应声?
- 会话回收策略是否存在,闲置多久强制结束?
- keywordBiasing 是否覆盖了业务专有名词?
- 通话录音与转写的合规声明是否就位?
结语
GPT-Live-1 的价值不在于「语音更自然」这种体感描述,而在于它把一个原本被级联架构强行耦合的问题拆成了两层:一层负责毫秒级的交互,一层负责秒级的思考。这个切分是对的,因为这两件事的工程约束本来就不同。
但它同时也把状态一致性的责任交给了你。级联架构里那些被流水线掩盖的边界情况——打断、迟到结果、话题漂移——在全双工里会全部浮到你的编排代码上。所以真正决定这套方案能不能上生产的,不是语音质量,而是你有没有认真写完那段处理过期结果的逻辑。