Passer au contenu

Créez un agent vocal en 20 minutes avec ElevenLabs et Twilio

Publié
Dernière mise à jour

ÉcouterÉcouter cet article

Un agent vocal peut répondre aux appels entrants, transcrire les appelants en temps réel avec Speech to Text (STT), générer une réponse avec un grand modèle de langage (LLM), puis répondre avec un module Text to Speech (TTS). Avec ElevenLabs et Twilio, vous pouvez avoir un agent opérationnel sur un vrai numéro de téléphone en environ 20 minutes.

Pour les développeurs, la stack complète utilisée ici est ElevenLabs pour la synthèse vocale (Flash v2.5) et la transcription (Scribe v2 Realtime), Twilio pour la téléphonie, et OpenAI ou Anthropic comme LLM. Tous ces éléments sont interchangeables, donc vous pouvez choisir les composants que vous connaissez le mieux.

Cet article montre comment créer un agent vocal en 20 minutes, avec Node.js et Typescript. Si vous préférez une alternative gérée qui prend en charge la gestion des tours de parole, les interruptions et la téléphonie sans gérer la cascade vous-même, rendez-vous sur ElevenAgents. 

Comment fonctionne l’architecture d’un agent vocal

Avant d’écrire du code, il est utile de comprendre comment les trois services de votre stack technique sont connectés.

  • Twilio : Gère l’appel téléphonique et le transport audio.
  • ElevenLabs : Gère le STT via Scribe v2 Realtime et le TTS via Flash v2.5.
  • LLM : Gère l’appel d’outils et la rédaction de la réponse.

Chaque étape est un simple adaptateur, ce qui permet d’en remplacer une par un autre service sans toucher au reste. Par exemple, vous pouvez remplacer le LLM OpenAI par Anthropic sans avoir à réécrire les autres composants.

Un appel téléphonique arrive sur votre serveur via Twilio. Twilio répond à l’appel PSTN, ouvre un WebSocket vers votre serveur et transmet l’audio de l’appelant sous forme de flux de trames mu-law encodées en base64. Votre serveur gère la cascade et renvoie l’audio synthétisé sur ce même WebSocket, que Twilio diffuse à l’appelant.

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

Voici le déroulement que vous utiliserez pour créer un agent vocal :

Un appelant compose votre numéro Twilio. Twilio récupère un document TwiML depuis votre webhook. Le TwiML indique à Twilio d’ouvrir un Media Stream vers votre endpoint WebSocket. Twilio transmet l’audio entrant sous forme d’événements JSON contenant des payloads mu-law base64 (ulaw_8000).

Votre serveur transmet les morceaux audio à Scribe v2 Realtime pour la transcription en streaming. Quand un tour de parole est terminé, vous envoyez la transcription au LLM, puis vous synthétisez la réponse avec Flash v2.5 en ulaw_8000. Vous renvoyez les trames mu-law synthétisées à Twilio via le WebSocket, encodées en base64, et Twilio les diffuse à l’appelant.

Scribe v2 Realtime fournit des transcriptions partielles avec environ 150 ms de latence, et Flash v2.5 fonctionne à environ 75 ms d’inférence, hors latence réseau et application. Le LLM est la partie la plus imprévisible du temps de réponse, et c’est là que la majorité de la latence se concentre. Pour réduire ce délai, nous diffusons la sortie du LLM token par token et commençons la synthèse avant la fin de la phrase.

Pour comprendre les choix de modèles, consultez la présentation des modèles et l’explication sur la gestion de la latence.

Ce qu’il vous faut avant de commencer à créer un agent vocal

Ce guide suppose que vous avez quatre éléments prêts. Chacun se configure rapidement, mais l’absence de l’un d’eux bloquera le serveur.

Voici les prérequis à vérifier :

  1. Un numéro Twilio avec la fonctionnalité Voice : Notez ce numéro ainsi que votre Account SID et Auth Token depuis la console Twilio.
  2. Clé API ElevenLabs : créée dans votre tableau de bord ElevenLabs. La clé circule dans l’en-tête xi-api-key et reste confidentielle, gardez-la uniquement côté serveur. Voir
  3. Clé API LLM : Ce tutoriel considère Anthropic Claude et OpenAI comme des backends interchangeables, choisissez donc l’un des deux.
  4. Ngrok (ou tout tunnel) pour le développement local : Twilio doit accéder à votre serveur via une URL HTTPS et WSS publique, et ngrok fournit cela sans déploiement.

Définissez vos secrets comme variables d’environnement, et ne les commitez jamais.

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"

Ensuite, lancez un tunnel pointant vers le port utilisé par votre serveur :

ngrok http 8080

Comprendre le protocole Twilio Media Streams

Twilio ne fournit pas de socket audio brut. Tout est encapsulé dans un protocole JSON structuré via WebSocket. Comprendre les quatre types d’événements et le format d’envoi vous permettra de bien écrire le handler WebSocket à l’étape 2.

Une fois connecté à votre WebSocket, Twilio envoie une séquence de messages texte JSON, qui peuvent avoir quatre types d’événements.

L’événement connected arrive en premier et confirme que le WebSocket est ouvert. L’événement start est envoyé une fois au début du flux média ; il contient un streamSid à stocker, car il sera nécessaire pour renvoyer de l’audio, ainsi que des métadonnées d’appel sous start.customParameters et start.callSid.

L’événement media est récurrent : media.payload est un morceau d’audio mu-law 8 kHz encodé en base64, 20 ms par trame, et media.track correspond à l’audio entrant de l’appelant. Enfin, stop est envoyé à la fin du flux, généralement quand l’appel est raccroché.

Pour rejouer de l’audio, vous envoyez un message de type media avec le même streamSid et un payload mu-law en base64. Pour interrompre l’audio déjà en file d’attente, vous envoyez un message clear avec le streamSid, ce qui vide le buffer sortant de Twilio.

Les encodages entrant et sortant sont identiques (ulaw_8000). Nous demandons ulaw_8000 à ElevenLabs Text to Speech et transmettons les octets directement à Twilio sans rééchantillonnage.

Étape 1 : Servir le webhook TwiML

Lorsqu’un appel arrive, Twilio fait une requête HTTP à votre webhook, et vous répondez avec du TwiML qui connecte l’appel à votre Media Stream. Le verbe <Connect><Stream> ouvre un WebSocket bidirectionnel. Utilisez <Connect> plutôt que <Start> ici : cela maintient l’appel actif pendant toute la durée du flux et vous permet de renvoyer de l’audio, ce qui est le but de cette configuration.

Le TwiML renvoyé par le webhook est :

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

En Express, il s’agit d’un simple handler POST qui complète l’hôte et renvoie le document :

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

Dans la console Twilio, définissez le webhook "A call comes in" du numéro sur https://your-subdomain.ngrok.app/incoming-call en HTTP POST.

Étape 2 : Accepter le WebSocket Media Stream

Le handler WebSocket lit les événements Twilio, gère la cascade et renvoie l’audio.

On conserve un petit état par appel : le streamSid, une connexion STT et un indicateur pour savoir si l’agent parle. Le handler décode chaque trame media entrante depuis base64 et transmet les octets mu-law bruts au 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;
    }
  });
});

Étape 3 : Transcrire avec Scribe v2 Realtime

Scribe v2 Realtime accepte les morceaux audio en streaming et renvoie des transcriptions partielles et finales. Il prend en charge l’encodage mu-law directement, donc on lui transmet les trames Twilio sans modification.

Il propose aussi la détection d’activité vocale (VAD) pour segmenter selon les silences et un contrôle manuel pour finaliser un segment. Pour un agent téléphonique, la segmentation basée sur la VAD est généralement le meilleur choix, car une pause naturelle est le signal le plus fiable de fin de tour de parole.

Voici les étapes : Ouvrir un flux STT au début de l’appel. Envoyer chaque morceau mu-law entrant. Réagir aux transcriptions finalisées en appelant le LLM.

L’interface du client STT temps réel évolue encore, donc la structure ci-dessous est encapsulée dans un petit adaptateur TypeScript (openRealtimeStt) à implémenter selon l’API en direct plutôt qu’un schéma figé. Considérez onFinal comme le point de passage d’un tour de parole terminé à l’étape suivante.

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 latence de reconnaissance en temps réel est d’environ 150 ms pour les transcriptions partielles, ce qui réduit l’écart perçu entre la fin de parole de l’appelant et le début de réponse de l’agent. Pour la version batch et toutes les fonctionnalités, consultez la documentation Speech to Text et la page produit Speech to Text temps réel.

Étape 4 : Générer une réponse avec un LLM

C’est l’étape qui produit la réponse. Le LLM prend l’historique de la conversation et renvoie le texte de l’assistant. Diffusez la réponse pour pouvoir commencer la synthèse dès la première phrase.

Ici, c’est OpenAI qui est utilisé :

// ... 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 de modèle ci-dessus, gpt-4.1-mini, est un exemple de choix à faible latence ; claude-haiku-4-5 est une option comparable chez Anthropic. Les deux fournisseurs peuvent être utilisés pour le même contrat llmReply ; il suffit de changer le corps de la fonction, le reste de l’agent ne change pas.

Le prompt système limite la longueur de la réponse, ce qui est important au téléphone : des réponses longues paraissent lentes et sont difficiles à interrompre naturellement.

Étape 5 : Synthétiser avec Flash TTS en ulaw_8000

Le texte doit maintenant devenir un audio que Twilio peut lire. Demandez Flash v2.5 avec outputFormat : "ulaw_8000" pour que les octets correspondent à l’encodage attendu par Twilio, puis diffusez l’audio et renvoyez chaque morceau sur le WebSocket comme événement media.

Regroupez les tokens LLM en fragments de taille phrase et synthétisez chaque fragment dès qu’il est prêt, au lieu d’attendre toute la réponse. Cela réduit le temps avant la première réponse audio, car l’appelant entend la première phrase pendant que le modèle génère la suivante. Pour un contrôle plus fin de la synthèse incrémentale, le guide TTS temps réel WebSocket montre comment envoyer du texte dans une seule socket de synthèse ouverte ; l’approche HTTP streaming ci-dessous est plus simple et suffisante pour des échanges courts.

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

Renforcer votre agent vocal IA pour la production

Après ces cinq étapes, vous avez un agent fonctionnel. Ce n’est pas encore un déploiement en production.

Il y a plusieurs points à surveiller avant de mettre l’agent sur une vraie ligne téléphonique.

Validez les signatures webhook Twilio

Toute personne connaissant votre URL webhook peut y faire un POST, donc la première étape est de vérifier que la requête vient bien de Twilio. Twilio signe chaque requête avec votre Auth Token dans l’en-tête X-Twilio-Signature, et vous devez rejeter toute requête qui échoue à la validation. La signature est calculée sur l’URL complète et les paramètres POST, donc vous devez la calculer comme Twilio.

L’outil Twilio s’en charge pour vous :

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

Gérez correctement les secrets

Gardez ELEVENLABS_API_KEY, la clé LLM et TWILIO_AUTH_TOKEN dans un gestionnaire de secrets, pas dans le code source ni dans des fichiers env en clair dans un dépôt. Limitez la clé ElevenLabs aux endpoints nécessaires et fixez un quota de crédits pour limiter l’impact en cas de fuite.

Les offres Entreprise peuvent aussi restreindre une clé à certaines plages IP grâce au whitelisting. Ce serveur utilise la clé API directement car elle ne quitte jamais votre backend ; si une partie de la logique audio passait côté navigateur ou mobile, il faudrait passer à des tokens à usage unique pour ne jamais exposer la clé côté client.

Comprenez la limite de concurrence

Chaque offre a une limite de concurrence qui varie selon la famille de modèles, et cette limite compte le nombre de requêtes générant de l’audio en même temps.

Pour un agent téléphonique, ce calcul joue en votre faveur. La génération audio est plus rapide que la lecture, donc chaque appel ne consomme la concurrence TTS que pendant les brefs moments où une réponse est synthétisée, pas pendant toute la durée de l’appel. En règle générale, une limite de concurrence d’environ cinq permet de gérer une centaine de conversations simultanées, car la génération se termine bien avant la lecture.

Malgré tout, surveillez la marge plutôt que de deviner. Les réponses ElevenLabs incluent les en-têtes current-concurrent-requests et maximum-concurrent-requests ; enregistrez-les et déclenchez une alerte à l’approche du maximum. Si vous dépassez la limite, les requêtes sont mises en file d’attente par priorité, ce qui ajoute généralement environ 50 ms, et une surcharge prolongée renvoie une erreur HTTP 429.

Gérez les réponses HTTP 429 avec un court délai avant de réessayer. Si elles persistent, augmentez la limite sur la page de tarification ou, pour les clients Entreprise, via votre gestionnaire de compte.

Gérez le barge-in et les interruptions

Un appelant qui commence à parler pendant que l’agent parle s’attend à ce que l’agent s’arrête. C’est le barge-in, et bien le gérer est essentiel pour que l’agent paraisse naturel et non scripté.

Détectez la parole de l’appelant pendant la lecture de l’agent grâce au signal VAD du STT. Quand c’est le cas, faites deux choses : arrêtez d’envoyer les morceaux TTS (le flag agentSpeaking dans speak gère déjà cela en interrompant la boucle), puis envoyez à Twilio un message clear pour vider l’audio déjà en file d’attente.

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

Si vous sautez le clear, Twilio continue de lire l’audio mis en tampon après que vous avez arrêté d’envoyer, donc l’agent semble parler par-dessus l’appelant.

Journalisez, surveillez et gérez les erreurs proprement

Instrumentez chaque étape pour pouvoir attribuer la latence si un appel paraît lent. Mesurez l’écart entre la transcription finale et le premier token LLM, entre le premier token LLM et le premier octet TTS, puis entre le premier octet TTS et la trame envoyée à Twilio. Vous verrez que la plus grande variabilité de latence se situe au niveau du LLM ; les étapes STT et TTS sont plus stables.

Prévoyez ensuite les échecs partiels. Le LLM peut expirer, le flux STT peut se couper, et le temps de trajet réseau vers ElevenLabs varie de 20 à 200 ms selon la géographie. Placez votre serveur près de vos appelants, pas seulement près d’ElevenLabs, car ElevenLabs route déjà vers le cluster le plus proche (Amérique du Nord, Europe, Asie du Sud-Est).

Si une étape échoue, ne laissez pas l’appelant dans le silence : synthétisez une courte phrase de secours (« Désolé, pouvez-vous répéter ? ») et gardez l’appel actif. Encapsulez chaque étape dans un timeout et un try/catch pour qu’un échec ne coupe pas tout le WebSocket.

Quelques autres paramètres valent la peine d’être définis avant de passer en production :

  • Limitez la longueur des réponses dans le prompt système, comme montré, pour garder les échanges courts et faciles à interrompre.
  • Limitez l’historique de la conversation pour éviter que les longs appels n’augmentent indéfiniment le contexte du LLM.
  • Définissez une durée maximale d’appel pour éviter que des sessions bloquées ne consomment la concurrence en silence.

Pour continuer à optimiser ce que vous contrôlez, le document sur la latence explique d’où vient le temps avant la première réponse audio, la présentation des modèles détaille les compromis vitesse/qualité, et le guide TTS temps réel WebSocket montre comment réduire la latence de synthèse avec un texte incrémental.

Créez des agents vocaux prêts pour la production avec ElevenAPI

Après ces 20 minutes, vous avez toutes les couches d’un agent vocal prêt pour la production. Twilio gère la téléphonie, Scribe v2 Realtime transcrit, un LLM génère les réponses, et Flash v2.5 répond sur le même WebSocket.

Si vous ne souhaitez pas gérer la cascade vous-même, ElevenAgents propose la gestion des tours de parole, des interruptions et l’intégration téléphonie en service géré, basé sur les mêmes modèles que vous venez de connecter à la main.

Pour continuer à optimiser une stack que vous contrôlez, explorez la page produit ElevenAPI pour les offres, les limites de concurrence et la voice library. Sinon, inscrivez-vous et lancez-vous dès aujourd’hui pour passer votre premier appel.

FAQ : Créer un agent vocal avec Twilio et ElevenLabs

Articles similaires

Créez avec l'audio IA de la plus haute qualité