跳至主要内容

OpenAI Realtime API:示例指南

学习如何使用 OpenAI 的 Realtime API 构建实时 AI 应用。本教程涵盖 WebSocket、Node.js 设置、文本/音频消息、函数调用,以及部署 React 语音助手演示。
· 15分钟 阅读

使用 AI 探索

新推出的OpenAI Realtime API使我们能够将快速、低延迟、多模态的体验集成到应用中。借助该 API,我们可以在用户与大语言模型(LLM)之间创建无缝的语音交互。

该 API 将语音体验所需的多种模型整合为一体,无需再拼接多个服务。它不仅旨在降低延迟,还能更好地保留情感细节和对话的自然流畅性。

在本文中,我们将学习如何使用 OpenAI Realtime API 构建语音控制的 AI 助手。我们会使用 Node.js 创建持久的 WebSocket 连接,并展示如何在终端中与 API 通信。此外,我还会带您部署一个使用该 API 能力的 React 应用。

为 OpenAI Realtime API 生成 API 密钥

要使用OpenAI Realtime API,我们首先需要生成一个 API 密钥。前往API 密钥页面(需要账号)。在页面顶部,点击“Create new secret key”。

为 OpenAI Realtime API 创建新的密钥

会弹出一个窗口。使用默认选项并点击“Create secret key”。

创建 OpenAI API 密钥的默认选项

密钥创建后,您将有机会复制它。请在关闭窗口前完成复制,因为这是唯一一次显示它的时机。

复制 OpenAI API 密钥

如果密钥丢失,我们可以随时删除并重新创建一个新的。

为了存储密钥,我们建议创建一个名为 .env 的文件,并按以下格式保存密钥:

OPENAI_API_KEY=<paste_they_key_here>

本文接下来都会通过该文件来连接 API。

Realtime API 的成本考量

在继续之前请注意,Realtime API 并非免费使用,我们需要在账号中充值才可调用。可在个人资料中的计费页面进行充值。

为便于了解成本,以下是我在撰写本文时试验 API 的花费概览:

使用 OpenAI Realtime API 的成本

我总共花了大约 5 美元。这并不算多,但相比 OpenAI 提供的其他 API 要昂贵不少。请在阅读本文时留意这一点。

大部分费用(第一根柱状)来自我在本文结尾体验的 React 控制台应用。其余约半美元,是通过 WebSocket 使用 API 的花费。因此,跟着本文实践的成本完全可以控制在 1 美元以内。

有关 API 定价的更多详情,请查看其定价页面中“Realtime API”部分。

通过 WebSocket 使用 Realtime API

不同于OpenAI API 的其他组件,Realtime API 使用 WebSocket。WebSocket 是一种通信协议,可在客户端与服务器之间建立双向通信通道。与传统的 HTTP 请求-响应模型不同,WebSocket 支持持续、实时的交互。这使得 WebSocket 特别适合实时应用,例如语音聊天。

本文将介绍 WebSocket 的工作方式,并包含多个与 Realtime API 交互的示例。

我们将使用 Node.js,因此请确保您的电脑已安装。如果尚未安装,可从其官方网站下载安装。

初始化脚本

建议创建一个包含上文 .env 文件的文件夹。进入该文件夹后,运行以下命令初始化脚本:

npm init -y && touch index.js

命令执行完成后,文件夹中应包含下列文件:

初始目录结构

安装依赖

首先安装两个包:

  • ws:WebSocket 包,与 API 交互所需的核心包。
  • dotenv:用于从 .env 文件加载 API 密钥的工具包。

运行以下命令进行安装:

npm install ws dotenv

连接 Realtime API

要与 Realtime API 建立连接,我们创建一个新的 WebSocket 对象,传入 API 地址以及必须的请求头:

// Import the web socket library
const WebSocket = require("ws");
// Load the .env file into memory so the code has access to the key
const dotenv = require("dotenv");
dotenv.config();
function main() {
  // Connect to the API
  const url = "wss://api.openai.com/v1/realtime?model=gpt-4o-realtime-preview-2024-10-01";
  const ws = new WebSocket(url, {
      headers: {
          "Authorization": "Bearer " + process.env.OPENAI_API_KEY,
          "OpenAI-Beta": "realtime=v1",
      },
  });
}
main();

设置连接打开时的动作

上述代码已创建与 API 的 WebSocket 连接,但尚未进行任何操作。

WebSocket 允许我们为特定事件注册回调。我们可以监听 open 事件,在连接建立后执行指定代码。

添加事件监听器的一般语法如下:

ws.on(<event>, <function>);

将 <event> 替换为事件名字符串,将 <function> 替换为事件发生时要执行的函数。

如下示例展示了如何在连接就绪后输出文本:

// Add inside the main() function of index.js after creating ws
async function handleOpen() {
  console.log("Connection is opened");
}
ws.on("open", handleOpen);

运行代码的命令为:

node index.js

如果 API 密钥配置正确,终端将看到“Connection is open”消息。由于连接保持打开,脚本会持续运行,需要手动停止。

设置收到消息时的动作

另一个可响应的事件是 message。每当从服务器接收到消息时都会触发。我们来添加一个函数以显示每条收到的消息:

// Add inside the main() function of index.js
 async function handleMessage(messageStr) {
    const message = JSON.parse(messageStr);
    console.log(message);
}
ws.on("message", handleMessage);

现在运行脚本,还应显示会话初始化时 API 发送的session.created 事件。

其他 WebSocket 事件

上文我们学习了如何监听 open 和 message 事件。WebSocket 还支持另外两个事件,我们在示例中不会使用。

close 事件可用于在套接字关闭时添加回调:

async function handleClose() {
  console.log(“Socket closed”);
}
ws.on(“close”, handleClose);

error 事件用于在出错时添加回调:

async function handleError(error) {
  console.log(“Error”, error);
}
ws.on(“error”, handleError);

与 Realtime API 通信

使用 WebSocket 需要我们以事件驱动方式编程。消息在通道中来回传递,我们无法控制消息何时发送或接收。

发起通信的代码应写在 handleOpen() 中。O 否则可能在通道尚未建立前执行,导致报错。

处理消息的代码亦然,所有逻辑应写在 handleMessage() 函数中。

在接下来的示例中,我们将使用如下代码作为起点。大多数修改都发生在 handleOpen() 和 handleMessage()。

// Import the web socket library
const WebSocket = require("ws");
// Load the .env file into memory so the code has access to the key
const dotenv = require("dotenv");
dotenv.config();
function main() {
  // Connect to the API
  const url = "wss://api.openai.com/v1/realtime?model=gpt-4o-realtime-preview-2024-10-01";
  const ws = new WebSocket(url, {
      headers: {
          "Authorization": "Bearer " + process.env.OPENAI_API_KEY,
          "OpenAI-Beta": "realtime=v1",
      },
  });
  async function handleOpen() {
    console.log("Connection is opened");
  }
  ws.on("open", handleOpen);
  async function handleMessage(messageStr) {
    const message = JSON.parse(messageStr);
    console.log(message);
  }
  ws.on("message", handleMessage);  
}
main();

使用 Realtime API 收发文本

与 Realtime API 的通信通过事件来完成。OpenAI 的实时文档 API列出了所支持的事件。我们使用conversation.item.create 事件开启对话。事件以 JSON 对象表示,其字段含义见文档。

以下是一个conversation.item.create 事件示例,发送提示语“用一句话解释什么是 web socket(网页套接字)”:

const createConversationEvent = {
  type: "conversation.item.create",
  item: {
    type: "message",
    role: "user",
    content: [
      {
        type: "input_text",
        text: "Explain in one sentence what a web socket is"
      }
    ]
  }
};

该事件表明我们希望发起文本对话。这在 content 字段中指定,类型为 “input_text”,并提供文本提示。

我们使用 ws.send() 发送消息。WebSocket 包要求参数为字符串,因此需要使用 JSON.stringify() 将 JSON 事件转换为字符串。结合起来,如下发送事件:

ws.send(JSON.stringify(createConversationEvent));

这会发起对话,但并不会自动触发 API 返回。要触发回复,我们发送一个response.create 事件。示例如下:

const createResponseEvent = {
  type: "response.create",
  response: {
      modalities: ["text"],
      instructions: "Please assist the user.",
  }
}
ws.send(JSON.stringify(createResponseEvent));

该事件通过 modalities 参数请求文本回复。instructions 是最重要的部分,用于描述我们希望模型执行的任务,这里是通用的“请协助用户”。

我们将这两个事件写入 handleOpen(),这样一旦连接建立就立即开启对话。以下是本例中 handleOpen() 的完整实现:

async function handleOpen() {

  // Define what happens when the connection is opened

  // Create and send an event to initiate a conversation

  const createConversationEvent = {

    type: "conversation.item.create",

    item: {

      type: "message",

      role: "user",

      content: [

        {

          type: "input_text",

          text: "Explain in one sentence what a web socket is"

        }

      ]

    }

  };

  // Create and send an event to initiate a response

  ws.send(JSON.stringify(createConversationEvent));

  const createResponseEvent = {

    type: "response.create",

    response: {

        modalities: ["text"],

        instructions: "Please assist the user.",

    }

  }

  ws.send(JSON.stringify(createResponseEvent));

}

对于入站消息,本例中值得注意的有三个事件:response.text.delta、response.text.done 和 response.done:

我们可以在 handleMessage() 中用 switch 语句指定脚本如何响应这些事件:

async function handleMessage(messageStr) {
  const message = JSON.parse(messageStr);
  // Define what happens when a message is received
  switch(message.type) {
    case "response.text.delta":
      // We got a new text chunk, print it
      process.stdout.write(message.delta);
    break;
    case "response.text.done":
      // The text is complete, print a new line
      process.stdout.write("\n");
    break;
    case "response.done":
      // Response complete, close the socket
      ws.close();
    break;
  }
}

本例中,我们利用 response.text.delta 将收到的文本块实时打印到控制台。回复完成时触发 response.text.done,我们打印一个换行以表明输出结束。最后收到 response.done 时关闭 WebSocket。

要运行该示例,将这些函数粘贴进上述模板代码,并使用以下命令:

node index.js

终端会对提示语 “Explain in one sentence what a web socket is” 生成回复,类似于使用ChatGPT 的体验。

文本示例的完整代码见此处。

使用 Realtime API 收发音频

上一个示例展示了如何处理文本数据。然而,Realtime API 的真正价值在于创建能实时回复的语音助手。

处理音频较文本略为复杂。我们将略过一些与音频工作机制相关、但会分散注意力的细节。

首先安装两个包:

npm install node-record-lpcm16 speaker
  • node-record-lpcm16 用于从麦克风录音,以便发送语音提示。
  • speaker 用于播放 AI 的语音回复。

此外还需安装 SoX(Sound eXchange),这是一个命令行音频处理工具,Node 库将用它与麦克风交互并录音。macOS 使用 brew install sox,Linux 使用 sudo apt install sox。

安装完成后,我们引入这些包,并添加一个 startRecording() 函数来录制用户的语音提示。为不偏离主题,我们不展开解释该函数的内部实现。

在加载环境变量后,将以下代码添加至 index.js:

// Add to index.js before the main() function
// Import the web socket library
const WebSocket = require("ws");
// Load the .env file into memory so the code has access to the key
const dotenv = require("dotenv");
dotenv.config();
const Speaker = require("speaker");
const record = require("node-record-lpcm16");
// Function to start recording audio
function startRecording() {
  return new Promise((resolve, reject) => {
    console.log("Speak to send a message to the assistant. Press Enter when done.");
    // Create a buffer to hold the audio data
    const audioData = [];
    // Start recording in PCM16 format
    const recordingStream = record.record({
      sampleRate: 16000, // 16kHz sample rate (standard for speech recognition)
      threshold: 0, // Start recording immediately
      verbose: false,
      recordProgram: "sox", // Specify the program
    });
    // Capture audio data
    recordingStream.stream().on("data", (chunk) => {
      audioData.push(chunk); // Store the audio chunks
    });
    // Handle errors in the recording stream
    recordingStream.stream().on("error", (err) => {
      console.error("Error in recording stream:", err);
      reject(err);
    });
    // Set up standard input to listen for the Enter key press
    process.stdin.resume(); // Start listening to stdin
    process.stdin.on("data", () => {
      console.log("Recording stopped.");
      recordingStream.stop(); // Correctly stop the recording stream
      process.stdin.pause(); // Stop listening to stdin
      // Convert audio data to a single Buffer
      const audioBuffer = Buffer.concat(audioData);
      // Convert the Buffer to Base64
      const base64Audio = audioBuffer.toString("base64");
      resolve(base64Audio); // Resolve the promise with Base64 audio
    });
  });
};

startRecording() 函数会从麦克风录音,并在按下“Enter”后停止。

接下来在 main() 中初始化 Speaker(),用于播放 AI 的回复:

// Add to the main() function after ws is initialized
const speaker = new Speaker({
  channels: 1, // Mono or Stereo
  bitDepth: 16, // PCM16 (16-bit audio)
  sampleRate: 24000, // Common sample rate (44.1kHz)
});

准备就绪后,我们可以实现 handleOpen() 和 handleMessage() 来处理音频。

在 handleOpen() 中,我们只需调用 startRecording() 来录制用户的语音提示。同时稍微调整事件内容:

  1. 将 createConversationEvent 的 content 更新为类型 “input_audio”,并用 audio: base64AudioData 替换 text 字段。
  2. 在 createResponseEvent 的响应模态中加入 “audio”。

以下是更新后的 handleOpen():

async function handleOpen() {
  // Define what happens when the connection is opened
  const base64AudioData = await startRecording();
  const createConversationEvent = {
    type: "conversation.item.create",
    item: {
      type: "message",
      role: "user",
      content: [
        {
          type: "input_audio",
          audio: base64AudioData,
        },
      ],
    },
  };
  ws.send(JSON.stringify(createConversationEvent));
  const createResponseEvent = {
    type: "response.create",
    response: {
      modalities: ["text", "audio"],
      instructions: "Please assist the user.",
    },
  };
  ws.send(JSON.stringify(createResponseEvent));
}

在实现 handleMessage() 时,我们为 “response.audio.delta” 事件添加逻辑,更新音频缓冲并播放新的音频增量:

case "response.audio.delta":
  // We got a new audio chunk
  const base64AudioChunk = message.delta;
  const audioBuffer = Buffer.from(base64AudioChunk, "base64");
  speaker.write(audioBuffer);
  break;

我们从 switch 语句中移除 “response.text.done” 分支,并更新 “response.done” 事件以停止扬声器:

case "response.audio.done":
  speaker.end();
  ws.close();
  break;

最终的 handleMessage() 实现如下:

function handleMessage(messageStr) {
  const message = JSON.parse(messageStr);
  // Define what happens when a message is received
  switch (message.type) {
    case "response.audio.delta":
      // We got a new audio chunk
      const base64AudioChunk = message.delta;
      const audioBuffer = Buffer.from(base64AudioChunk, "base64");
      speaker.write(audioBuffer);
      break;
    case "response.audio.done":
      speaker.end();
      ws.close();
      break;
  }
}

要运行该示例,请将上述修改应用到模板代码,并使用以下命令:

node index.js

麦克风将开始录音。请说出您的请求,并按下“Enter”发送。随后,AI 的回复会通过扬声器播放(请确保麦克风未静音且扬声器音量适当)。

音频示例的完整代码见此处。

函数调用

OpenAI API 的一项实用功能是支持函数调用。我们可以为助手添加函数,当模型判断某个函数有助于回答时,会发送事件请求调用该函数。

OpenAI 文档提供了如下图,说明函数调用的生命周期:

函数调用生命周期

来源:OpenAI

从图中可以看到,客户端必须提供 LLM 可调用的函数定义。同时,函数实际执行发生在客户端:AI 会发送事件请求调用某个函数及其参数,随后我们负责将结果返回。

让我们为助手添加一个将两个数相加的函数。我们将在上面的音频示例基础上扩展。

要指定可用函数,我们需向 LLM 提供一个工具列表。每个工具是一个 JSON 对象,描述该函数的信息。以下是定义求和工具的方法:

const sumTool = {
  type: "function",
  name: "calculate_sum",
 description: "Use this function when asked to add numbers together, for example when asked 'What's 4 + 6'?.",
  parameters: {
      type: "object",
      properties: {
          "a": { "type": "number" },
          "b": { "type": "number" }
      },
      required: ["a", "b"]
  }
}

对象结构说明如下:

  • type 指明我们在定义一个函数。
  • name 是函数的标识。LLM 会用它告知我们希望调用哪个函数。
  • description 用于指示 LLM 何时应使用此函数。
  • parameters 用于指定函数参数。本例为两个数字 a 和 b。

下一步在代码中定义该函数。我们用一个以 calculate_sum 为键的字典,便于在响应函数调用事件时调用相应函数:

const functions = {
  calculate_sum: (args) => args.a + args.b,
}

API 会以与上述 parameters 相同结构的字典提供函数参数。例如将 3 与 5 相加,字典为 {“a”: 3, “b”: 5}。

sumTool 和 functions 常量可以添加到 index.js 顶部(导入之后、main() 之前)。

接着我们更新 response.create 事件,告知 LLM sumTools 可用。这通过在 response 中添加 tools 和 tool_choice 字段实现:

const createResponseEvent = {
  type: "response.create",
  response: {
    modalities: ["text", "audio"],
    instructions: "Please assist the user.",
    tools: [sumTool], // New
    tool_choice: "auto", // New
  },
};

当 LLM 决定调用函数时,会发出 response.function_call_arguments.done 事件。我们需要通过以下步骤响应:

  1. 获取函数信息并调用该函数。
  2. 发送函数调用结果。
  3. 请求生成回复。

我们在 hanldeMessage()(原文如此)函数的 switch 中添加如下分支来处理:

case "response.function_call_arguments.done":
  console.log(Using function ${message.name} with arguments ${message.arguments});
  // 1. Get the function information and call the function
  const function_name = message.name;
  const function_arguments = JSON.parse(message.arguments);
  const result = functions[function_name](function_arguments);
  // 2. Send the result of the function call
  const functionOutputEvent = {
    type: "conversation.item.create",
    item: {
      type: "function_call_output",
      role: "system",
      output: ${result},
    }
  };
  ws.send(JSON.stringify(functionOutputEvent));
  // 3. Request a response
  ws.send(JSON.stringify({type: "response.create"}));
  break;

现在运行脚本并请求两个数相加,模型应会调用该函数并给出结果。

该函数较为简单,但由于函数在客户端执行,它可以是任何逻辑。下一节我们将看到两个更复杂的函数示例。

本示例的完整代码见此处。

使用 OpenAI Realtime API 的演示应用

OpenAI 团队提供了一个用于展示 Realtime API 的 React 演示应用。这里我们将学习如何搭建并探索其工作原理。这是构建更复杂应用的良好起点。

应用搭建

无需掌握 React 知识即可跑起来。但若要修改或扩展,则需要对 React 有所了解。

该应用托管在此仓库。要进行设置,首先使用以下克隆命令(Git):

git clone [email protected]:openai/openai-realtime-console.git

或者也可以通过 GitHub 界面手动下载。

安装应用使用以下 NPM(Node 包管理器)命令:

npm install

安装完成后,在项目根目录创建 .env 文件,并粘贴以下内容:

OPENAI_API_KEY=<openai_api_key>
REACT_APP_LOCAL_RELAY_SERVER_URL=http://localhost:8081

将 <openai_api_key> 替换为您的 OpenAI API 密钥。

应用由两部分组成,准备就绪后即可运行:

  1. React 前端,即应用的网页界面。
  2. 中继服务器,用作前端与 OpenAI API 之间的中间层。

API、中继服务器与前端之间的交互

在前端与 OpenAI API 之间实现中继服务器的主要目的是安全存储 API 密钥。没有该密钥无法与 API 交互。

然而,如果密钥存储在前端,则任何用户都能访问。因此,需要搭建一个安全存储密钥的服务器,并在 API 与前端之间中转数据。在当前场景下,由于仅在本地运行,安全风险相对较小。

要启动应用,需要同时启动中继服务器与前端。启动中继服务器的命令:

npm run relay

启动 React 前端的命令:

npm start

加载完成后,浏览器会自动打开一个标签页并运行该应用。

实时控制台界面

使用应用

开始使用前,请确保电脑未静音,并允许应用使用麦克风。

首先点击“connect”按钮。这会向 Realtime API 发送“Hello”消息,我们会收到一个问候。

连接到 API

连接建立后,中央会出现一个新按钮,允许我们与 AI 助手对话。

录制语音消息

使用方法:长按按钮并说话,松开按钮后消息即发送。

应用还提供 VAD(语音活动检测)模式,在该模式下无需按键。应用会持续聆听,使我们可以与助手连续对话。启用方法:直接点击“vad”按钮并开始说话。

切换到语音活动检测模式

函数

如前所述,Realtime API 支持让 AI 执行特定函数。该演示包含两个函数:查询指定地点的天气预报,以及添加记忆项来个性化助手。

您可以通过询问“明天纽约的天气怎么样?”以及表达偏好“我最喜欢的颜色是蓝色”来体验这些功能。助手会以语音作答,同时信息也会显示在应用右侧。

位置与记忆函数显示

有时我请求天气报告时,回复会提示当前无法访问,但右侧信息区始终能显示出来。作为演示应用,其目标并非完全可用的产品,而是展示 API 的能力。

理解客户端

本节需要对 React 有一定了解,我们会覆盖一些实现细节。

我们将查看ConsolePage.tsx 文件。应用的大部分逻辑定义在这里。与我们在 Node.js 命令行示例中直接使用 WebSocket 不同,演示应用构建了一个实时客户端来辅助与 API 交互。文件顶部引入的是:

import { RealtimeClient } from '@openai/realtime-api-beta';

与 API 的集成定义在这个 useEffect() 调用中。该 useEffect() 中的代码在控制台页面初次渲染时执行。与我们的 Node.js 脚本类似,它定义了如何响应 API 事件。主要区别在于使用了 RealtimeClient 封装。

定义工具

RealtimeClient.addTool() 用于定义工具。它接收两个参数:

  • 工具定义的 JSON 对象。
  • 要执行的函数。

这种方式简化了工具集成,因为客户端已具备处理事件与自动调用函数的能力。记忆工具定义在此处,天气工具定义在此处。

例如,要添加前面定义的求和工具,可以这样做:

client.addTool(
  {
    name: "calculate_sum",
    description: "Use this function when asked to add numbers together, for example when asked 'What's 4 + 6'?.",
    parameters: {
        type: "object",
        properties: {
            "a": { "type": "number" },
            "b": { "type": "number" }
        },
        required: ["a", "b"]
    }
  },
  (a: number, b: number): number => a + b
);

请注意,该应用使用 TypeScript,因此需要在函数定义中注明类型。

监听事件

监听事件使用 RealtimeClient.on(),它接收两个参数:

  1. 事件名称。
  2. 要执行的回调函数。

这种方式与之前使用的 WebSocket.on() 类似,但实现的事件集不同。其 GitHub 页面列出了支持的事件。

本例中使用了以下事件:

  • realtime.event 事件(见此)用于记录所有事件日志。
  • error 事件(见此)用于将错误打印到控制台,便于调试。
  • conversation.interrupted 事件(见此)用于在对话被打断时取消请求。
  • 最后,conversation.updated 事件(见此)用于当 API 返回新音频块时,将其追加到音频流。

结语

在本教程中,我们探索了 OpenAI Realtime API 以及其如何通过 WebSocket 实现实时通信。我们完成了 Node.js 环境的搭建,学习了文本与音频消息的收发,并实现了增强功能的函数调用。

我们还介绍了 OpenAI 的 React 演示应用,展示了如何部署一个基础的语音助手应用。

如欲进一步了解最新的 OpenAI 开发工具,推荐阅读以下教程:

François Aubry

作者

François Aubry

全栈工程师,CheapGPT 创始人。我一直热爱教学。从学生时代起,我就积极寻找机会为同学提供辅导与帮助。这份热情促使我攻读博士学位,并在此期间担任助教,以支持我的学术追求。那些年里,我在传统课堂环境中收获了极大的成就感,既建立联系也促进学习。随着在线学习平台的兴起,我也看到了数字教育的变革潜力。事实上,我曾参与我们大学一款此类平台的建设。我致力于将传统教学理念与创新的数字方法相融合。我的热情在于打造既有吸引力、信息量充足,又能让当代学习者轻松获取的课程。

主题