Ir al contenido

Crea un agente de voz en 20 minutos con ElevenLabs y Twilio

Publicado
Última actualización

EscucharEscucha este artículo

Un agente de voz puede responder llamadas entrantes, transcribir a quienes llaman en tiempo real con Voz a Texto (STT), generar una respuesta con un modelo de lenguaje de gran tamaño (LLM) y responder mediante un módulo de Texto a Voz (TTS). Con ElevenLabs y Twilio, puedes tener un agente operativo con un número de teléfono real en unos 20 minutos.

Para desarrolladores, el stack completo que usarás incluye ElevenLabs para síntesis de voz (Flash v2.5) y transcripción (Scribe v2 Realtime), Twilio para telefonía y OpenAI o Anthropic como LLM. Dicho esto, todos estos componentes se pueden sustituir, así que puedes elegir los que mejor conozcas y utilizarlos en su lugar.

Este artículo muestra cómo crear un agente de voz en 20 minutos con Node.js y Typescript. Si quieres una alternativa gestionada que se encargue de los turnos de conversación, las interrupciones y la telefonía sin tener que mantener tú la cascada, visita ElevenAgents. 

Cómo funciona la arquitectura de un agente de voz

Antes de escribir código, conviene entender cómo se conectan los tres servicios de tu stack tecnológico.

  • Twilio: gestiona la llamada telefónica y el transporte de audio.
  • ElevenLabs: gestiona STT mediante Scribe v2 Realtime y TTS mediante Flash v2.5.
  • LLM: gestiona las llamadas a herramientas y redacta la respuesta.

Cada etapa es un adaptador ligero, por lo que puedes sustituir una por otro servicio sin tocar el resto. Por ejemplo, puedes cambiar el LLM de OpenAI por Anthropic sin tener que reescribir los demás componentes.

Una llamada telefónica llega a tu servidor a través de Twilio. Twilio responde la llamada PSTN, abre un WebSocket de vuelta a tu servidor y reenvía el audio de quien llama como un flujo de tramas mu-law codificadas en base64. Tu servidor ejecuta la cascada y transmite el audio sintetizado de vuelta a través de ese mismo WebSocket, y Twilio lo reproduce para quien llama.

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

Este es el flujo que usarás para crear un agente de voz: 

Una persona llama a tu número de Twilio. Twilio obtiene un documento TwiML de tu webhook. El TwiML indica a Twilio que abra un Media Stream hacia tu ruta de WebSocket. Twilio transmite el audio entrante como eventos JSON que contienen cargas útiles mu-law (ulaw_8000) codificadas en base64. 

Tu servidor reenvía fragmentos de audio a Scribe v2 Realtime para la transcripción en streaming. Cuando finaliza un turno de quien llama, envías la transcripción al LLM y luego sintetizas la respuesta con Flash v2.5 en ulaw_8000. Envías las tramas mu-law sintetizadas de vuelta a Twilio mediante el WebSocket, codificadas en base64, y Twilio las reproduce para quien llama.

Scribe v2 Realtime emite transcripciones parciales con una latencia de unos 150 ms, y Flash v2.5 funciona con una inferencia de modelo de aproximadamente 75 ms, sin contar la latencia de red y de la aplicación. El LLM es el mayor factor, y el menos predecible, del tiempo hasta el primer audio, y es donde se concentra la mayor parte del presupuesto de latencia. Para acortar la espera, transmitimos la salida del LLM token a token y empezamos a sintetizar antes de que el modelo haya terminado la frase.

Para conocer las concesiones entre modelos que hay detrás de estas decisiones, consulta la visión general de modelos y la explicación sobre cómo entender la latencia.

Qué necesitas antes de empezar a crear un agente de voz

Esta guía parte de que tienes cuatro elementos preparados. Cada uno se configura rápidamente, pero si falta cualquiera de ellos, el servidor no podrá ejecutarse.

Estos son los requisitos que debes comprobar:

  1. Un número de teléfono de Twilio con capacidad de voz: anota el número junto con tu Account SID y Auth Token de la consola de Twilio.
  2. Clave de API de ElevenLabs: créala en tu panel de ElevenLabs. La clave se envía en la cabecera xi-api-key y es secreta, así que mantenla solo en el servidor. Consulta autenticación de la API.
  3. Clave de API del LLM: este tutorial trata Anthropic Claude y OpenAI como backends intercambiables, así que elige uno.
  4. Ngrok (o cualquier túnel) para desarrollo local: Twilio debe poder llegar a tu servidor mediante una URL HTTPS y WSS pública, y ngrok lo proporciona sin tener que desplegar nada.

Configura tus secretos como variables de entorno y no los incluyas nunca en un 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"

Después, inicia un túnel que apunte al puerto que utilizará tu servidor:

ngrok http 8080

Entender el protocolo Twilio Media Streams

Twilio no te proporciona un socket de audio sin procesar. En su lugar, envuelve todo en un protocolo JSON estructurado mediante WebSocket. Entender los cuatro tipos de evento y el formato de envío hará que el controlador de WebSocket del paso 2 tenga todo el sentido antes de escribirlo.

Cuando Twilio se conecta a tu WebSocket, envía una secuencia de mensajes de texto JSON que pueden tener cuatro tipos de evento.

El evento connected llega primero y confirma que el WebSocket está activo. El evento start se envía una vez cuando empieza el flujo multimedia; incluye un streamSid que debes almacenar porque lo necesitas para enviar audio de vuelta, e incluye también metadatos de la llamada en start.customParameters y start.callSid. 

El evento media se repite: media.payload es un fragmento de audio mu-law de 8 kHz codificado en base64, de 20 ms por trama, y media.track es inbound para el audio de quien llama. Por último, stop se envía cuando termina el flujo, normalmente porque se ha colgado la llamada.

Para reproducir audio de vuelta, envías un mensaje de tipo media con el mismo streamSid y una carga útil mu-law codificada en base64. Para interrumpir audio que ya has puesto en cola, envías un mensaje clear con el streamSid, que vacía el búfer de salida de Twilio.

Las codificaciones de entrada y salida son idénticas (ulaw_8000). Solicitamos ulaw_8000 a Texto a Voz de ElevenLabs y reenviamos los bytes directamente a Twilio sin remuestrear nada entre medias.

Paso 1: servir el webhook de TwiML

Cuando llega una llamada, Twilio envía una solicitud HTTP a tu webhook y tú respondes con TwiML que conecta la llamada a tu Media Stream. El verbo <Connect><Stream> abre un WebSocket bidireccional. Usa <Connect> en vez de <Start>: mantiene la llamada activa durante todo el flujo y te permite enviar audio de vuelta, que es el objetivo de esta configuración.

El TwiML que devuelve el webhook es:

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

En Express, es un único controlador POST que completa el host y devuelve el 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);
});

En la consola de Twilio, configura el webhook "A call comes in" del número como https://your-subdomain.ngrok.app/incoming-call mediante HTTP POST.

Paso 2: aceptar el WebSocket de Media Stream

El controlador de WebSocket lee eventos de Twilio, dirige la cascada y envía el audio de vuelta.

Mantenemos una pequeña cantidad de estado por llamada: el streamSid, una conexión STT y un indicador de si el agente está hablando. El controlador decodifica cada trama multimedia entrante desde base64 y reenvía los bytes mu-law sin procesar 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;
    }
  });
});

Paso 3: transcribir con Scribe v2 Realtime

Scribe v2 Realtime acepta fragmentos de audio en streaming y devuelve transcripciones parciales y finales. Además, admite directamente la codificación mu-law, así que le enviamos las tramas de Twilio sin modificarlas. 

También ofrece detección de actividad de voz para segmentación basada en silencios y control de confirmación manual para finalizar un segmento. Para un agente telefónico, la segmentación controlada por VAD suele ser la opción adecuada porque una pausa natural es la señal más fiable de que ha terminado el turno de quien llama.

Estos son los pasos: abre un flujo STT cuando empieza la llamada. Envía cada fragmento mu-law entrante. Reacciona a las transcripciones finalizadas invocando el LLM.

La interfaz del cliente STT en tiempo real sigue evolucionando, por lo que la estructura de abajo se mantiene tras un pequeño adaptador de TypeScript (openRealtimeStt) que implementarás con la API activa, en lugar de con un conjunto fijo de nombres de campos. Trata onFinal como el hook que entrega un turno completado de quien llama a la siguiente etapa.

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 latencia del reconocimiento en tiempo real es de unos 150 ms para las transcripciones parciales, lo que reduce la pausa percibida entre el final de quien llama y el inicio del agente. Para la alternativa por lotes y el conjunto completo de funciones, consulta la documentación de Voz a Texto y la página de producto de Voz a Texto en Tiempo Real.

Paso 4: generar una respuesta con un LLM

Esta es la etapa que produce la respuesta. El LLM toma el historial de la conversación y devuelve el texto del asistente. Transmite la respuesta para poder empezar la síntesis con la primera frase.

Aquí usamos OpenAI como 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
  }
}

El ID de modelo anterior, gpt-4.1-mini, es un ejemplo de opción de baja latencia; claude-haiku-4-5 es una alternativa comparable de Anthropic. Cualquiera de los proveedores puede usar el mismo contrato de llmReply: cambia el cuerpo de la función y el resto del agente no se modifica.

El prompt de sistema limita la longitud de las respuestas, algo importante por teléfono: las respuestas largas parecen lentas y resulta incómodo interrumpirlas de forma natural.

Paso 5: sintetizar con Flash TTS en ulaw_8000

Ahora el texto debe convertirse en audio que Twilio pueda reproducir. Solicita Flash v2.5 con outputFormat: "ulaw_8000" para que los bytes coincidan con la codificación que espera Twilio; después, transmite el audio y reenvía cada fragmento mediante el WebSocket como un evento media.

Acumula los tokens del LLM en fragmentos del tamaño de una frase y sintetiza cada fragmento cuando se complete, en lugar de esperar a toda la respuesta. Así se reduce el tiempo hasta el primer audio, porque quien llama escucha la primera frase mientras el modelo aún genera la segunda. Para tener un control más detallado de la síntesis incremental, la guía de WebSocket de TTS en tiempo real explica cómo introducir texto en un único socket de síntesis abierto; el enfoque de streaming HTTP de abajo es más sencillo y suficiente para turnos conversacionales cortos.

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

Preparar tu agente de voz IA para producción

Después de los cinco pasos anteriores, tienes un agente operativo. Pero eso no es lo mismo que un despliegue en producción. 

Hay varios aspectos que debes tener en cuenta antes de poner el agente en una línea telefónica real.

Validar las firmas de los webhooks de Twilio

Cualquiera que conozca la URL de tu webhook puede enviarle una solicitud POST, así que lo primero es confirmar que la solicitud procede realmente de Twilio. Twilio firma cada solicitud con tu Auth Token en la cabecera X-Twilio-Signature, y debes rechazar todo lo que no supere la validación. La firma se calcula a partir de la URL completa y los parámetros POST, por lo que debes calcularla igual que Twilio.

El helper de Twilio lo hace por ti:

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

Gestionar los secretos correctamente

Guarda ELEVENLABS_API_KEY, la clave del LLM y TWILIO_AUTH_TOKEN en un gestor de secretos, no en el código fuente ni en archivos de variables de entorno de texto plano incluidos en un repositorio. Limita la clave de ElevenLabs solo a las rutas de API que necesita este servicio y asígnale una cuota de créditos para que una filtración tenga un impacto limitado, en vez de ilimitado. 

Los planes Enterprise permiten además restringir una clave a intervalos de IP específicos mediante listas blancas de IP. Este servidor usa la clave de API directamente porque nunca sale de tu backend; si alguna lógica de audio pasara a un navegador o cliente móvil, usarías tokens de un solo uso para que la clave nunca quede expuesta en el cliente.

Entender el límite de concurrencia

Cada plan tiene un límite de concurrencia distinto según la familia de modelos, y este límite cuenta cuántas solicitudes están generando audio activamente al mismo tiempo.

Para un agente telefónico, este cálculo te beneficia. La generación de audio es más rápida que la reproducción, así que cada llamada solo consume concurrencia de TTS durante las breves ventanas en las que se sintetiza una respuesta, no durante toda la llamada. Como orientación aproximada, un límite de concurrencia de alrededor de cinco puede admitir del orden de 100 conversaciones simultáneas, porque la generación termina mucho antes que la reproducción.

Aun así, supervisa el margen disponible en lugar de hacer suposiciones. Las respuestas de ElevenLabs incluyen las cabeceras current-concurrent-requests y maximum-concurrent-requests; regístralas y crea alertas al acercarte al máximo. Cuando superas el límite, las solicitudes se ponen en cola por prioridad, lo que normalmente añade unos 50 ms, y una sobrecarga sostenida devuelve HTTP 429. 

Gestiona las respuestas HTTP 429 con una breve espera antes de reintentar. Si persisten, aumenta los límites mejorando tu plan en la página de precios o, para clientes Enterprise, a través de tu gestor de cuenta.

Gestionar las interrupciones de quien llama

Quien empieza a hablar mientras el agente está hablando espera que el agente se detenga. Esto se conoce como barge-in, y gestionarlo correctamente es una parte importante de lo que hace que un agente parezca natural y no guionizado.

Detecta el habla de quien llama durante la reproducción del agente mediante la señal VAD de STT. Cuando la detectes, haz dos cosas. Primero, deja de reenviar fragmentos de TTS; el indicador agentSpeaking de speak ya lo gestiona al salir del bucle. Segundo, envía a Twilio un mensaje clear para vaciar el audio que ya has puesto en cola en su lado.

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

Si omites clear, Twilio seguirá reproduciendo el audio almacenado en búfer después de que hayas dejado de enviarlo, por lo que parecerá que el agente habla por encima de quien llama.

Registrar, supervisar y gestionar los errores correctamente

Instrumenta cada etapa para poder atribuir la latencia cuando una llamada parezca lenta. Mide el intervalo entre la transcripción final y el primer token del LLM, entre el primer token del LLM y el primer byte de TTS, y entre el primer byte de TTS y la trama enviada a Twilio. Verás que la mayor parte de la latencia variable se encuentra en la etapa del LLM; las etapas STT y TTS son comparativamente estables.

Después, prepárate para fallos parciales. El LLM puede agotar el tiempo de espera, el flujo STT puede interrumpirse y el viaje de ida y vuelta por red a ElevenLabs varía aproximadamente entre 20 y 200 ms en la internet pública según la ubicación geográfica. Ubica tu servidor cerca de quienes llaman, no solo cerca de ElevenLabs, ya que ElevenLabs enruta a su clúster más próximo de Norteamérica, Europa o el Sudeste Asiático. 

Cuando falle una etapa, no dejes a quien llama en silencio: sintetiza una breve frase alternativa ("Lo siento, ¿puedes repetirlo?") y mantén la llamada activa. Envuelve cada etapa en un timeout y un try/catch para que un turno fallido no cierre todo el WebSocket.

Conviene configurar algunos valores predeterminados más antes del lanzamiento:

  • Limita la longitud de las respuestas en el prompt de sistema, como se muestra, para que los turnos sean breves e interrumpibles. 
  • Limita el historial de la conversación para que las llamadas largas no hagan crecer el contexto del LLM sin límite. 
  • Establece una duración máxima de llamada como medida de respaldo ante sesiones bloqueadas que consuman concurrencia sin que te des cuenta.

Para seguir ajustando los componentes que controlas, el documento sobre latencia explica de dónde procede el tiempo hasta el primer audio; la visión general de modelos aborda el equilibrio entre velocidad y calidad, y la guía de WebSocket de TTS en tiempo real muestra cómo reducir aún más la latencia de síntesis con entrada de texto incremental.

Crea agentes de voz listos para producción con ElevenAPI

Ahora que han pasado 20 minutos, tienes todas las capas de un agente de voz para producción. Twilio gestiona la telefonía, Scribe v2 Realtime transcribe, un LLM genera las respuestas y Flash v2.5 responde mediante el mismo WebSocket. 

Si prefieres no mantener tú la cascada, ElevenAgents proporciona gestión de turnos, manejo de interrupciones e integración de telefonía como servicio gestionado, basado en los mismos modelos que acabas de conectar manualmente. 

Para seguir ajustando un stack que controlas, explora la página de producto de ElevenAPI para ver planes, límites de concurrencia y la biblioteca de voces. También puedes registrarte y empezar hoy mismo a hacer tu primera llamada.

Preguntas frecuentes sobre crear un agente de voz con Twilio y ElevenLabs

Artículos relacionados

Crea con el audio IA de la más alta calidad