실시간 음성 에이전트 2026년 2월판: STT/TTS를 “파이프라인”이 아닌 “스트리밍 런타임”으로 다루는 법
들어가며
2026년 2월 기준, “음성 AI”의 승부처는 모델 성능 자체보다 대화의 리듬(턴 전환, 끼어들기, 지연, 끊김 복구) 입니다. 텍스트 챗봇은 1~2초 지연도 용납되지만, 음성 대화는 첫 소리(First audio)까지 600~900ms, 턴 전체가 1~2초대를 넘기면 사용자가 즉시 “기계랑 말한다”는 느낌을 받습니다. (현업에서 흔히 보는 3초+ 지연은 대개 “버퍼링/턴 감지” 설계 문제로 귀결됩니다.)
최근 트렌드는 명확합니다.
- Speech-to-Speech(=STS) / Realtime 모델 + 스트리밍 프로토콜(WebRTC/WebSocket) 로 “텍스트 중간 단계를 최소화” (OpenAI Realtime/Voice Agents, Azure Realtime) (openai.github.io)
- VAD(Voice Activity Detection) 기반의 barge-in(끼어들기) + 서버가 즉시 generation cancel (Gemini Live API) (ai.google.dev)
- STT/TTS/LLM orchestration을 한 API로 묶어 지연과 복잡도를 줄이는 통합형 Voice Agent API (Deepgram Voice Agent API GA) (deepgram.com)
- 엔터프라이즈는 “내부망/클라우드 경계” 때문에 SageMaker 실시간 엔드포인트 같은 배포 경로를 선호 (Deepgram on SageMaker) (press.aboutamazon.com)
이 글은 “실시간 음성 대화 구현”을 목표로, 턴 설계/스트리밍/인터럽트를 중심으로 구조를 잡아봅니다.
🔧 핵심 개념
1) 파이프라인(ASR→LLM→TTS) vs Realtime/STS 런타임
전통 구조는 다음과 같습니다.
1) STT가 문장을 “확정(final)”
2) LLM이 텍스트 응답 생성
3) TTS가 음성 합성
문제는 (1)에서 final을 기다리는 순간 대화가 죽는다는 점입니다. 그래서 2025~2026의 실시간 API들은 다음을 기본으로 깝니다.
- Streaming input: 20ms~60ms 단위 오디오 프레임을 지속 전송
- Partial 결과: partial transcript/partial audio를 즉시 받음
- Interrupt: 사용자가 말하면 서버가 현재 생성 중 응답을 “취소(cancellation)”
Gemini Live API 문서에선 VAD로 끼어들기를 감지하면 진행 중 generation을 cancel/discard하고, 취소된 tool call까지 정리한다고 명시합니다. (ai.google.dev)
Azure OpenAI(=GPT Realtime) 쪽은 특히 WebRTC가 저지연에 유리하며 WebSocket은 “서버-서버 시나리오”에 더 가깝다고 가이드합니다. 즉, 브라우저/모바일 실시간 음성은 WebRTC를 우선 고려하는 게 맞습니다. (learn.microsoft.com)
OpenAI Agents SDK의 Voice Agents도 WebSocket/WebRTC 연결과 interruption handling을 “SDK 레벨 기능”으로 올려둔 게 포인트입니다. (openai.github.io)
2) 지연을 쪼개서 관리하라 (Latency Budget)
실시간 음성 에이전트의 지연은 대개 다음 합입니다.
- Capture/Encode: 마이크 캡처 + Opus/PCM 인코딩
- Network jitter: 업링크 지터/손실
- VAD 결정 지연: “사용자 발화가 끝났다”를 판단하는 hangover
- Model thinking: 첫 토큰/첫 오디오 청크
- Playback buffer: 재생측 버퍼(너무 두껍게 잡으면 체감 지연 급증)
여기서 가장 흔한 함정은:
- VAD를 너무 보수적으로 잡아 턴 종료를 늦게 확정
- 서버가 “사용자 오디오를 일정 길이로 모아서” 보내 가짜 스트리밍
- 재생 버퍼를 안전하게 잡다가 대화 리듬 파괴
3) “에이전트”는 음성만이 아니라 Tool/State 머신이다
요즘 Voice Agent API들이 “STT/TTS + orchestration”을 강조하는 이유는 단순합니다.
실전 음성봇은 결국:
- 상태(state): 인증/주문/예약/CS 티켓 등
- 툴(tool): DB 조회, CRM 업데이트, 검색, 결제, 콜 라우팅
- 가드레일(guardrails): 금칙어/개인정보/정책
Deepgram은 Voice Agent API를 단일 스트리밍 API로 통합해 latency/복잡도를 줄였다고 강조합니다. (deepgram.com)
💻 실전 코드
아래 예시는 “WebSocket 기반 실시간 음성 세션”의 최소 골격입니다. (브라우저 실시간은 WebRTC 권장이라는 점은 유지하되, 서버에서 빠르게 검증하기 좋은 형태로 작성) Azure OpenAI Realtime WebSocket 구조(세션/이벤트 기반)와 동일한 “이벤트 스트림” 패턴으로 설계합니다. (learn.microsoft.com)
예제 목표:
- 마이크 입력(PCM 16k) → 서버 → Realtime API
- 응답 오디오 chunk 수신 → 즉시 재생 큐에 적재
- 사용자 발화 감지 시 cancel(인터럽트) 를 넣을 수 있는 구조
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
# python 3.11+
# pip install websockets sounddevice numpy
import asyncio, base64, json
import numpy as np
import sounddevice as sd
import websockets
SAMPLE_RATE = 16000
FRAME_MS = 20
FRAME_SAMPLES = SAMPLE_RATE * FRAME_MS // 1000
# Azure OpenAI Realtime(WebSocket) 연결 예시 형식:
# wss://{resource}.openai.azure.com/openai/v1/realtime?model={deployment}
# (환경에 따라 api-version 파라미터가 필요할 수 있음) ([learn.microsoft.com](https://learn.microsoft.com/en-us/azure/ai-foundry/openai/how-to/realtime-audio-websockets?utm_source=openai))
AZURE_WSS_URL = "wss://YOUR_RESOURCE.openai.azure.com/openai/v1/realtime?model=YOUR_DEPLOYMENT"
AZURE_API_KEY = "YOUR_API_KEY"
def pcm16_to_b64(pcm16: np.ndarray) -> str:
# pcm16: int16 mono
return base64.b64encode(pcm16.tobytes()).decode("ascii")
async def mic_stream(q: asyncio.Queue):
# 마이크 캡처는 별도 스레드 콜백으로 들어오므로 asyncio queue로 브릿지
loop = asyncio.get_running_loop()
def callback(indata, frames, time, status):
if status:
# 실제 서비스라면 로깅/메트릭
pass
pcm16 = (indata[:, 0] * 32767.0).astype(np.int16)
loop.call_soon_threadsafe(q.put_nowait, pcm16)
with sd.InputStream(
channels=1,
samplerate=SAMPLE_RATE,
blocksize=FRAME_SAMPLES,
dtype="float32",
callback=callback,
):
while True:
await asyncio.sleep(1)
async def run():
mic_q = asyncio.Queue()
# 1) 마이크 태스크 시작
asyncio.create_task(mic_stream(mic_q))
headers = {
# WebSocket 핸드셰이크 헤더로 api-key 전달(브라우저는 불가한 경우가 많음) ([learn.microsoft.com](https://learn.microsoft.com/en-us/azure/ai-foundry/openai/how-to/realtime-audio-websockets?utm_source=openai))
"api-key": AZURE_API_KEY,
}
async with websockets.connect(AZURE_WSS_URL, extra_headers=headers) as ws:
# 2) (선택) 세션 설정 이벤트 전송
# 실제 이벤트 명/스키마는 제공자별로 다르지만,
# 핵심은 "입력 오디오 포맷/출력 모달리티(audio)"를 명시하는 것.
await ws.send(json.dumps({
"type": "session.update",
"session": {
"input_audio_format": {"type": "pcm16", "sample_rate_hz": SAMPLE_RATE},
"output_audio_format": {"type": "pcm16", "sample_rate_hz": SAMPLE_RATE},
"response_modalities": ["audio"],
}
}))
async def sender():
while True:
pcm16 = await mic_q.get()
await ws.send(json.dumps({
"type": "input_audio_buffer.append",
"audio": pcm16_to_b64(pcm16),
}))
async def receiver():
while True:
msg = json.loads(await ws.recv())
# 3) 서버가 보내는 스트리밍 이벤트를 받아 처리
t = msg.get("type")
if t == "response.audio.delta":
# 응답 오디오 조각(PCM16 base64)을 바로 재생 큐로 넣는 구조를 권장
audio_b64 = msg["delta"]
pcm = np.frombuffer(base64.b64decode(audio_b64), dtype=np.int16)
sd.play(pcm.astype(np.float32) / 32767.0, SAMPLE_RATE, blocking=False)
elif t == "response.completed":
# 한 턴 완료
pass
elif t == "input_audio_buffer.speech_started":
# 사용자 발화 시작을 감지했다면, 지금 재생 중인 TTS를 멈추고
# 생성 중 응답을 취소하는 "barge-in"을 구현할 수 있음
# (Gemini Live는 VAD 인터럽트 시 생성 취소를 명시) ([ai.google.dev](https://ai.google.dev/gemini-api/docs/live-guide))
await ws.send(json.dumps({"type": "response.cancel"}))
await asyncio.gather(sender(), receiver())
if __name__ == "__main__":
asyncio.run(run())
포인트:
- “실시간”은 append(오디오 프레임) → delta(오디오 청크) 가 끊기지 않는 게 핵심입니다.
- 인터럽트는 “UI에서 버튼”이 아니라 VAD 이벤트를 트리거로 자동화해야 체감이 좋아집니다.
- 브라우저/모바일이라면 WebRTC로 옮기고, 서버는 인증/툴 실행/로깅만 담당시키는 구성이 일반적으로 더 낮은 지연을 얻습니다. (learn.microsoft.com)
⚡ 실전 팁
1) VAD hangover(침묵 판정) 튜닝이 대화 리듬을 좌우
- 턴 종료를 너무 늦게 잡으면 응답이 느려지고,
- 너무 빠르게 잡으면 말을 끊고 끼어드는 느낌이 납니다.
- “사용자 발화 시작” 감지는 공격적으로, “발화 종료”는 약간 보수적으로(짧은 hangover) 가져가는 게 경험상 안정적입니다.
2) 재생 버퍼는 짧게, 대신 끊김 복구 전략을
- 버퍼를 길게 잡아 끊김을 숨기면 지연이 커져 UX가 망가집니다.
- 짧은 버퍼 + 패킷 손실 시 “짧은 무음/타임스트레치” 같은 완충을 두세요. (WebRTC가 이 쪽에 강함) (learn.microsoft.com)
3) Tool calling은 “음성 턴”과 분리해서 설계
- 음성 응답을 길게 끌면서 중간에 DB/외부 API를 때리면 지연이 폭발합니다.
- 패턴 추천:
- (a) 먼저 짧게 “확인 멘트”를 음성으로 내보내고
- (b) 백그라운드로 툴 실행
- (c) 결과가 오면 다음 턴에서 확정 답변
4) 통합형 Voice Agent API vs BYO 파이프라인 선택 기준
- 빠른 출시/운영 단순화: 통합형(예: Deepgram Voice Agent API GA) (deepgram.com)
- 커스텀 모델/정교한 정책/온프레미스 제약: BYO 파이프라인(+SageMaker/사내 배포 경로) (press.aboutamazon.com)
5) “3초 지연”의 80%는 진짜 모델이 아니라 구현
- 커뮤니티에서도 end-to-end 3초+는 “버퍼링/가짜 스트리밍”을 의심하라는 얘기가 반복됩니다(물론 경험담 수준).
- 측정은 반드시 구간별로:
- last user audio frame → first server ack
- VAD end → first model audio delta
- first delta → speaker output
🚀 마무리
2026년 2월의 실시간 음성 에이전트는 “STT/TTS 붙이면 끝”이 아니라, Streaming + VAD + Interrupt + Tool/State 를 하나의 런타임으로 다루는 싸움입니다.
정리하면:
- 저지연 목표라면 WebRTC 우선, WebSocket은 서버-서버/단순 검증에 적합 (learn.microsoft.com)
- 끼어들기(barge-in) 는 필수 기능이며, VAD 기반 cancel이 핵심 (ai.google.dev)
- 통합형 Voice Agent API가 “복잡도/지연”을 실제로 줄여주는 구간이 있고(Deepgram), 엔터프라이즈는 배포 경로까지 같이 봅니다. (deepgram.com)
다음 학습 추천:
- WebRTC 기반 실시간 오디오 파이프라인(Opus, jitter buffer, echo cancellation)
- 이벤트 기반 Realtime API(세션/대화/취소/partial) 상태 머신 설계
- 음성 UX(턴 길이, 백채널링 “네/좋아요” 같은 짧은 음성 토큰) 실험
원하시면, 위 예제를 브라우저(WebRTC) + 서버(툴 실행) + 음성 인터럽트까지 포함한 “프로덕션 아키텍처”로 확장한 버전(구성도/메트릭/부하 테스트 체크리스트 포함)으로 이어서 작성해드릴게요.