음성 에이전트 구축
세션 설정
섹션 제목: “세션 설정”오디오 처리
섹션 제목: “오디오 처리”기본 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 agentsession.sendAudio(newlyRecordedAudio);기저 전송이 지원하는 경우 session.muted는 현재 음소거 상태를 보고하고 session.mute(true | false)는 마이크 캡처를 전환합니다. OpenAIRealtimeWebSocket은 음소거를 구현하지 않습니다: session.muted는 null을 반환하고 session.mute()는 예외를 발생시키므로, 웹소켓 구성에서는 마이크가 다시 활성화되어야 할 때까지 캡처를 일시 중지하고 sendAudio() 호출을 중지해야 합니다.
세션 구성
섹션 제목: “세션 구성”생성 시 RealtimeSession에 추가 옵션을 전달하거나 connect(...)를 호출할 때 세션을 구성할 수 있습니다.
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', config: { outputModalities: ['text', 'audio'], audio: { input: { format: 'pcm16', transcription: { model: 'gpt-4o-mini-transcribe', }, }, output: { format: 'pcm16', }, }, },});이러한 전송 레이어는 session과 일치하는 모든 매개변수를 전달할 수 있습니다.
outputModalities, audio.input, audio.output이 포함된 최신 구성 형태를 권장합니다. modalities, inputAudioFormat, outputAudioFormat, inputAudioTranscription, turnDetection과 같은 레거시 최상위 필드는 하위 호환성을 위해 여전히 정규화되지만, 신규 코드는 여기에서 보여주는 중첩된 audio 구조를 사용해야 합니다.
RealtimeSessionConfig에 일치하는 매개변수가 없는 새로운 매개변수의 경우 providerData를 사용할 수 있습니다. providerData에 전달된 모든 항목은 session 객체의 일부로 직접 전달됩니다.
생성 시 설정할 수 있는 추가 RealtimeSession 옵션:
| Option | Type | Purpose |
|---|---|---|
context | TContext | 세션 컨텍스트에 병합할 추가 로컬 컨텍스트 |
historyStoreAudio | boolean | 로컬 히스토리 스냅샷에 오디오 데이터를 저장 (기본값: 비활성화) |
outputGuardrails | RealtimeOutputGuardrail[] | 세션의 출력 가드레일 (참고: 가드레일) |
outputGuardrailSettings | RealtimeOutputGuardrailSettings | 가드레일 검사 빈도와 동작 |
tracingDisabled | boolean | 세션에 대한 트레이싱 비활성화 |
groupId | string | 세션 또는 백엔드 실행 간 트레이스 그룹화 |
traceMetadata | Record<string, any> | 세션 트레이스에 첨부할 사용자 정의 메타데이터 |
workflowName | string | 트레이스 워크플로의 친숙한 이름 |
automaticallyTriggerResponseForMcpToolCalls | boolean | MCP 도구 호출 완료 시 모델 응답 자동 트리거 (기본값: true) |
toolErrorFormatter | ToolErrorFormatter | 모델에 반환되는 도구 승인 거부 메시지 커스터마이즈 |
connect(...) 옵션:
| Option | Type | Purpose |
|---|---|---|
apiKey | string | (() => string | Promise<string>) | 이 연결에 사용되는 API 키(또는 지연 로더) |
model | OpenAIRealtimeModels | string | 전송 연결에 대한 선택적 모델 재정의 |
url | string | 선택적 사용자 정의 Realtime 엔드포인트 URL |
callId | string | SIP로 시작된 기존 통화/세션에 연결 |
에이전트 기능
섹션 제목: “에이전트 기능”핸드오프
섹션 제목: “핸드오프”일반 에이전트와 마찬가지로 핸드오프를 사용하여 에이전트를 여러 에이전트로 분할하고 그 사이를 오케스트레이션하여 에이전트의 성능을 개선하고 문제의 범위를 더 잘 정의할 수 있습니다.
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],});일반 에이전트와 달리, 실시간 에이전트에서는 핸드오프가 약간 다르게 동작합니다. 핸드오프가 수행되면 진행 중인 세션이 새로운 에이전트 구성으로 업데이트됩니다. 이로 인해 에이전트는 자동으로 진행 중인 대화 기록에 접근할 수 있으며 입력 필터는 현재 적용되지 않습니다.
또한 이는 핸드오프의 일부로 voice 또는 model을 변경할 수 없음을 의미합니다. 다른 실시간 에이전트에만 연결할 수 있습니다. 예를 들어 gpt-5-mini 같은 추론 모델을 사용해야 하는 경우 도구를 통한 위임을 사용할 수 있습니다.
일반 에이전트와 마찬가지로 실시간 에이전트는 도구를 호출하여 작업을 수행할 수 있습니다. 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과 동일한 환경에서 실행됩니다. 즉, 세션을 브라우저에서 실행 중이라면 도구도 브라우저에서 실행됩니다. 민감한 작업을 수행해야 한다면 도구 내에서 백엔드로 HTTP 요청을 수행하세요.
호스티드 MCP 도구는 hostedMcpTool로 구성하며 원격으로 실행됩니다. MCP 도구 가용성이 변경되면 세션은 mcp_tools_changed 이벤트를 내보냅니다. MCP 도구 호출 완료 후 세션이 모델 응답을 자동 트리거하지 않도록 하려면 automaticallyTriggerResponseForMcpToolCalls: false로 설정하세요.
현재 필터링된 MCP 도구 목록은 session.availableMcpTools로도 제공됩니다. 해당 프로퍼티와 mcp_tools_changed 이벤트는 활성 에이전트에서 사용 가능한 호스티드 MCP 서버만을, 에이전트 구성의 allowed_tools 필터 적용 이후 상태로 반영합니다.
도구가 실행되는 동안 에이전트는 사용자의 새로운 요청을 처리할 수 없습니다. 경험을 개선하는 한 가지 방법은 도구를 실행하려고 할 때 에이전트가 이를 알리거나, 도구 실행 시간을 벌기 위해 특정 문구를 말하도록 지시하는 것입니다.
함수 도구가 즉시 추가적인 모델 응답을 트리거하지 않고 종료되어야 한다면, @openai/agents/realtime에서 backgroundResult(output)를 반환하세요.
이렇게 하면 도구 출력을 세션으로 보내면서 응답 트리거는 여러분이 제어할 수 있습니다.
함수 도구 타임아웃 옵션(timeoutMs, timeoutBehavior, timeoutErrorFunction)은 실시간 세션에서도 동일하게 동작합니다. 기본값인 error_as_result에서는 타임아웃 메시지가 도구 출력으로 전송됩니다. raise_exception의 경우 세션은 ToolTimeoutError와 함께 error 이벤트를 내보내며 해당 호출에 대한 도구 출력을 보내지 않습니다.
대화 기록 접근
섹션 제목: “대화 기록 접근”에이전트가 특정 도구를 호출할 때 전달한 인자 외에도, Realtime 세션에서 추적되는 현재 대화 기록의 스냅샷에 접근할 수 있습니다. 이는 현재 대화 상태에 기반해 더 복잡한 작업을 수행해야 하거나 도구를 통한 위임을 사용할 계획인 경우 유용합니다.
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를 표시할 수 있습니다.
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.rawItem);});가드레일
섹션 제목: “가드레일”가드레일은 에이전트가 말한 내용이 특정 규칙을 위반했는지 모니터링하고 응답을 즉시 차단하는 방법을 제공합니다. 이러한 가드레일 검사는 에이전트 응답의 성적(transcript)을 기반으로 수행되므로 모델의 텍스트 출력이 활성화되어 있어야 합니다(기본값으로 활성화).
제공한 가드레일은 모델 응답이 반환되는 동안 비동기적으로 실행되며, 예를 들어 “특정 금지어를 언급”과 같은 사전 정의된 분류 트리거를 기반으로 응답을 차단할 수 있습니다.
가드레일이 작동하면 세션은 guardrail_tripped 이벤트를 발생시킵니다. 이벤트는 또한 가드레일을 트리거한 itemId를 포함하는 details 객체를 제공합니다.
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 객체를 전달할 수 있습니다.
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 },});상호작용 흐름
섹션 제목: “상호작용 흐름”턴 감지/음성 활동 감지
섹션 제목: “턴 감지/음성 활동 감지”Realtime 세션은 사용자가 말할 때를 자동으로 감지하고 Realtime API의 내장된 음성 활동 감지 모드를 사용하여 새로운 턴을 트리거합니다.
세션 구성에서 audio.input.turnDetection을 전달하여 음성 활동 감지 모드를 변경할 수 있습니다.
import { RealtimeSession } from '@openai/agents/realtime';import { agent } from './agent';
const session = new RealtimeSession(agent, { model: 'gpt-realtime', config: { audio: { input: { turnDetection: { type: 'semantic_vad', eagerness: 'medium', createResponse: true, interruptResponse: true, }, }, }, },});턴 감지 설정을 수정하면 원치 않는 인터럽션(중단 처리)과 침묵에 대한 처리를 보정하는 데 도움이 될 수 있습니다. 다양한 설정에 대한 자세한 내용은 Realtime API 문서를 참조하세요
인터럽션(중단 처리)
섹션 제목: “인터럽션(중단 처리)”내장된 음성 활동 감지를 사용할 때, 에이전트가 말하는 중에 사용자가 말을 겹치면 에이전트는 자동으로 방금 말한 내용을 감지하고 컨텍스트를 업데이트합니다. 또한 audio_interrupted 이벤트를 발생시킵니다. 이는 모든 오디오 재생을 즉시 중지하는 데 사용할 수 있습니다(웹소켓 연결에만 적용).
import { session } from './agent';
session.on('audio_interrupted', () => { // handle local playback interruption});UI에 “중지” 버튼을 제공하는 등 수동으로 인터럽션을 수행하려면 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어느 방식이든 Realtime 세션은 에이전트의 생성 중단, 사용자에게 말한 내용의 절단, 히스토리 업데이트를 모두 처리합니다.
에이전트에 WebRTC로 연결하는 경우 오디오 출력도 지워집니다. WebSocket을 사용하는 경우, 대기열에 재생될 오디오의 재생을 직접 중지하여 처리해야 합니다.
텍스트 입력
섹션 제목: “텍스트 입력”에이전트에 텍스트 입력을 보내려면 RealtimeSession의 sendMessage 메서드를 사용할 수 있습니다.
이는 사용자에게 에이전트와 두 가지 모달리티로 상호작용할 수 있도록 하거나, 대화에 추가 컨텍스트를 제공하려는 경우에 유용합니다.
import { RealtimeSession, RealtimeAgent } from '@openai/agents/realtime';
const agent = new RealtimeAgent({ name: 'Assistant',});
const session = new RealtimeSession(agent, { model: 'gpt-realtime',});
session.sendMessage('Hello, how are you?');대화 상태와 위임
섹션 제목: “대화 상태와 위임”대화 기록 관리
섹션 제목: “대화 기록 관리”RealtimeSession은 history 프로퍼티에 대화 기록을 자동으로 관리합니다:
이를 사용하여 고객에게 히스토리를 렌더링하거나 추가 작업을 수행할 수 있습니다. 이 히스토리는 대화 진행 중 지속적으로 변경되므로 history_updated 이벤트를 수신할 수 있습니다.
메시지를 완전히 제거하거나 전사를 업데이트하는 등 히스토리를 수정하려면 updateHistory 메서드를 사용할 수 있습니다.
import { RealtimeSession, RealtimeAgent } from '@openai/agents/realtime';
const agent = new RealtimeAgent({ name: 'Assistant',});
const session = new RealtimeSession(agent, { model: 'gpt-realtime',});
await session.connect({ apiKey: '<client-api-key>' });
// listening to the history_updated eventsession.on('history_updated', (history) => { // returns the full history of the session console.log(history);});
// Option 1: explicit settingsession.updateHistory([ /* specific history */]);
// Option 2: override based on current state like removing all agent messagessession.updateHistory((currentHistory) => { return currentHistory.filter( (item) => !(item.type === 'message' && item.role === 'assistant'), );});제한 사항
섹션 제목: “제한 사항”- 현재로서는 나중에 함수 도구 호출을 업데이트/변경할 수 없습니다
- 히스토리의 텍스트 출력에는 전사와 텍스트 모달리티가 활성화되어 있어야 합니다
- 인터럽션으로 인해 잘린 응답에는 전사가 없습니다
도구를 통한 위임
섹션 제목: “도구를 통한 위임”
대화 기록과 도구 호출을 결합하여, 더 복잡한 작업을 수행하기 위해 대화를 다른 백엔드 에이전트에 위임하고 그 결과를 사용자에게 다시 전달할 수 있습니다.
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 actions를 통해 실행됩니다.
// This runs on the serverimport '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-mini', 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);}