跳到内容

用 ElevenLabs 和 Twilio 20 分钟搭建语音智能体

发布时间
最近更新

收听收听本文

语音智能体可接听来电,实时转写来电内容,配合

开发者将用到的完整技术栈包括:ElevenLabs 负责语音合成(Flash v2.5)和转写(Scribe v2 Realtime),Twilio 负责电话通信,LLM 可选用 OpenAI 或 Anthropic。这些组件都可互换,可以根据熟悉程度自由选择。

本文将演示如何用 Node.js 和 Typescript 在 20 分钟内搭建语音智能体。如果想要托管版,自动处理轮流说话、中断和电话通信,无需自己维护流程,可前往 ElevenAgents。 

语音智能体架构原理

在写代码前,先了解三项服务如何连接。

  • Twilio: 负责电话接入和音频传输。
  • ElevenLabs: 通过 Scribe v2 Realtime 实现 STT,通过 Flash v2.5 实现 TTS。
  • LLM: 负责调用工具并生成回复。

每个环节都是独立适配器,因此可以随时替换服务而不影响其他部分。例如,可以将 OpenAI LLM 换成 Anthropic,无需重写其他组件。

电话通过 Twilio 接入服务器。Twilio 接听 PSTN 电话,建立 WebSocket 连接到服务器,并将来电音频以 base64 编码的 mu-law 流发送。服务器运行流程,并将合成音频通过同一 WebSocket 返回,Twilio 播放给来电者。

Build a voice agent diagram of a call processing system using Twilio for speech-to-text conversion and LLM for response.

搭建语音智能体的流程如下:

来电者拨打 Twilio 号码。Twilio 从 webhook 获取 TwiML 文档。TwiML 指示 Twilio 打开到 WebSocket 端点的 Media Stream。Twilio 以包含 base64 mu-law(ulaw_8000)数据的 JSON 事件流式传输来电音频。

服务器将音频片段转发到 Scribe v2 实时版 进行流式转写。来电者说完后,将转写文本发送给 LLM,再用 Flash v2.5 以 ulaw_8000 合成回复。合成后的 mu-law 数据以 base64 编码通过 WebSocket 返回 Twilio,Twilio 播放给来电者。

Scribe v2 Realtime 部分转写延迟约 150 毫秒,Flash v2.5 推理约 75 毫秒(不含网络和应用延迟)。LLM 是影响首次音频输出延迟的最大因素。为缩短延迟,LLM 输出时按 token 流式传输,模型未结束时即可开始合成。

关于模型选择的权衡,可参考 模型概览 以及 延迟解析.

搭建语音智能体前的准备

本指南假设已准备好以下四项,均可快速配置,缺少任意一项都无法运行服务器。

请确认以下前置条件:

  1. 具备支持语音的 Twilio 电话号码:请记录号码、Account SID 和 Auth Token,可在 Twilio 控制台查看。
  2. ElevenLabs API 密钥:可在 ElevenLabs 控制台创建。密钥通过 xi-api-key header 传递,属于敏感信息,请仅在服务器端使用。详见
  3. LLM API 密钥:本教程支持 Anthropic Claude 和 OpenAI,任选其一即可。
  4. 本地开发用 Ngrok(或其他隧道工具): Twilio 需通过公网 HTTPS 和 WSS 访问服务器,ngrok 可实现,无需部署。

将密钥设置为环境变量,切勿提交到代码库。

export ELEVENLABS_API_KEY="..."
export ANTHROPIC_API_KEY="..."          # or OPENAI_API_KEY
export TWILIO_AUTH_TOKEN="..."          # used for webhook signature validation
export PUBLIC_HOST="your-subdomain.ngrok.app"

然后启动隧道,指向服务器端口:

ngrok http 8080

理解 Twilio Media Streams 协议

Twilio 不直接提供原始音频 socket,而是通过 WebSocket 以结构化 JSON 协议传输。了解四种事件类型和发送格式,有助于后续编写 WebSocket 处理逻辑。

Twilio 连接 WebSocket 后,会发送一系列 JSON 文本消息,共有四种事件类型。

首先收到 connected 事件,确认 WebSocket 已建立。media stream 开始时发送 start 事件,包含需保存的 streamSid(用于回传音频),以及 start.customParameters 和 start.callSid 下的通话元数据。

media 事件为循环事件:media.payload 是 base64 编码的 8kHz mu-law 音频片段,每帧 20 毫秒,media.track 表示来电音频。stream 结束时发送 stop,通常因挂断电话。

回传音频时,发送类型为 media 的消息,带相同 streamSid 和 base64 mu-law 数据。若需中断已排队音频,发送类型为 clear 的消息,带 streamSid,可清空 Twilio 的输出缓冲区。

收发音频编码一致(ulaw_8000)。请求 ElevenLabs 文本转语音时指定 ulaw_8000,直接将字节转发给 Twilio,无需重采样。

第 1 步:提供 TwiML webhook

来电时,Twilio 会向 webhook 发起 HTTP 请求,返回的 TwiML 连接通话到 Media Stream。<Connect><Stream> 标签打开双向 WebSocket。此处用 <Connect> 而非 <Start>,可保持通话持续并支持回传音频,正是本方案的目的。

webhook 返回的 TwiML 如下:

<?xml version="1.0" encoding="UTF-8"?>
<Response>
  <Connect>
    <Stream url="wss://your-subdomain.ngrok.app/media" />
  </Connect>
</Response>

在 Express 中,只需一个 POST 处理器填充 host 并返回文档:

// ... imports and app setup
app.post("/incoming-call", (_req, res) => {
  const twiml = `<?xml version="1.0" encoding="UTF-8"?>
<Response>
  <Connect>
    <Stream url="wss://${process.env.PUBLIC_HOST}/media" />
  </Connect>
</Response>`;
  res.type("application/xml").send(twiml);
});

在 Twilio 控制台,将号码的 “A call comes in” webhook 设置为 https://your-subdomain.ngrok.app/incoming-call,使用 HTTP POST。

第 2 步:接收 Media Stream WebSocket

WebSocket 处理器读取 Twilio 事件,驱动流程,并回传音频。

每通电话需保存少量状态:streamSid、STT 连接,以及智能体是否正在说话的标志。处理器将每帧 media 解码为原始 mu-law 字节,直接转发给 STT:

import { WebSocketServer } from "ws";
// ... http server bound to the same port as Express

const wss = new WebSocketServer({ server, path: "/media" });

wss.on("connection", (ws) => {
  const state = { streamSid: null as string | null, agentSpeaking: false };

  ws.on("message", async (raw) => {
    const event = JSON.parse(raw.toString());
    switch (event.event) {
      case "start":
        state.streamSid = event.start.streamSid;
        await startSttSession(ws, state);
        break;
      case "media":
        await forwardToStt(Buffer.from(event.media.payload, "base64"), state);
        break;
      case "stop":
        await teardown(state);
        ws.close();
        break;
    }
  });
});

第 3 步:用 Scribe v2 Realtime 转写

Scribe v2 Realtime 支持流式音频片段输入,返回部分和最终转写,且可直接处理 mu-law 编码,因此可直接传递 Twilio 的音频帧。

还支持基于静音的语音活动检测(VAD)进行分段,并可手动提交以完成分段。对于

具体步骤:通话开始时打开 STT 流,推送每个 mu-law 音频片段,转写完成后调用 LLM。

实时 STT 客户端接口仍在迭代,建议用 TypeScript 适配器(openRealtimeStt)对接实时 API,而非依赖固定字段。onFinal 可作为转交已完成说话轮次的钩子。

async function startSttSession(ws, state) {
  // openRealtimeStt is a thin adapter over the realtime STT API:
  // model_id="scribe_v2_realtime", mu-law encoding, 8kHz, VAD on
  // so turns finalize on silence.
  const session = await openRealtimeStt({
    modelId: "scribe_v2_realtime",
    encoding: "ulaw",
    sampleRate: 8000,
  });
  state.stt = session;

  session.onFinal(async (text: string) => {
    if (text.trim()) await handleTurn(ws, state, text);
  });
}

async function forwardToStt(audioBytes, state) {
  if (state.stt) await state.stt.sendAudio(audioBytes);
}

实时识别延迟约 150 毫秒,能有效缩短来电者说完到智能体开始回复的间隔。批量转写和完整功能见 Speech to Text 文档 以及 实时 Speech to Text 产品页。

第 4 步:用 LLM 生成回复

此阶段生成回复。LLM 接收对话历史,返回助手文本。建议流式输出,便于第一句话生成后立即合成。

此处以 OpenAI 为例:

// ... client init: new OpenAI({ apiKey: process.env.OPENAI_API_KEY })
const SYSTEM_PROMPT =
  "You are a concise phone assistant. Keep replies to one or two sentences.";

async function llmReply(history) {
  const stream = await llm.chat.completions.create({
    model: "gpt-4.1-mini",
    stream: true,
    messages: [{ role: "system", content: SYSTEM_PROMPT }, ...history],
  });
  for await (const part of stream) {
    const token = part.choices[0]?.delta?.content;
    if (token) yield token; // incremental tokens
  }
}

上述模型 ID gpt-4.1-mini 是低延迟示例;Anthropic 可用 claude-haiku-4-5。两者均可用于 llmReply,只需替换函数体,其他部分无需更改。

系统提示词限制回复长度,电话场景下尤为重要:回复过长会显得慢且难以自然打断。

第 5 步:用 Flash TTS(ulaw_8000)合成语音

文本需转为 Twilio 可播放的音频。请求 Flash v2.5 时指定 outputFormat: "ulaw_8000",确保字节格式与 Twilio 匹配,然后流式传输音频,每段作为 media 事件通过 WebSocket 返回。

将 LLM token 累积为句子片段,片段完成即合成,而非等全部回复生成后再合成。这样可缩短首次音频输出时间,来电者能在模型生成第二句话时听到第一句话。需更细致控制时,可参考实时 TTS WebSocket 指南,单 socket 增量输入文本;下方 HTTP 流式方案适合短对话。

import { ElevenLabsClient } from "@elevenlabs/elevenlabs-js";

const eleven = new ElevenLabsClient(); // reads ELEVENLABS_API_KEY
const VOICE_ID = "JBFqnCBsd6RMkjVDRZzb"; // George, a default voice

async function speak(ws, state, text: string) {
  state.agentSpeaking = true;
  const stream = await eleven.textToSpeech.stream(VOICE_ID, {
    text,
    modelId: "eleven_flash_v2_5",
    outputFormat: "ulaw_8000",
  });
  for await (const chunk of stream) {
    if (!state.agentSpeaking) break; // interrupted by barge-in
    ws.send(
      JSON.stringify({
        event: "media",
        streamSid: state.streamSid,
        media: { payload: Buffer.from(chunk).toString("base64") },
      })
    );
  }
  state.agentSpeaking = false;
}

让 AI 语音智能体达到生产级

完成上述五步后,智能体已可运行,但这还不是生产部署。

上线前还需注意以下问题:

校验 Twilio webhook 签名

任何人只要知道 webhook 地址都能 POST,因此需确认请求确实来自 Twilio。Twilio 每次请求都用 Auth Token 生成 X-Twilio-Signature header,校验失败即拒绝。签名基于完整 URL 和 POST 参数,需与 Twilio 算法一致。

Twilio 提供了校验工具:

import twilio from "twilio";

app.post("/incoming-call", express.urlencoded({ extended: false }), (req, res) => {
  const url = `https://${process.env.PUBLIC_HOST}/incoming-call`;
  const valid = twilio.validateRequest(
    process.env.TWILIO_AUTH_TOKEN!,
    req.header("X-Twilio-Signature") || "",
    url,
    req.body
  );
  if (!valid) return res.sendStatus(403);
  // ... return TwiML as before
});

妥善管理密钥

将 ELEVENLABS_API_KEY、LLM 密钥和 TWILIO_AUTH_TOKEN 存放在密钥管理器中,勿写入源码或明文 env 文件。ElevenLabs 密钥仅授权所需接口,并设置额度,防止泄露造成大范围损失。

企业版还可通过 IP 白名单限制密钥使用范围。本服务端直接用 API 密钥,因为密钥不会离开后端;如有音频逻辑需在浏览器或移动端运行,建议改用一次性 token,避免密钥暴露。

了解并发限制

每个套餐的并发上限按模型系列不同,统计当前同时生成音频的请求数。

电话智能体并发统计有利于你。音频生成速度快于播放,因此每通电话只在合成回复时占用 TTS 并发,不会持续占用。一般来说,5 路并发可支持约 100 路同时对话,因为生成早于播放结束。

建议实时监控余量,不要凭估算。ElevenLabs 响应头包含 current-concurrent-requests 和 maximum-concurrent-requests,建议记录并接近上限时告警。超限时请求按优先级排队,通常增加约 50 毫秒,持续超载会返回 HTTP 429。

收到 HTTP 429 时应短暂重试,若持续出现可在定价页升级套餐,企业版可联系客户经理提升上限。

处理插话和中断

来电者在智能体说话时插话,期望智能体立刻停下。这就是插话,正确处理能让智能体更自然。

用 STT 的 VAD 信号检测来电者说话。检测到后,需做两件事:一是停止转发 TTS 音频(speak 中的 agentSpeaking 标志已实现),二是向 Twilio 发送 clear 消息,清空已排队音频。

function interrupt(ws, state) {
  state.agentSpeaking = false;
  ws.send(JSON.stringify({ event: "clear", streamSid: state.streamSid }));
}

若未发送 clear,Twilio 会继续播放缓冲音频,导致智能体与来电者抢话。

日志、监控与容错

为每个环节打点,便于定位通话延迟。统计从最终转写到首个 LLM token、首个 LLM token 到首个 TTS 字节、首个 TTS 字节到发送给 Twilio 的时间。大部分延迟通常在 LLM 阶段,STT 和 TTS 较为稳定。

还需考虑部分失败。LLM 可能超时,STT 流可能断开,ElevenLabs 网络往返延迟因地理位置约 20-200 毫秒。建议服务器靠近来电者,而非仅靠近 ElevenLabs,ElevenLabs 已自动路由到最近的数据中心。

某环节失败时,不要让来电者陷入静音:可合成一句简短兜底语(如“抱歉,请再说一遍?”),保持通话。每个环节都应加超时和异常捕获,避免单次失败导致整个 WebSocket 断开。

上线前还建议设置以下默认项:

  • 系统提示词限制回复长度,确保对话简短、易于插话。
  • 限制对话历史长度,避免长时间通话导致 LLM 上下文无限增长。
  • 设置通话最长时长,防止异常会话持续占用并发。

如需持续优化可控环节,详见 延迟文档,了解首次音频输出的延迟来源,模型概览 介绍速度与质量权衡,实时 TTS WebSocket 指南 展示如何通过增量文本输入进一步降低合成延迟。

用 ElevenAPI 构建生产级语音智能体

现在 20 分钟过去,已经搭建好每一层生产级

如果不想自己维护流程,ElevenAgents 提供轮流说话、中断处理和电话集成的托管服务,底层用的就是你刚刚串联的同款模型。

如需持续优化自有技术栈,可查看 ElevenAPI 产品页,了解套餐、并发限制和声音库。也可直接注册,立即体验首次通话。

用 Twilio 和 ElevenLabs 搭建语音智能体常见问题

相关内容

用高质量 AI 音频创作