Voice AI Agent (2) Turn-taking과 한국어 Endpointing 디버깅
들어가며
1편에서는 Voice AI Agent의 파이프라인 전체 구조를 큰 그림으로 봤습니다. 사용자의 음성이 STT와 VAD로 갈라지고, 발화 종료를 판정한 뒤, LLM이 답을 만들고 TTS가 음성으로 돌려준다는 흐름이었습니다.
그런데 막상 한국어로 에이전트를 돌려보니, 가장 먼저 부딪힌 벽이 바로 이 ‘발화 종료 판정’ 부분이었습니다. 분명히 말을 다 끝냈는데도 에이전트가 3초 가까이 멍하니 침묵하다가 뒤늦게 답을 시작하는 일이 반복됐습니다. 더 크게 말하거나 같은 말을 한 번 더 해야 그제야 반응하는 식이었습니다.
이번 글에서는 이 Turn-taking(대화 턴 전환) 이 실제로 어떻게 동작하는지를 따라가면서, 한국어에서 마주친 이 문제의 원인을 로그로 추적하고 어떻게 튜닝했는지를 정리해 보겠습니다. 그리고 에이전트가 말하는 도중에 사용자가 끼어드는 Barge-in(끼어들기) 처리를 살펴본 뒤, 마지막으로 이 끼어들기를 단순히 처리하는 데 그치지 않고 하나의 품질 지표로 기록해 본 실험까지 함께 나눠보겠습니다.
VAD가 여는 턴 전환
Turn-taking에서 가장 먼저 움직이는 컴포넌트는 VAD입니다. VAD는 파이프라인에서 대화 턴 전환을 담당하는 1차 관문이자 필터 역할을 합니다. 1편에서 봤듯이 SileroVAD(CNN+LSTM 계열)는 오디오 스트림을 짧은 프레임 단위로 모델에 통과시켜 음성 존재 확률을 실시간으로 산출하고, 이를 바탕으로 START_OF_SPEECH(음성 감지)와 END_OF_SPEECH(침묵 감지) 이벤트를 발생시킵니다.
Turn-taking 관점에서 VAD의 출력은 두 가지 핵심 역할을 합니다.
1) EOU Turn Detector를 트리거합니다. VAD가 END_OF_SPEECH를 발생시키면, EOU Turn Detector(Qwen 0.5B) 가 STT 텍스트(대화 맥락)와 결합해 “발화가 정말 끝났는가”를 판정합니다. 이 판정 결과(EOU 확률)에 따라 다음 단계인 Endpointing Delay 로 넘어가고, 거기서 EOT(End of Turn, 턴 종료) 확정 여부가 결정됩니다.
즉 Turn-taking에서 VAD는 “침묵 감지”라는 1차 필터를, EOU Turn Detector는 “대화 맥락 기반의 의미적 판단”이라는 2차 필터를 담당합니다. 이 2단계 구조 덕분에 단순 침묵만 보는 방식(VAD only)보다 자연스러운 턴 전환이 가능합니다.
2) Barge-in 감지의 시작점이 됩니다. 에이전트가 응답하는 도중에도 VAD는 상시 동작합니다. 이때 START_OF_SPEECH가 감지되면 사용자 끼어들기(Barge-in) 처리 파이프라인이 작동합니다. 이 부분은 글 뒤에서 따로 다루겠습니다.
Endpointing Delay, 얼마나 더 기다릴까
Endpointing Delay는 VAD가 침묵(END_OF_SPEECH)을 감지한 뒤, 사용자의 발화가 정말 끝났는지를 최종 확정하기 위해 추가로 더 기다리는 시간입니다. 이 대기 시간이 지나도록 사용자가 다시 말하지 않으면 EOT가 확정되고 LLM 응답 생성이 시작됩니다.
LiveKit에서는 EOU Turn Detector의 예측 결과에 따라 이 대기 시간이 둘로 갈립니다.
| 조건 | 적용되는 delay | 기본값 | 의미 |
|---|---|---|---|
| EOU prob >= threshold | min_endpointing_delay | 0.5초 | 발화 종료 가능성 높음. 짧게 대기 |
| EOU prob < threshold | max_endpointing_delay | 3.0초 | 아직 말하는 중일 가능성 높음. 길게 대기 |
이 대기 시간을 어떻게 잡느냐에 따라 정반대의 문제가 생깁니다.
너무 짧으면(False Endpoint) 사용자가 문장 중간에 잠시 멈춘 것(생각을 정리하거나 숨을 고르는 순간)을 발화 종료로 오인합니다. 에이전트가 사용자의 말을 끊고 응답을 시작해 버려서 대화 흐름이 끊기고, 사용자는 “아직 말 안 끝났는데”라는 부정적 경험을 하게 됩니다.
너무 길면 사용자가 말을 끝낸 뒤에도 에이전트가 한참 침묵해서 “답답한” 대화가 됩니다. 실시간 음성 대화에서 응답 지연은 곧바로 체감 품질로 이어집니다.
한국어에서 마주친 문제
문제는 기본 설정 그대로 한국어 대화를 테스트할 때 드러났습니다. min_endpointing_delay=0.5초, max_endpointing_delay=3.0초 상태였는데, 사용자가 발화를 끝내도 에이전트가 3초 가까이 침묵하다가 뒤늦게 응답을 시작하는 현상이 관찰됐습니다. 반복해서 말하거나 더 크게 말해야 응답이 시작되곤 했습니다.
원인을 찾으려고 로그를 확인해 보니, MultilingualModel(Qwen 0.5B) 기반 EOU Turn Detector가 한국어 짧은 문장의 EOU 확률을 매우 낮게 예측하고 있었습니다.
| 발화 | EOU 확률 | 해석 |
|---|---|---|
| ”안녕” | 0.0006 | 거의 0. “말 안 끝남”으로 판단 |
| ”너 왜 이름이 뭐야” | 0.366 | 중간. 애매하게 판단 |
| ”라이브에 대해 알려줘” | 0.191 | 낮음. “아직 더 말할 것”으로 판단 |
| ”공연에 대해 알려줘” | 0.062 | 거의 0. “말 안 끝남”으로 판단 |
대부분의 한국어 발화가 threshold를 넘지 못했습니다. 그래서 매번 max_endpointing_delay(3.0초) 경로로 빠지고, 최대 대기시간을 다 소진한 뒤에야 EOT가 확정되는 구조적인 문제였습니다. MultilingualModel이 영어 중심으로 학습되어, 한국어 짧은 문장(특히 1~2어절)을 “아직 말이 안 끝남”으로 판단하는 경향이 원인으로 추정됐습니다.
1차 조치로 다음 세 가지 방향을 검토하고 조합해서 적용했습니다.
max_endpointing_delay축소(3.0초 → 1.0초). EOU 확률이 낮아도 최대 대기시간 자체를 줄여서 응답 지연을 완화합니다. 가장 직관적이지만, 사용자가 실제로 말을 이어갈 때 끊길 가능성이 있습니다.unlikely_threshold하향(기본값 → 0.3). Turn Detector가 “말 끝남”으로 인정하는 EOU 확률 기준을 낮춰서,min_endpointing_delay경로로 진입하는 비율을 높입니다.min_endpointing_delay미세 축소(0.5초 → 0.3초). 발화 종료 판정 후 최소 대기시간도 줄여 체감 응답 속도를 개선합니다.
실제로 적용한 설정은 다음과 같습니다.
# 실제 적용한 설정
session = AgentSession(
turn_detection=MultilingualModel(),
min_endpointing_delay=0.3, # 기본 0.5초 -> 0.3초
max_endpointing_delay=1.0, # 기본 3.0초 -> 1.0초
)이 조합을 적용한 뒤 한국어 짧은 문장에서도 체감 응답 지연이 크게 줄었습니다. 다만 분명히 해둘 점은, 이것이 근본적인 해결(모델 자체의 한국어 성능 개선)은 아니라는 것입니다. 어디까지나 파라미터 튜닝을 통한 우회적 개선이었습니다.
아래 VAD-EOU Flow 그래프를 보면, END_OF_SPEECH에서 출발해 EOU Turn Detector, THRESHOLD 분기, min/max_endpointing_delay 대기를 거쳐 EOT 확정 또는 취소로 이어지는 흐름이 보입니다. 이 경로가 바로 Endpointing Delay의 핵심 메커니즘입니다. 대기 중에 사용자가 다시 발화하면(START_OF_SPEECH) EOT 태스크가 취소되어 대화가 자연스럽게 이어집니다.
출처: 이번 실험 VAD-EOU 흐름 정리
결국 한국어 음성 에이전트에서는 모델을 기본값 그대로 쓰는 것만으로 자연스러운 턴 전환이 되지 않았습니다. 영어 중심으로 학습된 Turn Detector가 한국어 짧은 문장을 늘 “아직 안 끝남”으로 분류해 매번 최대 대기를 소진했고, 이를 파라미터 튜닝 으로 우회해야 했습니다. 다만 이건 어디까지나 우회였고, 근본 해결은 아니었습니다.
Barge-in, 끼어들기 처리
Barge-in은 에이전트가 응답(TTS 출력) 중일 때 사용자가 끼어들어 말하는 상황입니다. 자연스러운 음성 대화라면 사용자가 언제든 끼어들 수 있어야 하고, 이를 적절히 처리하는 것이 대화 품질에 직결됩니다.
앞서 말했듯 VAD와 STT는 에이전트가 응답하는 도중에도 상시 동작합니다. 그래서 사용자가 말을 시작하면 즉시 감지할 수 있습니다. LiveKit Agents 기준으로 대응 과정을 따라가 보겠습니다.
- VAD가
START_OF_SPEECH를 감지합니다. - interruption 조건을 확인합니다. 순간적인 소음이나 기침을 실제 끼어들기와 구분하기 위한 안전장치입니다.
min_interruption_duration(기본 0.5초). 감지된 음성이 이 시간 이상 지속되어야 합니다.min_interruption_words(기본 0). STT가 인식한 단어 수가 이 값 이상이어야 합니다.
- 거짓 인터럽션(False Interruption)을 처리합니다. 예를 들어
false_interruption_timeout=2.0초,resume_false_interruption=True 라면 이렇게 동작합니다.- 조건이 충족되면 TTS 출력을 일시정지(pause) 합니다.
- timeout(2.0초) 안에 STT가 인식한 텍스트가 없으면 거짓 인터럽션 으로 판정하고, 중단됐던 지점부터 에이전트 발화를 재개(
resume)합니다. - 인식된 텍스트가 있으면 실제 인터럽션 으로 처리하고, LLM과 TTS를 즉시 중단(
interrupt)합니다. - 만약
false_interruption_timeout=None 이면 거짓 인터럽션 처리가 비활성화되어 모든 끼어들기를 실제로 간주합니다.
- 중단 후 처리를 합니다. 사용자 발화를 새로 처리해서 EOU/EOT 판정을 거쳐 새 LLM 응답을 생성합니다. 이때 대화 히스토리는 사용자가 실제로 들은 부분까지만 유지(truncate)해서 문맥 일관성을 지킵니다.
참고로
allow_interruptions=False를say()또는generate_reply()에 설정하면 특정 발화에 한해 끼어들기를 막을 수 있습니다. 다만session.interrupt()를 명시적으로 호출하면 이 설정과 무관하게 중단할 수 있습니다(LiveKit 공식 문서).
아래 Barge-in Flow 그래프가 이 전체 처리 흐름을 보여줍니다. VAD START_OF_SPEECH 감지에서 출발해, min_interruption_duration/min_interruption_words 조건 확인, false_interruption_timeout 분기(일시정지 또는 즉시 중단), 그리고 에이전트 발화 재개 또는 사용자 발화 처리로의 전환까지가 한 흐름으로 표현되어 있습니다.
출처: 이번 실험 Barge-in 처리 흐름 정리
Barge-in을 품질 지표로 기록해 보기
1편에서 Barge-in 처리 흐름을 보면서, 끼어들기가 단순한 예외 상황이 아니라 대화 품질을 가늠하는 지표가 될 수 있겠다는 생각이 들었습니다. 그래서 끼어들기를 그냥 처리하고 흘려보내는 대신, 발생 횟수와 시점을 직접 기록해 보기로 했습니다.
구현 자체는 단순합니다. LiveKit은 에이전트와 사용자의 상태 변화를 agent_state_changed, user_state_changed 이벤트로 알려줍니다. 에이전트가 말하는 중(agent_state가 "speaking")에 사용자 발화(user_state가 "speaking")가 감지되면 그 순간을 Barge-in으로 판정하고, 에이전트가 발화를 시작한 지 몇 초 만에 끊겼는지(agent_spoke_for)를 함께 기록했습니다.
@session.on("user_state_changed")
def on_user_state_changed(ev):
# 에이전트가 말하는 중 + 사용자가 말하기 시작 -> Barge-in
if ev.new_state == "speaking" and barge_in_state["agent_speaking"]:
duration = time.time() - barge_in_state["agent_speak_start"]
barge_in_state["count"] += 1
barge_in_state["logs"].append({
"count": barge_in_state["count"],
"time": time.strftime("%H:%M:%S"),
"agent_spoke_for": round(duration, 2),
})연속으로 네 번 끼어들어 보며 기록한 결과는 이랬습니다. 끼어든 시점이 각각 에이전트 발화 1.55초, 2.3초, 10.98초, 5.81초였고 평균은 5.16초였습니다. 흥미로웠던 건 이 ‘끊긴 시점’이 그냥 통계가 아니라 진단 단서가 된다는 점이었습니다.
끼어든 시점이 응답 초반(1~2초)이면 false endpoint를 의심하고, 한참 뒤(5초 이후)면 응답이 너무 길거나 핵심을 벗어났을 가능성을 의심해 볼 수 있습니다.
조금 더 풀어보면 이렇습니다. 응답 초반에 끼어들기가 잦다면, 사실은 끼어들기가 아니라 발화 보충일 수 있습니다. 사용자가 잠깐 멈췄을 뿐인데 에이전트가 EOT를 확정하고 말을 시작해 버린 경우입니다. 이때는 min_endpointing_delay와 max_endpointing_delay를 조금 높이면, 대기 중에 사용자가 다시 말할 때 EOT가 취소되어 발화가 이어지므로 false endpoint 빈도를 줄일 수 있습니다.
그런데 여기서 짚어둘 게 있습니다. 이건 앞에서 한국어 응답이 너무 느리다고 delay를 낮췄던 것과 정확히 반대 방향 의 조정입니다. 둘은 같은 endpointing 파라미터를 두고 서로 반대쪽을 당기고 있습니다. 한쪽 끝에는 ‘응답이 너무 느려짐’이, 다른 쪽 끝에는 ‘사용자 말을 끊어버림(false endpoint)‘이 있습니다. delay를 낮추면 빨라지는 대신 사용자를 끊을 위험이 커지고, 높이면 끊지 않는 대신 응답이 느려집니다. 어느 한쪽을 공짜로 얻을 수는 없습니다.
결국 endpointing은 한 번 맞춰두고 끝나는 값이 아니라, 응답 속도와 발화 보존 사이의 트레이드오프를 어디에 둘지 정하는 파라미터입니다. 정해진 정답이 있다기보다, 과제의 성격과 실제 사용자의 발화 패턴에 맞춰 그때그때 조정해야 하는 값에 가깝습니다.
반대로 응답 중반 이후에 끼어들기가 잦다면, 에이전트가 충분히 말하는 도중에 사용자가 끊은 것이므로 응답이 너무 길거나 핵심을 벗어났을 가능성이 있습니다. 이 경우는 시스템 프롬프트를 조정해 응답을 짧게 하거나 핵심 정보를 먼저 전달하도록 유도하는 편이 낫습니다. 이렇게 보면 끼어들기 로그 하나가 endpointing 튜닝과 프롬프트 설계 양쪽의 정량적 근거가 됩니다.
다만 이 방식에는 분명한 한계도 있었습니다. 지금 구현은 “에이전트가 말하는 중 + 사용자가 말하기 시작”이라는 조건만 보기 때문에, 앞서 본 false_interruption_timeout에 의해 거짓 인터럽션으로 판정된 경우와 실제 끼어들기를 구분하지 못합니다. 기침이나 짧은 소음으로 잠깐 멈췄다가 발화를 재개한 경우에도 이벤트 레벨에서는 똑같이 Barge-in으로 기록됩니다. 또한 통계가 메모리 안에만 있어서 세션이 끝나면 사라집니다. 여러 세션에 걸친 끼어들기 패턴을 보려면 외부 저장소에 쌓는 구조가 필요합니다.
마무리
이번 글에서는 Voice AI Agent에서 가장 손이 많이 갔던 Turn-taking을 따라가 봤습니다. VAD가 침묵을 감지하는 1차 필터, EOU Turn Detector가 맥락을 읽는 2차 필터로 동작하고, 그 사이의 Endpointing Delay가 “얼마나 더 기다릴지”를 결정한다는 구조였습니다. 그리고 영어 중심으로 학습된 Turn Detector 때문에 한국어 짧은 문장이 자꾸 “안 끝남”으로 분류되던 문제를, 로그로 원인을 좁히고 파라미터를 조합해 우회했던 과정을 정리했습니다. 마지막으로 끼어들기를 품질 지표로 기록해 보니, 끊긴 시점만으로도 false endpoint 때문인지 응답이 길어서인지를 가늠할 수 있다는 점이 흥미로웠습니다.
돌이켜보면 이번 작업에서 가장 크게 남은 교훈은, 잘 만들어진 파이프라인이라도 언어와 환경에 따라 다시 튜닝해야 비로소 자연스러워진다 는 점이었습니다. 특히 영어 중심으로 학습된 모델을 한국어에 그대로 쓰는 상황에서는 기본값을 그대로 믿기보다, 로그를 열어 실제 확률값을 들여다보는 게 결국 가장 빠른 길이었습니다. 다음에 기회가 된다면 파라미터 튜닝을 넘어, Turn Detector 자체를 한국어에 맞게 개선하는 방향도 추적해 보고 싶습니다.