에이전트가 말을 하기 전, 발신자는 2초간의 침묵을 경험했습니다
요약
음성 에이전트의 응답 지연 시간(latency) 문제를 분석하고, 파이프라인의 각 단계별 타임스탬프 측정을 통해 2초의 정적이 발생하는 원인을 규명합니다. LLM 스트리밍과 TTS 연결 유지 등을 통해 지연 시간을 단축하는 최적화 방안을 다룹니다.
핵심 포인트
- 음성 에이전트의 2초 지연은 사용자 경험을 심각하게 저해함
- ASR, LLM, TTS, 네트워크 각 단계의 타임스탬프 측정이 필수적임
- LLM의 전체 응답을 기다리지 않고 토큰 스트리밍을 활용해 TTS를 조기 실행해야 함
- TTS 연결을 매번 새로 생성하지 않고 유지하여 콜드 스타트 지연을 방지해야 함
매번 첫 번째 턴마다 고장 난 것처럼 느껴졌던 음성 에이전트, 그리고 그 2초의 정적을 찾아내기 위해 단계별로 해체해야 했던 지연 시간(latency) 예산에 대한 이야기입니다.
버그 리포트는 단 한 문장이었습니다: "발신자들이 계속 인사말 도중에 말을 끊고 들어옵니다." 저는 녹음본을 들었고 매번 똑같은 패턴을 확인했습니다. 전화가 연결됩니다. 발신자가 기다립니다. 1초의 정적. 2초의 정적. 그러다 발신자가 포기하고 "여보세요? 거기 계세요?"라고 말하는 바로 그 순간, 에이전트가 인사를 시작합니다. 이제 두 사람 모두 말을 하고 있고, 통화 전체가 충돌(collision) 상태로 시작됩니다.
아무도 무례하게 굴려고 에이전트의 말을 끊는 것이 아니었습니다. 그들은 전화가 끊겼다고 생각했기 때문에 말을 끊은 것입니다. 전화 통화에서 2초의 정적은 영겁과도 같습니다. 사람들은 그 공백을 채우려 합니다.
그 수치가 가진 가치를 알기 전, 저를 괴롭히던 그 숫자에 이름을 붙였습니다: 첫 오디오 바이트까지의 시간(time to first audio byte). 발신자가 자신의 차례를 마친 시점과 에이전트의 목소리 첫 샘플이 실제로 발신자의 귀에 도달하는 시점 사이의 간격입니다. 이 시스템에서 첫 번째 턴의 지연 시간은 약 2초였으며, 그 시간은 체감상 정말 길게 느껴졌습니다.
추측 대신 파이프라인의 타이밍 측정하기
제가 가장 먼저 한 일은 이론적인 추측을 멈추고 모든 단계의 경계에 타임스탬프(timestamp)를 찍는 것이었습니다. 우리의 파이프라인은 직선 구조였습니다: 음성-텍스트 변환(speech-to-text)이 최종 전사(transcript)를 생성하면, 전사 데이터가 LLM으로 전달되고, LLM이 전체 완성본(completion)을 생성하면, 완성본이 텍스트-음성 변환(text-to-speech)으로 전달되어 TTS가 오디오를 생성하고, 그 오디오가 발신자에게 전달됩니다. 저는 각 인계 시점마다 단조 증가하는 타임스탬프(monotonic timestamp)를 기록하며 열 번의 테스트 전화를 걸었습니다.
평균값, 첫 번째 턴, 콜드 스타트(cold start) 기준:
- ASR 최종(발신자 음성 종료부터 확정된 전사까지): 약 300 ms
- LLM(요청 전송부터 전체 완성본 반환까지): 약 1100 ms
- TTS(텍스트 전송부터 첫 오디오 청크 수신까지): 약 480 ms
- 발신자가 듣기 전 네트워크 및 재생 큐잉(playout queueing): 약 120 ms
그것들은 대략 2초에 달했고, 그 형태가 모든 이야기를 말해주었습니다. 저는 LLM이 완전한 문장 반 개짜리 인사말을 끝낼 때까지 기다린 후에야 TTS로 단 하나의 문자도 보내는 비용을 지불하고 있었고, 심지어 TTS가 첫 청크를 생성하기 전에 새로운 연결을 설정하는 데 드는 비용까지 지불하고 있었습니다. 모든 단계가 이전 단계가 완전히 완료되기를 공손하게 기다렸습니다. 마치 각 주자가 바통을 넘기기 전에 결승선을 통과해야 한다고 고집하는 계주 경주 같았습니다.
제가 필요하지 않았던, 제가 돈을 내고 지불했던 대기 시간들
이러한 대기 시간 중 세 가지는 피할 수 있었으며, 그 어떤 것도 더 빠른 모델이나 더 빠른 목소리를 요구하지 않았습니다.
첫 번째 대기: 전체 LLM 완성 버퍼링. 저는 LLM을 호출하고 전체 문자열을 기다린 후에야 TTS에 전달했습니다. 하지만 인사말의 첫 문장은 마지막 문장보다 훨씬 일찍 존재합니다. 만약 제가 LLM에서 토큰을 스트리밍하고 완전한 문장이 되는 순간 합성(synthesizing)을 시작한다면, LLM이 여전히 작성하는 동안 TTS가 말하기를 시작할 수 있습니다.
두 번째 대기: TTS 콜드 스타트(cold start). 이 합성기는 매 턴마다 새로운 스트리밍 연결을 열었습니다. 그 핸드셰이크 과정이 480ms의 큰 부분을 차지했습니다. 호출자가 말을 마치기도 전에 유지되고 예열된 연결은 첫 청크 지연 시간(first-chunk latency)을 크게 줄여줍니다.
세 번째 대기: 확인 부족. 위 모든 것에도 불구하고, 제거할 수 없는 최소한의 간격이 존재합니다. 마지막 구간에 대해서는 에이전트를 더 빠르게 만들려고 노력하는 대신, 존재하는 것처럼 들리게 만드는 데 집중했습니다. 짧은 말로 된 확인(예:
여기에 중요한 부분, 즉 두 스트림(stream) 사이의 경계 인식 브리지(boundary-aware bridge)가 있습니다.
import asyncio
import re
...
핵심적인 디테일은 두세 문장의 여유로운 버퍼(buffer)를 기다리는 것이 아니라, 첫 번째 경계에서 즉시 플러싱(flushing)한다는 점입니다. "전화 주셔서 감사합니다. 고객 지원 센터입니다."라는 인사말은 첫 번째 마침표가 찍히는 순간 바로 말할 수 있는 오디오가 됩니다. 스트리밍 완료(streamed completion) 환경에서 이는 1,100ms가 아니라 불과 몇 백 밀리초(ms) 만에 이루어집니다.
나머지를 가능하게 한 필러 토큰 (filler token)
문장 스트리밍(Sentence streaming)을 통해 LLM이 첫 오디오를 생성하기까지의 시간을 약 1,100ms에서 약 250ms로 단축했습니다. TTS 연결을 예열(Warming)함으로써 첫 번째 청크(first-chunk) 지연 시간(latency)을 약 480ms에서 약 90ms로 줄였습니다. 결과는 좋았지만, 첫 대화 단계에서는 여전히 모든 과정 앞에 ASR 최종 확정 대기 시간이 있었고, LLM의 첫 토큰(first token) 생성이 느릴 경우 여전히 박자감이 느껴질 수 있었습니다.
그래서 저는 경주에서 이기려고 노력하는 대신, 인지(perception)를 속이기로 했습니다. LLM이 단 하나의 토큰도 생성하기 전, ASR이 최종 전사(final transcript)를 확정하는 즉시 발신자에게 미리 합성된 짧은 확인 응답(acknowledgement)을 보냅니다.
async def handle_turn(session, transcript):
# 라인이 끊긴 것처럼 들리지 않도록 즉각적인 확인 응답을 보냅니다.
# 이 오디오는 미리 예열되어 있으며 100ms 이내에 재생을 시작합니다.
...
이 확인 응답은 부정적인 의미의 '시간 끌기용 말(filler)'이 아닙니다. 인간 상담사도 정확히 이렇게 행동합니다. 질문을 이해하는 즉시 "네, 확인해 보겠습니다"라고 말하면, 고객은 자신의 말이 전달되었다는 소리를 듣고 안심하게 됩니다. 이제 발신자는 문장을 마친 후 약 150ms 이내에 무언가를 듣게 됩니다. 실제 답변은 그 밑단에서 준비되어 도착하며, 이 두 요소는 하나의 연속적인 턴(turn)으로 자연스럽게 결합됩니다.
출시된 결과물, 그리고 과거의 나에게 해주고 싶은 말
실제 프로덕션(production)에 적용된 사항: 문장 경계마다 플러시(flush)를 수행하며 LLM에서 스트리밍 TTS(streaming TTS)로 토큰 레벨 스트리밍(token-level streaming)을 적용했습니다. 또한, 발신자가 말을 마치기 전에 TTS 연결을 미리 예열(warmed)하고 열린 상태로 유지했으며, ASR(자동 음성 인식)이 종료되는 즉시 캐시된 확인 응답(cached acknowledgement)을 발송하여 실제 답변이 합성되는 동안 회선이 침묵 상태로 머물지 않도록 했습니다. 첫 번째 턴에서 첫 오디오가 나오기까지 측정된 시간은 약 2초에서, 확인 응답까지 약 150ms, 그리고 실제 내용까지 대략 400ms로 단축되었습니다. 이제 더 이상 인사를 할 때 상대방이 말을 가로채지 않는데, 그 이유는 채워야 할 무음(dead air)이 없기 때문입니다.
만약 제가 그 첫 번째 파이프라인(pipeline)을 구축했던 과거의 저에게 메모 한 장을 보낼 수 있다면 이렇게 말하고 싶습니다. 지연 시간(latency)을 단순히 줄여야 할 하나의 숫자로 취급하지 마세요. 그것은 네 단계에 걸쳐 분할된 예산이며, 가장 큰 비중을 차지하는 항목은 제가 예상했던 곳이 거의 아니었습니다. 저는 그 비용이 모델(model)에 있을 것이라고 확신했을 것입니다. 하지만 실제로는 제가 모든 단계가 시작되기 전 이전 단계가 완전히 끝날 때까지 기다리게 만들었다는 사실에 있었습니다. 스트리밍(streaming)이 이를 변화시켰으며, 이 파이프라인에서는 스트리밍이 대부분의 역할을 수행했습니다. 첫 번째 문장이 생성되는 즉시, 그것을 말하세요.
두 번째 메모는 그 초기 200ms에 관한 것입니다. 그 시점에 발신자에게 필요한 것은 정답이 아니라, 그 어떤 소리라도 있는 것입니다. 전화기 너머의 침묵은 통화가 끊긴 것으로 간주되며, 통화가 끊겼다고 생각한 발신자는 말을 시작하게 됩니다. 그러면 당신은 스스로의 침묵 때문에 만들어낸 충돌(collision)을 디버깅하게 됩니다. 그래서 저는 작고 즉각적인 무언가를 말하고, 그 밑으로 실제 답변이 도착하게 합니다. 첫 시작의 박자에서는, 정확한 것보다 빠른 것이 더 중요했습니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기