“실시간 음성 에이전트” 구현의 승부처는 모델이 아니라 **파이프라인과 전송(WebRTC)** 이다
실시간 음성 에이전트(voice-to-voice agent)가 풀어주는 문제는 명확합니다. “사람이 말하면 1초 안에(체감상) 끊김 없이 대답하는 대화 UX” 입니다.
들어가며
실시간 음성 에이전트(voice-to-voice agent)가 풀어주는 문제는 명확합니다. “사람이 말하면 1초 안에(체감상) 끊김 없이 대답하는 대화 UX” 입니다. 여기서 실시간은 STT 정확도나 TTS 자연스러움만이 아니라, turn-taking(말 끊기/양보), TTFA(Time To First Audio), echo, 네트워크 지터까지 포함한 시스템 성질입니다. OpenAI의 GPT-Realtime처럼 WebRTC/WebSocket/SIP로 오디오 입출력을 직접 처리하는 모델이 GA로 자리 잡았고1, Google도 Gemini Live API로 저지연 음성 스트리밍을 전면에 내세웁니다2. AWS 또한 Bedrock AgentCore Runtime에 WebRTC 양방향 스트리밍을 추가하며 “브라우저/모바일 저지연”을 명시했습니다3.
언제 쓰면 좋은가
- 콜센터/예약/접수/상담처럼 hands-free가 핵심인 인터랙션
- 게임 NPC, 로봇/차량 인터페이스처럼 “말로 조작” 이 자연스러운 도메인
- 다국어 통역/실시간 번역처럼 지연이 곧 가치 하락인 제품
언제 쓰면 안 되는가
- 감사/컴플라이언스가 강한 환경에서 텍스트 로그(누가 무엇을 말했는지)가 필수인데, speech-to-speech 단일 모델에 의존하면 “감사 가능성”이 약해질 수 있습니다(텍스트 trail이 제한적이라는 점을 LiveKit도 비교표로 명시)4
- 감정/억양/말투가 의미인 케이스(상담/멘탈헬스 등)에서, 최신 실시간 시스템이 음성을 “듣지만(hears) 실제로는 transcript처럼 처리”하는 경향이 있다는 연구가 나왔습니다5
- 빠른 프로토타입이 아니라 장기 운영이라면 “모델 하나로 끝” 접근보다, 관측/비용/장애 격리가 되는 구조가 필요합니다
🔧 핵심 개념
1) 2026년 실시간 음성 아키텍처 3종: Cascaded vs Realtime vs Half-cascade
요즘 프레임워크/플랫폼(예: LiveKit Agents)은 파이프라인을 세 가지로 정리합니다4.
- STT → LLM → TTS (cascaded / sequential or streaming)
- 장점: 구성요소를 교체하기 쉽고(“STT만 교체” 같은), 텍스트 로그/툴콜/정형출력이 성숙
- 단점: 지연이 누적(특히 endpointing/EOT)되고, barge-in(끼어들기)과 cancellation이 어렵다
- 핵심은 “직렬”이 아니라 스트리밍 파이프라이닝입니다. 각 단계가 부분 결과를 다음 단계에 흘려줘야 1초대 TTFA가 나옵니다.6
- Realtime speech-to-speech(단일 모델이 audio in/out)
- Half-cascade(입력 이해는 realtime, 출력은 별도 TTS)
- “대화의 반응성”은 realtime로 확보하면서, “브랜드 음성/발음 사전/스크립트 say()” 같은 출력 제어는 TTS로 가져가는 타협안입니다4
- 실무에서 꽤 현실적인 선택지입니다. 특히 고객센터처럼 고정 멘트 + 동적 응답이 섞일 때.
2) 지연(latency)의 실제 구성: 모델보다 “전송 + turn detection + cancel”이 먼저다
실시간에서 사용자가 느끼는 지연은 대략:
- 입력 오디오 캡처/인코딩(20ms frame)
- 전송(WebRTC면 UDP/SRTP + jitter buffer / WebSocket이면 TCP HOL)
- VAD/endpointing(EOT): Deepgram도 “final은 endpoint detection 때문에 늦어진다”고 분리해서 측정하라고 안내합니다8
- LLM 첫 토큰 생성
- TTS TTFA + 오디오 버퍼링
- barge-in 시: TTS mid-stream cancel + 모델 상태 정리
그리고 2026년 트렌드는 “모델을 바꾸면 빨라질 것”이 아니라,
- WebRTC를 기본 전송으로(AWS도 WebRTC를 ‘저지연 미디어 전송’ 포지션으로 추가)3
- 파이프라인을 스트리밍으로 겹치기(연구/튜토리얼에서 반복 강조)9
- 관측(각 단계 p50/p95)을 먼저 만들기(운영에서 병목이 레이어마다 다름)10 로 정리됩니다.
3) “Listen-Think-Speak”가 왜 중요한가
사람은 상대가 말 끝내기 전에 이미 생각을 시작합니다. 그런데 많은 구현은 EOT(말끝) → LLM → TTS로 직렬 처리해서 지연이 커집니다. 2026년 연구 흐름 중 하나가 Listen-Think-Speak / incremental reasoning로 “언제 생각을 시작할지”를 분리하는 겁니다11.
실무적으로는 아래처럼 번역됩니다:
- STT partial이 들어오면 “확신이 생긴 순간”부터 LLM을 프리페치(혹은 작은 모델로 intent 추정)
- 다만, 너무 공격적으로 시작하면 잘못된 가정 기반 응답이 늘어 barge-in이 잦아지고 TTS 비용이 폭증합니다
💻 실전 코드
아래 예제는 “브라우저/모바일 WebRTC까지 다 붙이기”는 분량이 커지니, 서버 측에서 실시간 WebSocket 음성 세션을 열고, 1) 클라이언트가 PCM16 프레임을 스트리밍 업로드
2) 서버가 OpenAI Realtime로 오디오를 밀어 넣고
3) 모델이 내보내는 오디오를 다시 클라이언트로 스트리밍
하는 구조로 잡겠습니다. (프로덕션에서는 여기서 전송을 WebRTC로 바꾸는 게 일반적이지만, 서버/파이프라인의 핵심은 동일합니다. OpenAI Realtime은 WebRTC/WebSocket/SIP를 지원합니다1.)
0) 의존성/환경
1
2
3
4
5
# server
python -m venv .venv && source .venv/bin/activate
pip install fastapi uvicorn websockets numpy soundfile
export OPENAI_API_KEY="..."
1) FastAPI: 클라이언트 ↔ 서버 WebSocket + OpenAI Realtime 브릿지
- 현실적인 시나리오: “주문/예약 상담” 에이전트
- 포인트: barge-in(사용자 말 시작 시 TTS stop), 턴 단위 로그, TTFA 측정 훅(최소한의 형태)
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
# server.py
import os, json, time, asyncio, base64
from fastapi import FastAPI, WebSocket
import websockets
OPENAI_API_KEY = os.environ["OPENAI_API_KEY"]
OPENAI_REALTIME_URL = "wss://api.openai.com/v1/realtime?model=gpt-realtime" # 모델/파라미터는 운영 정책에 맞게 고정
app = FastAPI()
SYSTEM_PROMPT = """You are a Korean-speaking voice agent for a food delivery service.
- Be concise (1-2 sentences) unless user asks for details.
- If you need an action, call a tool by emitting structured JSON in text (we will parse it).
- Always confirm order items and address before finalizing.
"""
def b64_pcm16(raw_bytes: bytes) -> str:
return base64.b64encode(raw_bytes).decode("ascii")
@app.websocket("/ws/voice")
async def voice_ws(client_ws: WebSocket):
await client_ws.accept()
# Connect to OpenAI Realtime over WebSocket (server-to-server)
headers = [
("Authorization", f"Bearer {OPENAI_API_KEY}"),
("OpenAI-Beta", "realtime=v1"),
]
async with websockets.connect(OPENAI_REALTIME_URL, extra_headers=headers) as oai_ws:
session_start = time.time()
last_user_audio_ts = None
first_audio_out_ts = None
# 1) Configure session (instructions, audio formats, turn-taking hints)
# Note: exact event schema can vary by provider/version; keep this as a template and align with latest docs.
await oai_ws.send(json.dumps({
"type": "session.update",
"session": {
"instructions": SYSTEM_PROMPT,
"input_audio_format": "pcm16",
"output_audio_format": "pcm16",
# turn detection / interruption behavior is crucial in real agents
"turn_detection": {"type": "server_vad"},
}
}))
async def pump_client_to_oai():
nonlocal last_user_audio_ts
try:
while True:
msg = await client_ws.receive_bytes() # raw PCM16 16kHz mono frames
last_user_audio_ts = time.time()
await oai_ws.send(json.dumps({
"type": "input_audio_buffer.append",
"audio": b64_pcm16(msg),
}))
except Exception:
# client disconnected
pass
async def pump_oai_to_client():
nonlocal first_audio_out_ts
try:
while True:
raw = await oai_ws.recv()
evt = json.loads(raw)
t = evt.get("type")
# --- interruption / barge-in handling (server side) ---
# Many realtime stacks emit events when user speech starts; use it to cancel agent speech.
if t in ("input_audio_buffer.speech_started", "turn.start"):
await oai_ws.send(json.dumps({"type": "response.cancel"}))
# --- audio out ---
if t in ("response.output_audio.delta", "output_audio.delta"):
if first_audio_out_ts is None:
first_audio_out_ts = time.time()
ttfa_ms = int((first_audio_out_ts - (last_user_audio_ts or session_start)) * 1000)
await client_ws.send_text(json.dumps({"type": "metric", "ttfa_ms": ttfa_ms}))
chunk_b64 = evt.get("delta") or evt.get("audio")
if chunk_b64:
await client_ws.send_bytes(base64.b64decode(chunk_b64))
# --- optional: transcript for UI/observability ---
if t in ("response.output_text.delta", "output_text.delta"):
await client_ws.send_text(json.dumps({"type": "assistant_text_delta", "delta": evt.get("delta", "")}))
if t in ("conversation.item.input_audio_transcription.completed", "input_audio_transcription.completed"):
await client_ws.send_text(json.dumps({"type": "user_transcript", "text": evt.get("transcript", "")}))
except Exception:
pass
await asyncio.gather(pump_client_to_oai(), pump_oai_to_client())
예상 동작
- 클라이언트가 마이크 PCM16 프레임을 보내면, 서버는 이를 Realtime 세션으로 append
- 모델이 음성으로 답하기 시작하면 서버가 오디오 델타를 그대로 클라이언트로 전달
- 사용자가 말을 시작하면(
speech_started) 즉시response.cancel로 TTS를 끊고 사용자 턴으로 전환
2) (확장) “Cascaded(STT→LLM→TTS)”로 바꿔야 하는 순간
Realtime 모델이 편해 보여도, 프로덕션에서 아래가 필요하면 cascaded가 유리합니다(감사/툴콜/스크립트 say, 교체 용이성)4.
- STT 결과를 정규화(금칙어 마스킹, 주소/전화번호 포맷팅) 후 LLM 투입
- 특정 문장은 회사 TTS 보이스로 “고정 음색/발음” 강제
- 단계별 비용을 분리 청구/최적화
LiveKit 문서/가이드가 이 3가지 파이프라인 선택을 비교표로 정리해두니, 팀 합의할 때 “감”이 아니라 기준으로 결정하기 좋습니다4.
⚡ 실전 팁 & 함정
Best Practice (2~3개)
1) TTFA를 “모델 시간”이 아니라 “사용자 발화 끝/중간 지점 기준”으로 측정
- STT의 final이 늦는 가장 큰 이유는 endpointing(EOT)입니다. Deepgram도 transcript latency와 EOT latency를 분리하라고 가이드합니다8
- 실무에서는 “사용자 발화가 끝났다고 판단한 시점”부터 “첫 오디오 재생”까지를 핵심 KPI로 둡니다.
2) barge-in을 ‘기능’이 아니라 ‘예산 절감 장치’로 취급
- 끼어들기(cancel)가 없으면 사용자가 다시 말하는 순간에도 TTS가 계속 흘러 대화 품질도 망하고 비용도 폭증합니다.
- 구현은 단순히 “stop 재생”이 아니라, (a) 서버에서 response.cancel, (b) 클라이언트 버퍼 flush, (c) 상태 머신 전환까지 원자적으로 처리해야 합니다.
3) 관측(Observability)은 “오디오 경로 밖에서”
- 실시간 오디오 경로에 로깅/프로파일러를 끼우면 지연이 늘어납니다.
- 운영에서 병목은 레이어마다 다르게 터집니다(미디어/SFU, 워커, 추론, 텔레포니, 외부 API 등)10
- 최소한 turn 단위로
stt_seconds,llm_tokens,tts_chars,ttfa_ms,p95를 저장하세요. LiveKit도 파이프라인/노드 훅 기반 커스터마이징을 문서화해두었습니다12.
흔한 함정/안티패턴
- “WebSocket이면 충분”: 데스크톱/사내망에선 되지만, 모바일 LTE/5G에서 지터/손실이 늘면 체감이 급락합니다. 2026년 흐름이 WebRTC로 가는 이유가 여기에 있습니다(AWS도 WebRTC를 저지연 실시간 미디어 전송으로 강조)3
- 서버 VAD만 믿고 튜닝 안 함: 도메인(조용한 사무실 vs 주방/매장)마다 VAD 파라미터가 달라집니다. “말끝을 늦게 잡는” 순간 TTFA가 망가집니다.
- speech-to-speech 모델을 감정 이해까지 과신: 실시간 음성 AI가 prosody를 충분히 활용하지 못하고 transcript처럼 행동할 수 있다는 경고가 있습니다5. 중요한 도메인이라면 별도 신호(말속도/피치/볼륨 특징량) 추출을 병행하거나, 최소한 “불확실성 처리(재확인 질문)”를 설계하세요.
비용/성능/안정성 트레이드오프
- Realtime(단일 모델): latency는 유리하지만, 디버깅/감사/세밀 제어가 손해일 수 있음4
- Cascaded: 각 컴포넌트를 독립 최적화 가능(예: STT만 교체) 대신, 잘못 만들면 직렬 지연이 누적
- 결론: “최저 지연”이 목표면 realtime, “운영/감사/제어”가 목표면 cascaded, “브랜드 음성+입력 자연성”이면 half-cascade가 실무적으로 자주 맞습니다4
🚀 마무리
2026년 8월 기준 실시간 음성 에이전트는 “TTS/STT 붙이면 되는 기능”이 아니라, 전송(WebRTC) + 스트리밍 파이프라인 + turn-taking + cancel + 관측이 합쳐진 시스템입니다. OpenAI의 Realtime처럼 WebRTC/WebSocket/SIP 기반 speech-to-speech 모델이 GA가 되었고1, Google Gemini Live API도 저지연 실시간 음성 스트리밍을 전면에 둡니다2. AWS 역시 AgentCore Runtime에 WebRTC를 추가해 브라우저/모바일 저지연 양방향 스트리밍을 공식 지원하기 시작했습니다3.
도입 판단 기준(현실 체크리스트)
- “내 제품은 텍스트 로그/감사가 필수인가?” → Yes면 cascaded/half-cascade 우선4
- “모바일 네트워크에서 1초 내 응답이 꼭 필요한가?” → Yes면 WebRTC 중심으로 설계
- “사용자가 자주 끼어들까?” → Yes면 cancel/barge-in을 1순위 기능으로
- “음성의 감정/억양이 핵심 신호인가?” → 현재 시스템의 한계 가능성을 전제로 설계5
다음 학습 추천
- LiveKit의 파이프라인 타입 비교표로 팀 아키텍처 결정을 문서화4
- STT latency 측정 시 endpointing 분리(“왜 느린지”부터 정확히 보기)8
- incremental/Listen-Think-Speak 류 연구 흐름을 참고해 “생각 시작 시점”을 설계11
https://developers.openai.com/api/docs/models/gpt-realtime ↩︎ ↩︎2 ↩︎3 ↩︎4
https://ai.google.dev/gemini-api/docs/live-api?authuser=8 ↩︎ ↩︎2
https://aws.amazon.com/about-aws/whats-new/2026/03/amazon-bedrock-webrtc/ ↩︎ ↩︎2 ↩︎3 ↩︎4
https://docs.livekit.io/agents/models/pipelines/ ↩︎ ↩︎2 ↩︎3 ↩︎4 ↩︎5 ↩︎6 ↩︎7 ↩︎8 ↩︎9 ↩︎10 ↩︎11
https://livekit.com/blog/voice-agent-architecture-stt-llm-tts-pipelines-explained ↩︎
https://learn.microsoft.com/en-gb/azure/foundry/openai/how-to/realtime-audio-sip ↩︎
https://developers.deepgram.com/docs/measuring-streaming-latency ↩︎ ↩︎2 ↩︎3
https://www.reddit.com/r/livekit/comments/1v3hfrg/scaling_voice_agents_breaks_in_a_different_place/ ↩︎ ↩︎2