포스트

실시간 음성 에이전트 2026년 3월판: STT/TTS를 “파이프라인”이 아니라 “스트림”으로 재설계하기

실시간 음성 에이전트 2026년 3월판: STT/TTS를 “파이프라인”이 아니라 “스트림”으로 재설계하기

들어가며

2026년 3월 기준, 음성 AI 제품의 승부처는 “정확도”만이 아니라 대화감(conversational feel) 입니다. 사용자는 전화처럼 말하다가 끼어들고(barge-in), 잠깐 멈추고, 다시 이어 말합니다. 그런데 전통적인 STT → LLM → TTS 직렬 파이프라인은 각 단계가 “완료”될 때까지 다음 단계가 기다리기 쉬워, 체감 지연이 급격히 커집니다(특히 Time-to-First-Audio가 길어짐). 이를 해결하기 위해 2025~2026에 걸쳐 업계는 공통적으로 persistent streaming(웹소켓/WebRTC), VAD 기반 턴테이킹, listen-while-think / speak-while-think 구조로 이동하고 있습니다. (platform.openai.com)

또한 “모델” 관점에서도 큰 변화가 있습니다. OpenAI는 gpt-realtime 기반의 Realtime API를 WebRTC/WebSocket/SIP까지 확장해 “음성-대-음성” 상호작용을 전면에 두었고, Google도 Vertex AI의 Gemini 2.5 Flash Live API(native audio)로 실시간 음성 대화를 공식 스펙으로 제공합니다. (developers.openai.com)


🔧 핵심 개념

1) 직렬(cascaded) vs. 스트리밍(duplex streaming)

  • 직렬 파이프라인(STT→LLM→TTS): 구현은 단순하지만, 사용자가 한 문장을 길게 말할수록 STT가 “끝”을 기다리고, LLM도 “완성된 텍스트”를 기다리면서 지연이 누적됩니다.
  • 스트리밍 파이프라인: 오디오를 프레임 단위로 흘려보내고, partial transcript / partial reasoning / partial audio를 연쇄적으로 흘려보냅니다. 최근 연구들도 “사람처럼” 듣는 동안 생각하고, 생각하는 동안 말하는 프레임워크가 지연-정확도 트레이드오프에서 유리하다고 보고합니다. (arxiv.org)

2) VAD(Voice Activity Detection)와 턴테이킹

실시간 음성 UX의 절반은 모델이 아니라 턴테이킹입니다.

  • VAD: “사용자가 말하는 중/멈춘 상태”를 빠르게 판정해 STT flush 시점을 앞당깁니다.
  • 목표는 단순히 최종 transcript 정확도가 아니라, 언제 모델이 응답을 시작할지를 안정적으로 결정하는 것(“말 끝 감지”)입니다. (dev.to)

3) Barge-in(끼어들기) = TTS cancel + 상태기계

사용자가 에이전트가 말하는 중에 끼어들면, 좋은 에이전트는: 1) 현재 TTS 스트림 재생을 즉시 중단 2) 출력 오디오가 입력 STT로 재유입되지 않도록 AEC(에코 캔슬) 또는 분리된 오디오 라우팅 3) “말하는 상태”에서 “듣는 상태”로 전환하고 새 턴을 시작
이걸 안 하면 “로봇처럼 끝까지 읽는 상담원”이 됩니다. (dev.to)

4) Transport: WebRTC가 이기는 구간

브라우저/모바일에서 “실시간 음성”은 보통 WebSocket도 가능하지만,

  • 오디오 장치/코덱/지터버퍼/네트워크 적응까지 고려하면 WebRTC가 자연스러운 선택이 됩니다. OpenAI는 브라우저 음성 앱에 WebRTC 연결 가이드를 제공하고, ephemeral token 기반으로 세션을 여는 패턴을 권장합니다. (platform.openai.com)

💻 실전 코드

아래 예제는 브라우저 → OpenAI Realtime API(WebRTC) 로 연결해,

  • 마이크 입력을 모델로 보내고
  • 모델의 오디오 출력 트랙을 재생하며
  • 바지-인(barge-in) 시 출력 오디오를 즉시 mute/stop 하는 “뼈대”를 보여줍니다.

전제: 서버에서 ephemeral key(임시 토큰)를 발급해 /session 같은 엔드포인트로 내려준다고 가정합니다(클라이언트에 API Key 하드코딩 금지). OpenAI 문서도 브라우저에서 ephemeral token 사용 패턴을 안내합니다. (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
// realtime-voice-agent.js
// 실행 환경: modern browser (HTTPS), WebRTC 사용
// 목표: mic -> Realtime model, model audio -> speaker, barge-in 시 TTS 즉시 중단

let pc, dc;
let remoteAudioEl;
let speaking = false; // "에이전트가 말하는 중" 상태 플래그

async function fetchEphemeralToken() {
  // 여러분 서버에서 ephemeral token을 발급해 주는 API (예: /api/realtime-token)
  const res = await fetch("/api/realtime-token", { method: "POST" });
  if (!res.ok) throw new Error("failed to fetch ephemeral token");
  return res.json(); // { token: "..." }
}

async function startRealtime() {
  const { token } = await fetchEphemeralToken();

  pc = new RTCPeerConnection();

  // 1) 원격 오디오(모델이 말하는 음성)를 재생할 audio element 준비
  remoteAudioEl = document.createElement("audio");
  remoteAudioEl.autoplay = true;
  remoteAudioEl.playsInline = true;
  document.body.appendChild(remoteAudioEl);

  pc.ontrack = (e) => {
    // 모델 오디오 트랙이 들어오면 재생
    remoteAudioEl.srcObject = e.streams[0];
    speaking = true;
  };

  // 2) 마이크 입력을 가져와 peerConnection에 추가 (업로드)
  const mic = await navigator.mediaDevices.getUserMedia({
    audio: {
      echoCancellation: true, // AEC는 barge-in 품질에 매우 중요
      noiseSuppression: true,
      autoGainControl: true
    }
  });

  mic.getTracks().forEach((t) => pc.addTrack(t, mic));

  // 3) DataChannel: 이벤트/제어 메시지(예: response.cancel, function calling 등) 용도
  dc = pc.createDataChannel("oai-events");
  dc.onmessage = (e) => {
    // 모델 이벤트를 수신 (구체 포맷은 API 가이드 참고)
    // 여기서는 디버그 용도로만 출력
    console.log("event:", e.data);
  };

  // 4) SDP offer/answer 교환: OpenAI Realtime WebRTC 엔드포인트로 offer를 보내 answer 받기
  const offer = await pc.createOffer();
  await pc.setLocalDescription(offer);

  // OpenAI 문서 가이드의 WebRTC 세션 생성 패턴을 따름
  // (정확한 URL/헤더/모델 지정은 문서 기준으로 맞추세요)
  const ansRes = await fetch("https://api.openai.com/v1/realtime?model=gpt-realtime-2025-08-28", {
    method: "POST",
    headers: {
      "Authorization": `Bearer ${token}`,
      "Content-Type": "application/sdp"
    },
    body: offer.sdp
  });

  const answerSdp = await ansRes.text();
  await pc.setRemoteDescription({ type: "answer", sdp: answerSdp });

  // 5) 간단한 barge-in: 사용자가 말 시작(마이크 레벨 상승)하면 에이전트 TTS 즉시 중단
  // 프로덕션에서는 AudioWorklet + VAD(webrtcvad 등) 권장.
  setupNaiveBargeIn(mic);
}

function setupNaiveBargeIn(micStream) {
  const ctx = new AudioContext();
  const src = ctx.createMediaStreamSource(micStream);
  const analyser = ctx.createAnalyser();
  analyser.fftSize = 512;

  src.connect(analyser);
  const buf = new Uint8Array(analyser.frequencyBinCount);

  function loop() {
    analyser.getByteTimeDomainData(buf);

    // 매우 단순한 레벨 감지(RMS 비슷하게)
    let sum = 0;
    for (const v of buf) {
      const x = (v - 128) / 128;
      sum += x * x;
    }
    const rms = Math.sqrt(sum / buf.length);

    // 사용자가 말하기 시작했다고 추정되면:
    if (rms > 0.06 && speaking) {
      // (A) 로컬 재생을 즉시 mute (가장 단순/확실)
      remoteAudioEl.muted = true;

      // (B) 모델에 "말 끊기" 이벤트 전송(지원되는 이벤트명은 API 문서 기준)
      // 예: dc.send(JSON.stringify({ type: "response.cancel" }));
      try {
        dc?.send(JSON.stringify({ type: "response.cancel" }));
      } catch {}

      speaking = false;
      // 다시 말이 끝나면 unmute는 "모델의 새 오디오가 도착할 때" 또는 상태 이벤트로 처리
      setTimeout(() => (remoteAudioEl.muted = false), 300);
    }

    requestAnimationFrame(loop);
  }

  loop();
}

// 버튼 등에서 호출
// startRealtime().catch(console.error);

핵심은 “완벽한 VAD”가 아니라 (1) TTS를 즉시 멈출 수 있는 제어 경로(2) 오디오 입출력 루프를 끊지 않는 persistent connection입니다. OpenAI는 Realtime API에서 WebRTC/WebSocket 기반의 실시간 세션과 이벤트 기반 상호작용을 전제로 문서를 구성하고 있습니다. (platform.openai.com)


⚡ 실전 팁

1) 지표는 WER보다 Time-to-First-Audio(TTFA)
실시간 상담/에이전트는 “맞게 말하기”보다 “빨리 반응하기”가 더 크게 체감됩니다. TTFA를 별도 KPI로 잡고, STT/LLM/TTS 각각의 first-byte 시간을 분해 측정하세요. (plavno.io)

2) “말 끝”을 기다리지 말고 flush 전략을 설계
VAD로 침묵이 감지되면 버퍼를 빨리 flush해서 턴을 닫아야 합니다. 다만 너무 공격적이면 사용자의 짧은 망설임(“음…”)에도 끊기므로, min-silence(ms) 같은 파라미터를 AB 테스트로 튜닝하세요. (dev.to)

3) Barge-in은 기능이 아니라 상태기계(state machine)
권장 상태 예시: LISTENING → THINKING → SPEAKING, 그리고 어디서든 USER_SPEAKING_DETECTED 이벤트가 오면 SPEAKING을 즉시 cancel하고 LISTENING으로 전환. Azure 쪽 가이드/사례도 “재생 중단 + 다시 듣기”를 핵심 UX로 강조합니다. (techcommunity.microsoft.com)

4) streaming TTS는 “정확도 손실” 비용이 있다
초저지연 streaming TTS는 문맥을 덜 보고 발음/억양 결정을 내려 품질 손실이 날 수 있습니다. 즉, 최저 지연만 추구하면 “부정확하지만 빠른” 음성이 될 수 있으니, 서비스 톤에 맞춰 지연/품질 균형점을 잡으세요. (deepgram.com)

5) 벤더 선택은 “모델”보다 연결 방식+운영성을 보라
OpenAI는 WebRTC/WebSocket/SIP까지 표면적이 넓고(gpt-realtime), Google은 Vertex AI에서 Live API(native audio)를 공식 제공하는 등, 이제는 “가능하다/불가능하다”가 아니라 운영(모니터링/레이트리밋/리트라이/지역)이 차이를 만듭니다. (developers.openai.com)


🚀 마무리

2026년 3월의 실시간 음성 에이전트는 더 이상 “STT 결과를 받아 LLM에 넣고 TTS로 읽는 봇”이 아닙니다. 핵심은 스트리밍 기반 duplex 아키텍처, VAD/턴테이킹, barge-in을 전제로 한 상태기계, 그리고 TTFA 최적화입니다. 연구 흐름도 listen-while-think / speak-while-think가 직렬 파이프라인의 한계를 실제로 줄일 수 있음을 보여줍니다. (arxiv.org)

다음 학습 추천:

  • OpenAI Realtime API의 WebRTC 가이드와 이벤트/함수호출(Realtime function calling) 패턴 정독 (platform.openai.com)
  • VAD 기반 턴테이킹 + barge-in state machine을 “테스트 가능한 모듈”로 분리
  • TTFA/지연 분해 측정(네트워크/버퍼/첫 토큰/첫 오디오)을 대시보드화

원하시면, 위 예제를 (1) 서버에서 ephemeral token 발급 구현(Node/FastAPI), (2) AudioWorklet 기반 VAD로 개선, (3) tool/function calling을 붙여 “실시간 음성 + 업무 처리” 에이전트로 확장한 버전까지 이어서 작성해드릴게요.

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.