Entwickler
KI-Agenten mit Telefonanrufen über die Sinch Voice-API v2 verbinden
Einen KI-Agenten mit einem Live-Telefonanruf zu verbinden, bedeutet eine Brücke zwischen dem Telefonnetz und dem Agenten zu schlagen. Dies kann eine SIP-Integration oder einen direkten Medienpfad über einen WebSocket sowie Speech-to-Text, Text-to-Speech und die Verwaltung der Konversationswechsel zwischen der anrufenden Person und dem Agenten umfassen. Das ist eine Menge Telefonie- und Medieninfrastruktur, die rund um einen KI-Agenten aufgebaut und betrieben werden muss.
Sinch Voice-API v2 nimmt Ihnen diese Arbeit ab. Das Voice Relay-Ziel verbindet einen Live-Anruf mit Ihrem WebSocket-Endpunkt, Sinch führt Speech-to-Text sowie Text-to-Speech aus und Ihre Anwendung tauscht reinen Text aus. Die Voice-API v2 fügt zudem Voice Streams hinzu. Das ist ein direkter bidirektionaler Audiopfad für Fälle, in denen Sie die Medien selbst verarbeiten möchten.
Sie befindet sich in der öffentlichen Vorschau. Daher können sich Funktionen, Limits, Dokumentation und Verhalten noch ändern und es gilt kein SLA, aber sie ist für Tests, Evaluierungen und frühen Produktionsdatenverkehr offen. Wir haben den gesamten Pfad durchgehend mit dem Tutorial-Beispielausgeführt: Ein LangChain-Agent beantwortet einen echten Telefonanruf, einschließlich Unterbrechungen.
Warum Voice Relay wichtig ist
Ein KI-Agent beginnt normalerweise als Textanwendung. Er nimmt eine Nachricht an, ruft ein Modell auf und gibt eine Textantwort zurück. Voice Relay verlagert die Audiogrenze in die Sinch-Plattform. So behalten Sie den Agenten, seine Tools, Prompts und seinen Status und arbeiten mit Text statt mit direkten Audiodaten. Unterbrechungen sind inbegriffen, sodass Anrufende dazwischenreden können, während eine Antwort abgespielt wird.
Das macht Voice Relay zu einem praktischen Einstiegspunkt für eine KI-Rezeption, einen Produktdemo-Bot und einen internen Helpdesk.
Das Voice-API v2-Modell
Die Voice-API v2 organisiert eine Interaktion anhand von drei Ressourcen:
- Eine Sitzung gruppiert zugehörige Anrufe und Verbindungen. Sie bleibt aktiv, bis die zugehörigen Anrufe beendet sind.
- Ein Anruf stellt die Verbindung einer teilnehmenden Person dar, beispielsweise ein Telefon-, SIP- oder Streaming-Abschnitt.
- Eine Brücke verbindet Anrufe innerhalb einer Sitzung, wenn mehrere Teilnehmende kommunizieren müssen.
SVAML, die Sinch Voice-API Markup Language, beschreibt, was während der Interaktion geschieht. Ein dial-Befehl kann einen Anrufabschnitt erstellen. Verschachtelte Ereignisse können definieren, was passiert, wenn der Anruf entgegengenommen wird, besetzt ist, eine Zeitüberschreitung auftritt oder fehlschlägt. Ein Webhook kann übernehmen, wenn die Anwendung eine dynamische Entscheidung treffen muss.
Dieses Modell ist für KI-Agenten wichtig, da die Konversation und der Anruf zwar zusammenhängen, aber nicht dasselbe sind. Der KI-Agent verwaltet die Konversation. SVAML und die Voice-API verwalten den Call-Flow. Diese Trennung ermöglicht es Ihnen, eine Weiterleitung an eine reale Person, einen weiteren Anrufabschnitt oder einen anderen Medienpfad hinzuzufügen, ohne diese gesamte Logik im Modell-Prompt unterbringen zu müssen.
Sie benötigen Folgendes:
- Ein Sinch-Konto mit Voice-API v2-Zugang, auf der Sinch Build Dashboard
- Eine Sinch-Projekt-ID, eine Access-Key-ID und ein Access-Key-Secret
- Eine aktivierte virtuelle Nummer von Sinch
- Eine Voice-API v2 Service-ID, unter Voice > Programmable Voice > Services im Dashboard
- Python 3.10 oder neuer
- Einen API-Schlüssel für OpenAI, Anthropic Claude oder Google Gemini
ngrokmit einem registrierten Authtoken oder eine andere Möglichkeit, einen lokalen Server überwss://bereitzustellen
Diese Anleitung verwendet das sinch-voice-tutorials -Repository. Das Tutorial 4.1-voice-relay ist ein von LangChain unterstützter Python-WebSocket-Server, der seine Konfiguration aus Umgebungsvariablen liest und OpenAI, Anthropic Claude sowie Google Gemini unterstützt.
Einen Agenten mit Voice Relay verbinden
Klonen Sie das Tutorials-Repository und wechseln Sie in das Voice Relay-Beispiel:
git clone https://github.com/sinch/sinch-voice-tutorials.git
cd sinch-voice-tutorials/4.1-voice-relay
Erstellen Sie eine Umgebung und installieren Sie die Abhängigkeiten des Beispiels:
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
Diese Befehle gelten für macOS und Linux. Verwenden Sie unter Windows python anstelle von python3 und aktivieren Sie die Umgebung mit .venv\Scripts\activate.
Kopieren Sie die Beispielkonfiguration:
cp .env.example .env
Legen Sie einen Anbieter und seinen API-Schlüssel in der .env fest:
PROVIDER=openai
API_KEY=YOUR_LLM_API_KEY
Die .env.example des Repositorys verwendet openai als Standardanbieter. Das Beispiel unterstützt außerdem claude für Anthropic Claude und gemini für Google Gemini. Die requirements.txt installiert die Pakete für alle drei. Ein Wechsel des Anbieters bedeutet also nur, PROVIDER und API_KEY zu ändern. Sie können auch MODEL, TEMPERATURE, MAX_TOKENS und PORT festlegen. Deren Standardwerte werden im folgenden Startprotokoll angezeigt. Außerdem können Sie GREETING festlegen, um das Hello! zu ändern, mit dem sich der Agent meldet.
Starten Sie den lokalen Server:
python server.py
Der Server meldet den geladenen System-Prompt, danach seine Anbieter- und Modelleinstellungen und dann die lokale Adresse, auf der er lauscht:
[*] Loaded system prompt (3424 chars)
[*] Provider: openai Model: gpt-4o Temp: 0.7 MaxTokens: 1024
[*] Agent Relay listening on ws://0.0.0.0:8765
Überprüfen Sie diese Ausgabe, bevor Sie einen Anruf tätigen. Ein fehlender Anbieterschlüssel oder eine fehlerhafte Abhängigkeit wird hier angezeigt und nicht erst mitten in einer Konversation.
Das Verhalten des Agenten wird durch die system_prompt.md bestimmt. Bearbeiten Sie diese Datei, um Persona, Domain oder Anweisungen zu ändern. Starten Sie dann die server.py neu, da der Prompt beim Start geladen wird.
Sinch stellt eine eingehende Verbindung zu Ihrem Relay-Server her, daher wird eine öffentliche Adresse benötigt. Lassen Sie die server.py im ersten Terminal laufen und öffnen Sie ein zweites. Wenn Sie ngrok noch nicht haben, installieren Sie es und registrieren Sie ein Authtoken über die Seite Your Authtoken im ngrok-Dashboard. Die kostenlose Stufe reicht für diese Anleitung aus. ngrok startet keinen Tunnel, bevor nicht ein Token konfiguriert ist:
brew install ngrok
ngrok config add-authtoken YOUR_NGROK_AUTHTOKEN
ngrok http 8765
ngrok übernimmt das Terminal und gibt eine Statustabelle aus. Die entscheidende Zeile ist die Weiterleitungsadresse:
Forwarding https://YOUR_NGROK_ID.ngrok-free.dev -> http://localhost:8765
Nehmen Sie diese Adresse, ändern Sie nur das Schema in wss:// und verwenden Sie sie als Voice Relay-Endpunkt:
ngrok address: https://YOUR_NGROK_ID.ngrok-free.dev
Sinch endpoint: wss://YOUR_NGROK_ID.ngrok-free.dev
Dieser wss://-Wert gehört in die Sinch-Servicekonfiguration und nicht in den Relay-Server, der weiterhin auf dem lokalen Port 8765 lauscht. Lassen Sie von nun an sowohl die server.py als auch ngrok laufen.
Das minimale Voice Relay-Ziel sieht so aus:
{
"type": "VOICE_RELAY",
"voiceRelay": {
"endpoint": "wss://YOUR_NGROK_ID.ngrok-free.dev",
"ttsVoice": "Tiffany",
"sttLanguage": "en-US"
}
}
| Feld | Erforderlich | Hinweise |
endpoint | Ja | Die wss://-Adresse, mit der sich Sinch verbindet |
ttsVoice | Ja | Tiffany passt zu diesem Tutorial. Lesen Sie den Referenz für unterstützte Voice-Profile bevor Sie diesen Wert ändern |
sttLanguage | Ja | Ein BCP-47-Sprach-Tag |
enableInterruptions | Nein | Standardwert ist true |
callHeaders | Nein | Bis zu 16 Schlüssel/Wert-Paare, jeder Schlüssel und Wert mit maximal 255 Zeichen |
Ihr Service benötigt die Nummer, bevor dies alles relevant wird. Öffnen Sie den Service im Dashboard, gehen Sie zu Voice-Kanälen, wählen Sie in der Zeile „Phone“ die Option „Configure“, klicken Sie dann auf „Add numbers“ und wählen Sie Ihre virtuelle Nummer aus. Eine Nummer gehört immer nur zu einem Service. Wenn sie also bereits einem anderen Service zugewiesen ist, bittet Sie das Dashboard, die Neuzuweisung zu bestätigen. Eingehende Anrufe an diese Nummer werden von da an an den neuen Service weitergeleitet.
Sobald die Nummer eingerichtet ist, routen Sie den Anruf, indem Sie dem Service ein statisches Anrufverhalten oder einen Webhook zuweisen, der SVAML zurückgibt. Die statische Konfiguration erfüllt fünf Aufgaben:
- Nimmt den eingehenden Anruf entgegen.
- Fügt den eingehenden Abschnitt zur
main-bridgehinzu. - Erstellt einen Anrufabschnitt mit einem
VOICE_RELAY-Ziel. - Fügt den Relay-Abschnitt bei Entgegennahme zur selben
main-bridgehinzu. - Beendet den Relay-Abschnitt, wenn der eingehende Abschnitt endet.
server.py und ngrok belegen nun beide ihre Terminals. Öffnen Sie daher für diesen Schritt ein drittes Terminal. Füllen Sie die fünf Werte oben in der Anfrage aus und führen Sie sie aus. Wenn Sie lieber das Dashboard verwenden möchten, fügen Sie den inneren SVAML-Body aus der static.txt stattdessen in dessen Editor für vordefiniertes Anrufverhalten ein.
export PROJECT_ID=YOUR_PROJECT_ID
export KEY_ID=YOUR_ACCESS_KEY_ID
export SERVICE_ID=YOUR_SERVICE_ID
export VOICE_RELAY_ENDPOINT=wss://YOUR_NGROK_ID.ngrok-free.dev
# prompts without echoing, so the secret stays out of shell history
printf 'Access key secret: '
read -rs KEY_SECRET
echo
export KEY_SECRET
curl -X PATCH \
-u "$KEY_ID:$KEY_SECRET" \
"https://voice.api.sinch.com/v2/projects/$PROJECT_ID/services/$SERVICE_ID" \
-H "Content-Type: application/json" \
-d @- <<JSON
{
"callBehavior": {
"type": "STATIC",
"static": {
"callName": "caller",
"commands": [
{ "command": "answer" },
{ "command": "bridgeCall", "bridgeName": "main-bridge" },
{
"command": "dial",
"callName": "voice_relay_call",
"to": {
"type": "VOICE_RELAY",
"voiceRelay": {
"endpoint": "$VOICE_RELAY_ENDPOINT",
"ttsVoice": "Tiffany",
"sttLanguage": "en-US"
}
},
"events": {
"onAnswer": [
{ "command": "bridgeCall", "bridgeName": "main-bridge" }
]
}
}
],
"events": {
"onHangup": [
{ "command": "hangup", "callName": "voice_relay_call" }
]
}
}
}
}
JSON
Ein erfolgreicher PATCH gibt den aktualisierten Service zurück. So können Sie Ihren Endpunkt aus der Antwort ablesen und bestätigen, dass er übernommen wurde:
{
"serviceId": "YOUR_SERVICE_ID",
"projectId": "YOUR_PROJECT_ID",
"name": "voice-relay-test",
"isDefault": true,
"callBehavior": {
"type": "STATIC",
"static": {
"callName": "caller",
"commands": [
{ "command": "answer" },
{ "command": "bridgeCall", "bridgeName": "main-bridge" },
{
"command": "dial",
"to": {
"type": "VOICE_RELAY",
"voiceRelay": {
"endpoint": "wss://YOUR_NGROK_ID.ngrok-free.dev",
"ttsVoice": "Tiffany",
"sttLanguage": "en-US"
}
},
"events": {
"onAnswer": [
{ "command": "bridgeCall", "bridgeName": "main-bridge" }
]
},
"callName": "voice_relay_call"
}
],
"events": {
"onHangup": [
{ "command": "hangup", "callName": "voice_relay_call" }
]
}
}
}
}
Der innere SVAML-Body ist auch verfügbar in der static.txt.
So sieht der Erfolg aus
Rufen Sie die mit dem Service verknüpfte Sinch-Nummer an. Sie sollten Hello!hören, oder die Begrüßung, die Sie mit GREETING festgelegt haben. Stellen Sie dann eine Frage und warten Sie auf die Antwort des Agenten.
Das Server-Terminal protokolliert beide Richtungen des WebSockets. Dieses Protokoll ist das nützlichste Hilfsmittel auf dem Bildschirm während eines ersten Anrufs. Ein Beispielprotokoll, gekürzt auf einen Austausch, sieht so aus:
[+] Connected: ('127.0.0.1', 65438)
WS << {"callHeaders":{},"callId":"01M25YACD3G61GH4Y0NQNWR4WD","interruptionsEnabled":true,...}
connect callId=01M25YACD3G61GH4Y0NQNWR4WD serviceId=YOUR_SERVICE_ID
WS >> {"command": "answer"}
WS >> {"command": "text", "text": "Hello!", "isLast": true, "isInterruptible": true}
WS << {"batchSequence":0,"command":"textPlaybackStart"}
WS << {"batchSequence":0,"command":"textPlaybackStop"}
WS << {"reason":"speech-detected","command":"interrupt"}
WS << {"sttLanguage":"en-US","text":"Hi.","isCorrection":false,"command":"text"}
STT → 'Hi.'
LLM ← 'Hey there! How can I assist you today? If it involves voice tech or a good dad j'…
WS >> {"command": "text", "text": "Hey there! How can I assist you today?...", "isLast": true}
WS << {"batchSequence":1,"command":"textPlaybackStart"}
WS << {"batchSequence":1,"command":"textPlaybackStop"}
[-] Connection closed (1006):
[-] Session ended callId=01M25YACD3G61GH4Y0NQNWR4WD
Zwei Dinge in diesem Protokoll sind vor Ihrem ersten Anruf wichtig. Die connect-Nachricht enthält interruptionsEnabled: true. Hier können Sie den Standardwert für Unterbrechungen überprüfen, anstatt ihn aus der Konfigurationsreferenz zu entnehmen. Und die Verbindung wird mit dem Code 1006 geschlossen, wenn die anrufende Person auflegt. Dies sieht wie eine abnormale Schließung aus, ist hier aber das normale Verhalten beim Auflegen.
Ein interrupt mit reason=speech-detected erscheint bei jedem Wechsel, da Sinch einen sendet, sobald die anrufende Person gehört wird. Seine Position ist aussagekräftig: Nach textPlaybackStop markiert er lediglich den Beginn des Sprechanteils der anrufenden Person, während er zwischen textPlaybackStart und textPlaybackStop ein Dazwischenreden darstellt. Als ich während einer Antwort gesprochen habe, wurde die Wiedergabe gestoppt. Das nächste Transkript enthielt das, was ich dazwischengerufen hatte:
WS << {"batchSequence":5,"command":"textPlaybackStart"}
WS << {"reason":"speech-detected","command":"interrupt"}
WS << {"batchSequence":5,"command":"textPlaybackStop"}
WS << {"sttLanguage":"en-US","text":"I'm going to stop you right there.",...,"command":"text"}
Das ist ein Voice-Interface vor einem textbasierten KI-Agenten, ohne dass Sie selbst auch nur einen einzigen Audio-Frame verarbeiten müssen.
Wo sich Ihre Anwendung befindet
Sinch und der WebSocket-Server tauschen ein kleines JSON-Protokoll mit einem Befehl pro Nachricht aus, was die server.py für Sie übernimmt. Transkriptkorrekturen sind der Teil, den Sie verstehen sollten, bevor Sie auf dem Beispiel aufbauen.
Sinch sendet ein frühes Transkript und möglicherweise danach ein korrigiertes Transkript für dieselbe Äußerung mit isCorrection: true. Der korrigierte Text enthält den früheren Text, anstatt nur den geänderten Teil zu ersetzen. Es passierte einmal bei drei Anrufen: Auf Thank you. folgte Thank you.\nOh, that's nice., und beide wurden an das Modell gesendet, sodass der Agent auf dieselbe Äußerung zweimal antwortete. Entscheiden Sie, ob Sie auf eine Korrektur warten, die laufende Anfrage abbrechen oder Korrekturen vollständig ignorieren.
Die Anwendungsgrenze sieht so aus:
Sprache der anrufenden Person
|
Sinch Speech-to-Text
|
Voice Relay-SMS
|
KI-Agent
|
Voice Relay-Textantwort
|
Sinch Text-to-Speech
|
Anrufende Person hört die Antwort
Voice Relay oder direktes Audio-Streaming?
| Wählen Sie | Wenn |
| Voice Relay | Ihr Agent Text akzeptiert und Text zurückgibt, und Sinch STT sowie TTS übernimmt. |
| Voice Streams | Sie direkte Audiodaten benötigen und über eine eigene STT/TTS-Pipeline verfügen. |
Voice Streams eignet sich für eine benutzerdefinierte Sprach-Pipeline, einen spezialisierten Audioprozessor oder einen Anbieter, dessen Protokoll direkten Audiozugriff erfordert. Es bedeutet auch, dass Sie mehr Verantwortung für das Latenz- und Fehlerverhalten tragen.
Überlegungen zur Produktion
Voice-Interaktionen machen Latenz spürbar. Eine langsame Modellantwort führt zu Stille bei der anrufenden Person. Messen Sie daher die Zeit vom eingehenden Sprachereignis bis zur ersten Antwort und bis zur vollständigen Antwort. Verwenden Sie ein Modell und einen Prompt, die für eine Telefonkonversation geeignet sind, anstatt nur auf maximale Antwortqualität zu optimieren.
Das Beispiel im Repository wartet auf die vollständige Modellantwort, bevor es etwas zurücksendet. Anrufende hören also so lange nichts, bis das Modell die Generierung abgeschlossen hat. Das ist der erste Ansatzpunkt, wenn sich die Pausen zu lang anfühlen.
Das Beispiel behält zudem den Konversationsverlauf für jede WebSocket-Verbindung im Arbeitsspeicher. Das ist für einen ersten Test in Ordnung, aber kein dauerhafter Konversationsspeicher. Entscheiden Sie, welcher Status zu einem Anruf gehört, welcher zu einem Kunden und welcher eine erneute Verbindung überdauern muss.
Unterbrechungen erfordern eine explizite Verarbeitung. Wenn anrufende Personen sprechen, während eine Antwort abgespielt wird, können Sie ein Unterbrechungsereignis erhalten, während noch eine LLM-Anfrage ausgeführt wird. Brechen Sie die Anfrage ab, wo dies möglich ist, oder fügen Sie jeder Antwort einen Turn-ID-Tag hinzu und verwerfen Sie veraltete Ausgaben, bevor Sie sie an Sinch zurücksenden. Dieselbe Turn-ID verarbeitet korrigierte Transkripte. Diese kommen als zweite text-Nachricht für eine Äußerung an, die Sie bereits an das Modell gesendet haben.
Das lokale Beispiel erfordert vor dem produktiven Einsatz auch eine robustere Fehlerbehandlung:
- Senden Sie eine kurze Fallback-Antwort, wenn der Modellanbieter ausfällt.
- Speichern Sie den Konversationsstatus dauerhaft, wenn der Anruf einen Prozessneustart überleben muss.
- Halten Sie den WebSocket-Endpunkt für die gesamte Lebensdauer des Anrufs verfügbar.
- Protokollieren Sie Kennungen für Anrufe, Sitzungen, Verbindungen und Modell-Timing, ohne sensible Konversationsinhalte unnötig zu speichern.
- Verwenden Sie OAuth 2.0-Client-Anmeldeinformationen für den produktiven API-Zugang. Die Basic-Authentifizierung ist für erste Tests nützlich.
Fehlerbehebung
Überprüfen Sie die Anbieterkonfiguration, bevor Sie einen Anruf tätigen. Ein fehlerhafter oder fehlender LLM-Schlüssel führt zu einem stummen Anruf anstelle eines Fehlers, da das Modell erst beim ersten Wechsel aufgerufen wird. Stellen Sie daher sicher, dass PROVIDER und API_KEY in der .env übereinstimmen.
Die kostenlose ngrok-URL ändert sich, wenn der Tunnel neu startet. Aktualisieren Sie den endpoint in der Voice-API v2-Servicekonfiguration, wann immer dies geschieht, da Sinch sonst den aktuellen Server nicht erreichen kann. Die server.py muss dafür nicht neu gestartet werden.
Vorschau-Status
Die Vorschau-Bedingungen erlauben Tests, Evaluierung, frühe kommerzielle Nutzung sowie Live-Traffic und besagen, dass die Plattform darauf ausgelegt ist, Produktionsvolumina zu unterstützen. Der Service wird „wie besehen“ und „wie verfügbar“ bereitgestellt. Es können Nutzungslimits gelten und es greift kein SLA. Sinch kann Funktionen hinzufügen und potenziell inkompatible Änderungen („breaking changes“) können vor der allgemeinen Verfügbarkeit nicht ausgeschlossen werden.
Die nächsten Schritte
Voice Relay ist ein Weg in die Voice-API v2. Dasselbe Plattformmodell unterstützt außerdem:
- Ausgehende Voice-Warnungen mit Text-to-Speech
- Anrufbeantworter-Erkennung
- Batch-Anrufe mit Call Pacing
- Anrufaufzeichnung und Transkription
- Rufnummernmaskierung
- SIP-Verbindungen
- Direktes Audio-Streaming
- Live-Anrufsteuerung über SVAML und Webhooks
Die Sinch Voice-API v2 Tutorials-Repository enthält funktionierende Beispiele für diese Wege. Beginnen Sie mit dem Tutorial 4.1-voice-relay für einen textbasierten Agenten und wechseln Sie dann zu 4.2-stream-audio, wenn Sie direkten Zugriff auf den Audio-Stream benötigen.
Die Voice-API v2 bietet einem vorhandenen Textagenten ein Routing in eine Live-Voice-Interaktion, ohne diesen Agenten in ein audioverarbeitendes System zu verwandeln. Klonen Sie 4.1-voice-relay, verweisen Sie in der system_prompt.md auf den Prompt Ihres eigenen Agenten und rufen Sie die Nummer an.