跳转到内容

构建语音智能体

某些传输层(例如默认的 OpenAIRealtimeWebRTC)会自动为您处理音频输入和输出。对于其他传输机制(如 OpenAIRealtimeWebSocket),则需要您自己处理会话音频:

import {
RealtimeAgent,
RealtimeSession,
TransportLayerAudio,
} from '@openai/agents/realtime';
const agent = new RealtimeAgent({ name: 'My agent' });
const session = new RealtimeSession(agent);
const newlyRecordedAudio = new ArrayBuffer(0);
session.on('audio', (event: TransportLayerAudio) => {
// play your audio
});
// send new audio to the agent
session.sendAudio(newlyRecordedAudio);

当底层传输支持时,session.muted 会报告当前静音状态,session.mute(true | false) 可切换麦克风采集。OpenAIRealtimeWebSocket 不实现静音:session.muted 返回 null,且 session.mute() 会抛出异常,因此在 websocket 配置中,您应在本地暂停采集,并在麦克风需要恢复前停止调用 sendAudio()

在创建 RealtimeSession 时配置会话本身,通常通过 model 选项和 config 对象。connect(...) 用于连接时相关事项,例如凭证、端点 URL 和 SIP 通话附加,而不是任意会话字段。

import { RealtimeAgent, RealtimeSession } from '@openai/agents/realtime';
const agent = new RealtimeAgent({
name: 'Greeter',
instructions: 'Greet the user with cheer and answer questions.',
});
const session = new RealtimeSession(agent, {
model: 'gpt-realtime-1.5',
config: {
outputModalities: ['audio'],
audio: {
input: {
format: 'pcm16',
transcription: {
model: 'gpt-4o-mini-transcribe',
},
},
output: {
format: 'pcm16',
},
},
},
});

在底层,SDK 会将此配置标准化为 Realtime session.update 结构。如果您需要某个原始会话字段,而它在 RealtimeSessionConfig 中没有对应属性,请使用 providerData,或者通过 session.transport.sendEvent(...) 发送原始 session.update

优先使用较新的 SDK 配置结构,即 outputModalitiesaudio.inputaudio.output。较旧的 SDK 别名,如 modalitiesinputAudioFormatoutputAudioFormatinputAudioTranscriptionturnDetection 仍会为向后兼容而被标准化,但新代码应使用此处展示的嵌套 audio 结构。

对于语音到语音会话,通常选择 outputModalities: ['audio'],这样可获得音频输出以及转录文本。只有在您只想要纯文本响应时,才切换为 ['text']

对于较新的参数,如果在 RealtimeSessionConfig 中没有对应参数,可以使用 providerData。传入 providerData 的任何内容都会作为原始 session 对象的一部分被转发。

可在构造时设置的其他 RealtimeSession 选项:

OptionTypePurpose
contextTContext合并到会话上下文中的额外本地上下文。
historyStoreAudioboolean在本地历史快照中存储音频数据(默认禁用)。
outputGuardrailsRealtimeOutputGuardrail[]会话的输出护栏(参见护栏)。
outputGuardrailSettings{ debounceTextLength?: number }护栏执行频率。默认值为 100;使用 -1 则仅在完整文本可用时运行一次。
tracingDisabledboolean禁用会话的追踪。
groupIdstring在会话或后端运行之间对追踪进行分组。需要 workflowName
traceMetadataRecord<string, any>附加到会话追踪的自定义元数据。需要 workflowName
workflowNamestring追踪工作流的友好名称。
automaticallyTriggerResponseForMcpToolCallsboolean当 MCP 工具调用完成时自动触发模型响应(默认:true)。
toolErrorFormatterToolErrorFormatter自定义返回给模型的工具审批拒绝消息。

connect(...) 选项:

OptionTypePurpose
apiKeystring | (() => string | Promise<string>)用于此次连接的 API 密钥(或惰性加载器)。
modelOpenAIRealtimeModels | string存在于传输层选项类型中。对于 RealtimeSession,请在构造函数中设置模型;原始传输也可在连接时使用模型。
urlstring可选的自定义 Realtime 端点 URL。
callIdstring附加到现有的 SIP 发起通话/会话。

RealtimeSession 构建在长连接的 Realtime 连接之上。它会保留一份本地对话历史副本,监听传输事件,运行工具和输出护栏,并使当前智能体配置与传输保持同步。

底层 API 的行为仍然很重要:

  • 成功连接会以 session.created 事件开始,后续配置更改会产生 session.updated
  • 大多数会话属性都可以随时间更改,但 model 不能在对话中途更改,voice 只能在会话尚未产生音频输出前更改,而追踪应在一开始就决定,因为 Realtime API 不允许在启用后修改追踪。
  • Realtime API 当前将单个会话限制为 60 分钟。
  • 输入音频转录是异步的,因此最新一次发言的转录文本可能会在响应生成已开始后才到达。

在 SDK 层,await session.connect() 表示“传输已准备就绪,可以开始对话”,但具体时点因传输方式而异:

  • 在默认的浏览器 WebRTC 传输中,SDK 会在数据通道打开后立即发送初始 session.update,并尝试在 connect() resolve 前等待相应的 session.updated 事件。这样可以避免在您的 instructions、工具和模态应用前,音频就已到达服务器。如果该确认始终未到达,connect() 会在短暂超时后回退为直接 resolve。
  • 在默认的服务端 WebSocket 传输中,connect() 会在 socket 打开且初始配置已发送后 resolve。因此,相应的 session.updated 事件可能会在 connect() 已经 resolve 后才到达。

如果您需要原始事件模型,请结合本页阅读官方的 Realtime conversations guide

默认情况下,Realtime 会话使用内置的语音活动检测(VAD),因此 API 可以判断用户何时开始或停止说话,以及何时创建响应。SDK 通过 audio.input.turnDetection 暴露此能力。

import { RealtimeSession } from '@openai/agents/realtime';
import { agent } from './agent';
const session = new RealtimeSession(agent, {
model: 'gpt-realtime-1.5',
config: {
audio: {
input: {
turnDetection: {
type: 'semantic_vad',
eagerness: 'medium',
createResponse: true,
interruptResponse: true,
},
},
},
},
});

两种常见模式是:

  • semantic_vad:目标是更自然的回合边界,当用户听起来似乎还没说完时,会稍微多等待一点。
  • server_vad:更依赖阈值驱动,并暴露 thresholdprefixPaddingMssilenceDurationMsidleTimeoutMs 等设置。

如果您想自行管理回合边界,请将 audio.input.turnDetection 设为 null。官方的 voice activity detection guideRealtime conversations guide 更详细地说明了底层行为。

启用 VAD 时,在智能体说话过程中插话可以中断当前响应。在 WebSocket 传输中,SDK 会监听 input_audio_buffer.speech_started,将助手音频截断为用户实际听到的部分,并发出 audio_interrupted 事件。在由您自行管理播放的 WebSocket 配置中,这个事件尤其有用。

import { session } from './agent';
session.on('audio_interrupted', () => {
// handle local playback interruption
});

如果您想提供手动停止按钮,请自行调用 interrupt()

import { session } from './agent';
session.interrupt();
// this will still trigger the `audio_interrupted` event for you
// to cut off the audio playback when using WebSockets

WebRTC 和 WebSocket 都会停止正在进行的响应,但底层机制因传输方式而异。WebRTC 会为您清除缓冲的输出音频。在 WebSocket 配置中,您仍需自行停止本地播放,而本地历史会在相应的截断和对话事件从传输层返回时更新。

当您想向实时对话中发送键入输入或额外的结构化用户内容时,请使用 sendMessage()

import { RealtimeSession, RealtimeAgent } from '@openai/agents/realtime';
const agent = new RealtimeAgent({
name: 'Assistant',
});
const session = new RealtimeSession(agent, {
model: 'gpt-realtime-1.5',
});
session.sendMessage('Hello, how are you?');

这对于混合文本与语音的 UI、带外上下文注入,或将语音输入与明确的文字补充说明配对时很有用。

Realtime 语音到语音会话也可以包含图像。在 SDK 中,使用 addImage() 将图像附加到当前对话。

import { RealtimeAgent, RealtimeSession } from '@openai/agents/realtime';
const agent = new RealtimeAgent({
name: 'Assistant',
});
const session = new RealtimeSession(agent, {
model: 'gpt-realtime-1.5',
});
const imageDataUrl = 'data:image/png;base64,...';
session.addImage(imageDataUrl, { triggerResponse: false });
session.sendMessage('Describe what is in this image.');

传入 triggerResponse: false 可让您先将图像与后续文本或音频回合一起批量发送,再请求模型响应。这与官方 Realtime conversations image input guidance 保持一致。

在更高层的 SDK 中,sendMessage()addImage() 默认会为您触发响应。当您处理原始传输事件、按住说话流程,或自定义审核/验证步骤时,手动响应控制就很重要。

import { RealtimeAgent, RealtimeSession } from '@openai/agents/realtime';
const agent = new RealtimeAgent({
name: 'Greeter',
instructions: 'Greet the user with cheer and answer questions.',
});
const session = new RealtimeSession(agent, {
model: 'gpt-realtime-1.5',
});
session.transport.on('*', (event) => {
// JSON parsed version of the event received on the connection
});
// Send any valid event as JSON. For example triggering a new response
session.transport.sendEvent({
type: 'response.create',
// ...
});

有两种常见情况:

  1. 如果您通过 audio.input.turnDetection = null 完全禁用 VAD,那么您需要负责提交音频回合,然后发送 response.create
  2. 如果您保留 VAD,但设置 turnDetection.interruptResponse = falseturnDetection.createResponse = false,API 仍会检测回合,但会将响应创建交由您控制。

第二种模式适用于您希望在模型响应前先检查或审核用户输入的场景。这与官方 Realtime conversations guidance on disabling automatic responses 一致。

与常规智能体类似,您可以使用交接将智能体拆分为多个智能体,并在它们之间进行编排,以提升性能并更好地限定问题范围。

import { RealtimeAgent } from '@openai/agents/realtime';
const mathTutorAgent = new RealtimeAgent({
name: 'Math Tutor',
handoffDescription: 'Specialist agent for math questions',
instructions:
'You provide help with math problems. Explain your reasoning at each step and include examples',
});
const agent = new RealtimeAgent({
name: 'Greeter',
instructions: 'Greet the user with cheer and answer questions.',
handoffs: [mathTutorAgent],
});

与常规智能体不同,交接在 Realtime Agents 中的行为略有不同。执行交接时,进行中的会话会被更新为新的智能体配置。正因如此,新智能体会自动访问当前对话历史,并且目前不会应用输入过滤器。

由于会话保持实时进行,该会话的模型在交接期间不会改变。语音切换遵循底层 Realtime API 规则:只能在会话尚未产生音频输出前生效。Realtime 交接主要用于在同一会话中切换 RealtimeAgent 配置;如果您需要使用不同模型,例如推理模型 gpt-5.4,或委派给非实时后端智能体,请使用通过工具委派

与常规智能体一样,Realtime Agents 也可以调用工具来执行操作。Realtime 支持函数工具(本地执行)和托管 MCP 工具(由 Realtime API 远程执行)。您可以使用与常规智能体相同的 tool() 辅助函数来定义函数工具。

import { tool, RealtimeAgent } from '@openai/agents/realtime';
import { z } from 'zod';
const getWeather = tool({
name: 'get_weather',
description: 'Return the weather for a city.',
parameters: z.object({ city: z.string() }),
async execute({ city }) {
return `The weather in ${city} is sunny.`;
},
});
const weatherAgent = new RealtimeAgent({
name: 'Weather assistant',
instructions: 'Answer weather questions.',
tools: [getWeather],
});

函数工具与您的 RealtimeSession 在同一环境中运行。这意味着如果您的会话运行在浏览器中,工具也会在浏览器中执行。如果您需要执行敏感操作,请在工具内部调用后端,并让服务器完成需要特权的工作。

这使浏览器端工具可以作为通向服务端逻辑的轻量回传通道。例如,examples/realtime-next 在浏览器中定义了一个 refundBackchannel 工具,它会将请求和当前对话历史转发到服务器上的 handleRefundRequest(...),在那里,单独的 Runner 可以使用不同的智能体或模型来评估退款,再将结果返回给语音会话。

托管 MCP 工具可通过 hostedMcpTool 配置,并由远程执行。当 MCP 工具可用性变化时,会话会发出 mcp_tools_changed。若要防止会话在 MCP 工具调用完成后自动触发模型响应,请设置 automaticallyTriggerResponseForMcpToolCalls: false

当前过滤后的 MCP 工具列表也可通过 session.availableMcpTools 获取。该属性和 mcp_tools_changed 事件仅反映当前智能体上已启用的托管 MCP 服务器,并且已应用智能体配置中的所有 allowed_tools 过滤器。

若将安全的服务器选择、headers 和审批视为连接前配置,则更容易理解托管 MCP 的设置。在 RealtimeSession.connect() 打开传输前,SDK 会解析当前智能体的托管 MCP 工具定义,并将受支持的 MCP 字段包含进发送给 Realtime API 的初始会话配置中。

这一时机在浏览器 WebRTC 应用中尤其重要。临时客户端密钥始终在您的服务器上签发,因此任何必须保密的托管 MCP 凭据或自定义 headers,都应在服务端 POST /v1/realtime/client_secrets 请求中作为初始 session 负载的一部分附加。不要将长期凭据放在浏览器代码中,也不要打算在 connect() 开始后再添加。

在 Realtime API 层面,后续的 session.update 调用仍可更改工具和其他可变会话字段,而 SDK 本身也会在活动智能体变更时发送 session.update。但在浏览器应用中,您应将安全的 Hosted MCP 初始化视为服务端的连接前事项,并让浏览器侧的 RealtimeSession 配置与服务器签发的内容保持一致。

在工具执行期间,智能体将无法处理来自用户的新请求。改善体验的一种方式是告知您的智能体:在即将执行工具时先提示,或者说一些特定短语,为工具执行争取时间。

如果某个函数工具应在完成后不立即触发另一轮模型响应,请从 @openai/agents/realtime 返回 backgroundResult(output)。这样会将工具输出回传到会话中,同时将响应触发权保留给您控制。

函数工具的超时选项(timeoutMstimeoutBehaviortimeoutErrorFunction)在 Realtime 会话中的工作方式相同。使用默认的 error_as_result 时,超时消息会作为工具输出发送。使用 raise_exception 时,会话会发出携带 ToolTimeoutErrorerror 事件,并且不会为该次调用发送工具输出。

除了智能体调用某个特定工具时传入的参数外,您还可以访问 Realtime Session 跟踪的当前对话历史快照。如果您需要基于当前对话状态执行更复杂的操作,或计划使用通过工具委派,这会很有帮助。

import {
tool,
RealtimeContextData,
RealtimeItem,
} from '@openai/agents/realtime';
import { z } from 'zod';
const parameters = z.object({
request: z.string(),
});
const refundTool = tool<typeof parameters, RealtimeContextData>({
name: 'Refund Expert',
description: 'Evaluate a refund',
parameters,
execute: async ({ request }, details) => {
// The history might not be available
const history: RealtimeItem[] = details?.context?.history ?? [];
// making your call to process the refund request
},
});

如果您将工具定义为 needsApproval: true,则智能体会在执行工具前发出 tool_approval_requested 事件。

通过监听此事件,您可以向用户显示一个 UI,让其批准或拒绝工具调用。

使用 await session.approve(request.approvalItem)await session.reject(request.approvalItem) 来处理该请求。对于函数工具,您可以传入 { alwaysApprove: true }{ alwaysReject: true },以在本次会话剩余时间内复用相同决策;也可以通过 session.reject(request.approvalItem, { message: '...' }) 向模型发送该次调用专属的自定义拒绝消息。托管 MCP 审批不支持持久化的批准/拒绝;请改用托管 MCP 的 allowedTools 配置来限制这些工具。

如果您没有传入单次调用的拒绝 message,会话会回退使用 toolErrorFormatter(如果已配置),然后再回退到 SDK 默认拒绝文本。

import { session } from './agent';
session.on('tool_approval_requested', (_context, _agent, request) => {
// show a UI to the user to approve or reject the tool call
// you can use the `session.approve(...)` or `session.reject(...)` methods to approve or reject the tool call
session.approve(request.approvalItem); // or session.reject(request.approvalItem);
});

护栏提供了一种方式,用于监控智能体所说内容是否违反某组规则,并立即截断响应。这些检查会针对智能体响应的转录流运行。在音频会话中,SDK 使用输出音频转录及其增量,因此关键前提是转录可用,而不是单独的文本输出模态。

您提供的护栏会在模型响应返回时异步运行,从而允许您基于预定义的分类触发器截断响应,例如“提到某个特定禁用词”。

当护栏被触发时,会话会发出 guardrail_tripped 事件。该事件还会提供一个 details 对象,其中包含触发护栏的 itemId

import {
RealtimeOutputGuardrail,
RealtimeAgent,
RealtimeSession,
} from '@openai/agents/realtime';
const agent = new RealtimeAgent({
name: 'Greeter',
instructions: 'Greet the user with cheer and answer questions.',
});
const guardrails: RealtimeOutputGuardrail[] = [
{
name: 'No mention of Dom',
async execute({ agentOutput }) {
const domInOutput = agentOutput.includes('Dom');
return {
tripwireTriggered: domInOutput,
outputInfo: { domInOutput },
};
},
},
];
const guardedSession = new RealtimeSession(agent, {
outputGuardrails: guardrails,
});

默认情况下,护栏每 100 个字符运行一次,并在最终转录可用时再次运行。由于说出文本通常比生成转录更慢,这往往使护栏能在用户听到之前截断不安全输出。

如果您想修改此行为,可以向会话传入 outputGuardrailSettings 对象。

当您只想在响应结束时对完整生成的转录进行一次评估,请设置 debounceTextLength: -1

import { RealtimeAgent, RealtimeSession } from '@openai/agents/realtime';
const agent = new RealtimeAgent({
name: 'Greeter',
instructions: 'Greet the user with cheer and answer questions.',
});
const guardedSession = new RealtimeSession(agent, {
outputGuardrails: [
/*...*/
],
outputGuardrailSettings: {
debounceTextLength: 500, // run guardrail every 500 characters or set it to -1 to run it only at the end
},
});

RealtimeSession 会自动维护一个本地 history 快照,用于跟踪用户消息、助手输出、工具调用和截断状态。您可以在 UI 中渲染它、在工具内部检查它,或在需要更正或移除条目时更新它。

随着对话变化,会话会发出 history_updated。如果您需要请求修改历史,请使用 updateHistory()。它会要求传输层对当前历史进行 diff,并发送必要的删除/创建事件;本地的 session.history 视图会在相应对话事件返回时更新。

import { RealtimeSession, RealtimeAgent } from '@openai/agents/realtime';
const agent = new RealtimeAgent({
name: 'Assistant',
});
const session = new RealtimeSession(agent, {
model: 'gpt-realtime-1.5',
});
await session.connect({ apiKey: '<client-api-key>' });
// listening to the history_updated event
session.on('history_updated', (history) => {
// returns the full history of the session
console.log(history);
});
// Option 1: explicit setting
session.updateHistory([
/* specific history */
]);
// Option 2: override based on current state like removing all agent messages
session.updateHistory((currentHistory) => {
return currentHistory.filter(
(item) => !(item.type === 'message' && item.role === 'assistant'),
);
});
  1. 目前您无法在事后编辑函数工具调用。
  2. 历史中的助手文本依赖可用的转录,包括 output_audio.transcript
  3. 因中断而被截断的响应不会保留最终转录。
  4. 对输入音频的转录最好只作为用户所说内容的大致参考,而不是模型如何理解音频的精确副本。

通过工具委派

通过将对话历史与工具调用结合,您可以将对话委派给另一个后端智能体来执行更复杂的操作,然后再将结果作为响应返回给用户。

import {
RealtimeAgent,
RealtimeContextData,
tool,
} from '@openai/agents/realtime';
import { handleRefundRequest } from './serverAgent';
import z from 'zod';
const refundSupervisorParameters = z.object({
request: z.string(),
});
const refundSupervisor = tool<
typeof refundSupervisorParameters,
RealtimeContextData
>({
name: 'escalateToRefundSupervisor',
description: 'Escalate a refund request to the refund supervisor',
parameters: refundSupervisorParameters,
execute: async ({ request }, details) => {
// This will execute on the server
return handleRefundRequest(request, details?.context?.history ?? []);
},
});
const agent = new RealtimeAgent({
name: 'Customer Support',
instructions:
'You are a customer support agent. If you receive any requests for refunds, you need to delegate to your supervisor.',
tools: [refundSupervisor],
});

下面的代码随后会在服务器上运行,此示例中通过 Next.js Server Action 执行。

// This runs on the server
import 'server-only';
import { Agent, run } from '@openai/agents';
import type { RealtimeItem } from '@openai/agents/realtime';
import z from 'zod';
const agent = new Agent({
name: 'Refund Expert',
instructions:
'You are a refund expert. You are given a request to process a refund and you need to determine if the request is valid.',
model: 'gpt-5.4',
outputType: z.object({
reasong: z.string(),
refundApproved: z.boolean(),
}),
});
export async function handleRefundRequest(
request: string,
history: RealtimeItem[],
) {
const input = `
The user has requested a refund.
The request is: ${request}
Current conversation history:
${JSON.stringify(history, null, 2)}
`.trim();
const result = await run(agent, input);
return JSON.stringify(result.finalOutput, null, 2);
}