如何使用 Twilio 和 VAPI 实现 Voice AI:一步步指南
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
入站流程:
- Twilio 接收到来电并执行你的 TwiML webhook。
- 音频通过 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
一个简单的 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 控制台:
- Phone Numbers → Active Numbers → [your number] → Voice Configuration
- 将 A Call Comes In webhook 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): 当用户中断时,向 VAPI 发送
{ type: 'cancel' }并清空 Twilio 的音频缓冲,以停止当前 TTS 播放。
Testing & Validation
- 拨打 Twilio 号码。
- 在日志中验证:
- TwiML webhook 被命中(200 响应)
- WebSocket 连接已建立
- VAPI 助手已初始化
- 双向音频包正在流动
- 延迟基准: 测量用户说完话到机器人开始响应的时间。目标 ≈ 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