Twilio와 VAPI를 사용한 Voice AI 구현 방법: 단계별 가이드
Source: Dev.to
TL;DR
대부분의 Twilio + VAPI 통합은 개발자가 호환되지 않는 오디오 스트림을 합치려 할 때 깨집니다.
해결책: 전화 통신 전송(PSTN → WebSocket)은 Twilio가, AI 처리(STT → LLM → TTS)는 VAPI가 담당하도록 합니다. Twilio의 Media Streams를 VAPI의 WebSocket 프로토콜에 연결하는 프록시 서버를 구축하고, µ‑law ↔ PCM 변환 및 양방향 오디오 흐름을 처리합니다. 이렇게 하면 오디오 끊김이나 연결 끊김 없이 실제 전화 통화를 처리할 수 있는 프로덕션 급 음성 AI를 만들 수 있습니다.
Prerequisites
API Access & Authentication
- VAPI API 키 (dashboard.vapi.ai)
- Twilio Account SID 및 Auth Token (console.twilio.com)
- 음성 기능이 활성화된 Twilio 전화번호
- Node.js 18+ (웹훅 서버용)
System Requirements
- 공개 HTTPS 엔드포인트 (예: 로컬 개발 시
ngrok http 3000) - SSL 인증서 (Twilio는 비 HTTPS 웹훅을 거부합니다)
- Node.js 프로세스를 위한 최소 512 MB RAM
- 웹훅 트래픽을 위한 포트 3000 개방
Technical Knowledge
- REST API 및 웹훅 패턴에 대한 친숙함
- 기본 TwiML (Twilio Markup Language) 지식
- JavaScript에서
async/await사용 경험 - 실시간 스트리밍을 위한 WebSocket 연결 이해
Cost Awareness
- Twilio 음성 통화: $0.0085 /분
- VAPI (GPT‑4 모델): ≈ $0.03 /분
- 예상 총 비용: $0.04–$0.05 /분 (프로덕션 트래픽 기준)
VAPI: Get started → Get VAPI
Step‑By‑Step Tutorial
Configuration & Setup
대부분의 Twilio + VAPI 통합이 실패하는 이유는 두 개의 호환되지 않는 콜 플로우를 합치려 하기 때문입니다.
실제 상황: Twilio는 전화 통신(SIP, PSTN 라우팅)을 담당하고, VAPI는 음성 AI(STT, LLM, TTS)를 담당합니다. 두 시스템이 직접 “통합”되는 것이 아니라 브리지 역할을 해야 합니다.
아키텍처 결정: 인바운드(Twilio가 수신 → VAPI로 전달) 또는 아웃바운드(VAPI가 시작 → Twilio를 운송업체로 사용) 중 하나를 선택합니다. 이 가이드에서는 인바운드만 다룹니다.
Install dependencies
npm install @vapi-ai/web express twilio
핵심 설정:
- VAPI는 공개 웹훅 엔드포인트가 필요합니다.
- Twilio는 TwiML 지시문이 필요합니다.
이 두 가지는 별도의 책임입니다.
Architecture & Flow
flowchart LR
A[Caller] -->|PSTN| B[Twilio Number]
B -->|TwiML Stream| C[Your Server]
C -->|WebSocket| D[VAPI Assistant]
D -->|AI Response| C
C -->|Audio Stream| B
B -->|PSTN| A
인바운드 흐름:
- Twilio가 전화를 받고 설정된 TwiML 웹훅을 실행합니다.
- 오디오는 Twilio Media Streams를 통해 서버로 전송됩니다.
- 서버는 오디오를 WebSocket을 통해 VAPI에 전달합니다.
- VAPI가 오디오를 처리합니다(STT → LLM → TTS).
- 생성된 오디오가 동일한 경로를 통해 다시 호출자에게 스트리밍됩니다.
Step‑By‑Step Implementation
1. Create VAPI Assistant
VAPI 대시보드(vapi.ai → Assistants → Create) 또는 API를 통해 어시스턴트를 생성합니다. 낮은 지연 시간을 위해 권장 설정:
- 모델: GPT‑4 (음성용으로는 GPT‑4‑turbo보다 낮은 지연)
- 음성: ElevenLabs (≈ 150 ms)
- 전사기: Deepgram Nova‑2,
endpointing = 300 ms침묵 임계값
프로덕션 경고: 기본 200 ms endpointing은 모바일 네트워크에서 잘못된 중단을 일으킬 수 있습니다. 300–400 ms로 늘리세요.
2. Set Up Twilio TwiML Webhook
Express 엔드포인트를 만들어 <Connect> 요소가 포함된 TwiML을 반환합니다. Twilio는 µ‑law 오디오를 지정한 URL로 스트리밍합니다.
// server.js
const express = require('express');
const app = express();
app.post('/twilio/voice', (req, res) => {
const twiml = `<?xml version="1.0" encoding="UTF-8"?>
<Response>
<Connect>
<Stream url="wss://yourdomain.com/media-stream"/>
</Connect>
</Response>`;
res.type('text/xml');
res.send(twiml);
});
app.listen(3000, () => console.log('Server listening on port 3000'));
참고:
wss://yourdomain.com/media-stream은 당신의 WebSocket 서버(다음 단계에서 구현)이며 VAPI 엔드포인트가 아닙니다. Twilio는 여기로 µ‑law 오디오를 스트리밍합니다.
3. Bridge Twilio Stream to VAPI
Twilio와 VAPI 사이의 오디오를 중계하고, 시작 이벤트와 양방향 미디어 흐름을 처리하는 간단한 WebSocket 브리지입니다.
// bridge.js
const WebSocket = require('ws');
const wss = new WebSocket.Server({ port: 8080 });
wss.on('connection', (twilioWs) => {
let vapiWs = null;
const pendingAudio = [];
twilioWs.on('message', (msg) => {
const data = JSON.parse(msg);
if (data.event === 'start') {
// Initialise VAPI connection
vapiWs = new WebSocket('wss://api.vapi.ai/ws');
vapiWs.on('open', () => {
vapiWs.send(JSON.stringify({
type: 'assistant-request',
assistantId: process.env.VAPI_ASSISTANT_ID,
metadata: { callSid: data.start.callSid }
}));
// Flush any buffered audio
while (pendingAudio.length) {
vapiWs.send(JSON.stringify({ type: 'audio', data: pendingAudio.shift() }));
}
});
// Forward VAPI audio back to Twilio
vapiWs.on('message', (vapiMsg) => {
const audio = JSON.parse(vapiMsg);
if (audio.type === 'audio') {
twilioWs.send(JSON.stringify({
event: 'media',
media: { payload: audio.data }
}));
}
});
}
if (data.event === 'media' && vapiWs && vapiWs.readyState === WebSocket.OPEN) {
// Forward Twilio audio to VAPI
vapiWs.send(JSON.stringify({ type: 'audio', data: data.media.payload }));
} else if (data.event === 'media') {
// Buffer until VAPI connection is ready
pendingAudio.push(data.media.payload);
}
});
});
경쟁 조건 경고: Twilio가 VAPI WebSocket이 열리기 전에 오디오를 보내면 손실이 발생합니다. 위와 같이 패킷을 버퍼링해 손실을 방지하세요.
4. Configure Twilio Phone Number
Twilio 콘솔에서:
- Phone Numbers → Active Numbers → [your number] → Voice Configuration
- A Call Comes In 웹훅 URL을
https://yourdomain.com/twilio/voice(HTTP POST) 로 설정합니다. - 로컬 테스트 시
ngrok http 3000으로 서버를 노출하고, 생성된 HTTPS URL을 사용합니다.
Error Handling & Edge Cases
- Twilio 타임아웃 (15 s): VAPI가 응답하지 않으면 Twilio가 전화를 끊습니다. 10 s마다 VAPI에 keep‑alive ping을 보내세요.
- 오디오 포맷 불일치: Twilio는 µ‑law 8 kHz를 스트리밍하고, VAPI는 PCM 16 kHz를 기대합니다. 브리지에서 트랜스코딩하거나 VAPI 전사기가 µ‑law를 지원하도록 설정하세요.
- Barge‑in: 사용자가 말을 중단하면
{ type: 'cancel' }을 VAPI에 보내고 Twilio 오디오 버퍼를 플러시해 현재 TTS 재생을 중지합니다.
Testing & Validation
- Twilio 번호로 전화를 겁니다.
- 로그에서 다음을 확인합니다:
- TwiML 웹훅 호출(200 응답)
- WebSocket 연결 성공
- VAPI 어시스턴트 초기화
- 양방향 오디오 패킷 흐름
- 지연 시간 벤치마크: 사용자 발화 종료 → 봇 응답 시작까지 측정합니다. 목표는 ≈ 1200 ms이며, 이보다 길면 체감이 나빠집니다.
Common Issues & Fixes
| 증상 | 가능한 원인 | 해결 방법 |
|---|---|---|
| 봇의 오디오가 들리지 않음 | VAPI가 PCM을 보내고 Twilio가 µ‑law를 기대 | 트랜스코딩 레이어를 추가하거나 Twilio 대신 VAPI 자체 전화 제공자를 사용 |
| 봇이 문장 중간에 끊김 | VAD endpointing이 너무 짧음 | transcriber.endpointing 을 400 ms 로 증가 |
| 웹훅 실패 | Twilio는 HTTPS를 요구 | 로컬 테스트 시 ngrok 사용하거나 유효한 SSL 인증서를 가진 서버에 배포 |
System Diagram
graph LR
Phone[Phone Call]
Gateway[Call Gateway]
IVR[Interactive Voice Response]
STT[Speech‑to‑Text]
NLU[Intent Detection]
LLM[Response Generation]
TTS[Text‑to‑Speech]
Error[Error Handling]
Output[Call Output]
Phone --> Gateway
Gateway --> IVR
IVR --> STT
STT --> NLU
NLU --> LLM
LLM --> TTS
TTS --> Output
Gateway -->|Call Drop/Error| Error