Aller 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 téléphoniques entrants, transcrire les appelants en temps réel avec la reconnaissance vocale (STT), générer une réponse avec un grand modèle de langage (LLM), puis répondre à voix haute grâce à un module de synthèse vocale (TTS). Avec ElevenLabs et Twilio, vous pouvez mettre en service un agent sur un vrai numéro de téléphone en environ 20 minutes.

Pour les développeurs, la stack complète utilise ElevenLabs pour la synthèse vocale (Flash v2.5) et la transcription (Scribe v2 Realtime), Twilio pour la téléphonie, ainsi qu’OpenAI ou Anthropic pour le LLM. Tous ces éléments sont interchangeables : choisissez les composants que vous connaissez le mieux.

Cet article explique comment créer un agent vocal en 20 minutes avec Node.js et Typescript. Pour une alternative gérée qui prend en charge la gestion des tours de parole, les interruptions et la téléphonie sans avoir à maintenir vous-même la cascade, découvrez ElevenAgents. 

Fonctionnement de l’architecture d’un agent vocal

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

  • 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 adaptateur léger ; vous pouvez donc remplacer un service sans toucher au reste. Par exemple, vous pouvez remplacer le LLM OpenAI par Anthropic sans réécrire les autres composants.

Un appel téléphonique atteint votre serveur via Twilio. Twilio répond à l’appel sur le RTPC, 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 exécute la cascade et renvoie l’audio synthétisé via ce même WebSocket, que Twilio lit à 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 flux utilisé 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 charges utiles mu-law (ulaw_8000) encodées en base64. 

Votre serveur transmet les segments audio à Scribe v2 Realtime pour une transcription en streaming. Lorsqu’un tour de parole de l’appelant est finalisé, vous envoyez la transcription au LLM, puis 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 lit à l’appelant.

Scribe v2 Realtime produit des transcriptions partielles avec une latence d’environ 150 ms, tandis que l’inférence du modèle Flash v2.5 prend environ 75 ms, hors latence réseau et applicative. Le LLM contribue le plus au délai avant le premier audio, de manière aussi la moins prévisible : c’est là que se concentre l’essentiel du budget de latence. Pour réduire ce délai, nous diffusons la sortie du LLM token par token et lançons la synthèse avant que le modèle ait terminé sa phrase.

Pour comprendre les compromis entre les modèles qui motivent ces choix, consultez la présentation des modèles et l’explication pour comprendre la latence.

Ce dont vous avez besoin avant de créer un agent vocal

Ce guide suppose que vous disposez de quatre éléments. Ils sont rapides à configurer, mais l’absence de l’un d’eux empêchera le serveur de fonctionner.

Voici les prérequis à vérifier :

  1. Un numéro de téléphone Twilio avec la fonctionnalité Voice : notez le numéro, ainsi que votre Account SID et votre Auth Token depuis la console Twilio.
  2. Clé API ElevenLabs : créée dans votre Dashboard ElevenLabs. La clé est transmise dans l’en-tête xi-api-key et est secrète : conservez-la uniquement côté serveur. Consultez l’authentification API.
  3. Clé API de LLM : ce tutoriel considère Anthropic Claude et OpenAI comme des backends interchangeables ; choisissez-en un.
  4. Ngrok (ou tout autre tunnel) pour le développement local : Twilio doit pouvoir atteindre votre serveur via une URL HTTPS et WSS publique, ce que ngrok permet sans déployer quoi que ce soit.

Définissez vos secrets comme variables d’environnement et ne les validez jamais dans votre dépôt.

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"

Démarrez ensuite un tunnel vers le port utilisé par votre serveur :

ngrok http 8080

Comprendre le protocole Twilio Media Streams

Twilio ne vous fournit pas un socket audio brut. Il encapsule tout dans un protocole JSON structuré via WebSocket. Comprendre les quatre types d’événements et le format d’envoi vous permettra de saisir entièrement le gestionnaire WebSocket de l’étape 2 avant de l’écrire.

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

L’événement connected arrive en premier et confirme que le WebSocket est actif. L’événement start est envoyé une seule fois au démarrage du flux média ; il contient un streamSid que vous devez stocker pour renvoyer l’audio, ainsi que les métadonnées d’appel sous start.customParameters et start.callSid. 

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

Pour lire l’audio en retour, envoyez un message de type media avec le même streamSid et une charge utile mu-law encodée en base64. Pour interrompre l’audio déjà mis en file d’attente, envoyez un message clear avec le streamSid afin de vider le tampon de sortie de Twilio.

Les encodages entrants et sortants sont identiques (ulaw_8000). Nous demandons ulaw_8000 à ElevenLabs Text to Speech et transmettons les octets directement à Twilio, sans rééchantillonnage intermédiaire.

Étape 1 : servir le webhook TwiML

Lorsqu’un appel arrive, Twilio adresse une requête HTTP à votre webhook, auquel vous répondez avec un TwiML qui connecte l’appel à votre Media Stream. La balise <Connect><Stream> ouvre un WebSocket bidirectionnel. Utilisez ici <Connect> plutôt que <Start> : cela maintient l’appel pendant toute la durée du flux et vous permet de renvoyer l’audio, ce qui est l’objectif 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>

Avec Express, un seul gestionnaire POST renseigne 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, configurez le webhook « A call comes in » du numéro sur https://your-subdomain.ngrok.app/incoming-call avec la méthode HTTP POST.

Étape 2 : accepter le WebSocket Media Stream

Le gestionnaire WebSocket lit les événements Twilio, pilote la cascade et renvoie l’audio.

Nous conservons quelques données d’état par appel : le streamSid, une connexion STT et un indicateur précisant si l’agent parle actuellement. Le gestionnaire décode chaque trame média entrante en 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 des segments audio en streaming et renvoie des transcriptions partielles et finales. Il prend directement en charge l’encodage mu-law ; nous lui transmettons donc les trames Twilio sans modification. 

Il propose également la détection d’activité vocale pour la segmentation basée sur le silence, ainsi qu’un contrôle manuel de validation pour finaliser un segment. Pour un agent téléphonique, la segmentation pilotée par la VAD est généralement le bon choix, car une pause naturelle est le signal le plus fiable de la fin d’un tour de parole.

Voici les étapes : ouvrez un flux STT au début de l’appel. Envoyez chaque segment mu-law entrant. Réagissez aux transcriptions finalisées en appelant le LLM.

L’interface client du STT en temps réel évolue encore. La structure ci-dessous est donc isolée derrière un petit adaptateur TypeScript (openRealtimeStt), que vous implémenterez avec l’API en production plutôt qu’avec un ensemble figé de noms de champs. Considérez onFinal comme le hook qui transmet 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 résultats partiels, ce qui réduit le délai perçu entre la fin de parole de l’appelant et le début de réponse de l’agent. Pour l’équivalent par lots et l’ensemble des fonctionnalités, consultez la documentation Speech to Text et la page produit Speech to Text en 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 reçoit l’historique de la conversation et renvoie le texte de l’assistant. Diffusez la réponse en streaming pour commencer la synthèse dès la première phrase.

Ici, l’implémentation s’appuie sur 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
  }
}

L’ID de modèle ci-dessus, gpt-4.1-mini, est un exemple de choix à faible latence ; claude-haiku-4-5 constitue une option comparable côté Anthropic. Les deux fournisseurs peuvent respecter le même contrat llmReply : remplacez le corps de la fonction, le reste de l’agent reste inchangé.

Le prompt système limite la longueur des réponses, ce qui est essentiel au téléphone : les réponses longues semblent 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" afin que les octets correspondent à l’encodage attendu par Twilio, puis diffusez l’audio et renvoyez chaque segment via le WebSocket comme événement media.

Regroupez les tokens du LLM en fragments de la taille d’une phrase et synthétisez chaque fragment dès qu’il est terminé, plutôt que d’attendre toute la réponse. Vous réduisez ainsi le délai avant le premier audio : l’appelant entend la première phrase pendant que le modèle produit encore la deuxième. Pour un contrôle plus poussé de la synthèse incrémentale, le guide WebSocket TTS en temps réel explique comment envoyer du texte vers un seul socket de synthèse ouvert ; l’approche de streaming HTTP ci-dessous est plus simple et suffisante pour de courts tours conversationnels.

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 les cinq étapes ci-dessus, vous disposez d’un agent fonctionnel. Ce n’est pas encore un déploiement en production. 

Plusieurs aspects doivent être pris en compte avant de mettre l’agent sur une vraie ligne téléphonique.

Valider les signatures des webhooks Twilio

Toute personne connaissant l’URL de votre webhook peut lui envoyer une requête POST. La première étape consiste donc à vérifier que la requête provient bien de Twilio. Twilio signe chaque requête avec votre Auth Token dans l’en-tête X-Twilio-Signature ; vous devez rejeter tout ce qui échoue à la validation. La signature est calculée sur l’URL complète et les paramètres POST : vous devez donc la calculer de la même manière que Twilio.

L’assistant 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érer correctement les secrets

Conservez ELEVENLABS_API_KEY, la clé du LLM et TWILIO_AUTH_TOKEN dans un gestionnaire de secrets, et non dans le code source ni dans des fichiers d’environnement en clair validés dans un dépôt. Limitez la clé ElevenLabs aux seuls endpoints nécessaires à ce service et attribuez-lui un quota de crédits afin de circonscrire l’impact d’une fuite. 

Les offres Enterprise permettent aussi de restreindre une clé à des plages d’adresses IP spécifiques via une liste blanche d’IP. Ce serveur utilise directement la clé API, car elle ne quitte jamais votre backend ; si une logique audio était déplacée vers un navigateur ou un client mobile, utilisez des jetons à usage unique afin que la clé ne soit jamais exposée côté client.

Comprendre la limite de requêtes simultanées

Chaque offre définit une limite de requêtes simultanées qui varie selon la famille de modèles. Cette limite correspond au nombre de requêtes générant activement de l’audio au même moment.

Pour un agent téléphonique, ce mode de calcul joue en votre faveur. La génération audio est plus rapide que la lecture ; chaque appel ne consomme donc des requêtes simultanées TTS que durant les brèves fenêtres de synthèse d’une réponse, et non pendant toute sa durée. À titre indicatif, une limite d’environ cinq requêtes simultanées peut prendre en charge de l’ordre de 100 appels conversationnels simultanés, car la génération se termine bien avant la lecture.

Surveillez néanmoins la marge disponible plutôt que de faire des suppositions. Les réponses ElevenLabs exposent les en-têtes current-concurrent-requests et maximum-concurrent-requests : consignez-les et configurez des alertes à l’approche du maximum. Lorsque 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 ; une surcharge prolongée renvoie une erreur HTTP 429. 

Traitez les réponses HTTP 429 avec un court délai de relance. Si elles persistent, augmentez les limites en effectuant un upgrade depuis la page des tarifs ou, pour les clients Enterprise, en contactant votre gestionnaire de compte.

Gérer l’interruption de parole et les interruptions

Un appelant qui commence à parler pendant que l’agent s’exprime s’attend à ce qu’il s’arrête. C’est l’interruption de parole, dont la bonne gestion est essentielle pour qu’un agent paraisse naturel plutôt que scripté.

Détectez la parole de l’appelant pendant la lecture de l’agent grâce au signal VAD du STT. Lorsque vous la détectez, faites deux choses. D’abord, cessez de transmettre les segments TTS : l’indicateur agentSpeaking dans speak le gère déjà en interrompant la boucle. Ensuite, envoyez à Twilio un message clear pour vider l’audio déjà mis en file d’attente de son côté.

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

Sans le clear, Twilio continue de lire l’audio en mémoire tampon après l’arrêt de l’envoi ; l’agent semble alors parler par-dessus l’appelant.

Journaliser, surveiller et échouer avec élégance

Instrumentez chaque étape pour identifier l’origine de la latence lorsqu’un appel semble lent. Mesurez le délai entre la transcription finale et le premier token du LLM, entre le premier token du LLM et le premier octet TTS, puis entre le premier octet TTS et la trame envoyée à Twilio. Vous constaterez que la majeure partie de la latence variable se situe au niveau du LLM ; les étapes STT et TTS sont comparativement stables.

Prévoyez ensuite les défaillances partielles. Le LLM peut expirer, le flux STT peut être interrompu et l’aller-retour réseau vers ElevenLabs varie d’environ 20 à 200 ms sur Internet public selon la zone géographique. Hébergez votre serveur près de vos appelants, et pas uniquement près d’ElevenLabs, car ElevenLabs achemine déjà les requêtes vers le cluster le plus proche en Amérique du Nord, en Europe ou en Asie du Sud-Est. 

Lorsqu’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 maintenez l’appel actif. Encadrez chaque étape par un délai d’expiration et un try/catch pour qu’un tour de parole défaillant ne ferme pas tout le WebSocket.

Quelques paramètres par défaut supplémentaires méritent d’être définis avant le déploiement :

  • Limitez la longueur des réponses dans le prompt système, comme indiqué, afin que les tours de parole restent courts et interruptibles. 
  • Limitez l’historique de conversation pour que les appels longs n’augmentent pas indéfiniment le contexte du LLM. 
  • Définissez une durée d’appel maximale pour éviter que des sessions bloquées ne consomment silencieusement des requêtes simultanées.

Pour continuer à optimiser les éléments que vous contrôlez, la documentation sur la latence explique l’origine du délai avant le premier audio, la présentation des modèles détaille les compromis entre vitesse et qualité, et le guide WebSocket TTS en temps réel explique comment réduire davantage la latence de synthèse grâce à une entrée texte incrémentale.

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

Au terme de ces 20 minutes, vous disposez de chaque couche 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 via le même WebSocket. 

Si vous préférez ne pas maintenir vous-même la cascade, ElevenAgents fournit la gestion des tours de parole, des interruptions et l’intégration téléphonique dans un service géré, conçu avec les mêmes modèles que vous venez d’assembler manuellement. 

Pour continuer à optimiser une stack que vous contrôlez, consultez la page produit ElevenAPI pour découvrir les offres, les limites de requêtes simultanées et la Voice Library. Vous pouvez aussi vous inscrire et lancer votre premier appel dès aujourd’hui.

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

Articles similaires

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