음성 AI 에이전트가 고객 말을 끊지 않게: VAD 침묵 기준과 턴 감지 모델을 함께 쓰는 구성법
채널톡 보이스 알프 출시로 드러난 턴 감지 문제와 LiveKit 턴 감지기, Silero VAD + Smart Turn 조합, 통화 로그로 대기 시간 정하기

핵심 요약
- 침묵 길이만으로는 머뭇거림과 발화 종료를 구분하지 못한다. VAD는 음성 유무만, 턴 감지 모델은 억양·리듬·단어로 턴 종료를 판단하게 나눈다.
- LiveKit 오디오 TurnDetector를 쓰면 엔드포인팅 기본값이 0.5초/3.0초에서 0.3초/2.5초로 줄어든다. 한국어는 지원되지만 STT를 설정하지 않으면 영어 임계값이 쓰인다.
- Smart Turn은 16kHz 모노·최대 8초 입력, 짧으면 앞쪽 0 패딩, VAD 침묵 후 턴 전체로 판정하고 말이 이어지면 전체로 다시 판정한다.
- 대기 시간은 hold 적중률, 이른 응답 비율, 응답 지연 p50 세 지표를 함께 보고 조정한다.
전화로 상담하는 AI 에이전트를 만들면 가장 먼저 부딪히는 문제가 "고객이 말을 끝냈는가"를 판단하는 일이다. 침묵 길이만 보면 고객이 잠깐 생각하는 사이에 AI가 끼어들고, 기다리는 시간을 늘리면 응답이 굼떠진다. 이 글은 VAD(음성 활동 감지)와 턴 감지 모델을 겹쳐 쓰는 구성을 LiveKit Agents와 오픈소스 조합(Silero VAD + Smart Turn) 두 가지로 정리하고, 통화 로그로 대기 시간을 조정하는 방법까지 다룬다.
보이스 알프 발표에서 드러난 턴 감지 문제
채널코퍼레이션은 2026년 10월 21일 채팅 상담 AI '알프'를 전화로 넓힌 '보이스 알프'를 정식 출시했다(ZDNet Korea, 전자신문). 보도에 따르면 고객의 억양과 망설임을 분석해 발화 종료를 판단하는 자체 턴 감지 모델을 넣었고, 사내 카페 AI 주문 데이터 약 1만 건을 분석한 결과 AI가 "아직 말이 끝나지 않았다"고 보고 기다린 경우의 87%에서 1.5초 안에 실제 요청이 이어졌다고 한다. 상담사 음성으로 학습한 한국어 TTS로 한영 혼용 표현과 날짜·시간·URL을 읽도록 했다는 설명도 있다.
이 발표에서 설계에 가져올 수 있는 힌트는 두 가지다. 침묵이 곧 발화 종료는 아니라는 것, 그리고 "기다렸더니 실제로 말이 이어졌는가"를 측정 지표로 쓸 수 있다는 것이다. 아래 구성은 이 두 가지를 직접 구현하는 방법이다.
침묵 길이만으로 발화 종료를 판단할 때의 실패
| 방식 | 판단 근거 | 대표적인 실패 |
|---|---|---|
| 짧은 침묵 기준(예: 0.5초) | VAD 침묵 길이 | "그러니까… 지난주에 주문한 게요" 사이의 쉼에서 AI가 끼어듦 |
| 긴 침묵 기준(예: 3초) | VAD 침묵 길이 | 말이 끝났는데 응답이 늦어 통화가 끊긴 것처럼 느껴짐 |
| VAD + 턴 감지 모델 | 침묵 + 억양·리듬·단어 | 모델 추론 지연이 추가되고, 언어별 정확도 차이가 생김 |
예시의 0.5초와 3초는 LiveKit 턴 감지기 문서가 밝힌 엔드포인팅 기본값(min_delay 0.5초, max_delay 3.0초)이다. 같은 문서는 오디오 턴 감지기를 붙이면 기본값이 각각 0.3초와 2.5초로 줄어든다고 설명한다. 모델이 "말이 끝났다"고 확신할 때는 더 빨리 답하고, 확신이 없을 때만 길게 기다릴 수 있기 때문이다.
STT→LLM→TTS 파이프라인에서 턴 감지가 들어가는 위치
- 오디오 프레임 수신 — WebRTC나 전화망에서 들어온 프레임마다 VAD가 음성인지 침묵인지 판정한다.
- 침묵 시작 시 턴 판정 — 침묵이 시작되면 지금까지 쌓인 사용자 발화 오디오 전체를 턴 감지 모델에 넣어 "턴이 끝났는가"를 묻는다.
- 대기 시간 결정 — 끝났다고 보면 최소 대기 후 STT 최종 결과를 LLM에 넘기고, 아니라고 보면 최대 대기 시간까지 기다린다.
- 말이 이어지면 다시 판정 — 대기 중 사용자가 다시 말하면 오디오를 이어 붙이고, 다음 침묵에서 턴 전체로 2단계를 반복한다.
- 끼어들기 처리 — TTS 재생 중 사용자가 말하면 재생을 멈추고 새 턴으로 받는다.
LLM 응답을 텍스트로 스트리밍하는 채팅 UI와 비교하면, 음성은 "언제 질문이 끝났는가"를 시스템이 정해야 한다는 점이 다르다. 텍스트 스트리밍의 상태·중단 처리는 Vercel AI SDK 스트리밍 채팅 UI 글에서 다뤘는데, 음성에서는 그 앞단에 이 턴 판정 단계가 하나 더 붙는다고 보면 된다.
구성 1: LiveKit Agents에 오디오 턴 감지기 붙이기
LiveKit Agents의 오디오 TurnDetector는 전사문 없이 사용자 오디오를 직접 인코딩해 단어 내용과 억양·리듬 같은 음향 단서를 함께 본다. Python은 livekit-agents 1.6.1 이상, Node.js는 @livekit/agents 1.4.7 이상이면 별도 패키지 없이 쓸 수 있다.
from livekit.agents import AgentSession, TurnHandlingOptions, inference
session = AgentSession(
turn_handling=TurnHandlingOptions(
turn_detection=inference.TurnDetector(),
),
# stt, llm, tts 설정은 기존 그대로
)
문서에 적힌 조건 중 한국어 상담에서 놓치기 쉬운 것들을 표로 모았다.
| 항목 | 문서 내용 | 적용할 때 |
|---|---|---|
| 지원 언어 | 오디오 감지기는 한국어 포함 14개 언어 | STT를 설정하지 않으면 영어 임계값을 기본으로 쓰므로 STT 언어를 지정 |
| 모델 버전 | v1(전체), v1-mini(로컬 CPU) | v1-mini는 CPU에서 돌아 타임아웃이 나지 않게 연산 최적화 인스턴스 사용 |
| 폴백 | 사용량 한도에서 v1-mini로 넘어가면 세션 끝까지 유지 | 품질 차이를 피하려면 Python에서 local_fallback=False |
| VAD 조건 | min_silence_duration 0.25초 미만이면 ValueError | Silero 기본값 0.55초는 통과 |
| 실시간 모델 | 실시간 모델과 함께 쓰면 모델 내장 턴 감지를 꺼야 함 | 실시간 모델 쪽 turn_detection=None |
| 임계값 | unlikely_threshold는 값 하나 또는 언어별 딕셔너리 | 한국어만 따로 조정 가능 |
문서에 따르면 v1은 LiveKit Cloud에서는 무료이고, 자체 호스팅에서는 월 개발 허용량을 넘으면 Ship·Scale 요금제에서 요청당 과금되며 Build 요금제는 v1-mini로 넘어간다. 요금 구조는 바뀔 수 있으므로 도입 전에 문서를 다시 확인한다. STT 전사문을 쓰는 예전 텍스트 턴 감지기(MultilingualModel)는 deprecated 상태이며 SDK 2.0에서 제거될 예정이다.
구성 2: Silero VAD와 Smart Turn으로 직접 만들기
프레임워크 없이 파이프라인을 직접 짠다면 VAD로 Silero VAD(MIT 라이선스, 8kHz·16kHz 지원)를, 턴 판정으로 Smart Turn(BSD 2-clause)을 조합할 수 있다. Smart Turn v3.2는 Whisper Tiny 백본에 선형 분류기를 붙인 약 8M 파라미터 모델로, 한국어를 포함한 23개 언어를 지원하고 일부 CPU에서 10ms, 대부분의 클라우드 인스턴스에서 100ms 이내로 추론한다고 README에 적혀 있다.
README가 정한 사용 규칙은 세 가지다. 입력은 16kHz 모노 PCM이며 최대 8초까지 받는다. 8초보다 짧으면 앞쪽을 0으로 채워 실제 음성이 입력 끝에 오게 한다. 그리고 VAD가 침묵을 감지한 뒤 턴 전체에 대해 실행하고, 판정 중 사용자가 다시 말하면 새 구간만이 아니라 턴 전체로 다시 판정한다. 이 규칙을 그대로 코드로 옮기면 다음과 같다.
pip install silero-vad numpy
import numpy as np
SAMPLE_RATE = 16000
MAX_TURN_SEC = 8
def prepare_turn_audio(turn_audio):
"""Smart Turn 입력 규칙: 16kHz 모노, 최대 8초, 짧으면 앞쪽을 0으로 채운다."""
max_len = SAMPLE_RATE * MAX_TURN_SEC
audio = turn_audio[-max_len:]
if len(audio) < max_len:
pad = np.zeros(max_len - len(audio), dtype=audio.dtype)
audio = np.concatenate([pad, audio])
return audio
class TurnTracker:
def __init__(self, is_complete, min_wait=0.3, max_wait=2.5):
self.is_complete = is_complete # Smart Turn의 predict_endpoint()를 감싼 함수
self.min_wait, self.max_wait = min_wait, max_wait
self.buffer = []
self.silence_started = None
self.verdict = None
def on_frame(self, frame, is_speech, now):
"""is_speech: Silero VAD 확률이 임계값 이상인지. 확정된 턴이면 오디오를 반환."""
if is_speech:
self.buffer.append(frame)
self.silence_started = None # 말이 이어지면 이전 판정을 버린다
self.verdict = None
return None
if not self.buffer:
return None
if self.silence_started is None:
self.silence_started = now
turn = prepare_turn_audio(np.concatenate(self.buffer))
self.verdict = self.is_complete(turn) # 항상 턴 전체로 판정
waited = now - self.silence_started
if (self.verdict and waited >= self.min_wait) or waited >= self.max_wait:
turn = np.concatenate(self.buffer)
self.buffer, self.silence_started, self.verdict = [], None, None
return turn # STT 최종 결과와 함께 LLM 단계로 넘긴다
return None
min_wait·max_wait의 출발값은 LiveKit 문서가 턴 감지기 사용 시 기본으로 둔 0.3초·2.5초를 빌렸다. is_complete에는 Smart Turn 저장소의 inference.py에 있는 predict_endpoint() 호출을 연결하고, 반환값에서 완료 여부를 꺼내는 부분은 저장소의 predict.py 예제를 따른다. VAD 프레임 크기와 확률 임계값도 Silero README의 권장값에 맞춘다. Smart Turn 추론이 실패하거나 시간이 오래 걸릴 때는 verdict를 False로 두어 max_wait 침묵 기준으로 물러나게 하면 서비스가 멈추지 않는다. 외부 의존성이 실패할 때 기능을 낮춰 계속 동작시키는 패턴은 AI 에이전트 서킷 브레이커와 재시도 전략과 같은 원리다.
통화 로그로 대기 시간과 임계값을 조정하기
대기 시간을 감으로 정하면 고객층과 업무에 따라 결과가 크게 달라진다. 보이스 알프 발표처럼 "기다린 판단이 맞았는지"를 로그로 세면 근거를 갖고 조정할 수 있다. 턴마다 아래 형태의 이벤트를 JSON Lines로 남긴다고 가정한다.
{"type": "hold", "resumed_after": 0.9}
{"type": "hold", "resumed_after": null}
{"type": "reply", "user_end_at": 12.40, "first_audio_at": 13.15, "user_spoke_within_sec": null}
{"type": "reply", "user_end_at": 30.02, "first_audio_at": 30.61, "user_spoke_within_sec": 0.4}
import json
def turn_metrics(path, resume_window=1.5, barge_window=1.0):
holds = resumed = replies = barge_ins = 0
latencies = []
with open(path, encoding="utf-8") as f:
for line in f:
e = json.loads(line)
if e["type"] == "hold": # 모델이 '미완료'로 보고 기다린 경우
holds += 1
r = e.get("resumed_after")
if r is not None and r <= resume_window:
resumed += 1
elif e["type"] == "reply": # AI가 응답을 시작한 경우
replies += 1
latencies.append(e["first_audio_at"] - e["user_end_at"])
b = e.get("user_spoke_within_sec")
if b is not None and b <= barge_window:
barge_ins += 1 # 응답 직후 고객이 다시 말함 = 너무 빨랐을 가능성
latencies.sort()
return {
"hold_hit_rate": resumed / holds if holds else None,
"early_reply_rate": barge_ins / replies if replies else None,
"latency_p50": latencies[len(latencies) // 2] if latencies else None,
}
| 지표 상태 | 뜻 | 조정 방향 |
|---|---|---|
early_reply_rate가 높음 | 고객이 말을 덜 끝냈는데 AI가 답함 | min_wait를 늘리거나 완료 판정 임계값을 높임 |
hold_hit_rate가 낮음 | 기다렸는데 고객이 말을 잇지 않음 | max_wait를 줄이거나 미완료 판정 임계값을 낮춤 |
latency_p50가 큼 | 발화 종료부터 첫 음성까지 오래 걸림 | 턴 모델 추론 시간, STT 최종 결과 지연, LLM 첫 토큰 시간을 나눠 측정 |
세 지표는 한쪽을 개선하면 다른 쪽이 나빠지는 관계라서 하나만 보고 바꾸지 않는다. 변경 전후를 같은 기간 길이로 비교하고, 지표는 업무 유형(주문, 예약 변경, 불만 접수)별로 나눠 보는 것이 좋다. 호출 단위 로그를 어떤 필드로 남길지는 LLMOps 모니터링 항목과 구현의 스키마를 확장해 쓸 수 있다.
한국어 전화 상담에서 따로 챙길 처리
- 전화망 샘플레이트 — 일반 전화망 오디오는 8kHz인 경우가 많다. Silero VAD는 8kHz를 바로 받지만 Smart Turn은 16kHz 입력을 요구하므로 턴 판정 직전에
torchaudio.functional.resample(wav, orig_freq=8000, new_freq=16000)같은 리샘플링을 넣는다. - STT 언어 지정 — LiveKit 감지기는 STT가 없으면 영어 임계값을 쓰므로, 한국어 상담이라면 STT 언어를 명시해 한국어 임계값이 적용되게 한다.
- TTS 전 텍스트 정규화 — 날짜, 시각, URL은 TTS 엔진마다 읽는 방식이 다르다. 엔진이 이미 처리하는지 먼저 들어 보고, 부족하면 LLM 응답을 TTS에 넘기기 전에 아래처럼 바꾼다.
- 통화 기록의 개인정보 — 지표 계산용 로그에 전사문을 남긴다면 이름·연락처를 가린 뒤 저장한다. 마스킹 흐름은 LLM 호출 전 개인정보 마스킹 설계를 참고한다.
import re
URL = re.compile(r"https?://(\S+)")
def normalize_for_tts(text):
text = re.sub(r"(\d{4})-(\d{2})-(\d{2})",
lambda m: f"{int(m[1])}년 {int(m[2])}월 {int(m[3])}일", text)
text = re.sub(r"(\d{1,2}):(\d{2})",
lambda m: f"{int(m[1])}시" + (f" {int(m[2])}분" if int(m[2]) else ""), text)
text = URL.sub(lambda m: m[1].replace(".", " 점 ").replace("/", " 슬래시 "), text)
return text
print(normalize_for_tts("2026-10-21 14:30에 https://example.com/help 에서 확인하세요"))
# 2026년 10월 21일 14시 30분에 example 점 com 슬래시 help 에서 확인하세요
# (https:// 없이 적힌 도메인은 바꾸지 않으므로, 서비스 도메인 목록은 따로 처리한다)
운영에 올리기 전에 정해 둘 기준
- 목표 지표 —
early_reply_rate상한과latency_p50상한을 숫자로 정한다. 파일럿 기간의 측정값을 기준선으로 삼는다. - 폴백 동작 — 턴 감지 모델이 실패하거나 느릴 때 침묵 기준(
max_wait)만으로 동작하도록 해 두고, 폴백 발생 횟수도 로그에 남긴다. - 끼어들기 정책 — TTS 재생 중 고객 음성이 들어오면 즉시 멈출지, 짧은 맞장구("네", "음")는 무시할지 업무별로 정한다.
- 녹음 고지와 보관 — 통화 오디오를 지표 분석이나 모델 개선에 쓸 계획이라면 고지·동의 절차와 보관 기간을 법무 담당과 먼저 확인한다.
초보자가 자주 실수하는 포인트
- 턴 감지 모델에 침묵 이후 새로 들어온 구간만 넣어 문맥 없이 판정하는 경우
- LiveKit 턴 감지기를 쓰면서 STT 언어를 지정하지 않아 한국어 통화에 영어 임계값이 적용되는 경우
- VAD의 min_silence_duration을 0.25초보다 짧게 잡아 LiveKit 오디오 감지기 초기화가 ValueError로 실패하는 경우
- 8kHz 전화망 오디오를 리샘플링하지 않고 16kHz를 요구하는 Smart Turn에 그대로 넣는 경우
체크리스트
- VAD와 턴 감지 모델의 역할을 나눴고, 턴 판정은 항상 턴 전체 오디오로 한다.
- min_wait·max_wait 출발값과 조정 기준 지표를 정했다.
- STT 언어를 한국어로 지정했고 필요하면 언어별 임계값을 따로 둔다.
- 턴 감지 모델 실패 시 침묵 기준으로 물러나는 폴백이 있다.
- hold·reply 이벤트를 남겨 hold_hit_rate, early_reply_rate, latency_p50을 계산할 수 있다.
- TTS 전에 날짜·시각·URL 정규화가 필요한지 실제 엔진 출력으로 확인했다.
자주 묻는 질문
OpenAI Realtime API 같은 실시간 음성 모델에도 별도 턴 감지기가 필요한가요?
실시간 모델은 자체 턴 감지를 갖고 있는 경우가 많습니다. LiveKit 문서는 실시간 모델과 LiveKit 턴 감지기를 함께 쓸 때 모델 내장 턴 감지를 꺼야 한다고 안내하므로, 둘 중 어느 쪽으로 판정할지 하나를 정해야 합니다.
맞장구("네", "아")도 끼어들기로 처리해야 하나요?
업무에 따라 다릅니다. 안내문을 길게 읽는 업무라면 짧은 맞장구로 TTS를 멈추면 같은 안내를 반복하게 될 수 있습니다. 발화 길이나 STT 결과로 짧은 맞장구를 걸러내는 규칙을 두고, 끼어들기 로그로 효과를 확인합니다.
참고 자료 · 검증 기준
- ZDNet Korea — 채널톡, AI 전화 보이스 알프 출시 (2026-10-21)
- 전자신문 — 채널코퍼레이션, 전화 상담 AI 보이스 알프 출시 (2026-10-21)
- LiveKit Docs — Turn detector
- Smart Turn GitHub (pipecat-ai/smart-turn)
- Silero VAD GitHub (snakers4/silero-vad)
위 자료와 내용을 대조한 날짜: . 도구·서비스 정책은 이후 바뀔 수 있으므로 적용 전 공식 문서를 다시 확인하세요.
이 글은 실제 실무 환경 테스트 및 검증된 기술 자료를 바탕으로 작성되었으며, 환경 변화에 따라 지속적으로 갱신됩니다.