Twilio와 VAPI를 사용한 Voice AI 구현 방법: 단계별 가이드

발행: (2025년 12월 11일 오전 11:19 GMT+9)
9 min read
원문: Dev.to

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

인바운드 흐름:

  1. Twilio가 전화를 받고 설정된 TwiML 웹훅을 실행합니다.
  2. 오디오는 Twilio Media Streams를 통해 서버로 전송됩니다.
  3. 서버는 오디오를 WebSocket을 통해 VAPI에 전달합니다.
  4. VAPI가 오디오를 처리합니다(STT → LLM → TTS).
  5. 생성된 오디오가 동일한 경로를 통해 다시 호출자에게 스트리밍됩니다.

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 콘솔에서:

  1. Phone Numbers → Active Numbers → [your number] → Voice Configuration
  2. A Call Comes In 웹훅 URL을 https://yourdomain.com/twilio/voice (HTTP POST) 로 설정합니다.
  3. 로컬 테스트 시 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

  1. Twilio 번호로 전화를 겁니다.
  2. 로그에서 다음을 확인합니다:
    • TwiML 웹훅 호출(200 응답)
    • WebSocket 연결 성공
    • VAPI 어시스턴트 초기화
    • 양방향 오디오 패킷 흐름
  3. 지연 시간 벤치마크: 사용자 발화 종료 → 봇 응답 시작까지 측정합니다. 목표는 ≈ 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
Back to Blog

관련 글

더 보기 »

FinOps 컨설팅을 그만두겠다

몇 달 전, 나는 다양한 고객들을 지원하기 시작했고, 자원 및 인프라 최적화 전략을 구현하는 일을 맡았다. 그것은 복잡한 결정이었다...