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에게 Media Stream을 WebSocket 엔드포인트로 열라고 지시합니다. Twilio는 base64 mu-law(ulaw_8000) 페이로드가 담긴 JSON 이벤트로 오디오를 스트리밍합니다.
서버는 오디오 청크를 Scribe v2 실시간에 전달해 스트리밍 음성 인식을 진행합니다. 발신자의 턴이 끝나면, 트랜스크립트를 LLM에 보내고, Flash v2.5로 답변을 ulaw_8000 형식으로 합성합니다. 합성된 mu-law 프레임을 WebSocket을 통해 Twilio로 base64 인코딩하여 보내면, Twilio가 이를 발신자에게 재생합니다.
Scribe v2 Realtime은 약 150ms의 지연으로 부분 트랜스크립트를 내보내고, Flash v2.5는 모델 추론에 약 75ms가 소요됩니다(네트워크 및 애플리케이션 지연 제외). LLM이 가장 큰 지연을 차지하며, 대부분의 지연 예산이 여기에 사용됩니다. 지연을 줄이기 위해 LLM의 출력을 토큰 단위로 스트리밍하고, 문장이 끝나기 전에 합성을 시작합니다.
이러한 선택의 모델 트레이드오프에 대해서는 모델 개요와 지연 이해하기.
보이스 에이전트 구축 전 준비 사항
이 가이드는 네 가지 준비물이 있다고 가정합니다. 각각 빠르게 준비할 수 있지만, 하나라도 없으면 서버가 실행되지 않습니다.
사전에 확인해야 할 사항:
- Voice 기능이 있는 Twilio 전화번호: Twilio 콘솔에서 번호와 Account SID, Auth Token을 확인하세요.
- ElevenLabs API 키: ElevenLabs 대시보드에서 생성합니다. 이 키는 xi-api-key 헤더에 포함되어 전송되며 비밀이므로 반드시 서버에서만 사용하세요. 자세한 내용은
- LLM API 키:이 튜토리얼에서는 Anthropic Claude와 OpenAI를 백엔드로 교체해 사용할 수 있으니, 원하는 것을 선택하세요.
- 로컬 개발용 Ngrok(또는 다른 터널링 도구): Twilio가 서버에 접근하려면 공개 HTTPS 및 WSS URL이 필요하며, ngrok을 사용하면 배포 없이도 이를 제공합니다.
비밀 값은 환경 변수로 설정하고, 절대 커밋하지 마세요.
이제 서버가 사용할 포트로 터널을 시작하세요:
Twilio Media Streams 프로토콜 이해하기
Twilio는 원시 오디오 소켓을 제공하지 않고, 모든 것을 구조화된 JSON 프로토콜로 WebSocket 위에서 전달합니다. 네 가지 이벤트 타입과 전송 형식을 이해하면 2단계의 WebSocket 핸들러가 쉽게 이해됩니다.
Twilio가 WebSocket에 연결되면, 네 가지 이벤트 타입 중 하나를 가진 JSON 텍스트 메시지 시퀀스를 보냅니다.
connected 이벤트가 먼저 도착해 WebSocket이 연결되었음을 확인합니다. start 이벤트는 미디어 스트림이 시작될 때 한 번 전송되며, 오디오를 다시 보낼 때 필요한 streamSid와 start.customParameters, start.callSid에 콜 메타데이터가 포함되어 있습니다.
media 이벤트는 반복적으로 발생합니다: media.payload는 8kHz mu-law 오디오의 base64 인코딩 청크(프레임당 20ms)이고, media.track은 발신자 오디오용입니다. 마지막으로 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단계: Flash TTS로 ulaw_8000 합성하기
이제 텍스트를 Twilio가 재생할 수 있는 오디오로 변환해야 합니다. Flash v2.5에 outputFormat: "ulaw_8000"으로 요청해 Twilio가 기대하는 인코딩과 맞추고, 오디오를 스트리밍해 각 청크를 media 이벤트로 WebSocket에 다시 전달합니다.
LLM 토큰을 문장 단위로 모아 각 문장이 완성될 때마다 합성하세요. 전체 답변을 기다리지 않고 첫 문장을 바로 들려줄 수 있어, 첫 오디오까지의 시간이 짧아집니다. 점진적 합성에 대한 더 깊은 제어는 실시간 TTS WebSocket 가이드에서 확인할 수 있고, 아래 HTTP 스트리밍 방식은 짧은 대화에 충분히 간단합니다.
AI 보이스 에이전트를 프로덕션 환경에 맞게 강화하기
위의 다섯 단계를 거치면 작동하는 에이전트가 완성됩니다. 하지만 이것이 곧 프로덕션 배포와 같지는 않습니다.
실제 전화 회선에 에이전트를 적용하기 전에 알아야 할 몇 가지 중요한 사항이 있습니다.
Twilio 웹훅 서명 검증하기
웹훅 URL을 알게 된 누구나 POST 요청을 보낼 수 있으므로, 요청이 실제 Twilio에서 왔는지 확인하는 것이 첫 번째입니다. Twilio는 모든 요청에 Auth Token으로 X-Twilio-Signature 헤더에 서명을 추가하며, 검증에 실패한 요청은 거부해야 합니다. 서명은 전체 URL과 POST 파라미터를 기반으로 계산되므로, Twilio와 동일한 방식으로 계산해야 합니다.
Twilio의 헬퍼가 이를 자동으로 처리해줍니다:
비밀 값 안전하게 관리하기
ELEVENLABS_API_KEY, LLM 키, TWILIO_AUTH_TOKEN은 소스나 평문 env 파일이 아닌 비밀 관리 도구에 보관하세요. ElevenLabs 키는 이 서비스에 필요한 엔드포인트로만 범위를 제한하고, 크레딧 한도를 설정해 유출 시 피해를 최소화하세요.
엔터프라이즈 요금제에서는 IP 화이트리스트로 특정 IP 범위에만 키를 제한할 수 있습니다. 이 서버는 키가 백엔드에서만 사용되므로 직접 API 키를 사용하지만, 오디오 로직이 브라우저나 모바일 클라이언트로 이동한다면 단일 사용 토큰으로 전환해 키가 클라이언트에 노출되지 않도록 해야 합니다.
동시 처리(concurrency) 한도 이해하기
각 요금제마다 모델별 동시 처리 한도가 다르며, 한도는 동시에 오디오를 생성 중인 요청 수를 기준으로 합니다.
전화 에이전트의 경우, 오디오 생성이 재생보다 빠르기 때문에, 각 통화는 답변 합성 중 짧은 시간만 TTS 동시 처리를 사용합니다. 대략적으로 동시 처리 한도 5개로 약 100개의 동시 대화 방송을 지원할 수 있습니다. 생성이 재생보다 훨씬 빨리 끝나기 때문입니다.
그래도 추측하지 말고 여유를 모니터링하세요. ElevenLabs 응답에는 current-concurrent-requests와 maximum-concurrent-requests 헤더가 포함되어 있으니, 이를 기록하고 최대치에 가까워지면 알림을 설정하세요. 한도를 초과하면 우선순위에 따라 요청이 대기열에 쌓이고, 보통 50ms 정도 추가 지연이 발생하며, 지속적으로 초과하면 HTTP 429가 반환됩니다.
HTTP 429 응답이 오면 짧게 대기 후 재시도하세요. 계속된다면 요금제 업그레이드나 엔터프라이즈 고객의 경우 담당자에게 문의해 한도를 높이세요.
바지인(barge-in)과 끼어들기 처리하기
에이전트가 말하는 중에 발신자가 말을 시작하면, 에이전트가 멈추길 기대합니다. 이것이 바지인(barge-in)이며, 이를 제대로 처리해야 에이전트가 자연스럽게 느껴집니다.
에이전트가 말하는 동안 STT VAD 신호로 발신자 음성을 감지하세요. 감지되면 두 가지를 해야 합니다. 첫째, TTS 청크 전달을 중단합니다(이미 speak 함수의 agentSpeaking 플래그로 루프를 끊어 처리됨). 둘째, Twilio에 clear 메시지를 보내 이미 큐에 있는 오디오를 비웁니다.
clear를 생략하면, Twilio가 버퍼에 남은 오디오를 계속 재생해 에이전트가 발신자 말을 덮어버리는 것처럼 보일 수 있습니다.
로깅, 모니터링, 장애 시 우아하게 처리하기
각 단계를 계측해 통화가 느릴 때 지연 원인을 파악하세요. 최종 트랜스크립트부터 첫 LLM 토큰까지, 첫 LLM 토큰부터 첫 TTS 바이트까지, 첫 TTS 바이트부터 Twilio로 프레임 전송까지의 시간을 측정하세요. 대부분의 가변 지연은 LLM 단계에 집중되어 있고, STT와 TTS 단계는 비교적 안정적입니다.
부분 실패도 대비하세요. LLM이 타임아웃될 수 있고, STT 스트림이 끊길 수 있으며, ElevenLabs까지의 네트워크 왕복 시간은 지역에 따라 20~200ms까지 달라질 수 있습니다. 서버는 ElevenLabs뿐 아니라 발신자와 가까운 곳에 두세요. ElevenLabs는 이미 북미, 유럽, 동남아 클러스터 중 가장 가까운 곳으로 라우팅합니다.
단계가 실패해도 발신자를 침묵 상태로 두지 말고, 짧은 대체 문장(예: "죄송합니다, 다시 말씀해주시겠어요?")을 합성해 통화를 유지하세요. 각 단계를 타임아웃과 try/catch로 감싸 한 번의 실패가 전체 WebSocket을 종료시키지 않도록 하세요.
출시 전 설정해두면 좋은 몇 가지 기본값:
- 시스템 프롬프트에서 답변 길이를 제한해 턴이 짧고 끊기 쉽게 유지하세요.
- 대화 기록 길이를 제한해 장시간 통화 시 LLM 컨텍스트가 무한히 커지지 않도록 하세요.
- 세션이 멈춰 동시 처리를 조용히 소모하지 않도록 최대 통화 시간을 설정하세요.
직접 제어하는 부분을 계속 최적화하려면 지연 문서에서 첫 오디오까지의 시간 구조를 확인하고, 모델 개요에서 속도와 품질 트레이드오프를, 실시간 TTS WebSocket 가이드에서 점진적 텍스트 입력으로 합성 지연을 줄이는 방법을 확인하세요.
ElevenAPI로 프로덕션급 보이스 에이전트 구축하기
이제 20분이 지나면, 실제 서비스에 사용할 수 있는 모든 계층의
직접 과정을 관리하고 싶지 않다면, ElevenAgents가 턴테이킹, 끼어들기 처리, 전화 통합을 관리형 서비스로 제공합니다. 방금 직접 연결한 모델 위에 구축되어 있습니다.
직접 제어하는 스택을 계속 최적화하려면 ElevenAPI 제품 페이지에서 요금제, 동시 처리 한도, 보이스 라이브러리를 확인하세요. 또는 회원가입하고 오늘 바로 첫 전화를 시작해보세요.
.webp&w=3840&q=80)
.webp&w=3840&q=80)

.webp&w=3840&q=80)
