말 끊김 없이 “대화가 되는” 2026년형 실시간 음성 에이전트: STT/TTS 파이프라인 vs Speech-to-Speech, WebRTC로 끝내기
들어가며
실시간 음성 에이전트에서 개발자가 실제로 겪는 문제는 단순히 STT 정확도가 아닙니다. “사용자가 말 끝나자마자 1초 안에 첫 소리가 나오는가”, “중간에 끼어들면(barge-in) 자연스럽게 멈추는가”, “네트워크가 흔들려도 세션이 살아남는가” 같은 대화 UX/시스템 문제가 핵심입니다. 엔터프라이즈에선 여기에 관측(Observability), 비용, 장애 격리, 개인정보가 덧붙습니다.
언제 쓰면 좋나?
- 콜센터/상담, 인앱 코치, 현장 작업 보조처럼 즉시 반응(낮은 latency) + 길게 대화(안정성)가 필요한 경우
- 도구 호출(function calling)로 업무 시스템과 연결해야 하는 경우(예약/검색/티켓 생성)
언제 쓰면 안 되나?
- 사용자 경험이 “대화”가 아니라 “녹음 후 처리”에 가까운 경우(회의록, 사후 분석). 이 경우 실시간 스트리밍 복잡도를 감수할 이유가 적습니다.
- 규제/보안으로 클라우드 전송이 어려운 환경인데 온프렘 구축 여력이 없을 때(실시간은 네트워크/인프라 의존도가 큼)
2026년 6월 시점 트렌드는 크게 두 갈래입니다. 1) 여전히 주류인 Cascaded Streaming 파이프라인(STT → LLM → TTS): 각 단계는 스트리밍으로 “겹쳐” 돌려서 시간을 줄입니다. 엔터프라이즈 튜토리얼 논문에서도 “핵심은 모델 하나가 아니라 streaming/pipelining”이라고 못 박습니다. (arxiv.org)
2) Speech-to-Speech(End-to-End) Realtime 모델: 입력도 오디오, 출력도 오디오로 받는 방식. OpenAI는 Realtime API와 새로운 음성 모델들을 발표했고 (openai.com), AWS도 WebRTC 기반 양방향 스트리밍을 AgentCore에 넣어 “브라우저/모바일” 실시간 경험을 전면에 둡니다. (aws.amazon.com)
🔧 핵심 개념
1) “실시간”의 정의: TTFA(Time To First Audio)
실무에서 체감 품질을 결정하는 지표는 보통 TTFA(P50/P90) 입니다.
- P50이 800~1000ms면 “빠르다” 느낌이 나고
- P90이 2초를 넘어가면 사용자(특히 상담/인터뷰)가 끊기거나 겹침이 생깁니다.
엔터프라이즈 튜토리얼은 Cascaded 구성으로도 P50 TTFA ~947ms(best 729ms)까지 측정합니다. (arxiv.org)
2) 두 가지 아키텍처 비교
A. Cascaded Streaming (STT → LLM → TTS)
구조/흐름
- 클라이언트 마이크 오디오를 짧은 프레임(예: 20ms)로 쪼개 전송
- STT가 partial transcript(중간 결과)를 계속 푸시
- LLM은 “문장 끝”을 기다리지 않고 증분(incremental)로 토큰 생성(스트리밍)
- TTS도 텍스트를 chunk로 받아 스트리밍 합성
- 사용자가 말 끊으면 VAD/ASR 이벤트로 TTS를 즉시 cancel
장점
- 도구 호출, 긴 컨텍스트, 고난도 추론을 LLM에 맡기기 쉬움(디버깅도 텍스트 기반)
- 벤더 교체가 상대적으로 쉬움(STT/TTS/LLM 분리)
단점
- 각 단계의 지연이 누적되기 쉬움(특히 네트워크 hop이 많으면 악화)
- partial transcript 기반의 “성급한 추론”이 생길 수 있음(말이 아직 안 끝났는데 결론 내림)
B. Speech-to-Speech Realtime (Audio In/Out)
구조/흐름
- 오디오 스트림을 WebRTC/WebSocket으로 모델에 넣으면 모델이 오디오로 바로 응답(중간에 텍스트를 만들지 않거나 선택적으로만 생성)
장점
- 파이프라인 단계가 줄어 TTFA가 낮아지기 쉬움
- turn-taking/backchannel(“음…”, “네”) 같은 대화적 리듬이 자연스럽게 나올 여지
단점
- 텍스트 중간 산출물이 없으면 디버깅/감사/로깅이 어려워질 수 있음(대신 별도 transcription 채널이 필요)
- 벤더 종속이 커질 수 있음(프로토콜/세션/이벤트 모델)
OpenAI는 Realtime API에서 WebRTC 연결을 권장하고(브라우저에서 일관된 성능) 데이터채널로 이벤트를 주고받는 패턴을 공식 가이드로 제공합니다. (platform.openai.com)
3) “더 빨리 말하기” 트릭: Speculative/Hybrid
2026년 논문들에서 흥미로운 방향은 “빠른 경로”와 “느린 경로”를 병렬로 돌려 첫 음성은 빨리, 내용은 나중에 더 정확하게 이어붙이는 방식입니다(예: RelayS2S). (arxiv.org)
즉, 실시간의 본질은 “한 방에 끝내는 모델”이 아니라 오케스트레이션입니다.
💻 실전 코드
아래 예제는 브라우저(WebRTC) ↔ 서버(툴/로그/권한) ↔ OpenAI Realtime 구성을 전제로 합니다.
- 브라우저는 WebRTC로 “말하고/듣는” 실시간 세션을 유지
- 서버는 Ephemeral token 발급, 도구 호출 처리, 대화 로그/관측을 담당
- 음성 에이전트는 “바로 응답 + barge-in + function calling”까지 고려
주의: OpenAI Realtime의 이벤트 타입/모델명은 제품 업데이트가 잦습니다. 아래 코드는 “구조/패턴”을 재사용하는 용도이며, 이벤트 스키마는 공식 문서 기준으로 맞추세요. (WebRTC 연결 + data channel 이벤트 송수신 패턴은 문서에 명시) (platform.openai.com)
0) 의존성/구성
1
2
3
4
5
6
# server
npm init -y
npm i express cors dotenv
npm i openai
# client는 Vite/Next 등 아무거나 가능(여기선 개념 코드)
.env
1
OPENAI_API_KEY=...
1) 서버: Ephemeral token 발급 + 툴 엔드포인트
브라우저에 API Key를 두지 말고, 서버에서 짧게 사는 토큰을 발급합니다(공식 WebRTC 가이드에서도 “ephemeral key” 흐름을 소개). (platform.openai.com)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// server/index.ts
import express from "express";
import cors from "cors";
import "dotenv/config";
import OpenAI from "openai";
const app = express();
app.use(cors());
app.use(express.json());
const client = new OpenAI({ apiKey: process.env.OPENAI_API_KEY! });
// 1) 브라우저가 WebRTC로 붙을 때 사용할 ephemeral token 발급
app.post("/api/realtime-token", async (req, res) => {
// 모델/권한/만료는 프로젝트 정책에 맞게
// (정확한 파라미터는 OpenAI 최신 문서에 맞춰 조정)
const token = await client.realtime.sessions.create({
model: "gpt-realtime", // 예시: 실제론 gpt-realtime 또는 최신 realtime 모델
// voice, modalities, tools 등 세션 기본값을 서버에서 강제할 수도 있음
});
res.json(token);
});
// 2) 함수 호출 예시: “티켓 생성”
app.post("/api/create-ticket", async (req, res) => {
const { title, detail, priority } = req.body;
// 실제로는 Jira/Linear/ServiceNow 등 연동 + 인증/감사로그 필요
const ticketId = `TCK-${Math.floor(Math.random() * 100000)}`;
res.json({ ticketId, title, priority, createdAt: new Date().toISOString() });
});
app.listen(8787, () => console.log("server on :8787"));
2) 클라이언트: WebRTC 세션 + DataChannel 이벤트 + Barge-in(핵심)
- 오디오 송수신은
RTCPeerConnection이 처리 - 제어 이벤트(대화 아이템 생성/툴 결과 전달/취소)는 data channel로 주고받습니다. (platform.openai.com)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
// client/realtime.ts (개념 코드)
async function startVoiceAgent() {
// 1) ephemeral token 받기
const tokRes = await fetch("http://localhost:8787/api/realtime-token", { method: "POST" });
const tok = await tokRes.json();
// 2) WebRTC PeerConnection 구성
const pc = new RTCPeerConnection();
// 2-1) 마이크 입력을 peer에 붙임
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
for (const track of stream.getAudioTracks()) pc.addTrack(track, stream);
// 2-2) 에이전트 음성 출력 재생
pc.ontrack = (e) => {
const audio = document.querySelector<HTMLAudioElement>("#agentAudio")!;
audio.srcObject = e.streams[0];
audio.play();
};
// 3) DataChannel로 이벤트 송수신
const dc = pc.createDataChannel("oai-events");
dc.onmessage = async (e) => {
const evt = JSON.parse(e.data);
// (A) 툴 호출 요청이 오면 서버에 위임 후 결과를 다시 모델에 전달
if (evt.type === "response.output_item.tool_call") {
const { name, arguments: args, call_id } = evt;
if (name === "create_ticket") {
const r = await fetch("http://localhost:8787/api/create-ticket", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify(JSON.parse(args)),
});
const result = await r.json();
dc.send(JSON.stringify({
type: "conversation.item.create",
item: {
type: "tool_result",
tool_call_id: call_id,
content: [{ type: "output_text", text: JSON.stringify(result) }],
},
}));
// 툴 결과 반영해서 후속 응답 생성 트리거(이벤트명은 최신 스펙에 맞춰 조정)
dc.send(JSON.stringify({ type: "response.create" }));
}
}
// (B) 디버깅/관측: partial transcript나 상태 이벤트를 로그로 남김
if (evt.type?.includes("transcription") || evt.type?.includes("session")) {
console.log("[realtime]", evt.type, evt);
}
};
// 4) SDP offer/answer 교환 (여기서는 개념적으로만 표시)
// 실제 구현은 OpenAI Realtime WebRTC 가이드의 절차를 따르세요. ([platform.openai.com](https://platform.openai.com/docs/guides/realtime-webrtc?utm_source=openai))
// - createOffer -> setLocalDescription
// - OpenAI endpoint에 SDP 전송 -> SDP answer 수신
// - setRemoteDescription
// 5) “barge-in” 처리: 사용자가 말 시작하면 에이전트 TTS를 취소
// 포인트: 클라이언트 VAD(간단) + 서버/모델 이벤트(정교) 조합이 안정적
const vad = createSimpleVAD(stream, () => {
// 말 시작 감지 시 현재 응답 취소 이벤트 전송(이벤트명은 최신 스펙 확인)
dc.send(JSON.stringify({ type: "response.cancel" }));
});
// 6) 첫 시스템 지시 + 툴 정의(세션 시작 시 한 번)
dc.onopen = () => {
dc.send(JSON.stringify({
type: "session.update",
session: {
instructions:
"너는 사내 헬프데스크 음성 에이전트다. 사용자의 이슈를 요약하고 필요하면 티켓을 생성한다.",
tools: [{
type: "function",
name: "create_ticket",
description: "헬프데스크 티켓 생성",
parameters: {
type: "object",
properties: {
title: { type: "string" },
detail: { type: "string" },
priority: { type: "string", enum: ["low","medium","high"] }
},
required: ["title","detail","priority"]
}
}]
}
}));
// 대화 시작 트리거(사용자 음성 기반이라면 자동으로 시작되도록 구성 가능)
dc.send(JSON.stringify({
type: "conversation.item.create",
item: {
type: "message",
role: "user",
content: [{ type: "input_text", text: "지금부터 음성으로 문의할게." }]
}
}));
dc.send(JSON.stringify({ type: "response.create" }));
};
}
// 매우 단순한 VAD 예시(실무는 WebAudio + energy + hangover + 노이즈 적응 필요)
function createSimpleVAD(stream: MediaStream, onSpeechStart: () => void) {
const ctx = new AudioContext();
const src = ctx.createMediaStreamSource(stream);
const analyser = ctx.createAnalyser();
analyser.fftSize = 1024;
src.connect(analyser);
const data = new Uint8Array(analyser.frequencyBinCount);
let speaking = false;
const tick = () => {
analyser.getByteFrequencyData(data);
const energy = data.reduce((a, b) => a + b, 0) / data.length;
if (!speaking && energy > 18) { // 임계값은 환경별 튜닝 필요
speaking = true;
onSpeechStart();
} else if (speaking && energy < 10) {
speaking = false;
}
requestAnimationFrame(tick);
};
tick();
return { stop: () => ctx.close() };
}
예상 동작(현실 시나리오)
- 사용자가 “VPN이 자꾸 끊겨요…”라고 말하는 중에도 partial events가 로깅되고
- 에이전트가 해결 절차를 말하다가 사용자가 끼어들면 즉시
response.cancel로 TTS가 멈추고 - “티켓 만들까요?” 흐름에서 모델이
create_ticket툴을 호출하면 서버가 실제 티켓 생성 후 결과를 다시 전달 - 이후 에이전트가 “TCK-12345 생성했어요”처럼 음성으로 마무리
⚡ 실전 팁 & 함정
Best Practice (2~3개)
1) WebSocket보다 WebRTC 우선(브라우저)
브라우저/모바일의 실시간 오디오는 WebRTC가 지연/지터 대응에 유리합니다. OpenAI도 브라우저 연결은 WebRTC를 권장합니다. (platform.openai.com)
2) 오디오 경로와 제어 경로를 분리
오디오는 WebRTC 미디어 트랙으로, 제어는 data channel(이벤트)로. 이 분리가 되어야
- barge-in 취소
- 도구 호출/결과 전달
- 세션 업데이트(정책/툴/프롬프트)
를 “끊김 없이” 처리합니다. (platform.openai.com)
3) 관측(Observability)을 처음부터
“대화가 이상하다”는 버그 리포트는 대부분 재현이 어렵습니다. 최소한 아래는 남기세요.
- TTFA(P50/P90), turn latency, cancel 횟수
- VAD 트리거 시점 vs 실제 사용자 발화 시점(오탐/미탐)
- tool call request/response payload(PII 마스킹 포함)
흔한 함정/안티패턴
- (안티패턴) STT 최종 결과만 기다렸다가 LLM 호출: 실시간이 아닙니다. 엔터프라이즈 튜토리얼도 스트리밍/파이프라이닝이 핵심이라고 지적합니다. (arxiv.org)
- (안티패턴) barge-in을 “클라이언트에서 오디오 mute”로만 처리: 상대(모델)는 계속 말하고 있어 상태가 꼬입니다. 반드시 “취소 이벤트”로 세션 상태를 동기화하세요.
- (함정) NAT/방화벽 환경에서 WebRTC 실패: TURN이 필요합니다. AWS AgentCore도 WebRTC를 쓰려면 TURN(관리형/서드파티/자가)을 언급합니다. (aws.amazon.com)
비용/성능/안정성 트레이드오프
- Speech-to-Speech 모델은 단계가 줄어 UX는 좋아지기 쉬우나, 감사/재처리/품질 관리(텍스트 로그)가 약해질 수 있어 별도 transcription 채널(또는 Realtime transcription 모델)을 함께 고려하는 편이 안전합니다. OpenAI도 Realtime transcription을 별도 가이드로 분리해 제공합니다. (openai.com)
- Cascaded는 구성 요소별 최적화가 가능하지만, hop이 늘어날수록 P90이 튀기 쉽습니다. 이때 RelayS2S 같은 speculative/hybrid(빠른 프리픽스 + 느린 고품질 이어쓰기)가 설계 대안이 됩니다. (arxiv.org)
🚀 마무리
정리하면, 2026년 6월의 “실시간 음성 에이전트”는 모델 선택보다 스트리밍 아키텍처와 오케스트레이션이 승부처입니다.
- 브라우저/모바일: WebRTC + data channel 이벤트 기반으로 “오디오/제어”를 분리하고 (platform.openai.com)
- barge-in, tool calling, 재연결/관측을 필수 기능으로 보고
- Speech-to-Speech(Realtime)로 단순화할지, Cascaded로 통제/교체 가능성을 가져갈지 결정하세요. 엔터프라이즈 튜토리얼이 말하듯, 복잡한 업무엔 아직 Cascaded가 강력한 현실 해법입니다. (arxiv.org)
도입 판단 기준(실무용 체크)
- P90 TTFA 목표가 1.5초 이내인가? → WebRTC + 스트리밍 필수
- 감사/요약/검색을 위해 텍스트 로그가 필수인가? → transcription/텍스트 경로를 반드시 설계
- 네트워크가 거친 환경(기업망/모바일)인가? → TURN/재연결/세션 복구를 예산에 포함 (aws.amazon.com)
다음 학습 추천
- OpenAI Realtime API WebRTC 가이드(이벤트/세션 모델 이해) (platform.openai.com)
- “Enterprise Realtime Voice Agents from Scratch” 논문(지연 측정/파이프라인 구성) (arxiv.org)
- RelayS2S 같은 speculative/hybrid 패턴(낮은 TTFA + 높은 품질의 공존) (arxiv.org)
원하면, 위 코드 예제를 (1) Next.js + Vite 프론트, (2) 실제 SDP 교환/시그널링, (3) 음성 UI(푸시투톡/자동감지) + 재연결 + 로그 수집까지 포함한 “프로덕션 스켈레톤” 형태로 확장해 드릴 수 있어요.