VAPI를 이용한 고객 지원용 AI 음성 에이전트 배포 방법

발행: (2025년 12월 4일 오후 05:45 GMT+9)
7 분 소요
원문: Dev.to

Source: Dev.to

(번역을 진행하려면 번역하고자 하는 본문 텍스트를 제공해 주시겠어요?)

TL;DR

대부분의 음성 에이전트는 고객이 중간에 말을 끊거나 통화량이 급증할 때 작동이 중단됩니다. 이 가이드는 VAPI의 기본 음성 인프라와 Twilio의 통신사 수준 전화 시스템을 활용하여 두 상황을 모두 처리할 수 있는 프로덕션 급 AI 음성 에이전트를 구축하는 방법을 보여줍니다. 500 ms 미만의 응답 시간, 적절한 바지인 처리, 그리고 API가 타임아웃될 때 자동 페일오버를 기대할 수 있습니다.

Stack

  • VAPI – 음성 AI
  • Twilio – 전화 라우팅
  • Webhook server – 비즈니스 로직 통합

API 액세스 및 인증

  • VAPI API 키 – dashboard.vapi.ai에서 획득
  • Twilio 계정 SID 및 Auth 토큰 – console.twilio.com에서 확인
  • 음성 기능이 활성화된 Twilio 전화번호
  • OpenAI API 키 – GPT‑4 모델 접근용

개발 환경

  • Node.js 18+ 또는 Python 3.9+
  • ngrok (또는 유사한 터널링 도구) 웹훅 테스트용
  • 버전 관리를 위한 Git

기술 요구 사항

  • 웹훅 핸들러용 퍼블릭 HTTPS 엔드포인트 (프로덕션)
  • SSL 인증서 (Let’s Encrypt 사용 가능)
  • 서버 메모리 ≥ 512 MB (권장 1 GB)
  • 안정적인 인터넷 연결 (실시간 오디오를 위한 업로드 ≥ 10 Mbps)

지식 가정

  • REST API 통합 경험
  • Webhook 이벤트 처리 패턴
  • 음성 프로토콜에 대한 기본 이해 (SIP, WebRTC)
  • JSON 구성 관리

비용 인식

서비스대략적인 비용
VAPI (model + voice synthesis)$0.05 – $0.10 per minute
Twilio (voice minutes)$0.0085 per minute
Twilio phone number$1 / month

Architecture Overview

flowchart LR
    A[Customer Calls] --> B[Twilio Number]
    B --> C[VAPI Assistant]
    C --> D[Your Webhook Server]
    D --> E[CRM/Database]
    E --> D
    D --> C
    C --> B
    B --> A

Twilio는 전화 라우팅을 담당하고, VAPI는 음성 AI를 담당합니다. 서버는 웹훅을 통해 두 시스템을 연결합니다. 이러한 책임을 분리하여 phantom‑audio 문제를 방지하십시오.

Assistant Configuration

const assistantConfig = {
  name: "Support Agent",
  model: {
    provider: "openai",
    model: "gpt-4",
    temperature: 0.7,
    systemPrompt: "You are a customer support agent. Extract: customer name, issue type, account number. If caller interrupts, acknowledge immediately and adjust."
  },
  voice: {
    provider: "11labs",
    voiceId: "21m00Tcm4TlvDq8ikWAM",
    stability: 0.5,
    similarityBoost: 0.75
  },
  transcriber: {
    provider: "deepgram",
    model: "nova-2",
    language: "en",
    endpointing: 255 // ms silence before considering speech ended
  },
  recordingEnabled: true,
  serverUrl: process.env.WEBHOOK_URL,
  serverUrlSecret: process.env.WEBHOOK_SECRET
};

Tip: endpointing = 255 ms는 공격적이며 빠른 지원 상호작용에 잘 작동합니다. 불안정한 모바일 네트워크에서 잘못된 중단이 발생하면 400 ms로 늘리세요.

웹훅 서버 (Node.js + Express)

const express = require('express');
const crypto = require('crypto');
const app = express();

app.use(express.json());

// Validate webhook signatures – production requirement
function validateSignature(req) {
  const signature = req.headers['x-vapi-signature'];
  const payload = JSON.stringify(req.body);
  const hash = crypto
    .createHmac('sha256', process.env.WEBHOOK_SECRET)
    .update(payload)
    .digest('hex');
  return signature === hash;
}

app.post('/webhook/vapi', async (req, res) => {
  if (!validateSignature(req)) {
    return res.status(401).json({ error: 'Invalid signature' });
  }

  const { message } = req.body;

  try {
    switch (message.type) {
      case 'function-call':
        // Example: fetch customer data from CRM
        const customerData = await fetchCustomerData(message.functionCall.parameters.accountNumber);
        res.json({ result: customerData });
        break;

      case 'end-of-call-report':
        // Log call metrics
        await logCallMetrics({
          callId: message.call.id,
          duration: message.call.endedAt - message.call.startedAt,
          cost: message.call.cost,
          transcript: message.transcript
        });
        res.sendStatus(200);
        break;

      case 'speech-update':
        // Real‑time transcript for live‑agent handoff
        if (message.status === 'in-progress') {
          await updateLiveTranscript(message.call.id, message.transcript);
        }
        res.sendStatus(200);
        break;

      default:
        res.sendStatus(200);
    }
  } catch (error) {
    console.error('Webhook error:', error);
    res.status(5xx).json({ error: 'Processing failed' });
  }
});

app.listen(3000);

Important: VAPI는 5초 이내에 응답을 받아야 합니다. 외부 호출이 오래 걸리는 경우, 즉시 res.sendStatus(202) 로 응답하고 작업을 비동기적으로 처리한 뒤, 결과를 VAPI API를 통해 나중에 전송하십시오.

Twilio를 VAPI에 연결하기

  1. VAPI 대시보드에서 어시스턴트의 전화 설정을 찾습니다.
  2. Twilio Console → Phone NumbersBuy NumberConfigure Webhook.
  3. 인바운드 웹훅 URL을 VAPI 어시스턴트의 전화 엔드포인트(VAPI 대시보드에 제공)로 설정합니다.

아웃바운드 호출은 지원 티켓이 생성되거나 에스컬레이션될 때 프로그래밍 방식으로 트리거합니다.

Monitoring Metrics (first 100 calls)

메트릭목표
Interruption accuracy> 95 %
False barge‑ins 92 %
Transcription accuracy (noisy)> 85 %

중단 정확도가 90 % 이하로 떨어지면, endpointing을 300 ms로 늘리고 음성 stability를 0.4로 낮춰 더 빠른 차단을 구현하십시오.

오디오 처리 파이프라인

graph LR
    A[Microphone] --> B[Audio Buffer]
    B --> C[Voice Activity Detection]
    C -->|Speech Detected| D[Speech-to-Text]
    C -->|No Speech| E[Error: Silence]
    D --> F[Intent Detection]
    F --> G[Response Generation]
    G --> H[Text-to-Speech]
    H --> I[Speaker]
    D -->|Error: Unrecognized Speech| J[Error Handling]
    J --> F
    F -->|Error: No Intent| K[Fallback Response]
    K --> G

ngrok을 사용한 로컬 테스트

# Start ngrok tunnel (run in terminal)
ngrok http 3000

예시 curl 테스트 (Node.js 스니펫)

const crypto = require('crypto');
const fetch = require('node-fetch');

const testPayload = {
  message: {
    type: "function-call",
    functionCall: {
      name: "getCustomerData",
      parameters: { customerId: "test-123" }
    }
  }
};

const hash = crypto
  .createHmac('sha256', process.env.VAPI_SERVER_SECRET)
  .update(JSON.stringify(testPayload))
  .digest('hex');

fetch('https://your-ngrok-url.ngrok.io/webhook/vapi', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'x-vapi-signature': hash
  },
  body: JSON.stringify(testPayload)
})
  .then(res => res.json())
  .then(data => console.log('Webhook response:', data))
  .catch(err => console.error('Webhook failed:', err));

Note: 무료 티어 ngrok URL은 2시간 후에 만료됩니다. 404 오류를 방지하려면 각 재시작 후 VAPI 대시보드에서 serverUrl을 업데이트하세요.

서명 검증

validateSignature 함수는 요청 페이로드의 HMAC‑SHA256 해시를 x-vapi-signature 헤더와 비교합니다. 서명이 일치하지 않으면 401 응답이 반환되어 재생 공격과 비용이 많이 드는 API 호출의 무단 트리거를 방지합니다.


고객 호출 → Twilio → VAPI → 귀하의 웹훅 → CRM/Database → (루프) → VAPI → Twilio → 고객.

Back to Blog

관련 글

더 보기 »