ElevenLabs와 Twilio로 20분 만에 보이스 에이전트 만들기
- 게시일
- 최종 업데이트
음성 에이전트는 수신 전화를 받고, 발신자의 음성을 실시간으로 음성 텍스트 변환 (STT)으로 전사하고, 대규모 언어 모델(LLM)로 응답을 생성한 뒤 텍스트 음성 변환 (TTS) 모듈로 응답을 음성으로 전달할 수 있습니다. ElevenLabs와 Twilio를 사용하면 약 20분 만에 실제 전화번호에서 작동하는 에이전트를 만들 수 있습니다.
개발자를 위한 전체 스택은 음성 합성(Flash v2.5) 및 전사(Scribe v2 Realtime)를 위한 ElevenLabs, 전화 통신을 위한 Twilio, 그리고 LLM으로서 OpenAI 또는 Anthropic으로 구성됩니다. 또한 이 모든 구성 요소는 교체할 수 있으므로, 가장 익숙한 구성 요소를 선택해 사용할 수 있습니다.
이 글에서는 Node.js 및 Typescript를 사용해 20분 만에 음성 에이전트를 만드는 방법을 소개합니다. 직접 캐스케이드를 유지 관리하지 않고 턴 전환, 끼어들기, 전화 통신을 처리하는 관리형 대안을 원한다면 ElevenAgents를 확인해 보세요.
음성 에이전트 아키텍처 작동 방식
코드를 작성하기 전에 기술 스택의 세 가지 서비스가 어떻게 연결되는지 이해하면 도움이 됩니다.
- Twilio: 전화 통화와 오디오 전송을 처리합니다.
- ElevenLabs: Scribe v2 Realtime을 통한 STT와 Flash v2.5를 통한 TTS를 처리합니다.
- LLM: 도구 호출과 응답 작성을 처리합니다.
각 단계는 가벼운 어댑터이므로 나머지 부분을 건드리지 않고도 다른 서비스로 교체할 수 있습니다. 예를 들어 다른 구성 요소를 다시 작성하지 않아도 OpenAI LLM을 Anthropic으로 바꿀 수 있습니다.
전화 통화는 Twilio를 통해 서버에 도달합니다. Twilio는 PSTN 전화를 받고 서버로 연결되는 WebSocket을 열며, 발신자 오디오를 base64로 인코딩된 mu-law 프레임 스트림으로 전달합니다. 서버는 캐스케이드를 실행하고 동일한 WebSocket을 통해 합성된 오디오를 다시 스트리밍하며, Twilio는 이를 발신자에게 재생합니다.

음성 에이전트를 구축할 때 사용할 흐름은 다음과 같습니다:
발신자가 Twilio 번호로 전화를 겁니다. Twilio는 웹훅에서 TwiML 문서를 가져옵니다. TwiML은 Twilio에 WebSocket 엔드포인트로 연결되는 Media Stream을 열도록 지시합니다. Twilio는 base64 mu-law(ulaw_8000) 페이로드를 포함한 JSON 이벤트로 수신 오디오를 스트리밍합니다.
서버는 오디오 청크를 Scribe v2 Realtime으로 전달해 스트리밍 전사를 수행합니다. 발신자의 턴이 완료되면 전사문을 LLM에 보내고, 이어서 Flash v2.5를 사용해 ulaw_8000 형식으로 응답을 합성합니다. 합성된 mu-law 프레임을 base64로 인코딩해 WebSocket을 통해 Twilio로 다시 보내면, Twilio가 발신자에게 재생합니다.
Scribe v2 Realtime은 약 150ms 지연 시간으로 부분 전사 결과를 내보내며, Flash v2.5의 모델 추론 시간은 네트워크 및 애플리케이션 지연 시간을 제외하고 약 75ms입니다. 첫 오디오까지 걸리는 시간에 가장 크고 예측하기 어려운 영향을 주는 요소는 LLM이며, 대부분의 지연 시간 예산이 여기에 사용됩니다. 간격을 짧게 유지하기 위해 LLM 출력을 토큰 단위로 스트리밍하고 모델이 문장을 끝내기 전부터 합성을 시작합니다.
이러한 선택에 따른 모델 간 절충 사항은 모델 개요와 지연 시간 이해하기를 참고하세요.
음성 에이전트 구축 전 필요한 사항
이 가이드에서는 네 가지 사항이 준비되어 있다고 가정합니다. 모두 빠르게 설정할 수 있지만, 하나라도 빠지면 서버를 실행할 수 없습니다.
다음 사전 준비 사항을 확인하세요:
- 음성 기능이 있는 Twilio 전화번호: Twilio 콘솔에서 해당 번호와 Account SID, Auth Token을 기록해 두세요.
- ElevenLabs API 키: ElevenLabs 대시보드에서 생성합니다. 이 키는 xi-api-key 헤더로 전송되는 비밀 정보이므로 서버 측에만 보관하세요. API 인증을 참고하세요.
- LLM API 키: 이 튜토리얼에서는 Anthropic Claude와 OpenAI를 서로 교체 가능한 백엔드로 취급하므로, 하나를 선택하세요.
- 로컬 개발용 Ngrok(또는 다른 터널): Twilio가 공개 HTTPS 및 WSS URL을 통해 서버에 접근해야 하며, ngrok를 사용하면 별도 배포 없이 이를 제공할 수 있습니다.
비밀 정보는 환경 변수로 설정하고 절대 커밋하지 마세요.
그런 다음 서버에서 사용할 포트를 가리키는 터널을 시작하세요:
Twilio Media Streams 프로토콜 이해하기
Twilio는 원시 오디오 소켓을 제공하지 않습니다. 대신 WebSocket을 통해 구조화된 JSON 프로토콜로 모든 것을 감쌉니다. 네 가지 이벤트 유형과 전송 형식을 이해하면 작성 전에 2단계의 WebSocket 핸들러를 완전히 이해할 수 있습니다.
Twilio가 WebSocket에 연결되면 네 가지 이벤트 유형을 가질 수 있는 일련의 JSON 텍스트 메시지를 전송합니다.
connected 이벤트가 먼저 도착하여 WebSocket이 연결되었음을 확인합니다. media stream이 시작될 때 start 이벤트가 한 번 전송됩니다. 여기에는 오디오를 다시 보내는 데 필요한 streamSid가 포함되므로 저장해야 하며, start.customParameters 및 start.callSid 아래에 통화 메타데이터도 포함됩니다.
media 이벤트는 반복적으로 발생합니다. media.payload는 프레임당 20ms인 8kHz mu-law 오디오의 base64 인코딩 청크이고, media.track은 발신자 오디오의 경우 inbound입니다. 마지막으로 스트림이 끝나면 stop 이벤트가 전송되며, 보통 통화가 종료되었기 때문입니다.
오디오를 재생하려면 동일한 streamSid와 base64 mu-law 페이로드를 포함한 media 유형의 메시지를 보냅니다. 이미 큐에 넣은 오디오를 중단하려면 streamSid가 포함된 clear 메시지를 보내 Twilio의 송신 버퍼를 비웁니다.
수신 및 송신 인코딩은 동일합니다(ulaw_8000). ElevenLabs 텍스트 음성 변환에 ulaw_8000을 요청하고, 중간에 리샘플링하지 않고 바이트를 Twilio로 바로 전달합니다.
1단계: TwiML 웹훅 제공
전화가 오면 Twilio가 웹훅에 HTTP 요청을 보내고, 통화를 Media Stream에 연결하는 TwiML로 응답합니다. <Connect><Stream> 동사는 양방향 WebSocket을 엽니다. 여기서는 <Start> 대신 <Connect>를 사용하세요. 스트림이 지속되는 동안 통화를 유지하고 오디오를 다시 보낼 수 있게 해 주기 때문입니다. 이것이 이 설정의 목적입니다.
웹훅이 반환하는 TwiML은 다음과 같습니다:
Express에서는 호스트를 채워 넣고 문서를 반환하는 POST 핸들러 하나로 구현할 수 있습니다:
Twilio 콘솔에서 번호의 "A call comes in" 웹훅을 https://your-subdomain.ngrok.app/incoming-call로 설정하고 HTTP POST를 사용하세요.
2단계: Media Stream WebSocket 수락
WebSocket 핸들러는 Twilio 이벤트를 읽고, 캐스케이드를 구동하며, 오디오를 다시 전송합니다.
통화별 상태는 streamSid, STT 연결, 그리고 에이전트가 현재 말하고 있는지를 나타내는 플래그 정도만 유지합니다. 핸들러는 각 수신 media 프레임을 base64에서 디코딩하고 원시 mu-law 바이트를 STT로 전달합니다:
3단계: Scribe v2 Realtime으로 전사
Scribe v2 Realtime은 스트리밍 오디오 청크를 받아 부분 및 최종 전사 결과를 반환하며, mu-law 인코딩을 직접 지원하므로 Twilio 프레임을 변경 없이 전달합니다.
또한 무음 기반 세분화를 위한 음성 활동 감지와 세그먼트 완료를 위한 수동 커밋 제어도 제공합니다. 전화 에이전트의 경우 자연스러운 휴지가 발신자 턴이 끝났다는 가장 신뢰할 수 있는 신호이므로, 일반적으로 VAD 기반 세분화가 적합합니다.
단계는 다음과 같습니다. 통화가 시작되면 STT 스트림을 엽니다. 모든 수신 mu-law 청크를 전송합니다. 완료된 전사 결과에 반응해 LLM을 호출합니다.
실시간 STT 클라이언트 인터페이스는 아직 발전 중이므로, 아래 형태는 고정된 필드 이름 집합 대신 실제 API에 맞춰 구현하는 작은 TypeScript 어댑터(openRealtimeStt) 뒤에 두었습니다. onFinal을 완료된 발신자 턴을 다음 단계로 넘기는 훅으로 취급하세요.
실시간 인식의 부분 결과 지연 시간은 약 150ms로, 발신자가 말을 마친 뒤 에이전트가 시작할 때까지 느껴지는 간격을 짧게 유지합니다. 배치 버전과 전체 기능 세트는 음성 텍스트 변환 문서 및 실시간 음성 텍스트 변환 제품 페이지를 참고하세요.
4단계: LLM으로 응답 생성
이 단계에서 응답을 생성합니다. LLM은 대화 기록을 받아 어시스턴트의 텍스트를 반환합니다. 첫 문장부터 합성을 시작할 수 있도록 응답을 스트리밍하세요.
여기서는 OpenAI를 백엔드로 사용합니다:
위 모델 ID인 gpt-4.1-mini는 낮은 지연 시간을 위한 선택지의 예이며, Anthropic에서는 claude-haiku-4-5가 비슷한 옵션입니다. 어느 제공업체든 동일한 llmReply 계약을 지원할 수 있습니다. 함수 본문만 바꾸면 나머지 에이전트는 변경되지 않습니다.
시스템 프롬프트는 응답 길이를 제한합니다. 전화에서는 이 점이 중요합니다. 응답이 길면 느리게 느껴지고 자연스럽게 끼어들기도 어렵습니다.
5단계: ulaw_8000으로 Flash TTS 합성
이제 텍스트를 Twilio가 재생할 수 있는 오디오로 변환해야 합니다. 바이트가 Twilio에서 예상하는 인코딩과 일치하도록 outputFormat: "ulaw_8000"으로 Flash v2.5를 요청한 뒤, 오디오를 스트리밍하고 각 청크를 media 이벤트로 WebSocket을 통해 다시 전달하세요.
전체 응답을 기다리지 말고 LLM 토큰을 문장 크기 조각으로 누적한 뒤 각 조각이 완성되는 대로 합성하세요. 모델이 두 번째 문장을 생성하는 동안 발신자가 첫 번째 문장을 들을 수 있으므로 첫 오디오까지 걸리는 시간이 줄어듭니다. 점진적 합성을 더 세밀하게 제어하려면 실시간 TTS WebSocket 가이드에서 하나의 열린 합성 소켓에 텍스트를 입력하는 방법을 확인하세요. 아래 HTTP 스트리밍 방식은 더 간단하며 짧은 대화 턴에 적합합니다.
프로덕션 환경을 위한 AI 음성 에이전트 강화
위의 다섯 단계를 마치면 작동하는 에이전트가 완성됩니다. 하지만 이것이 프로덕션 배포와 같지는 않습니다.
에이전트를 실제 전화선에 연결하기 전에 알아두어야 할 몇 가지 사항이 있습니다.
Twilio 웹훅 서명 검증
웹훅 URL을 아는 사람은 누구나 POST 요청을 보낼 수 있으므로, 가장 먼저 요청이 실제로 Twilio에서 왔는지 확인해야 합니다. Twilio는 X-Twilio-Signature 헤더에서 Auth Token으로 모든 요청에 서명하며, 검증에 실패한 요청은 거부해야 합니다. 서명은 전체 URL과 POST 파라미터를 기준으로 계산되므로 Twilio와 같은 방식으로 계산해야 합니다.
Twilio의 헬퍼가 이를 처리합니다:
비밀 정보 올바르게 관리하기
ELEVENLABS_API_KEY, LLM 키, TWILIO_AUTH_TOKEN은 소스 코드나 리포지토리에 커밋된 일반 텍스트 환경 변수 파일이 아닌 시크릿 관리자에 보관하세요. ElevenLabs 키는 이 서비스에 필요한 엔드포인트로만 범위를 제한하고 크레딧 할당량을 설정하세요. 그래야 키가 유출되더라도 피해 범위가 무제한으로 커지지 않습니다.
엔터프라이즈 요금제에서는 IP 허용 목록을 사용해 특정 IP 범위로 키를 추가 제한할 수 있습니다. 이 서버는 키가 백엔드를 벗어나지 않으므로 API 키를 직접 사용합니다. 오디오 로직이 브라우저나 모바일 클라이언트로 이동한다면, 키가 클라이언트 측에 노출되지 않도록 일회용 토큰으로 전환해야 합니다.
동시성 한도 이해하기
각 요금제에는 모델 패밀리별로 다른 동시성 한도가 있으며, 이 한도는 동시에 오디오를 활발히 생성하는 요청 수를 계산합니다.
전화 에이전트에서는 이 계산 방식이 유리하게 작용합니다. 오디오 생성은 재생보다 빠르므로, 각 통화는 통화 전체 시간이 아니라 응답을 합성하는 짧은 구간에만 TTS 동시성을 사용합니다. 대략적인 기준으로 동시성 한도가 약 5이면 생성이 재생보다 훨씬 먼저 끝나므로 약 100개의 동시 대화형 통화를 지원할 수 있습니다.
그렇더라도 추측하지 말고 여유 용량을 모니터링하세요. ElevenLabs 응답은 current-concurrent-requests 및 maximum-concurrent-requests 헤더를 제공하므로, 이를 기록하고 최대치에 가까워지면 알림을 설정하세요. 한도를 초과하면 요청은 우선순위에 따라 큐에 들어가며, 보통 약 50ms가 추가됩니다. 과부하가 지속되면 HTTP 429가 반환됩니다.
HTTP 429 응답은 짧은 백오프로 처리하세요. 지속된다면 요금제 페이지에서 업그레이드하거나, 엔터프라이즈 고객인 경우 계정 관리자에게 문의해 한도를 높이세요.
끼어들기 및 인터럽트 처리
에이전트가 말하는 중에 발신자가 말을 시작하면 에이전트가 멈추기를 기대합니다. 이를 barge-in이라고 하며, 이를 올바르게 처리하는 것은 에이전트를 스크립트처럼이 아니라 자연스럽게 느끼게 하는 데 중요한 부분입니다.
에이전트 재생 중 STT VAD 신호를 사용해 발신자의 음성을 감지하세요. 감지되면 두 가지를 수행합니다. 먼저 speak의 agentSpeaking 플래그가 루프를 중단해 처리하듯 TTS 청크 전달을 중지합니다. 다음으로 Twilio에 clear 메시지를 보내 이미 Twilio 측에 큐잉한 오디오를 비웁니다.
clear를 생략하면 전송을 멈춘 뒤에도 Twilio가 버퍼링된 오디오를 계속 재생하므로, 에이전트가 발신자의 말 위로 계속 말하는 것처럼 보입니다.
로그 기록, 모니터링 및 우아한 실패 처리
통화가 느리게 느껴질 때 지연 시간의 원인을 파악할 수 있도록 각 단계를 계측하세요. 최종 전사 결과부터 첫 LLM 토큰까지, 첫 LLM 토큰부터 첫 TTS 바이트까지, 첫 TTS 바이트부터 Twilio로 전송된 프레임까지의 간격을 측정하세요. 변동성 있는 지연 시간 대부분은 LLM 단계에 있으며, STT와 TTS 단계는 비교적 안정적이라는 점을 알게 될 것입니다.
그런 다음 부분 실패에 대비하세요. LLM은 시간 초과될 수 있고 STT 스트림은 끊길 수 있으며, ElevenLabs까지의 네트워크 왕복 시간은 지역에 따라 공용 인터넷에서 약 20~200ms까지 달라집니다. ElevenLabs는 이미 북미, 유럽, 동남아시아 클러스터 중 가장 가까운 곳으로 라우팅하므로, 서버는 ElevenLabs뿐 아니라 발신자와도 가까운 곳에 배치하세요.
단계가 실패했을 때 발신자를 침묵 속에 두지 마세요. 짧은 대체 문구("죄송합니다, 다시 말씀해 주시겠어요?")를 합성하고 통화를 유지하세요. 하나의 실패한 턴이 전체 WebSocket을 종료하지 않도록 각 단계를 타임아웃과 try/catch로 감싸세요.
출시 전에 몇 가지 기본 설정을 추가로 적용할 가치가 있습니다:
- 앞서 보인 것처럼 시스템 프롬프트에서 응답 길이를 제한해 턴을 짧고 중단 가능하게 유지하세요.
- 긴 통화에서 LLM 컨텍스트가 무한정 커지지 않도록 대화 기록에 제한을 두세요.
- 멈춘 세션이 동시성을 계속 소모하는 상황에 대비해 최대 통화 시간을 설정하세요.
직접 제어할 수 있는 요소를 계속 조정하려면 지연 시간 문서에서 첫 오디오까지 걸리는 시간이 발생하는 원리를 확인하고, 모델 개요에서 속도와 품질 간의 절충 사항을 살펴보세요. 또한 실시간 TTS WebSocket 가이드에서는 점진적 텍스트 입력으로 합성 지연 시간을 더 낮추는 방법을 소개합니다.
ElevenAPI로 프로덕션 준비가 된 음성 에이전트 구축
20분이 지나면 프로덕션용 음성 에이전트의 모든 계층을 갖추게 됩니다. Twilio가 전화 통신을 처리하고, Scribe v2 Realtime이 전사하며, LLM이 응답을 생성하고, Flash v2.5가 동일한 WebSocket을 통해 음성으로 응답합니다.
직접 캐스케이드를 유지 관리하고 싶지 않다면 ElevenAgents가 방금 수동으로 연결한 것과 동일한 모델을 기반으로 구축된 관리형 서비스로서 턴 전환, 인터럽트 처리, 전화 통신 통합을 제공합니다.
직접 제어하는 스택을 계속 조정하려면 ElevenAPI 제품 페이지에서 요금제, 동시성 한도 및 보이스 라이브러리를 살펴보세요. 또는 가입하여 오늘 첫 통화를 시작해 보세요.



