ai newsFr., 23. Okt.

Entwickler baut AI-Telefonzentrale für Kleinbetriebe mit Webhooks und strukturiertem LLM-Output

Kleine Unternehmen verpassen viele Anrufe, weil das Personal gerade anders beschäftigt ist. Ein Entwickler hat mit Aethely eine AI-Telefonzentrale für kanadische Kleinbetriebe gebaut, die zustandslose Gesprächszüge und strukturierte LLM-Ausgaben nutzt.

dev.toFreitag 03:51 · 23.10.DE4 Min. Lesezeit
Bild: dev.to

Kleinunternehmen verpassen viele Anrufe. Ein Klempner liegt unter einem Waschbecken, die Rezeption einer Zahnarztpraxis nimmt Patienten an, eine Salonbesitzerin sitzt mitten im Färben und der Anrufer legt auf und wählt den nächsten Betrieb bei Google.

Ich habe eine KI-Telefonzentrale für Kleinunternehmen in Kanada entwickelt (Aethely), die genau diese Anrufe abdeckt. Dieser Beitrag zeigt, wie ein Anruf tatsächlich abläuft, welche Designentscheidungen wichtig waren und was ich jedem sagen würde, der etwas Ähnliches baut.

Das Ganze ist eine Schleife aus Webhooks zwischen dem Telefonnetz und einem kleinen serverless Backend.

Jeder Schritt ist eine zustandslose HTTP-Anfrage, was den Betrieb auf serverless Funktionen günstig macht.

Der Anbieter (in meinem Fall Twilio) erwartet XML-Anweisungen. Die Begrüßung ist bewusst ehrlich und sagt, dass es sich um eine KI handelt:

<Response> <Gather input= "speech" action= "/api/voice/turn?s=STATE" speechTimeout= "auto" language= "en-CA" actionOnEmptyResult= "true" > <Say> Hi, thanks for calling Northside Heating. I'm the virtual assistant, an AI. How can I help? </Say> </Gather> </Response> Enter fullscreen mode Exit fullscreen mode actionOnEmptyResult="true" ist wichtig: Wenn der Anrufer nichts sagt, erhalten wir trotzdem einen Webhook und können die Stille selbst behandeln, statt dass das Gespräch einfach endet.

Jeder Zug ist eine separate Anfrage, also wo liegt der Gesprächsverlauf? Man könnte eine Datenbank oder einen Cache mit der Anruf-ID als Schlüssel verwenden. Ich habe es einfacher gemacht: Den Verlauf komprimieren und ihn in der Callback-URL mitführen.

import { deflateRawSync, inflateRawSync } from ' node:zlib '; export const packState = ( obj ) => deflateRawSync ( Buffer. from ( JSON. stringify ( obj ))). toString ( ' base64url ' ); export const unpackState = ( s ) => JSON. parse ( inflateRawSync ( Buffer. from ( s, ' base64url ' )). toString ()); Enter fullscreen mode Exit fullscreen mode Ein typischer Anruf hat weniger als zehn Züge, also bleibt der komprimierte Verlauf klein. Keine Datenbank, keine Aufräumjobs, und jeder Funktionsaufruf hat alles, was er braucht.

(Wenn man das macht, sollte man die Zahl der Züge begrenzen und alles validieren, was man entpackt, denn es stammt aus einer URL.)

Die wichtigste Entscheidung: Nicht das Modell um freien Text bitten, sondern um ein JSON-Objekt mit sowohl dem zu Sprechenden als auch dem, was es gelernt hat:

const TURN_SCHEMA = { type: ' object ', properties: { say: { type: ' string ' }, // next line to speak end_call: { type: ' boolean ' }, // conversation is finished urgent: { type: ' boolean ' }, // matches the owner's urgent rules transfer: { type: ' boolean ' }, // put through to the owner now details: { type: ' object ', properties: { name: { type: ' string ' }, callback: { type: ' string ' }, need: { type: ' string ' }, address: { type: ' string ' }, booking: { type: ' string ' }, }, }, }, required: [ ' say ', ' end_call ', ' urgent ', ' transfer ', ' details ' ], }; Enter fullscreen mode Exit fullscreen mode Mit einem Schema muss das Backend niemals Fließtext parsen. transfer: true wird zu einem <Dial> ans Mobiltelefon des Inhabers, end_call: true beendet das Gespräch und sendet die Zusammenfassung, und details füllt die Textnachricht aus, alles deterministische Codepfade.

Der Prompt enthält die Öffnungszeiten des Betriebs, die Leistungen, die Preise, die er nennen darf, Buchungszeitfenster und was als dringend gilt. Der wichtigste Teil ist jedoch die Liste der Dinge, die er nicht tun darf:

Die Inhaber vertrauten dem System, sobald sie diese Grenzen sahen. Nicht die Funktionen schufen Vertrauen, sondern die Beschränkungen.

Stille. Wenn der Anrufer nichts sagt, einmal nachfragen ("Entschuldigung, sind Sie noch da?"). Bei der zweiten Stille höflich beenden und dem Inhaber trotzdem alles Zusammengestellte zusenden.

Kurze Anrufe und Aufleger. Wenn der Anruf endet, wird ein Status-Callback ausgelöst. Dauerte er nur wenige Sekunden oder wurde nie angenommen, senden wir dem Anrufer eine freundliche SMS von der Nummer des Betriebs, damit der Lead nicht verloren geht.

Sicherheit. Jeder Webhook verifiziert die Signatur der Anfrage des Anbieters (einen HMAC über URL und Parameter), bevor irgendetwas passiert. Andernfalls könnte jeder gefälschte "Anrufe" an die Endpunkte senden.

Latenz. Telefonate wirken kaputt, wenn die Antworten zu lange dauern. Die Ausgabe des Modells kurz halten, den Prompt cachen und die Zusammenfassungs-SMS nach dem Gespräch senden, nicht währenddessen.

Der Moment, in dem Inhaber sagten "ach, jetzt verstehe ich", war nicht das Gespräch selbst. Es war der Erhalt einer SMS wie dieser, Sekunden nachdem man den Schraubenschlüssel weggelegt oder einen Kunden fertig behandelt hatte:

DRINGEND — Neuer Anruf für Northside Heating Anrufer: Jordan · (780) 555-0142 Anliegen: Heizung funktioniert nicht, Haus ist kalt Gebucht/gewünscht: Heute 14–16 Uhr

Kein Durchwühlen von Sprachnachrichten. Alles andere im System existiert, um diese Nachricht zuverlässig zu erzeugen.

Wer neugierig ist, wie das für einen echten Betrieb aussieht, findet eine Live-Demo und Preise auf der Aethely AI-Rezeptionsseite. Ich beantworte gerne Fragen zur Architektur in den Kommentaren.

Automatisch übersetzt. Das Original auf Englisch ist unten aufklappbar.

Original (Englisch) anzeigen

Small businesses miss a lot of calls. A plumber is under a sink, a dentist's front desk is checking in patients, a salon owner is mid-colour — and the caller hangs up and dials the next business on Google.

I built an AI phone receptionist for small businesses in Canada ( Aethely ) to cover exactly those calls. This post walks through how a call actually flows, the design decisions that mattered, and a few things I'd tell anyone building something similar.

The whole thing is a loop of webhooks between the phone network and a small serverless backend:

Every step is a stateless HTTP request, which makes it cheap to run on serverless functions.

The provider (Twilio, in my case) expects XML instructions. The greeting is deliberately honest — it says it's an AI:

<Response> <Gather input= "speech" action= "/api/voice/turn?s=STATE" speechTimeout= "auto" language= "en-CA" actionOnEmptyResult= "true" > <Say> Hi, thanks for calling Northside Heating. I'm the virtual assistant, an AI. How can I help? </Say> </Gather> </Response> Enter fullscreen mode Exit fullscreen mode actionOnEmptyResult="true" matters: if the caller says nothing, we still get a webhook, so we can handle silence ourselves instead of the call just ending.

Each turn is a separate request, so where does the conversation history live? You could use a database or a cache keyed by call ID. I went simpler: compress the history and carry it in the callback URL.

import { deflateRawSync, inflateRawSync } from ' node:zlib '; export const packState = ( obj ) => deflateRawSync ( Buffer. from ( JSON. stringify ( obj ))). toString ( ' base64url ' ); export const unpackState = ( s ) => JSON. parse ( inflateRawSync ( Buffer. from ( s, ' base64url ' )). toString ()); Enter fullscreen mode Exit fullscreen mode A typical call is under ten turns, so the compressed history stays small. No database, no cleanup jobs, and every function invocation has everything it needs.

(If you do this, cap the number of turns and validate everything you unpack — it came from a URL.)

The key decision: don't ask the model for free text. Ask for a JSON object with both what to say and what it has learned:

const TURN_SCHEMA = { type: ' object ', properties: { say: { type: ' string ' }, // next line to speak end_call: { type: ' boolean ' }, // conversation is finished urgent: { type: ' boolean ' }, // matches the owner's urgent rules transfer: { type: ' boolean ' }, // put through to the owner now details: { type: ' object ', properties: { name: { type: ' string ' }, callback: { type: ' string ' }, need: { type: ' string ' }, address: { type: ' string ' }, booking: { type: ' string ' }, }, }, }, required: [ ' say ', ' end_call ', ' urgent ', ' transfer ', ' details ' ], }; Enter fullscreen mode Exit fullscreen mode With a schema, the backend never has to parse prose. transfer: true becomes a <Dial> to the owner's mobile, end_call: true hangs up and sends the summary, and details fills the text message — all deterministic code paths.

The prompt contains the business's hours, services, prices it's allowed to share, booking windows and what counts as urgent. But the most important part is the list of things it must not do:

Owners trusted the system once they saw those limits. Features didn't build trust; boundaries did.

Silence. If the caller says nothing, ask once ("Sorry, are you still there?"). On the second silence, end politely and still send the owner whatever was captured.

Short calls and hang-ups. A status callback fires when the call ends. If it lasted only a few seconds, or was never answered, we send the caller a friendly text from the business's number so the lead isn't lost.

Security. Every webhook verifies the provider's request signature (an HMAC over the URL and parameters) before doing anything. Otherwise anyone could post fake "calls" to your endpoints.

Latency. Phone conversations feel broken if replies take too long. Keep the model's output short, keep the prompt cached, and do the summary SMS after the call — not during it.

The moment that made owners say "oh, I get it" wasn't the call itself. It was getting a text like this seconds after putting down a wrench or finishing with a client:

URGENT — New call for Northside Heating Caller: Jordan · (780) 555-0142 Needs: Furnace not working, house is cold Booked/requested: Today 2–4 PM

No voicemail to dig through. Everything else in the system exists to produce that message reliably.

If you're curious what this looks like for a real business, there's a live demo and pricing on the Aethely AI receptionist page. I'm happy to answer questions about the architecture in the comments.

Originaltitel: How I Built an AI Phone Receptionist: Webhooks, Stateless Turns and Structured LLM OutputQuelle: dev.to (dev.to). Original öffnen