Pular para o conteúdo

Crie um agente de voz em 20 minutos com ElevenLabs e Twilio

Publicado
Última atualização

OuvirOuça este artigo

Um agente de voz pode atender chamadas telefônicas recebidas, transcrever quem liga em tempo real com Speech to Text (STT), gerar uma resposta com um modelo de linguagem grande (LLM) e responder usando um módulo de Text to Speech (TTS). Com a ElevenLabs e a Twilio, você pode ter um agente funcional em um número de telefone real em cerca de 20 minutos.

Para desenvolvedores, a stack completa usada será a ElevenLabs para síntese de fala (Flash v2.5) e transcrição (Scribe v2 Realtime), a Twilio para telefonia e a OpenAI ou a Anthropic como LLM. Ainda assim, todos esses componentes são intercambiáveis, ou seja, você pode escolher os que conhece melhor e usá-los no lugar.

Este artigo mostra como criar um agente de voz em 20 minutos usando Node.js e Typescript. Se você quiser uma alternativa gerenciada que lide com alternância de turnos, interrupções e telefonia sem precisar manter a cascata por conta própria, acesse o ElevenAgents. 

Como funciona a arquitetura de um agente de voz

Antes de escrever qualquer código, é útil entender como os três serviços da sua stack se conectam.

  • Twilio: Gerencia a chamada telefônica e o transporte de áudio.
  • ElevenLabs: Gerencia o STT com o Scribe v2 Realtime e o TTS com o Flash v2.5.
  • LLM: Gerencia chamadas de ferramentas e a criação da resposta.

Cada etapa é um adaptador leve, por isso você pode trocar uma por outro serviço sem alterar o restante. Por exemplo, você pode substituir o LLM da OpenAI pelo da Anthropic sem precisar reescrever os outros componentes.

Uma chamada telefônica chega ao seu servidor pela Twilio. A Twilio atende a chamada PSTN, abre um WebSocket de volta para seu servidor e encaminha o áudio de quem liga como um fluxo de frames mu-law codificados em base64. Seu servidor executa a cascata e transmite o áudio sintetizado de volta pelo mesmo WebSocket, e a Twilio o reproduz para quem ligou.

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

Este é o fluxo que você usará para criar um agente de voz: 

Uma pessoa liga para seu número da Twilio. A Twilio busca um documento TwiML no seu webhook. O TwiML instrui a Twilio a abrir um Media Stream para seu endpoint WebSocket. A Twilio transmite o áudio de entrada como eventos JSON contendo cargas úteis mu-law (ulaw_8000) em base64. 

Seu servidor encaminha blocos de áudio ao Scribe v2 Realtime para transcrição em streaming. Quando o turno de quem liga é finalizado, você envia a transcrição ao LLM e sintetiza a resposta com o Flash v2.5 em ulaw_8000. Você envia os frames mu-law sintetizados de volta para a Twilio pelo WebSocket, codificados em base64, e a Twilio os reproduz para quem ligou.

O Scribe v2 Realtime emite transcrições parciais com cerca de 150 ms de latência, e o Flash v2.5 tem aproximadamente 75 ms de inferência do modelo, sem incluir a latência da rede e do aplicativo. O LLM é a maior e menos previsível contribuição para o tempo até o primeiro áudio, e é onde vai a maior parte do orçamento de latência. Para manter esse intervalo curto, transmitimos a saída do LLM token por token e começamos a sintetizar antes de o modelo terminar a frase.

Para entender as escolhas e compensações entre os modelos, consulte a visão geral dos modelos e o guia sobre como entender a latência.

O que você precisa antes de começar a criar um agente de voz

Este guia pressupõe que você tenha quatro itens prontos. Cada um é rápido de configurar, mas a ausência de qualquer um deles impedirá o servidor de funcionar.

Confira os pré-requisitos:

  1. Um número da Twilio com capacidade de voz: Anote o número, o Account SID e o Auth Token no console da Twilio.
  2. Chave de API da ElevenLabs: crie-a no painel da ElevenLabs. A chave é enviada no cabeçalho xi-api-key e é secreta, portanto mantenha-a apenas no servidor. Consulte a autenticação da API.
  3. Chave de API do LLM: Este tutorial trata o Claude da Anthropic e a OpenAI como backends intercambiáveis, então escolha um deles.
  4. Ngrok (ou qualquer túnel) para desenvolvimento local: A Twilio precisa alcançar seu servidor por uma URL HTTPS e WSS pública, e o ngrok oferece isso sem que você precise fazer um deploy.

Defina seus segredos como variáveis de ambiente e nunca os envie para o repositório.

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"

Em seguida, inicie um túnel apontando para a porta que seu servidor usará:

ngrok http 8080

Entendendo o protocolo Twilio Media Streams

A Twilio não fornece um socket de áudio bruto. Em vez disso, ela envolve tudo em um protocolo JSON estruturado via WebSocket. Ao entender os quatro tipos de evento e o formato de envio, o handler do WebSocket da Etapa 2 fará todo sentido antes mesmo de você escrevê-lo.

Quando a Twilio se conecta ao seu WebSocket, ela envia uma sequência de mensagens de texto JSON, que pode ter quatro tipos de evento.

O evento connected chega primeiro e confirma que o WebSocket está ativo. O evento start é enviado uma vez quando o fluxo de mídia começa; ele traz um streamSid, que você precisa armazenar para enviar áudio de volta, além de metadados da chamada em start.customParameters e start.callSid. 

O evento media é recorrente: media.payload é um bloco de áudio mu-law de 8 kHz codificado em base64, com 20 ms por frame, e media.track é inbound para o áudio de quem liga. Por fim, stop é enviado quando o fluxo termina, normalmente porque a chamada foi encerrada.

Para reproduzir áudio de volta, envie uma mensagem do tipo media com o mesmo streamSid e uma carga útil mu-law em base64. Para interromper o áudio que você já enfileirou, envie uma mensagem clear com o streamSid, que limpa o buffer de saída da Twilio.

As codificações de entrada e saída são idênticas (ulaw_8000). Solicitamos ulaw_8000 ao Text to Speech da ElevenLabs e encaminhamos os bytes diretamente à Twilio, sem reamostrar nada no caminho.

Etapa 1: disponibilize o webhook TwiML

Quando uma chamada chega, a Twilio envia uma solicitação HTTP ao seu webhook, e você responde com um TwiML que conecta a chamada ao seu Media Stream. O verbo <Connect><Stream> abre um WebSocket bidirecional. Use <Connect>, e não <Start>, aqui: ele mantém a chamada ativa durante todo o fluxo e permite enviar áudio de volta, que é o objetivo desta configuração.

O TwiML retornado pelo webhook é:

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

No Express, basta um único handler POST que preenche o host e retorna o documento:

// ... 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);
});

No console da Twilio, defina o webhook “A call comes in” do número como https://your-subdomain.ngrok.app/incoming-call usando HTTP POST.

Etapa 2: aceite o WebSocket do Media Stream

O handler do WebSocket lê os eventos da Twilio, executa a cascata e envia o áudio de volta.

Mantemos um pequeno estado por chamada: o streamSid, uma conexão STT e uma flag que indica se o agente está falando. O handler decodifica cada frame de mídia de entrada de base64 e encaminha os bytes mu-law brutos ao 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;
    }
  });
});

Etapa 3: transcreva com o Scribe v2 Realtime

O Scribe v2 Realtime aceita blocos de áudio em streaming e retorna transcrições parciais e finais. Ele também é compatível diretamente com a codificação mu-law, então enviamos os frames da Twilio sem alterações. 

Ele também oferece Detecção de Atividade de Voz para segmentação baseada em silêncio e controle de confirmação manual para finalizar um segmento. Para um agente telefônico, a segmentação orientada por VAD costuma ser a escolha certa, pois uma pausa natural é o sinal mais confiável de que o turno de quem liga terminou.

Estes são os passos: abra um fluxo STT quando a chamada começar. Envie cada bloco mu-law de entrada. Reaja às transcrições finalizadas chamando o LLM.

A interface do cliente de STT em tempo real ainda está evoluindo, portanto o formato abaixo fica por trás de um pequeno adaptador TypeScript (openRealtimeStt), que você implementa com base na API atual, e não em um conjunto fixo de nomes de campos. Trate onFinal como o hook que entrega um turno concluído de quem ligou à próxima etapa.

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);
}

A latência do reconhecimento em tempo real é de cerca de 150 ms para parciais, o que mantém curto o intervalo percebido entre a pessoa terminar de falar e o agente começar. Para a versão em lote e o conjunto completo de recursos, consulte a documentação de Speech to Text e a página do produto Speech to Text em tempo real.

Etapa 4: gere uma resposta com um LLM

Esta é a etapa que produz a resposta. O LLM recebe o histórico da conversa e retorna o texto do assistente. Transmita a resposta para poder iniciar a síntese já na primeira frase.

Aqui, usamos a 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
  }
}

O ID de modelo acima, gpt-4.1-mini, é um exemplo de opção com baixa latência; claude-haiku-4-5 é uma opção comparável da Anthropic. Qualquer um dos provedores pode usar o mesmo contrato llmReply; troque o corpo da função e o restante do agente continuará igual.

O prompt de sistema limita o tamanho da resposta, algo importante em chamadas telefônicas: respostas longas parecem lentas e são difíceis de interromper naturalmente.

Etapa 5: sintetize com Flash TTS em ulaw_8000

Agora o texto precisa virar áudio que a Twilio possa reproduzir. Solicite o Flash v2.5 com outputFormat: "ulaw_8000" para que os bytes correspondam à codificação esperada pela Twilio. Em seguida, transmita o áudio e encaminhe cada bloco de volta pelo WebSocket como um evento media.

Acumule tokens do LLM em fragmentos do tamanho de frases e sintetize cada fragmento assim que for concluído, em vez de esperar a resposta inteira. Isso reduz o tempo até o primeiro áudio, pois quem liga ouve a primeira frase enquanto o modelo ainda produz a segunda. Para um controle mais detalhado da síntese incremental, o guia de WebSocket TTS em tempo real mostra como enviar texto para um único socket de síntese aberto; a abordagem de streaming HTTP abaixo é mais simples e suficiente para turnos conversacionais curtos.

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;
}

Preparando seu agente de voz IA para produção

Após as cinco etapas acima, você terá um agente funcional. Isso não é o mesmo que uma implantação em produção. 

Há vários pontos que você precisa considerar antes de colocar o agente em uma linha telefônica real.

Valide as assinaturas dos webhooks da Twilio

Qualquer pessoa que descubra a URL do seu webhook pode enviar um POST para ela. Portanto, a primeira tarefa é confirmar que a solicitação realmente veio da Twilio. A Twilio assina cada solicitação com seu Auth Token no cabeçalho X-Twilio-Signature, e você deve rejeitar qualquer solicitação que não passe na validação. A assinatura é calculada sobre a URL completa e os parâmetros POST, então você precisa calculá-la da mesma forma que a Twilio.

O helper da Twilio faz isso para você:

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
});

Gerencie os segredos corretamente

Mantenha ELEVENLABS_API_KEY, a chave do LLM e TWILIO_AUTH_TOKEN em um gerenciador de segredos, não no código-fonte nem em arquivos env de texto simples enviados a um repositório. Restrinja a chave da ElevenLabs apenas aos endpoints de que este serviço precisa e defina uma cota de créditos, para que um vazamento tenha impacto limitado em vez de ilimitado. 

Os planos Enterprise também podem restringir uma chave a faixas de IP específicas com uma lista de permissões de IP. Este servidor usa a chave de API diretamente porque ela nunca sai do backend. Se alguma lógica de áudio fosse movida para um navegador ou cliente móvel, você usaria tokens de uso único para que a chave nunca fosse exposta no cliente.

Entenda o limite de simultaneidade

Cada plano tem um limite de simultaneidade diferente para cada família de modelos, e esse limite conta quantas solicitações estão gerando áudio ativamente ao mesmo tempo.

Para um agente telefônico, essa contagem trabalha a seu favor. A geração de áudio é mais rápida que a reprodução, então cada chamada só consome simultaneidade de TTS nas breves janelas em que uma resposta está sendo sintetizada, e não durante toda a chamada. Como estimativa, um limite de simultaneidade de cerca de cinco pode atender aproximadamente 100 conversas simultâneas, porque a geração termina bem antes da reprodução.

Ainda assim, monitore a capacidade disponível em vez de estimar. As respostas da ElevenLabs expõem os cabeçalhos current-concurrent-requests e maximum-concurrent-requests; registre-os e crie alertas ao se aproximar do máximo. Quando você excede o limite, as solicitações entram na fila por prioridade, o que normalmente adiciona cerca de 50 ms, e uma sobrecarga sustentada retorna HTTP 429. 

Trate respostas HTTP 429 com uma breve espera antes de tentar novamente. Se elas persistirem, aumente os limites fazendo upgrade na página de preços ou, para clientes Enterprise, com seu gerente de conta.

Lide com interrupções e barge-in

Quem começa a falar enquanto o agente está falando espera que ele pare. Isso é chamado de barge-in, e tratá-lo corretamente é uma parte importante do que faz um agente parecer natural, e não roteirizado.

Detecte a fala de quem liga durante a reprodução do agente usando o sinal VAD do STT. Ao detectá-la, faça duas coisas. Primeiro, pare de encaminhar blocos de TTS, o que a flag agentSpeaking em speak já faz ao interromper o loop. Segundo, envie uma mensagem clear para a Twilio limpar o áudio que você já enfileirou no lado dela.

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

Se você não enviar clear, a Twilio continuará reproduzindo o áudio em buffer depois que você parar de enviá-lo, e o agente parecerá falar por cima de quem ligou.

Registre, monitore e lide bem com falhas

Instrumente cada etapa para identificar a origem da latência quando uma chamada parecer lenta. Meça o intervalo entre a transcrição final e o primeiro token do LLM, entre o primeiro token do LLM e o primeiro byte de TTS, e entre o primeiro byte de TTS e o frame enviado à Twilio. Você verá que a maior parte da latência variável está na etapa do LLM; as etapas de STT e TTS são comparativamente estáveis.

Em seguida, planeje-se para falhas parciais. O LLM pode expirar, o fluxo STT pode cair, e a ida e volta pela rede até a ElevenLabs varia de cerca de 20 a 200 ms na internet pública, dependendo da localização geográfica. Mantenha seu servidor próximo de quem liga, e não apenas da ElevenLabs, já que a ElevenLabs encaminha para o cluster mais próximo na América do Norte, Europa e Sudeste Asiático. 

Quando uma etapa falhar, não deixe quem ligou em silêncio: sintetize uma breve mensagem alternativa ("Desculpe, poderia repetir?") e mantenha a chamada ativa. Envolva cada etapa em um timeout e um try/catch para que um turno com falha não encerre todo o WebSocket.

Vale a pena configurar mais alguns padrões antes do lançamento:

  • Limite o tamanho da resposta no prompt de sistema, como mostrado, para que os turnos permaneçam curtos e interrompíveis. 
  • Limite o histórico da conversa para que chamadas longas não aumentem o contexto do LLM indefinidamente. 
  • Defina uma duração máxima para a chamada como proteção contra sessões travadas que consomem simultaneidade silenciosamente.

Para continuar ajustando as partes que você controla, o documento sobre latência explica de onde vem o tempo até o primeiro áudio, a visão geral dos modelos aborda as compensações entre velocidade e qualidade, e o guia de WebSocket TTS em tempo real mostra como reduzir ainda mais a latência de síntese com entrada incremental de texto.

Crie agentes de voz prontos para produção com a ElevenAPI

Agora que os 20 minutos passaram, você tem todas as camadas de um agente de voz pronto para produção. A Twilio cuida da telefonia, o Scribe v2 Realtime transcreve, um LLM gera respostas e o Flash v2.5 responde pelo mesmo WebSocket. 

Se preferir não manter a cascata por conta própria, o ElevenAgents oferece alternância de turnos, tratamento de interrupções e integração com telefonia como um serviço gerenciado, criado com os mesmos modelos que você acabou de conectar manualmente. 

Para continuar ajustando uma stack que você controla, explore a página do produto ElevenAPI para conhecer os planos, os limites de simultaneidade e a Voice Library. Ou cadastre-se e comece hoje mesmo a fazer sua primeira chamada.

Perguntas frequentes sobre como criar um agente de voz com Twilio e ElevenLabs

Artigos relacionados

Crie com o áudio de IA da mais alta qualidade