使用全新的 OpenAI Realtime API 构建低延迟音频管道,使开发人员能够在生产环境中推出类似人类的对话式语音智能体。传统上,构建语音界面意味着将三个独立的模型层链接在一起:自动语音识别(ASR)、基于文本的 LLM 逻辑层,以及文本转语音(TTS)合成。该多步骤管道引入了显著的往返网络延迟,使得自然对话变得不可能。通过持久的 WebSocket 连接进行原生音频处理改变了这一现状,将网络延迟降低到 300 毫秒以下。本指南阐述了如何建立连接状态、流式传输原始音频缓冲区以及优化会话配置。
[!IMPORTANT] API 安全警告:切勿在客户端浏览器脚本中直接公开您的 OpenAI API 密钥。始终通过安全的边缘中间件(如 Cloudflare Worker)代理 WebSocket 连接,该中间件会在将数据包转发到 OpenAI 之前附加授权请求头。
核心要点:
- WebSocket 连接:使用边缘代理直接连接到 OpenAI 的实时 WebSocket 网关。
- 原生模态(Modalities):在初始会话更新配置有效负载中同时指定
text和audio。- 音频格式:将用户语音流式传输为 24kHz 下以 base64 编码的单声道 PCM16 数据块。
- 语音打断:监控服务器发出的
speech-started信号,以便立即停止客户端的音频播放。
原生音频 LLM 的架构
传统的语音技术栈将语音识别和语音合成模型视为中心文本模型周围的外部包装器。原生音频模型通过直接处理语音来消除这一开销。
随着 OpenAI 模型系列的发布,网络原生处理音频输入和输出。模型接收原始音频波形,分析音调、语调起伏以及内容,并直接生成自然的语音输出。因此,该管道消除了 ASR 转录错误和 TTS 合成瓶颈。
管理这种持久连接依赖于 WebSockets。连接在整个通话期间保持开启状态,允许智能体在检测到用户说话时中断其输出,从而使体验与真实的电话通话完全匹配。
获取 AI 集成服务前提条件
在编写任何代码之前,请确保您的环境已准备就绪。此构建假设您熟悉异步 JavaScript 并已具备以下条件:
- 具有实时访问权限的 OpenAI 帐户以及已充值且处于活动状态的 API 密钥。
- Node.js 20 或更高版本,或者使用
npm create cloudflare@latest构建的 Cloudflare Workers 项目。 - WebSocket 客户端 — 用于 Node.js 网关的
ws软件包,或 Workers 内部可用的原生全局WebSocket。 - 浏览器前端 — 可以通过 Web Audio API(
getUserMedia加上用于重采样的AudioWorklet)捕获麦克风音频。 - base64 和 PCM 音频的实用知识 — 因为您发送或接收的每个帧都是 base64 编码的 PCM16 有效负载。
也为帐户设置预留一点时间:必须在您的组织上启用实时访问和计费,网关才会接受会话。
建立 WebSocket 连接
首先,您打开与 OpenAI 实时网关的连接,并在请求头中指定 realtime 实时模型。
下面的 JavaScript 代码演示了如何初始化连接、配置会话模态以及处理输入和输出流音频缓冲区:
1import WebSocket from "ws";
2
3export async function startVoiceAgent(env) {
4 // Connect to the OpenAI Realtime WebSocket gateway
5 const url = "wss://api.openai.com/v1/realtime?model=gpt-realtime";
6 const ws = new WebSocket(url, {
7 headers: {
8 "Authorization": `Bearer ${env.OPENAI_API_KEY}`,
9 "OpenAI-Beta": "realtime=v1"
10 }
11 });
12
13 ws.on("open", () => {
14 console.log("WebSocket connection established with OpenAI Realtime API");
15
16 // Configure session modalities and voice parameters
17 const sessionConfig = {
18 type: "session.update",
19 session: {
20 modalities: ["text", "audio"],
21 instructions: "You are a helpful customer service assistant for Mecanik.",
22 voice: "alloy",
23 input_audio_format: "pcm16",
24 output_audio_format: "pcm16",
25 temperature: 0.7
26 }
27 };
28 ws.send(JSON.stringify(sessionConfig));
29 });
30
31 ws.on("message", (data) => {
32 const event = JSON.parse(data);
33
34 // Handle incoming audio content from the server
35 if (event.type === "response.audio.delta") {
36 const audioBuffer = Buffer.from(event.delta, "base64");
37 // Output buffer to client audio player
38 playAudioChunk(audioBuffer);
39 }
40 });
41}
构建此处理程序时,请确保您的 API 密钥对客户端浏览器保持不可见。具体来说,在您的服务器上建立边缘中间件,以便在代理 WebSocket 连接之前处理授权。要了解边缘 API 配置,请阅读我们关于 使用 Cloudflare Workers 构建无服务器 API 的指南。
构建安全的边缘代理
上面的代码段从受信任的服务器连接,但它没有显示保证密钥安全的部分:代理本身。在 Cloudflare Workers 上,您无法将自定义请求头附加到 new WebSocket() 构造函数中,因此您可以使用 fetch 和 Upgrade 请求头打开上游连接。Worker 接受浏览器的套接字,使用附加的密钥呼叫 OpenAI,然后在两者之间传输帧数据。
1export default {
2 async fetch(request, env) {
3 if (request.headers.get("Upgrade") !== "websocket") {
4 return new Response("Expected a WebSocket upgrade", { status: 426 });
5 }
6
7 // 1. Accept the browser <-> Worker socket
8 const [client, server] = Object.values(new WebSocketPair());
9 server.accept();
10
11 // 2. Open the Worker <-> OpenAI socket with the secret key attached
12 const upstreamResponse = await fetch(
13 "https://api.openai.com/v1/realtime?model=gpt-realtime",
14 {
15 headers: {
16 Upgrade: "websocket",
17 Authorization: `Bearer ${env.OPENAI_API_KEY}`,
18 "OpenAI-Beta": "realtime=v1"
19 }
20 }
21 );
22
23 const upstream = upstreamResponse.webSocket;
24 if (!upstream) {
25 return new Response("Upstream refused the upgrade", { status: 502 });
26 }
27 upstream.accept();
28
29 // 3. Pipe frames in both directions
30 server.addEventListener("message", (e) => upstream.send(e.data));
31 upstream.addEventListener("message", (e) => server.send(e.data));
32
33 const close = () => { try { server.close(); upstream.close(); } catch {} };
34 server.addEventListener("close", close);
35 upstream.addEventListener("close", close);
36
37 return new Response(null, { status: 101, webSocket: client });
38 }
39};
使用 wrangler secret put OPENAI_API_KEY 将密钥存储为加密的机密数据(secret),而不是放在 wrangler.toml 中,以防止它进入您的版本控制库。浏览器现在连接到 wss://your-worker.workers.dev 且永远无法获取您的 API 密钥凭据。Cloudflare 在其 Workers WebSockets 参考文档
中详细记录了这种双向模式。
处理用户音频流式传输
一旦会话更新被接受,您的客户端必须捕获麦克风输入,将其压缩为 24kHz 单声道 PCM16 数据,并将其作为 base64 分段进行流式传输。由于连接是持久的,处理网络断开至关重要。本地分块缓冲区可确保短暂的移动连接中断不会导致音频包丢失或智能体响应抖动:客户端将保留缓冲区并在重新连接后立即重放。
1// Example of streaming user microphone data
2function streamMicrophoneChunk(ws, base64AudioChunk) {
3 const audioEvent = {
4 type: "input_audio_buffer.append",
5 audio: base64AudioChunk
6 };
7 ws.send(JSON.stringify(audioEvent));
8}
每当用户停止说话时,服务器会自动处理累积的音频缓冲区并触发模型响应。有关完整的 API 事件列表,请查看 OpenAI 实时指南 。
此外,在前端播放器中实施回声消除。如果麦克风拾取了扬声器的音频输出,智能体就会将自己的声音解释为用户的打断,从而导致会话循环失败。要了解有关前端优化的更多信息,请查看我们关于 WordPress 与自定义 Web 开发对比 的指南。
管理发言权检测与打断
默认情况下,您必须告诉模型发言何时结束。启用服务器端语音活动检测(VAD)可将该工作交给 OpenAI:网关监控输入缓冲区,确定用户何时停止说话,并自动触发响应。在握手期间发送的会话更新中添加一个 turn_detection 块。
1const sessionConfig = {
2 type: "session.update",
3 session: {
4 modalities: ["text", "audio"],
5 voice: "alloy",
6 input_audio_format: "pcm16",
7 output_audio_format: "pcm16",
8 turn_detection: {
9 type: "server_vad",
10 threshold: 0.5,
11 prefix_padding_ms: 300,
12 silence_duration_ms: 500
13 }
14 }
15};
启用 VAD 后,一旦服务器在智能体说话时听到用户发声,它就会发出 input_audio_buffer.speech_started 信号。请将该事件视为立即停止播放的指令:清空前端播放器中所有排队的音频块,否则前面的响应会在新的响应到达时继续播放。
1let playbackQueue = [];
2
3ws.on("message", (raw) => {
4 const event = JSON.parse(raw);
5 if (event.type === "input_audio_buffer.speech_started") {
6 // User is interrupting: drop everything still buffered
7 playbackQueue = [];
8 stopSpeaker();
9 }
10});
提高 silence_duration_ms 会使智能体在回复前等待更长时间,这适合说话慢或犹豫不决的人;降低它会使交流感觉更敏捷,但有在半句中打断说话者的风险。
语音智能体部署步骤
要部署您的语音智能体中间件,首先在无服务器边缘节点上配置安全的 WebSocket 路由网关以保护您的 OpenAI 凭证。这可以防止爬虫程序发现您的密钥。
接下来,仔细起草系统提示词和规则。在初始负载更新会话事件中设置音调、词汇和响应指示。因此,这为对话流建立了清晰的范围。
然后实施强大的说话者打断逻辑。您应该监听 input_audio_buffer.speech_started 事件并立即在前端播放器中停止音频播放。最后,通过在离用户较近的区域部署 Workers 来减少路由跃点(routing hops),从而优化全局延迟。要探索边缘托管选项,请阅读我们的 Cloudflare Workers AI 教程
。
常见陷阱与故障排除
大多数首次运行失败都是由少数可预测的错误引起的。下表将您看到的症状映射到其常见原因和修复方法。
| 异常现象 | 可能的原因 | 解决方法 |
|---|---|---|
连接关闭且立即提示 401 | 缺失或格式错误的 Authorization 请求头 | 确认代理附加了 Bearer <key> 且该密钥具有实时访问权限 |
| 智能体只能听见静音或杂乱的语音 | 音频发送的采样率或位深度错误 | 在进行 base64 编码之前重采样为 24kHz 单声道 PCM16 |
| 用户说话后模型从不回应 | turn_detection 已禁用且没有发送手动提交指令 | 启用 server_vad,或者发送 input_audio_buffer.commit 然后发送 response.create |
| 智能体在用户说话时叠音播放 | speech_started 处理程序未清除排队音频 | 监听到该事件时清空播放缓冲区并停止播放器声音 |
| 响应在半句中切断 | max_response_output_tokens 设置得太低 | 提高上限或将其保留为 inf |
有两个更隐蔽的问题值得注意。带低余额到达的 rate_limits.updated 事件是并发会话即将被限制的早期警告;记录该事件并进行限流,而不是强行发起重连。此外,如果智能体偶尔会回答自己上一句话,则是麦克风捕获到了扬声器输出,请加强回声消除或使用耳机测试。
测试与生产环境注意事项
本地测试很麻烦,因为您需要双向流动的真实音频。最快的循环方式是使用 wrangler dev 运行 Worker,将一个简单的浏览器页面指向它,并在关注音质之前在控制台中查看事件流。在开发过程中记录每个入站事件类型;一旦 session.updated、speech_started、response.audio.delta 和 response.done 的序列看起来正确,您就掌握了可靠的集成通路。
在您发版前,权衡几个生产实际情况:
- 成本:实时音频按输入和输出音频 Token 的分钟计费,比纯文本 Token 贵得多。设置会话时长上限,并对较长的信息型回答考虑使用文本备选。
- 延迟:在靠近用户的地方部署代理并使其保持轻量。浏览器、边缘和 OpenAI 之间的每个额外跃点都会增加您努力缩小的往返延迟。
- 重连:移动网络上的套接字容易断开。在客户端上保留一段较短的轮转缓冲区,并在重连后重放,这样丢失的一帧就不会破坏整个对话。
- 可观测性:针对会话启动、中断次数和错误事件输出结构化日志,以便您在用户投诉前发现降级行为。
- 安全与隐私:语音是个人数据。明确数据保留期限,如果您的智能体处理敏感的工作流,请在转录文本上添加服务器端内容审查。
在进行任何广泛发布之前,与真实通话者进行短暂试运行,这会暴露没有脚本测试覆盖的口音、噪音和中断等极端情况。
核心要点
- OpenAI Realtime API 通过持久的 WebSockets 原生处理音频,将延迟保持在 300 毫秒以下。
- 始终在初始 WebSocket 握手期间配置模态、格式和提示词。
- 实时将用户音频流式传输为单声道 PCM16 base64 分段。
- 通过监视开始讲话事件来处理说话者打断事件。
- 通过使用边缘 Worker 路由代理连接细节来维护 API 凭证安全。
常见问题(FAQ)
什么是 OpenAI Realtime API? OpenAI Realtime API 是一种 WebSocket 接口,允许开发人员将原始音频流式传输输入和输出模型,从而绕过独立的 ASR/TTS 模块。通过原生处理音频,模型保留了情感音调、口音起伏和说话细微差别,实现 300 毫秒以下的延迟时间。
我该如何处理语音打断?
监听 input_audio_buffer.speech_started 服务器事件。收到后,清空您的前端音频缓冲区并立即停止播放器音频。因此,这创建了自然的对话流,使得 AI 智能体在用户开始说话时立即停止发声。
API 支持哪些音频格式? API 原生支持 24kHz 单声道 PCM16(原始 16 位有符号整数)和 G.711(u-law 和 a-law)音频格式。开发人员必须在客户端捕获麦克风数据,转换为这些特定格式,并在 JSON WebSocket 帧内以 base64 编码的字符串形式流式传输。
我需要单独的服务器来协调 WebSocket 流量吗? 需要。强烈建议运行无服务器边缘协调器(例如 Cloudflare Workers 或轻量级 Node.js 网关)。该代理接收来自客户端浏览器的麦克风输入,附加安全的授权请求头,并将二进制流重定向到 OpenAI 的网关。
如何防止实时语音智能体中的回声循环? 当扬声器音频泄漏回客户端麦克风,从而触发虚假的说话者打断事件时,就会发生回声循环。开发人员必须在客户端实施回声消除算法,或在测试期间使用耳机,以防止反馈循环破坏会话。
评论