如何使用 Twilio 和 VAPI 实现 Voice AI:一步步指南

发布: (2025年12月11日 GMT+8 10:19)
7 min read
原文: Dev.to

Source: Dev.to

TL;DR

大多数 Twilio + VAPI 集成会失败,因为开发者尝试合并不兼容的音频流。
解决方案: 使用 Twilio 负责电话传输(PSTN → WebSocket),VAPI 负责 AI 处理(STT → LLM → TTS)。构建一个代理服务器,将 Twilio 的 Media Streams 桥接到 VAPI 的 WebSocket 协议,处理 µ‑law ↔ PCM 转换以及双向音频流。这样即可得到一个生产级的语音 AI,能够在真实电话通话中运行而不会出现音频卡顿或掉线。

Prerequisites

API Access & Authentication

  • VAPI API key(dashboard.vapi.ai)
  • Twilio Account SID 和 Auth Token(console.twilio.com)
  • 已启用语音功能的 Twilio 电话号码
  • Node.js 18+(用于 webhook 服务器)

System Requirements

  • 公网 HTTPS 端点(例如 ngrok http 3000 用于本地开发)
  • SSL 证书(Twilio 拒绝非 HTTPS webhook)
  • Node.js 进程最低 512 MB RAM
  • 端口 3000 对 webhook 流量开放

Technical Knowledge

  • 熟悉 REST API 与 webhook 模式
  • 基础 TwiML(Twilio Markup Language)知识
  • 有 JavaScript 中 async/await 的使用经验
  • 理解用于实时流式传输的 WebSocket 连接

Cost Awareness

  • Twilio 语音通话:$0.0085 /分钟
  • VAPI(GPT‑4 模型):≈ $0.03 /分钟
  • 预计综合成本:$0.04–$0.05 /分钟(生产流量)

VAPI: 入门 → 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 需要一个公网 webhook 端点。
  • 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 webhook。
  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

一个简单的 WebSocket 桥接,将音频在 Twilio 与 VAPI 之间转发,并处理 start 事件以及双向媒体流。

// 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 webhook 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): 当用户中断时,向 VAPI 发送 { type: 'cancel' } 并清空 Twilio 的音频缓冲,以停止当前 TTS 播放。

Testing & Validation

  1. 拨打 Twilio 号码。
  2. 在日志中验证:
    • TwiML webhook 被命中(200 响应)
    • WebSocket 连接已建立
    • VAPI 助手已初始化
    • 双向音频包正在流动
  3. 延迟基准: 测量用户说完话到机器人开始响应的时间。目标 ≈ 1200 ms;更高会感觉卡顿。

Common Issues & Fixes

症状可能原因解决办法
机器人没有声音VAPI 发送 PCM 而 Twilio 期待 µ‑law添加转码层或使用 VAPI 自带的电话运营商(绕过 Twilio)。
机器人在句子中途被截断VAD endpointing 设得太低transcriber.endpointing 提升至 400 ms。
Webhook 失败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

相关文章

阅读更多 »

哎呦!2025

我的 YOW! 体验 我已经关注 YOW! 会议超过十年了。它们在澳大利亚的三个城市举办——墨尔本、布里斯班和悉尼——并且 f...