Salta al contenuto

Crea un agente vocale in 20 minuti con ElevenLabs e Twilio

Pubblicato
Ultimo aggiornamento

AscoltaAscolta questo articolo

Un agente vocale può rispondere alle chiamate in entrata, trascrivere in tempo reale i chiamanti con

Per gli sviluppatori, lo stack completo che userai è ElevenLabs per la sintesi vocale (Flash v2.5) e la trascrizione (Scribe v2 Realtime), Twilio per la telefonia e OpenAI o Anthropic come LLM. Tutti questi componenti sono intercambiabili, quindi puoi scegliere quelli che conosci meglio e usarli al loro posto.

Questo articolo mostra come creare un agente vocale in 20 minuti, usando Node.js e Typescript. Se vuoi un'alternativa gestita che si occupa di turni di conversazione, interruzioni e telefonia senza dover gestire tu la cascata, vai su ElevenAgents. 

Come funziona l’architettura di un agente vocale

Prima di scrivere codice, è utile capire come si collegano i tre servizi principali del tuo stack tecnologico.

  • Twilio: gestisce la chiamata telefonica e il trasporto audio.
  • ElevenLabs: gestisce STT tramite Scribe v2 Realtime e TTS tramite Flash v2.5.
  • LLM: gestisce le chiamate agli strumenti e la scrittura della risposta.

Ogni fase è un semplice adattatore, per questo puoi sostituire un servizio con un altro senza toccare il resto. Ad esempio, puoi passare da OpenAI LLM ad Anthropic senza dover riscrivere gli altri componenti.

Una chiamata telefonica raggiunge il tuo server tramite Twilio. Twilio risponde alla chiamata PSTN, apre un WebSocket verso il tuo server e inoltra l’audio del chiamante come uno stream di frame mu-law codificati in base64. Il tuo server gestisce la cascata e invia l’audio sintetizzato tramite lo stesso WebSocket, che Twilio riproduce al chiamante.

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

Ecco il flusso che userai per creare un agente vocale:

Un chiamante compone il tuo numero Twilio. Twilio recupera un documento TwiML dal tuo webhook. Il TwiML indica a Twilio di aprire uno stream Media verso il tuo endpoint WebSocket. Twilio invia l’audio in ingresso come eventi JSON che contengono payload mu-law base64 (ulaw_8000).

Il tuo server inoltra i chunk audio a Scribe v2 Realtime per la trascrizione in streaming. Quando il turno del chiamante si conclude, invii la trascrizione al LLM e poi sintetizzi la risposta con Flash v2.5 in ulaw_8000. Invi i frame mu-law sintetizzati a Twilio tramite il WebSocket, codificati in base64, e Twilio li riproduce al chiamante.

Scribe v2 Realtime emette trascrizioni parziali con circa 150ms di latenza, e Flash v2.5 ha un’inferenza modello di circa 75ms, esclusa la latenza di rete e applicativa. Il LLM è la parte più grande e meno prevedibile per il tempo di risposta audio, ed è dove va la maggior parte del budget di latenza. Per ridurre il ritardo, lo stream del LLM viene inviato token per token e si inizia a sintetizzare prima che il modello abbia finito la frase.

Per i compromessi tra i modelli dietro queste scelte, vedi la panoramica dei modelli e la guida su come funziona la latenza.

Cosa serve prima di iniziare a creare un agente vocale

La guida presuppone che tu abbia quattro elementi pronti. Ognuno si configura velocemente, ma la mancanza di uno blocca l’avvio del server.

Ecco cosa controllare prima di iniziare:

  1. Un numero Twilio con funzionalità Voice: Prendi nota del numero insieme al tuo Account SID e Auth Token dalla console Twilio.
  2. Chiave API di ElevenLabs: Creata nella tua dashboard di ElevenLabs. La chiave viaggia nell'header xi-api-key ed è segreta, quindi tienila solo lato server. Vedi
  3. Chiave API LLM:In questo tutorial Anthropic Claude e OpenAI sono backend intercambiabili, quindi scegli uno dei due.
  4. Ngrok (o altro tunnel) per sviluppo locale: Twilio deve raggiungere il tuo server tramite un URL pubblico HTTPS e WSS, e ngrok lo permette senza dover pubblicare nulla.

Imposta i tuoi segreti come variabili d’ambiente e non commetterli mai.

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"

Poi avvia un tunnel puntando alla porta che userà il tuo server:

ngrok http 8080

Come funziona il protocollo Twilio Media Streams

Twilio non ti fornisce un socket audio grezzo. Invece, incapsula tutto in un protocollo JSON strutturato su WebSocket. Capire i quattro tipi di evento e il formato di invio ti aiuterà a scrivere il gestore WebSocket al passaggio 2.

Quando Twilio si connette al tuo WebSocket, invia una sequenza di messaggi di testo JSON, che possono essere di quattro tipi di evento.

L’evento connected arriva per primo e conferma che il WebSocket è attivo. L’evento start viene inviato una sola volta quando inizia lo stream media; contiene uno streamSid che devi salvare, perché ti serve per inviare audio indietro, e include anche i metadati della chiamata in start.customParameters e start.callSid.

L’evento media è ricorrente: media.payload è un chunk di audio mu-law 8kHz codificato in base64, 20ms per frame, e media.track è inbound per l’audio del chiamante. Infine, stop viene inviato quando lo stream termina, di solito perché la chiamata è stata chiusa.

Per riprodurre audio, invii un messaggio di tipo media con lo stesso streamSid e un payload mu-law base64. Per interrompere l’audio già in coda, invii un messaggio clear con lo streamSid, che svuota il buffer di uscita di Twilio.

Le codifiche in ingresso e in uscita sono identiche (ulaw_8000). Richiediamo ulaw_8000 da ElevenLabs Text to Speech e inoltriamo i byte direttamente a Twilio senza ricampionare nulla.

Passaggio 1: Fornire il webhook TwiML

Quando arriva una chiamata, Twilio invia una richiesta HTTP al tuo webhook e tu rispondi con TwiML che collega la chiamata al tuo Media Stream. Il verbo <Connect><Stream> apre un WebSocket bidirezionale. Usa <Connect> invece di <Start>: mantiene la chiamata attiva per tutta la durata dello stream e ti permette di inviare audio indietro, che è lo scopo di questa configurazione.

Il TwiML che restituisce il webhook è:

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

In Express, è un singolo handler POST che inserisce l’host e restituisce il 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);
});

Nella console Twilio, imposta il webhook "A call comes in" del numero su https://your-subdomain.ngrok.app/incoming-call usando HTTP POST.

Passaggio 2: Accetta il WebSocket Media Stream

Il gestore WebSocket legge gli eventi Twilio, gestisce la cascata e scrive l’audio di risposta.

Manteniamo un piccolo stato per ogni chiamata: lo streamSid, una connessione STT e un flag che indica se l’agente sta parlando. Il gestore decodifica ogni frame media in ingresso da base64 e inoltra i byte mu-law grezzi a 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;
    }
  });
});

Passaggio 3: Trascrivi con Scribe v2 Realtime

Scribe v2 Realtime accetta chunk audio in streaming e restituisce trascrizioni parziali e finali, supportando direttamente la codifica mu-law, quindi gli inviamo i frame di Twilio senza modificarli.

Offre anche Voice Activity Detection per la segmentazione basata sul silenzio e il controllo manuale del commit per finalizzare un segmento. Per un

Ecco i passaggi: apri uno stream STT quando inizia la chiamata. Invia ogni chunk mu-law in ingresso. Rispondi alle trascrizioni finalizzate invocando il LLM.

Il client STT realtime è ancora in evoluzione, quindi la struttura qui sotto è gestita da un piccolo adattatore TypeScript (openRealtimeStt) che implementi contro l’API live invece che su un set fisso di campi. Considera onFinal come l’hook che passa un turno completato del chiamante alla fase successiva.

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

La latenza di riconoscimento realtime è circa 150ms per le parziali, così il gap percepito tra la fine del chiamante e l’inizio dell’agente resta minimo. Per la versione batch e tutte le funzionalità, vedi la documentazione Speech to Text e la pagina prodotto Speech to Text realtime.

Passaggio 4: Genera una risposta con un LLM

Questa è la fase che produce la risposta. Il LLM prende la cronologia della conversazione e restituisce il testo dell’assistente. Esegui lo streaming della risposta così puoi iniziare la sintesi già dalla prima frase.

Qui è supportato da 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
  }
}

Il model ID sopra, gpt-4.1-mini, è un esempio a bassa latenza; claude-haiku-4-5 è un’opzione simile su Anthropic. Entrambi possono essere usati per lo stesso contratto llmReply; basta cambiare il corpo della funzione e il resto dell’agente resta invariato.

Il system prompt limita la lunghezza della risposta, cosa importante al telefono: risposte lunghe sembrano lente e sono difficili da interrompere in modo naturale.

Passaggio 5: Sintetizza con Flash TTS in ulaw_8000

Ora il testo deve diventare audio che Twilio può riprodurre. Richiedi Flash v2.5 con outputFormat: "ulaw_8000" così i byte corrispondono alla codifica attesa da Twilio, poi esegui lo streaming dell’audio e inoltra ogni chunk tramite il WebSocket come evento media.

Accumula i token LLM in frammenti di dimensione frase e sintetizza ogni frammento appena è pronto, invece di aspettare tutta la risposta. Così riduci il tempo di attesa perché il chiamante sente la prima frase mentre il modello sta ancora producendo la seconda. Per un controllo più avanzato sulla sintesi incrementale, la guida TTS realtime via WebSocket mostra come inviare testo a un unico socket di sintesi aperto; l’approccio HTTP streaming qui sotto è più semplice e va bene per turni conversazionali brevi.

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

Rendi il tuo agente vocale IA pronto per la produzione

Dopo questi cinque passaggi hai un agente funzionante. Non è però ancora una distribuzione in produzione.

Ci sono diversi aspetti da considerare prima di mettere l’agente su una vera linea telefonica.

Valida le firme webhook di Twilio

Chiunque conosca il tuo URL webhook può inviare richieste POST, quindi la prima cosa da fare è confermare che la richiesta arrivi davvero da Twilio. Twilio firma ogni richiesta con il tuo Auth Token nell’header X-Twilio-Signature, e devi rifiutare tutto ciò che non supera la validazione. La firma è calcolata sull’URL completo e sui parametri POST, quindi devi calcolarla nello stesso modo di Twilio.

L’helper di Twilio lo fa per te:

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

Gestisci i segreti correttamente

Tieni ELEVENLABS_API_KEY, la chiave LLM e TWILIO_AUTH_TOKEN in un secrets manager, non nel codice sorgente né in file env in chiaro nel repo. Limita la chiave ElevenLabs solo agli endpoint necessari e imposta un limite di credito così, in caso di leak, il danno è limitato. 

I piani Enterprise possono anche limitare una chiave a specifici range IP tramite whitelist. Questo server usa la chiave API direttamente perché non lascia mai il backend; se la logica audio passasse su browser o mobile, useresti token monouso così la chiave non viene mai esposta lato client.

Comprendi il limite di concorrenza

Ogni piano ha un limite di concorrenza che varia per famiglia di modelli, e il limite conta quante richieste stanno generando audio contemporaneamente.

Per un agente telefonico, questo conteggio gioca a tuo favore. La generazione audio è più veloce della riproduzione, quindi ogni chiamata consuma concorrenza TTS solo durante la breve finestra in cui viene sintetizzata la risposta, non per tutta la durata della chiamata. Come regola generale, un limite di concorrenza di circa cinque può supportare circa 100 trasmissioni conversazionali simultanee, perché la generazione finisce molto prima della riproduzione.

Tuttavia, monitora il margine invece di indovinare. Le risposte ElevenLabs espongono gli header current-concurrent-requests e maximum-concurrent-requests; registrali e imposta alert quando ti avvicini al massimo. Se superi il limite, le richieste vengono messe in coda per priorità, di solito aggiungendo circa 50ms, e un sovraccarico prolungato restituisce HTTP 429.

Gestisci le risposte HTTP 429 con un breve backoff. Se persistono, aumenta i limiti dalla pagina prezzi o, per clienti Enterprise, tramite il tuo account manager.

Gestisci barge-in e interruzioni

Un chiamante che inizia a parlare mentre l’agente sta parlando si aspetta che l’agente si fermi. Questo è il barge-in, e gestirlo bene è fondamentale per rendere l’agente naturale e non robotico.

Rileva la voce del chiamante durante la riproduzione dell’agente usando il segnale VAD dello STT. Quando lo rilevi, fai due cose: interrompi l’invio dei chunk TTS (il flag agentSpeaking in speak già gestisce il ciclo) e invia a Twilio un messaggio clear per svuotare l’audio già in coda dal suo lato.

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

Se salti il clear, Twilio continua a riprodurre l’audio bufferizzato anche dopo che hai smesso di inviare, così l’agente sembra parlare sopra il chiamante.

Fai log, monitora e gestisci i fallimenti in modo elegante

Strumenta ogni fase così puoi attribuire la latenza quando una chiamata sembra lenta. Misura il tempo dal transcript finale al primo token LLM, dal primo token LLM al primo byte TTS, e dal primo byte TTS al frame inviato a Twilio. Scoprirai che la maggior parte della latenza variabile è nella fase LLM; le fasi STT e TTS sono molto più stabili.

Poi pianifica i fallimenti parziali. Il LLM può andare in timeout, lo stream STT può cadere e il round-trip di rete verso ElevenLabs varia da circa 20 a 200ms su internet pubblico a seconda della geografia. Colloca il server vicino ai tuoi chiamanti, non solo vicino a ElevenLabs, visto che ElevenLabs già instrada verso il cluster più vicino tra Nord America, Europa e Sud-Est Asiatico.

Quando una fase fallisce, non lasciare il chiamante in silenzio: sintetizza una breve frase di fallback ("Scusa, puoi ripetere?") e mantieni la chiamata attiva. Avvolgi ogni fase in un timeout e in un try/catch così un turno fallito non chiude tutto il WebSocket.

Ci sono ancora alcune impostazioni di default utili prima di andare in produzione:

  • Limita la lunghezza delle risposte nel system prompt, come mostrato, così i turni restano brevi e facilmente interrompibili.
  • Limita la cronologia della conversazione così le chiamate lunghe non fanno crescere il contesto LLM all’infinito.
  • Imposta una durata massima della chiamata come protezione contro sessioni bloccate che consumano concorrenza.

Per continuare a ottimizzare le parti che controlli, la documentazione sulla latenza spiega da dove arriva il tempo di risposta audio, la panoramica dei modelli copre i compromessi tra velocità e qualità, e la guida TTS realtime via WebSocket mostra come ridurre la latenza di sintesi con input testuale incrementale.

Crea agenti vocali pronti per la produzione con ElevenAPI

Ora che sono passati 20 minuti, hai ogni livello di un

Se preferisci non gestire tu la cascata, ElevenAgents offre turn-taking, gestione delle interruzioni e integrazione telefonica come servizio gestito, basato sugli stessi modelli che hai appena collegato a mano.

Per continuare a ottimizzare uno stack che controlli, esplora la pagina prodotto ElevenAPI per piani, limiti di concorrenza e la voice library. In alternativa, registrati e inizia oggi stesso a fare la tua prima chiamata.

FAQ: crea un agente vocale con Twilio ed ElevenLabs

Articoli simili

Crea con l'audio IA della massima qualità