Salta al contenuto

Crea un agente vocale in 20 minuti con ElevenLabs e Twilio

Pubblicato

AscoltaAscolta questo articolo

Un agente vocale può rispondere alle chiamate in entrata, trascrivere in tempo reale con Speech to Text (STT), generare una risposta con un large language model (LLM) e parlare tramite un modulo Text to Speech (TTS). Con ElevenLabs e Twilio, puoi avere un agente funzionante su un vero numero di telefono in circa 20 minuti.

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 parola, 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 tra loro i tre servizi del tuo stack tecnologico.

  • Twilio: gestisce la chiamata e il trasporto audio.
  • ElevenLabs: gestisce STT tramite Scribe v2 Realtime e TTS tramite Flash v2.5.
  • LLM: gestisce le chiamate agli strumenti e scrive la 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 arriva al tuo server tramite Twilio. Twilio risponde alla chiamata PSTN, apre un WebSocket verso il tuo server e inoltra l’audio del chiamante come stream di frame mu-law codificati in base64. Il tuo server gestisce la cascata e invia l’audio sintetizzato sullo 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 contenenti payload mu-law (ulaw_8000) in base64.

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, poi sintetizzi la risposta con Flash v2.5 in ulaw_8000. Invi i frame mu-law sintetizzati a Twilio tramite 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 della latenza. Per ridurre il ritardo, trasmettiamo l’output del LLM token per token e iniziamo la sintesi prima che il modello abbia finito la frase.

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

Cosa ti serve prima di iniziare a creare un agente vocale

La guida presuppone che tu abbia quattro elementi pronti. Ognuno si configura rapidamente, 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 ElevenLabs: creata nella tua dashboard ElevenLabs. La chiave va nell’header xi-api-key ed è segreta, quindi tienila solo lato server. Vedi autenticazione API.
  3. Chiave API LLM: In questo tutorial Anthropic Claude e OpenAI sono backend intercambiabili, quindi scegli quello che preferisci.
  4. Ngrok (o un tunnel simile) per lo sviluppo locale: Twilio deve raggiungere il tuo server tramite un URL pubblico HTTPS e WSS, e ngrok lo fornisce 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 dà 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 permette di scrivere il gestore WebSocket al passo 2 con chiarezza.

Quando Twilio si connette al tuo WebSocket, invia una sequenza di messaggi di testo JSON, che possono avere 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é serve per inviare audio di ritorno, 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 in 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.

Passo 1: Fornisci il webhook TwiML

Quando arriva una chiamata, Twilio invia una richiesta HTTP al tuo webhook e tu rispondi con un 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 di ritorno, 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.

Passo 2: Accetta il WebSocket Media Stream

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

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

Passo 3: Trascrivi con Scribe v2 Realtime

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

Offre anche Voice Activity Detection per la segmentazione basata sul silenzio e il controllo manuale per finalizzare un segmento. Per un agente telefonico, la segmentazione guidata da VAD è di solito la scelta migliore perché una pausa naturale è il segnale più affidabile che il turno del chiamante sia finito.

Ecco i passaggi: apri uno stream STT quando inizia la chiamata. Invia ogni chunk mu-law in ingresso. Rispondi alle trascrizioni finali 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 nomi di campo. 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.

Passo 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. Trasmetti la 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 con 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.

Passo 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 trasmetti l’audio e inoltra ogni chunk tramite WebSocket come evento media.

Accumula i token LLM in frammenti di dimensione frase e sintetizza ogni frammento appena è pronto, invece di aspettare l’intera 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 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ò inviarvi richieste POST, quindi la prima cosa è confermare che la richiesta arrivi davvero da Twilio. Twilio firma ogni richiesta con il tuo Auth Token nell’header X-Twilio-Signature, e tu 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 esce mai dal backend; se parte della 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 le brevi finestre in cui si sta sintetizzando una risposta, non per tutta la durata della chiamata. Come regola generale, un limite di concorrenza di circa cinque può supportare circa 100 conversazioni simultanee, perché la generazione finisce molto prima della riproduzione.

Tuttavia, monitora sempre il margine invece di andare a stima. 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 i 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 questo interrompendo il ciclo) e invia a Twilio un messaggio clear per svuotare l’audio già in coda.

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 inviarlo, così l’agente sembra parlare sopra il chiamante.

Fai log, monitora e gestisci gli errori 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 più stabili.

Poi prevedi 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 tuo server vicino ai 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 errore 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, il documento 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 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 tutti i livelli di un agente vocale pronto per la produzione. Twilio gestisce la telefonia, Scribe v2 Realtime trascrive, un LLM genera le risposte e Flash v2.5 parla tramite lo stesso WebSocket.

Se preferisci non gestire tu la cascata, ElevenAgents offre turni di parola, 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.

Crea un agente vocale con Twilio ed ElevenLabs - FAQ

Articoli simili

Crea con l'audio IA della massima qualità