Pomiń

Zbuduj agenta głosowego w 20 minut z ElevenLabs i Twilio

Opublikowano
Ostatnia aktualizacja

PosłuchajPosłuchaj tego artykułu

Agent głosowy może odbierać połączenia, transkrybować rozmówców na żywo (speech to text, STT), generować odpowiedzi przez duży model językowy (LLM) i mówić z powrotem przez moduł text to speech (TTS). Z ElevenLabs i Twilio uruchomisz działającego agenta na prawdziwym numerze w około 20 minut.

Dla deweloperów cały stack to ElevenLabs do syntezy mowy (Flash v2.5) i transkrypcji (Scribe v2 Realtime), Twilio do obsługi połączeń i OpenAI lub Anthropic jako LLM. Każdy z tych elementów możesz zamienić na inny, jeśli wolisz.

W tym artykule pokazujemy, jak zbudować agenta głosowego w 20 minut, używając Node.js i Typescript. Jeśli wolisz gotowe rozwiązanie, które samo obsługuje zmiany rozmówcy, przerywanie i połączenia, bez konieczności samodzielnego zarządzania całością, sprawdź ElevenAgents. 

Jak działa architektura agenta głosowego

Zanim zaczniesz pisać kod, warto zrozumieć, jak łączą się wszystkie trzy usługi w stacku.

  • Twilio: obsługuje połączenie telefoniczne i przesyłanie dźwięku.
  • ElevenLabs: obsługuje STT przez Scribe v2 Realtime i TTS przez Flash v2.5.
  • LLM: odpowiada za wywołania narzędzi i generowanie odpowiedzi.

Każdy etap to cienka warstwa, więc możesz wymienić dowolny element bez ruszania reszty. Na przykład możesz zamienić LLM OpenAI na Anthropic bez przepisywania pozostałych komponentów.

Połączenie telefoniczne trafia na twój serwer przez Twilio. Twilio odbiera połączenie PSTN, otwiera WebSocket do twojego serwera i przesyła dźwięk rozmówcy jako strumień ramek mu-law zakodowanych w base64. Twój serwer obsługuje całą kaskadę i odsyła zsyntetyzowany dźwięk przez ten sam WebSocket, a Twilio odtwarza go rozmówcy.

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

Tak wygląda przepływ, który wykorzystasz do zbudowania agenta głosowego:

Rozmówca dzwoni na twój numer Twilio. Twilio pobiera dokument TwiML z twojego webhooka. TwiML mówi Twilio, by otworzył Media Stream do twojego endpointu WebSocket. Twilio przesyła dźwięk jako zdarzenia JSON z ładunkami mu-law (ulaw_8000) w base64.

Twój serwer przekazuje fragmenty dźwięku do Scribe v2 Realtime do transkrypcji na żywo. Gdy rozmówca skończy mówić, wysyłasz transkrypt do LLM, a potem generujesz odpowiedź przez Flash v2.5 w ulaw_8000. Zsyntetyzowane ramki mu-law odsyłasz do Twilio przez WebSocket (base64), a Twilio odtwarza je rozmówcy.

Scribe v2 Realtime daje częściowe transkrypcje z opóźnieniem ok. 150 ms, a Flash v2.5 działa z opóźnieniem ok. 75 ms (bez sieci i aplikacji). Najwięcej czasu zajmuje LLM – to on decyduje o długości oczekiwania na pierwsze dźwięki. Żeby skrócić przerwę, przesyłamy odpowiedź LLM token po tokenie i zaczynamy syntezę zanim model skończy zdanie.

Więcej o wyborze modeli znajdziesz w przeglądzie modeli i w wyjaśnieniu jak działa opóźnienie.

Co musisz mieć, zanim zaczniesz budować agenta głosowego

Potrzebujesz czterech rzeczy. Każdą da się szybko skonfigurować, ale brak którejkolwiek zablokuje uruchomienie serwera.

Sprawdź, czy masz:

  1. Numer Twilio z obsługą Voice: Zanotuj numer, Account SID i Auth Token z konsoli Twilio.
  2. Klucz API ElevenLabs: utworzysz go w swoim panelu ElevenLabs. Klucz przesyłamy w nagłówku xi-api-key i jest tajny, więc trzymaj go tylko po stronie serwera. Zobacz
  3. Klucz API LLM:W tym poradniku możesz użyć Anthropic Claude lub OpenAI – wybierz jeden.
  4. Ngrok (lub inny tunel) do lokalnych testów: Twilio musi dotrzeć do twojego serwera przez publiczny HTTPS i WSS, a ngrok to umożliwia bez wdrażania czegokolwiek.

Ustaw swoje sekrety jako zmienne środowiskowe i nigdy ich nie commituj.

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"

Następnie uruchom tunel na porcie, którego użyje twój serwer:

ngrok http 8080

Jak działa protokół Twilio Media Streams

Twilio nie daje surowego gniazda audio. Zamiast tego wszystko opakowuje w ustrukturyzowany JSON przez WebSocket. Znając cztery typy zdarzeń i format wysyłki, łatwo zrozumiesz handler WebSocket z kroku 2.

Po połączeniu z twoim WebSocketem Twilio wysyła sekwencję wiadomości JSON, które mogą mieć cztery typy zdarzeń.

Najpierw przychodzi zdarzenie connected, które potwierdza połączenie WebSocket. Start pojawia się raz na początku streamu; zawiera streamSid, który musisz zapisać, bo będzie potrzebny do wysyłania dźwięku, oraz metadane połączenia w start.customParameters i start.callSid.

Media to zdarzenie powtarzalne: media.payload to fragment 8kHz mu-law w base64 (20 ms na ramkę), a media.track to dźwięk rozmówcy. Na końcu stop pojawia się, gdy stream się kończy, zwykle po rozłączeniu.

Aby odtworzyć dźwięk, wysyłasz wiadomość typu media z tym samym streamSid i ładunkiem mu-law w base64. Aby przerwać już zakolejkowany dźwięk, wysyłasz wiadomość clear ze streamSid, co czyści bufor Twilio.

Kodowanie wejścia i wyjścia jest identyczne (ulaw_8000). Zamawiamy ulaw_8000 z ElevenLabs Text to Speech i przekazujemy bajty prosto do Twilio bez żadnej konwersji.

Krok 1: Obsłuż webhook TwiML

Gdy przychodzi połączenie, Twilio wysyła żądanie HTTP do twojego webhooka, a ty odpowiadasz TwiML, który łączy rozmowę z Media Stream. <Connect><Stream> otwiera dwukierunkowy WebSocket. Użyj <Connect>, nie <Start>: utrzymuje połączenie przez cały czas streamu i pozwala odsyłać dźwięk, co jest tu kluczowe.

Webhook zwraca taki TwiML:

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

W Express to pojedynczy handler POST, który wstawia hosta i zwraca dokument:

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

W konsoli Twilio ustaw webhook "A call comes in" na https://your-subdomain.ngrok.app/incoming-call z użyciem HTTP POST.

Krok 2: Obsłuż WebSocket Media Stream

Handler WebSocket czyta zdarzenia Twilio, obsługuje kaskadę i odsyła dźwięk.

Trzymamy trochę stanu na połączenie: streamSid, połączenie STT i flagę, czy agent właśnie mówi. Handler dekoduje każdą ramkę media z base64 i przekazuje surowe bajty mu-law do 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;
    }
  });
});

Krok 3: Transkrybuj przez Scribe v2 Realtime

Scribe v2 Realtime przyjmuje strumieniowe fragmenty dźwięku i zwraca częściowe oraz końcowe transkrypcje. Obsługuje mu-law bezpośrednio, więc przekazujemy mu ramki Twilio bez zmian.

Dodatkowo oferuje Voice Activity Detection do segmentacji na podstawie ciszy i ręczną kontrolę zatwierdzania segmentów. Dla agenta telefonicznego segmentacja przez VAD jest zwykle najlepsza, bo naturalna pauza to najpewniejszy sygnał końca wypowiedzi rozmówcy.

Kroki: Otwórz stream STT na początku rozmowy. Przesyłaj każdą ramkę mu-law. Na końcowe transkrypty reaguj wywołaniem LLM.

Klient STT na żywo wciąż się rozwija, więc poniższy kształt trzymamy za małym adapterem TypeScript (openRealtimeStt), który piszesz pod API na żywo, a nie sztywny zestaw pól. Traktuj onFinal jako hook przekazujący zakończoną wypowiedź do kolejnego etapu.

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

Opóźnienie rozpoznawania na żywo to ok. 150 ms dla częściowych wyników, więc przerwa między końcem wypowiedzi rozmówcy a startem agenta jest krótka. O wersji batch i pełnych możliwościach przeczytasz w dokumentacji Speech to Text oraz na stronie produktu Speech to Text na żywo.

Krok 4: Generuj odpowiedź przez LLM

To etap, w którym powstaje odpowiedź. LLM bierze historię rozmowy i zwraca tekst asystenta. Strumieniuj odpowiedź, żeby zacząć syntezę już od pierwszego zdania.

Tutaj używamy 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
  }
}

Powyższy model, gpt-4.1-mini, to przykład niskiego opóźnienia; claude-haiku-4-5 to podobna opcja od Anthropic. Każdy z nich może obsłużyć ten sam kontrakt llmReply – wystarczy podmienić ciało funkcji, reszta agenta zostaje bez zmian.

System prompt ogranicza długość odpowiedzi, co ma znaczenie przez telefon: długie odpowiedzi są wolne i trudno je naturalnie przerwać.

Krok 5: Syntezuj przez Flash TTS w ulaw_8000

Teraz tekst trzeba zamienić na dźwięk, który Twilio odtworzy. Zamów Flash v2.5 z outputFormat: "ulaw_8000", by bajty pasowały do kodowania Twilio, potem strumieniuj dźwięk i odsyłaj każdy fragment przez WebSocket jako zdarzenie media.

Zbieraj tokeny LLM w fragmenty zdań i syntezuj każdy fragment od razu, zamiast czekać na całą odpowiedź. Dzięki temu rozmówca słyszy pierwsze zdanie, gdy model generuje kolejne. Jeśli chcesz większą kontrolę nad syntezą krok po kroku, zobacz przewodnik po TTS WebSocket na żywo – poniższy sposób HTTP jest prostszy i wystarcza do krótkich wypowiedzi.

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

Jak przygotować agenta głosowego AI do produkcji

Po tych pięciu krokach masz działającego agenta. To nie to samo co wdrożenie produkcyjne.

Zanim podłączysz agenta do prawdziwej linii, musisz pamiętać o kilku rzeczach.

Weryfikuj podpisy webhooków Twilio

Każdy, kto pozna twój URL webhooka, może do niego POSTować, więc najpierw sprawdź, czy żądanie faktycznie pochodzi z Twilio. Twilio podpisuje każde żądanie twoim Auth Tokenem w nagłówku X-Twilio-Signature – odrzucaj wszystko, co nie przejdzie walidacji. Podpis liczy się po pełnym URL i parametrach POST, więc musisz liczyć go tak samo jak Twilio.

Pomaga w tym gotowa biblioteka Twilio:

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

Dbaj o bezpieczeństwo sekretów

Trzymaj ELEVENLABS_API_KEY, klucz LLM i TWILIO_AUTH_TOKEN w menedżerze sekretów, nie w kodzie ani w jawnych plikach env w repo. Ogranicz klucz ElevenLabs tylko do endpointów potrzebnych tej usłudze i ustaw limit wydatków, żeby ewentualny wyciek nie był groźny. 

W planach Enterprise możesz dodatkowo ograniczyć klucz do wybranych adresów IP. Ten serwer używa klucza bezpośrednio, bo nigdy nie opuszcza backendu; jeśli logika audio trafiłaby do przeglądarki lub aplikacji mobilnej, przejdź na tokeny jednorazowe, by klucz nie był widoczny po stronie klienta.

Zrozum limit współbieżności

Każdy plan ma limit współbieżności, inny dla każdej rodziny modeli. Limit liczy, ile żądań jednocześnie generuje dźwięk.

Dla agenta telefonicznego to działa na twoją korzyść. Generowanie dźwięku jest szybsze niż odtwarzanie, więc każde połączenie zużywa TTS tylko przez chwilę, gdy trwa synteza odpowiedzi, a nie przez cały czas rozmowy. W praktyce limit współbieżności ok. 5 pozwala obsłużyć nawet 100 rozmów naraz, bo generowanie kończy się dużo szybciej niż odtwarzanie.

Mimo to monitoruj zapas, nie zgaduj. Odpowiedzi ElevenLabs mają nagłówki current-concurrent-requests i maximum-concurrent-requests – loguj je i ustaw alerty, gdy zbliżasz się do limitu. Po przekroczeniu limitu żądania trafiają do kolejki (zwykle +50 ms), a długotrwałe przeciążenie daje HTTP 429.

Obsługuj HTTP 429 krótkim opóźnieniem. Jeśli się powtarzają, zwiększ limity na stronie cen lub – w Enterprise – przez opiekuna konta.

Obsłuż barge-in i przerywanie

Rozmówca, który zaczyna mówić, gdy agent mówi, oczekuje, że agent się zatrzyma. To barge-in – poprawna obsługa sprawia, że agent brzmi naturalnie, a nie jak automat.

Wykrywaj mowę rozmówcy podczas odtwarzania agenta przez sygnał VAD z STT. Gdy ją wykryjesz, zrób dwie rzeczy: przestań przesyłać fragmenty TTS (flaga agentSpeaking już to obsługuje, przerywając pętlę) i wyślij do Twilio wiadomość clear, by wyczyścić już zakolejkowany dźwięk.

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

Jeśli pominiesz clear, Twilio dalej odtwarza buforowany dźwięk, więc agent mówi ponad rozmówcą.

Loguj, monitoruj i obsługuj błędy z klasą

Mierz każdy etap, by wiedzieć, gdzie pojawia się opóźnienie. Zmierz czas od końcowego transkryptu do pierwszego tokena LLM, od tokena LLM do pierwszego bajtu TTS i od TTS do ramki wysłanej do Twilio. Najwięcej zmienności znajdziesz w LLM; STT i TTS są stabilne.

Planuj częściowe awarie. LLM może się rozłączyć, stream STT może się zerwać, a czas do ElevenLabs przez internet waha się od 20 do 200 ms w zależności od lokalizacji. Umieść serwer blisko rozmówców, nie tylko blisko ElevenLabs – ElevenLabs już kieruje ruch do najbliższego klastra (Ameryka Północna, Europa, Azja Południowo-Wschodnia).

Gdy etap zawiedzie, nie zostawiaj rozmówcy w ciszy: wygeneruj krótką linijkę awaryjną ("Przepraszam, możesz powtórzyć?") i utrzymaj połączenie. Każdy etap opakuj timeoutem i try/catch, by jedna nieudana tura nie zerwała całego WebSocket.

Warto ustawić jeszcze kilka domyślnych opcji przed wdrożeniem:

  • Ogranicz długość odpowiedzi w system prompt, by wypowiedzi były krótkie i łatwe do przerwania.
  • Ogranicz historię rozmowy, by długie połączenia nie rozrastały kontekstu LLM bez końca.
  • Ustaw maksymalny czas rozmowy, by uniknąć zablokowania współbieżności przez zawieszone sesje.

Aby dalej optymalizować to, na co masz wpływ, zobacz dokument o opóźnieniach, który wyjaśnia, skąd bierze się czas do pierwszego dźwięku, przegląd modeli omawia kompromisy szybkości i jakości, a przewodnik po TTS WebSocket na żywo pokazuje, jak jeszcze bardziej skrócić opóźnienie przez podawanie tekstu fragmentami.

Buduj gotowych do produkcji agentów głosowych z ElevenAPI

Po 20 minutach masz już wszystkie warstwy produkcyjnego agenta głosowego. Twilio obsługuje połączenia, Scribe v2 Realtime transkrybuje, LLM generuje odpowiedzi, a Flash v2.5 mówi przez ten sam WebSocket.

Jeśli nie chcesz samodzielnie zarządzać całą kaskadą, ElevenAgents oferuje obsługę zmian rozmówcy, przerywania i integrację z telefonią jako gotową usługę na tych samych modelach, które właśnie połączyłeś.

Jeśli chcesz dalej optymalizować własny stack, sprawdź stronę produktu ElevenAPI z planami, limitami współbieżności i biblioteką głosów. Albo po prostu zarejestruj się i zacznij już dziś – wykonaj pierwsze połączenie.

FAQ: Jak zbudować agenta głosowego z Twilio i ElevenLabs

Podobne artykuły

Twórz z najwyższej jakości audio AI