Vai 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 chi chiama in tempo reale con Speech to Text (STT), generare una risposta con un modello linguistico di grandi dimensioni (LLM) e rispondere a voce con un modulo Text to Speech (TTS). Con ElevenLabs e Twilio, puoi avere un agente funzionante su un numero telefonico reale in circa 20 minuti.

Per gli sviluppatori, lo stack completo che userai comprende 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 sostituibili: puoi quindi 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 preferisci un'alternativa gestita che gestisca l'alternanza dei turni, le interruzioni e la telefonia senza dover mantenere tu stesso la cascata, vai a ElevenAgents. 

Come funziona l'architettura di un agente vocale

Prima di scrivere codice, è utile capire come si collegano i tre servizi 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 il tool calling e la generazione della risposta.

Ogni fase è un adapter leggero, quindi puoi sostituirne una con un altro servizio senza modificare il resto. Ad esempio, puoi sostituire l'LLM di OpenAI con 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 di chi chiama come stream di frame mu-law codificati in base64. Il tuo server esegue la cascata e trasmette l'audio sintetizzato sullo stesso WebSocket; Twilio lo riproduce per chi chiama.

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: 

Chi chiama compone il tuo numero Twilio. Twilio recupera un documento TwiML dal tuo webhook. Il TwiML comunica a Twilio di aprire un Media Stream verso il tuo endpoint WebSocket. Twilio trasmette l'audio in entrata come eventi JSON contenenti payload mu-law (ulaw_8000) codificati in base64. 

Il tuo server inoltra i chunk audio a Scribe v2 Realtime per la trascrizione in streaming. Quando si conclude un turno di chi chiama, invii la trascrizione all'LLM e 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 per chi chiama.

Scribe v2 Realtime genera trascrizioni parziali con una latenza di circa 150 ms, mentre Flash v2.5 esegue l'inferenza del modello in circa 75 ms, esclusa la latenza di rete e dell'applicazione. L'LLM contribuisce maggiormente e in modo meno prevedibile al tempo fino al primo audio, assorbendo gran parte del budget di latenza. Per ridurre l'intervallo, trasmettiamo l'output dell'LLM token per token e iniziamo la sintesi prima che il modello abbia terminato la frase.

Per i compromessi tra modelli alla base di queste scelte, consulta la panoramica dei modelli e l'approfondimento su come comprendere la latenza.

Cosa ti serve prima di iniziare a creare un agente vocale

Questa guida presuppone che tu abbia già quattro elementi. Ognuno è rapido da configurare, ma se ne manca anche uno il server non potrà essere eseguito.

Ecco i prerequisiti da verificare:

  1. Un numero telefonico Twilio con funzionalità Voice: annota il numero insieme ad Account SID e Auth Token dalla console Twilio.
  2. Chiave API di ElevenLabs: creala nella dashboard di ElevenLabs. La chiave viene inviata nell'header xi-api-key ed è segreta, quindi conservala esclusivamente sul server. Consulta autenticazione API.
  3. Chiave API dell'LLM: questo tutorial considera Anthropic Claude e OpenAI come backend intercambiabili, quindi scegline uno.
  4. Ngrok (o qualsiasi tunnel) per lo sviluppo locale: Twilio deve poter raggiungere il tuo server tramite un URL HTTPS e WSS pubblico, e ngrok lo consente senza dover distribuire nulla.

Imposta i segreti come variabili d'ambiente e non eseguirne mai il commit.

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"

Quindi avvia un tunnel verso la porta che utilizzerà il tuo server:

ngrok http 8080

Comprendere il protocollo Twilio Media Streams

Twilio non fornisce un socket audio raw. Al suo posto, incapsula tutto in un protocollo JSON strutturato su WebSocket. Comprendere i quattro tipi di evento e il formato di invio ti permetterà di capire completamente l'handler WebSocket nel Passaggio 2 prima ancora di scriverlo.

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 volta all'avvio dello stream multimediale; contiene uno streamSid che devi memorizzare, poiché ti serve per rimandare l'audio, 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 a 8 kHz codificato in base64, di 20 ms per frame, e media.track è inbound per l'audio di chi chiama. Infine, stop viene inviato quando lo stream termina, in genere perché la chiamata è stata chiusa.

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

Le codifiche in entrata e in uscita sono identiche (ulaw_8000). Richiediamo ulaw_8000 a ElevenLabs Text to Speech e inoltriamo i byte direttamente a Twilio senza alcun ricampionamento intermedio.

Passaggio 1: pubblica 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 qui <Connect> anziché <Start>: mantiene attiva la chiamata per tutta la durata dello stream e ti consente di rimandare l'audio, che è lo scopo di questa configurazione.

Il TwiML restituito dal webhook è:

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

In Express, si tratta di un unico 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

L'handler WebSocket legge gli eventi Twilio, gestisce la cascata e rimanda l'audio.

Manteniamo una piccola quantità di stato per chiamata: streamSid, una connessione STT e un flag che indica se l'agente sta parlando. L'handler decodifica ogni frame multimediale in entrata da base64 e inoltra i byte mu-law raw 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; supporta inoltre direttamente la codifica mu-law, quindi gli inviamo i frame di Twilio senza modificarli. 

Offre inoltre Voice Activity Detection per la segmentazione basata sul silenzio e il controllo manuale del commit per finalizzare un segmento. Per un agente telefonico, la segmentazione basata su VAD è solitamente la scelta giusta, poiché una pausa naturale è il segnale più affidabile che il turno di chi chiama è terminato.

Ecco i passaggi: apri uno stream STT all'avvio della chiamata. Invia ogni chunk mu-law in entrata. Reagisci alle trascrizioni finalizzate invocando l'LLM.

L'interfaccia del client STT realtime è ancora in evoluzione, quindi la struttura seguente è mantenuta dietro un piccolo adapter TypeScript (openRealtimeStt) che implementi rispetto all'API live, anziché a un insieme fisso di nomi dei campi. Considera onFinal come l'hook che passa un turno completato di chi chiama 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 del riconoscimento realtime è di circa 150 ms per le trascrizioni parziali, mantenendo ridotto l'intervallo percepito tra la fine dell'intervento di chi chiama e l'inizio dell'agente. Per l'equivalente batch e il set completo di funzionalità, consulta la documentazione Speech to Text e la pagina del prodotto Speech to Text realtime.

Passaggio 4: genera una risposta con un LLM

Questa è la fase che produce la risposta. L'LLM riceve la cronologia della conversazione e restituisce il testo dell'assistente. Trasmetti la risposta in streaming così da poter avviare la sintesi già dalla prima frase.

Qui usiamo OpenAI come backend:

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

L'ID del modello indicato sopra, gpt-4.1-mini, è un esempio di scelta a bassa latenza; claude-haiku-4-5 è un'opzione comparabile sul lato Anthropic. Entrambi i provider possono supportare lo stesso contratto llmReply: sostituisci il body della funzione e il resto dell'agente rimane invariato.

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

Passaggio 5: sintetizza con Flash TTS in ulaw_8000

Il testo ora deve diventare audio che Twilio possa riprodurre. Richiedi Flash v2.5 con outputFormat: "ulaw_8000" affinché i byte corrispondano alla codifica prevista da Twilio, quindi trasmetti l'audio in streaming e inoltra ogni chunk sul WebSocket come evento media.

Accumula i token dell'LLM in frammenti delle dimensioni di una frase e sintetizza ogni frammento non appena è completo, invece di attendere l'intera risposta. Questo riduce il tempo fino al primo audio perché chi chiama sente la prima frase mentre il modello sta ancora generando la seconda. Per un controllo più approfondito sulla sintesi incrementale, la guida al WebSocket TTS realtime mostra come inviare testo a un unico socket di sintesi aperto; l'approccio HTTP streaming seguente è più semplice e adeguato per brevi turni conversazionali.

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

Rendere il tuo agente vocale IA pronto per la produzione

Dopo i cinque passaggi sopra, hai un agente funzionante. Ma questo non equivale a una distribuzione in produzione. 

Prima di mettere l'agente su una linea telefonica reale, ci sono diversi aspetti di cui devi tenere conto.

Convalida le firme dei webhook Twilio

Chiunque conosca l'URL del tuo webhook può inviargli richieste POST, quindi il primo compito è confermare che la richiesta provenga 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 convalida. La firma viene 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 correttamente i segreti

Conserva ELEVENLABS_API_KEY, la chiave dell'LLM e TWILIO_AUTH_TOKEN in un secrets manager, non nel codice sorgente né in file env in testo semplice di cui è stato eseguito il commit in un repository. Limita la chiave ElevenLabs agli endpoint necessari a questo servizio e assegnale una quota di crediti, così una perdita ha un impatto circoscritto anziché illimitato. 

I piani Enterprise possono inoltre limitare una chiave a intervalli IP specifici tramite IP whitelisting. Questo server usa direttamente la chiave API perché non lascia mai il tuo backend; se una parte della logica audio venisse spostata in un browser o in un client mobile, passeresti a token monouso affinché la chiave non venga mai esposta lato client.

Comprendi il limite di concorrenza

Ogni piano prevede un limite di concorrenza diverso per ogni famiglia di modelli; il limite conta quante richieste stanno generando audio attivamente nello stesso momento.

Per un agente telefonico, questo calcolo gioca a tuo favore. La generazione dell'audio è più rapida della riproduzione, quindi ogni chiamata usa la concorrenza TTS soltanto nelle brevi finestre in cui una risposta viene sintetizzata, non per l'intera durata della chiamata. Come stima indicativa, un limite di concorrenza di circa cinque può supportare nell'ordine di 100 chiamate conversazionali simultanee, poiché la generazione termina ben prima della riproduzione.

Anche così, monitora il margine disponibile anziché fare supposizioni. Le risposte di ElevenLabs espongono gli header current-concurrent-requests e maximum-concurrent-requests; registrali nei log e configura avvisi quando ti avvicini al massimo. Quando superi il limite, le richieste vengono accodate in base alla priorità, aggiungendo in genere circa 50 ms, mentre un sovraccarico prolungato restituisce HTTP 429. 

Gestisci le risposte HTTP 429 con un breve backoff. Se persistono, aumenta i limiti passando a un piano superiore nella pagina dei prezzi o, per i clienti Enterprise, tramite il tuo account manager.

Gestisci il barge-in e le interruzioni

Chi inizia a parlare mentre l'agente sta parlando si aspetta che l'agente si fermi. Questo è il barge-in e gestirlo correttamente è una parte importante di ciò che rende un agente naturale anziché predefinito.

Rileva la voce di chi chiama durante la riproduzione dell'agente usando il segnale VAD di STT. Quando la rilevi, fai due cose. Prima, interrompi l'inoltro dei chunk TTS: il flag agentSpeaking in speak lo gestisce già interrompendo il loop. Seconda cosa, invia a Twilio un messaggio clear per svuotare l'audio che hai già accodato sul suo lato.

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

Se ometti il clear, Twilio continua a riprodurre l'audio nel buffer dopo che hai smesso di inviarlo, quindi l'agente sembrerà parlare sopra chi chiama.

Registra nei log, monitora e gestisci gli errori in modo sicuro

Strumenta ogni fase per poter attribuire la latenza quando una chiamata sembra lenta. Misura l'intervallo dalla trascrizione finale al primo token dell'LLM, dal primo token dell'LLM al primo byte TTS e dal primo byte TTS al frame inviato a Twilio. Noterai che gran parte della latenza variabile è nella fase LLM; le fasi STT e TTS sono relativamente stabili.

Poi pianifica i guasti parziali. L'LLM può andare in timeout, lo stream STT può interrompersi e il round trip di rete verso ElevenLabs varia da circa 20 a 200 ms sulla rete Internet pubblica, a seconda della geografia. Colloca il tuo server vicino a chi chiama, non soltanto vicino a ElevenLabs, dato che ElevenLabs instrada già verso il più vicino dei suoi cluster in Nord America, Europa e Sud-est asiatico. 

Quando una fase fallisce, non lasciare chi chiama nel silenzio: sintetizza una breve frase di fallback ("Mi dispiace, puoi ripeterlo?") e mantieni attiva la chiamata. Avvolgi ogni fase in un timeout e in un try/catch affinché un singolo turno non riuscito non chiuda l'intero WebSocket.

Prima della distribuzione, vale la pena impostare anche qualche altra configurazione predefinita:

  • Limita la lunghezza delle risposte nel system prompt, come mostrato, affinché i turni restino brevi e interrompibili. 
  • Limita la cronologia della conversazione affinché le chiamate lunghe non facciano crescere senza limiti il contesto dell'LLM. 
  • Imposta una durata massima della chiamata come protezione contro sessioni bloccate che consumano concorrenza senza farsi notare.

Per continuare a ottimizzare le parti che controlli, il documento sulla latenza spiega da cosa dipende il tempo fino al primo audio, la panoramica dei modelli illustra i compromessi tra velocità e qualità, mentre la guida al WebSocket TTS realtime mostra come ridurre ulteriormente la latenza di sintesi con l'input di testo incrementale.

Crea agenti vocali pronti per la produzione con ElevenAPI

Ora che sono trascorsi 20 minuti, hai tutti i livelli di un agente vocale di produzione. Twilio gestisce la telefonia, Scribe v2 Realtime trascrive, un LLM genera le risposte e Flash v2.5 risponde a voce sullo stesso WebSocket. 

Se preferisci non mantenere tu stesso la cascata, ElevenAgents offre l'alternanza dei turni, la gestione delle interruzioni e l'integrazione telefonica come servizio gestito basato sugli stessi modelli che hai appena collegato manualmente. 

Per continuare a ottimizzare uno stack che controlli, esplora la pagina del prodotto ElevenAPI per piani, limiti di concorrenza e la Voice Library. In alternativa, registrati e inizia oggi a effettuare la tua prima chiamata.

FAQ: crea un agente vocale con Twilio ed ElevenLabs

Articoli simili

Crea con l'audio IA della massima qualità