Integracja z własnym LLM

Zasil agenta telefonicznego Twilio własnym LLM za pomocą Speech Engine SDK.

Omówienie

Natywna integracja z Twilio w ElevenAgents obejmuje przypadek, gdy ElevenLabs hostuje LLM. Skorzystaj z tego przewodnika, gdy potrzebujesz pełnej kontroli nad LLM na własnym serwerze — własnego modelu, pipeline’u RAG, routingu wywołań funkcji lub innego rozumowania po stronie serwera — a agent nadal działa na numerze Twilio.

Część z własnym LLM zapewnia Speech Engine SDK, który otwiera WebSocket między ElevenLabs a twoim serwerem, dzięki czemu LLM może przesyłać odpowiedzi strumieniowo w trakcie rozmowy. Część Twilio używa Media Streams do przekazywania dźwięku rozmowy do agenta.

Architektura

Speech Engine SDK udostępnia w systemie rozmów agenta dwa endpointy WebSocket:

  • Brain WebSocket działa na twoim serwerze. ElevenLabs łączy się z nim, aby dostarczać transkrypcje i odbierać tekst wygenerowany przez LLM.
  • Conversation WebSocket działa w ElevenLabs. Klienci łączą się z nim, aby wysyłać dźwięk i odbierać zsyntetyzowany dźwięk. Most Twilio łączy się przez podpisany URL i przekazuje dźwięk μ-law w obu kierunkach.

Ponieważ Twilio Media Streams i Speech Engine obsługują ulaw_8000, most przekazuje dźwięk zakodowany w base64 bez transkodowania.

loop [Conversation] Dial number POST /incoming-call TwiML <Connect><Stream> WebSocket /media-stream Open conversation WebSocket (signed URL) Speak media event (μ-law base64) user_audio_chunk user_transcript agent_response (streamed) audio event (μ-law base64) media event Play audio Caller Twilio Bridge Server ElevenLabs (conversation WS) Brain Server

Most i serwer brain mogą działać w tym samym procesie, jeśli to wygodne — poniższy przykład je łączy.

Kiedy użyć tego wzorca

Zarówno ten przewodnik, jak i natywna integracja z Twilio umieszczają agenta na numerze Twilio. Różnica polega na tym, kto obsługuje LLM:

  • Natywna integracja: ElevenLabs hostuje LLM, a ty konfigurujesz go przez agenta. Prościej.
  • Własny LLM przez Speech Engine SDK (ten przewodnik): hostujesz LLM na własnym serwerze. Pełna kontrola nad modelem, RAG, wywołaniami funkcji i logiką biznesową. Więcej elementów do obsługi.

Jeśli logika LLM mieści się w standardowej konfiguracji agenta, wybierz natywną integrację. Skorzystaj z tego przewodnika, gdy brain musi uruchamiać kod w twojej infrastrukturze.

Ten wzorzec używa Speech Engine SDK, który komunikuje twój serwer z API ElevenLabs przez WebSocket. Możesz też użyć przewodnika Custom LLM, który zamiast Speech Engine SDK korzysta z endpointu HTTP zgodnego z OpenAI.

Główna różnica między nimi to WebSockety zamiast żądań HTTP. WebSockety utrzymują jedno połączenie zamiast tworzyć nowe połączenie HTTP dla każdej tury, co może zmniejszyć opóźnienia.

Wymagania wstępne

  • Konto Twilio i numer telefonu obsługujący rozmowy głosowe.
  • Zasób Speech Engine. Utwórz go zgodnie z krótkim przewodnikiem Speech Engine i poznaj wzorzec serwera brain.
  • Publiczny tunel HTTPS, np. ngrok. Twilio łączy się z twoim mostem przez publiczny internet.
  • Python 3.9+ lub Node.js 18+.

Skonfiguruj agenta dla dźwięku μ-law

Twilio Media Streams używa dźwięku μ-law 8 kHz. Skonfiguruj Speech Engine tak, by przyjmował i emitował ten sam format — wtedy most nie musi transkodować dźwięku.

import asyncio
import os
from elevenlabs import AsyncElevenLabs
elevenlabs = AsyncElevenLabs(api_key=os.environ["ELEVENLABS_API_KEY"])
async def update_engine():
await elevenlabs.speech_engine.update(
speech_engine_id="seng_8k3m9xr4hjnfg983brhmhkd98n6",
asr={"user_input_audio_format": "ulaw_8000"},
tts={
"model_id": "eleven_flash_v2",
"agent_output_audio_format": "ulaw_8000",
},
speech_engine={
"request_headers": {"x-api-key": os.environ["SHARED_SECRET"]},
},
)
asyncio.run(update_engine())

eleven_flash_v2 utrzymuje niskie opóźnienie zamiany tekstu na mowę, co ma znaczenie podczas rozmowy telefonicznej. Blok request_headers nakazuje ElevenLabs dodawać x-api-key: <shared-secret> do każdego połączenia WebSocket z brain — serwer brain sprawdza nagłówek, aby upewnić się, że tylko twój Speech Engine może się z nim połączyć.

Zbuduj serwer mostu

Most obsługuje trzy ścieżki:

  • POST /incoming-call — webhook Twilio. Zwraca TwiML, który nakazuje Twilio otworzyć Media Stream do /media-stream.
  • GET /media-stream — WebSocket Twilio Media Streams. Przekazuje dźwięk do i z conversation WebSocket Speech Engine.
  • GET /ws — Brain WebSocket. ElevenLabs łączy się tutaj po rozpoczęciu rozmowy. Uruchamia standardowy serwer engine.serve() / engine.attach().
1

Zainstaluj zależności

pip install "elevenlabs" "aiohttp" "twilio" "python-dotenv"
2

Wygeneruj podpisany URL dla Speech Engine

Most żąda podpisanego URL za każdym razem, gdy przychodzi nowe połączenie. URL zawiera identyfikator Speech Engine i jednorazowy podpis, więc most nigdy nie potrzebuje surowego klucza API.

from elevenlabs import AsyncElevenLabs
elevenlabs = AsyncElevenLabs(api_key=os.environ["ELEVENLABS_API_KEY"])
async def signed_url() -> str:
response = await elevenlabs.conversational_ai.conversations.get_signed_url(
agent_id=os.environ["SPEECH_ENGINE_ID"],
)
return response.signed_url
3

Obsłuż odpowiedź TwiML

Gdy przychodzi połączenie, Twilio wysyła POST do /incoming-call. Odpowiedzią jest TwiML, który otwiera Media Stream do własnego WebSocketu mostu /media-stream.

from aiohttp import web
from twilio.request_validator import RequestValidator
validator = RequestValidator(os.environ["TWILIO_AUTH_TOKEN"])
async def incoming_call(request: web.Request) -> web.Response:
form = await request.post()
signature = request.headers.get("X-Twilio-Signature", "")
url = str(request.url)
if not validator.validate(url, dict(form), signature):
return web.Response(status=403, text="forbidden")
host = request.headers.get("X-Forwarded-Host") or request.host
twiml = (
'<?xml version="1.0" encoding="UTF-8"?>'
"<Response><Connect>"
f'<Stream url="wss://{host}/media-stream"/>'
"</Connect></Response>"
)
return web.Response(text=twiml, content_type="text/xml")

RequestValidator (Python) i twilio.webhook({ validate: true }) (Node) sprawdzają nagłówek X-Twilio-Signature względem TWILIO_AUTH_TOKEN. Bez walidacji każdy w publicznym internecie mógłby wysłać POST do /incoming-call i naliczać rozmowy na twoje konto.

4

Połącz Media Stream

Media Stream to WebSocket wysyłający sekwencję zdarzeń JSON: connected, start, media (ładunek audio) i stop. Most otwiera conversation WebSocket Speech Engine przy start i przekazuje dźwięk w obu kierunkach, aż strumień zostanie zamknięty.

import asyncio
import json
import aiohttp
from aiohttp import web
async def media_stream(request: web.Request) -> web.WebSocketResponse:
twilio_ws = web.WebSocketResponse()
await twilio_ws.prepare(request)
stream_sid: str | None = None
el_session: aiohttp.ClientSession | None = None
el_ws: aiohttp.ClientWebSocketResponse | None = None
pump_task: asyncio.Task | None = None
async def pump_el_to_twilio(el: aiohttp.ClientWebSocketResponse):
async for msg in el:
if msg.type != aiohttp.WSMsgType.TEXT:
continue
event = json.loads(msg.data)
etype = event.get("type")
if etype == "audio":
await twilio_ws.send_str(json.dumps({
"event": "media",
"streamSid": stream_sid,
"media": {"payload": event["audio_event"]["audio_base_64"]},
}))
elif etype == "interruption":
await twilio_ws.send_str(json.dumps({
"event": "clear",
"streamSid": stream_sid,
}))
elif etype == "ping":
event_id = event.get("ping_event", {}).get("event_id")
await el.send_str(json.dumps({
"type": "pong", "event_id": event_id,
}))
try:
async for msg in twilio_ws:
if msg.type != aiohttp.WSMsgType.TEXT:
continue
event = json.loads(msg.data)
if event["event"] == "start":
stream_sid = event["start"]["streamSid"]
el_session = aiohttp.ClientSession()
el_ws = await el_session.ws_connect(await signed_url())
await el_ws.send_str(json.dumps({
"type": "conversation_initiation_client_data",
}))
pump_task = asyncio.create_task(pump_el_to_twilio(el_ws))
elif event["event"] == "media" and el_ws is not None:
await el_ws.send_str(json.dumps({
"user_audio_chunk": event["media"]["payload"],
}))
elif event["event"] == "stop":
break
finally:
if pump_task:
pump_task.cancel()
if el_ws and not el_ws.closed:
await el_ws.close()
if el_session and not el_session.closed:
await el_session.close()
return twilio_ws

Zdarzenie interruption ze Speech Engine wywołuje zdarzenie clear w strumieniu Twilio, które odrzuca zbuforowany dźwięk, dzięki czemu przerwanie wypowiedzi działa płynnie. Na zdarzenie ping odpowiadamy pong, aby utrzymać conversation WebSocket aktywny.

5

Uruchom równolegle serwer brain

Serwer brain to standardowy serwer Speech Engine pokazany w krótkim przewodniku. Jedynym dodatkiem jest sprawdzenie wspólnego sekretu przy aktualizacji WebSocketu — zaakceptuj połączenie tylko wtedy, gdy x-api-key pasuje do wartości ustawionej w Speech Engine.

import os
from elevenlabs import AsyncElevenLabs
elevenlabs = AsyncElevenLabs(api_key=os.environ["ELEVENLABS_API_KEY"])
SHARED_SECRET = os.environ["SHARED_SECRET"]
async def brain_ws(request: web.Request) -> web.WebSocketResponse:
if request.headers.get("x-api-key") != SHARED_SECRET:
return web.Response(status=401, text="unauthorized")
ws = web.WebSocketResponse()
await ws.prepare(request)
engine = await elevenlabs.speech_engine.get(os.environ["SPEECH_ENGINE_ID"])
session = engine.create_session(ws)
async def on_transcript(transcript):
# Replace this with your own LLM call; see the quickstart.
await session.send_response("Hello, you've reached the demo.")
session.on("user_transcript", on_transcript)
await session.run()
return ws
def make_app() -> web.Application:
app = web.Application()
app.router.add_post("/incoming-call", incoming_call)
app.router.add_get("/media-stream", media_stream)
app.router.add_get("/ws", brain_ws)
return app
if __name__ == "__main__":
web.run_app(make_app(), port=3001)

Pełną implementację on_transcript, w tym wywołanie LLM i odpowiedź strumieniową, znajdziesz w krótkim przewodniku Speech Engine.

Skieruj Twilio do mostu

1

Uruchom most i publiczny tunel

ngrok http 3001
python bridge.py

Zapisz URL https://, który wyświetli ngrok — Twilio będzie wysyłać do niego POST.

2

Zaktualizuj ws_url Speech Engine

Ustaw speech_engine.ws_url na publiczny URL WebSocket endpointu brain, aby ElevenLabs wiedziało, gdzie się połączyć.

await elevenlabs.speech_engine.update(
speech_engine_id="seng_8k3m9xr4hjnfg983brhmhkd98n6",
speech_engine={"ws_url": "wss://abc123.ngrok.io/ws"},
)
3

Skonfiguruj numer Twilio

W konsoli Twilio otwórz Voice Configuration swojego numeru telefonu:

  • A call comes in: Webhook
  • URL: https://abc123.ngrok.io/incoming-call
  • HTTP method: POST

Jeśli numer jest podłączony do Elastic SIP Trunk, najpierw go odłącz — numer Twilio kieruje rozmowy albo do trunku, albo do webhooka, nie do obu naraz.

4

Zadzwoń na numer

Zadzwoń na numer z dowolnego telefonu. Agent odbierze; zacznij mówić, a usłyszysz jego odpowiedź. Przy włączonym logowaniu debugowania most zapisuje SID rozmowy, identyfikator rozmowy i format audio dla każdej tury.

Kwestie produkcyjne

  • Walidacja webhooka: zawsze weryfikuj X-Twilio-Signature dla /incoming-call. Powyższy przykład używa biblioteki pomocniczej Twilio — nie pomijaj tego kroku.
  • Współdzielony sekret: wymuszaj współdzielony sekret w WebSocket serwera logiki. Bez niego każdy, kto odgadnie twój adres URL ngrok, może się połączyć i podszyć pod ElevenLabs.
  • Stały host: adresy URL w darmowym planie ngrok zmieniają się przy każdym restarcie. Użyj zarezerwowanej domeny ngrok lub własnej nazwy hosta, aby nie aktualizować ws_url Speech Engine i webhooka Twilio po każdym restarcie.
  • Opóźnienia: każde wywołanie dodaje dwa skoki sieciowe do czasu LLM do pierwszego tokena. Użyj modelu o niskich opóźnieniach i przesyłaj odpowiedzi strumieniowo, aby skrócić odczuwalne opóźnienie.
  • Jeden czy dwa procesy: przykład umieszcza most i serwer logiki na tym samym porcie, więc jeden tunel ngrok obejmuje wszystko. Na produkcji możesz rozdzielić je na dwie usługi, o ile każda ma publiczny adres URL.
  • Prompt injection: mowa z rozmowy telefonicznej to niezaufane dane wejściowe użytkownika. Weryfikuj transkrypcje, zanim wpłyną na wywołania narzędzi lub zapis do bazy danych.

Kolejne kroki