AIアシスタントにはバックエンドが必要:それをエッジに配置する
この記事では、Telnyx Edge Compute関数を使用して音声AIアシスタント用のバックエンドを構築する方法を紹介します。単一の関数で動的変数とWebhookツール呼び出しを処理し、リクエストを検証してビジネスロジックに接続することで、アーキテクチャを簡素化しパフォーマンスを向上させます。
音声AIアシスタントの構築はかつてないほど簡単になりました。プロンプトを書き、電話番号を接続し、モデルを選ぶだけで、数分以内にアシスタントが電話に出るようになります。最初の会話が成功すると、まるで魔法のように感じられます。しかし、誰かが「注文の場所を教えてくれますか?」とか「明日誰かを手配してもらえますか?」と尋ねると、アシスタントは情報を必要とします。CRM、スケジューリングシステム、自社のAPIと連携する必要があるのです。LLMはもはやアプリケーションそのものではなく、より大きなシステムの一部に過ぎません。これが、すべての本番AIアシスタントがバックエンドを必要とするポイントです。この記事では、Telnyx Edge Compute関数を1つ使って、そのバックエンドを構築します。別のWebhookサービスをデプロイする代わりに、1つのGo関数で動的変数とWebhookツール呼び出しの両方を処理し、受信リクエストを検証し、アシスタントを舞台裏で動作するビジネスロジックに接続します。
アーキテクチャ
サンプルアプリケーションは実際に何を行うのか見てみましょう。例として、架空のホームサービス会社で働くJordanという名前のアシスタントがあります。発信者の視点では、やり取りはシンプルに感じられます。アシスタントが電話に出て、挨拶し、情報を収集し、現場見積もりの予約を入れます。しかし舞台裏では、2つの異なるバックエンドのやり取りが行われます。Jordanが最初の言葉を発する前に、アシスタントはプロンプト内に置くべきでないランタイム情報を要求します。その後、発信者から十分な情報を集めたJordanは、予約をスケジュールする必要があります。両方のリクエストは、まったく同じEdge Compute関数で処理されます。概念的に、アーキテクチャは次のようになります。
異なる責任のために別々のWebhookサービスをデプロイする代わりに、1つの関数がすべてのアシスタントコールバックを担当します。これは小さな実装の詳細に見えるかもしれませんが、いくつかの利点があります。デプロイは1つ、エンドポイントは1つ、シークレット管理は1箇所、リクエスト検証は1箇所、ビジネスロジックへの接続は1箇所です。アプリケーションが成長するにつれて、このシンプルさは驚くほど貴重になります。
完全なソースコードはedge-ai-assistant-backend-goのサンプルリポジトリにあり、デプロイガイドではAIアシスタントとEdge Compute関数の設定を段階的に説明しています。
動的変数を理解する
最初のコールバックの1つは、アシスタントが話し始める前に行われます。ここで動的変数が登場します。誰かが会社に電話をかけるときの挨拶が静的でないことを考えてみてください。「Pinecrest Home Servicesにお電話いただきありがとうございます」と言いたいかもしれません。プレミアム顧客には異なる挨拶が聞こえるかもしれませんし、転送先の番号は現在営業中のオフィスによって変わるかもしれませんし、推定待ち時間は今日のスケジュールに依存するかもしれません。これらのどれもプロンプトに含めるべきではありません。プロンプトは動作を記述するものであり、ランタイムデータは別の場所に属します。動的変数はその問題を解決します。Telnyxは通話が開始されるたびにバックエンドにこれらの値を要求し、関数は次のようなJSONを返します。
{ "dynamic_variables": { "company_name": "Pinecrest Home Services", "timeframe": "two business days", "placeholder_transfer_destination": "+15551234567" } }
見落としがちな詳細はラッパーオブジェクトです。応答はdynamic_variablesキーの下で返さなければなりません。フラットなJSONオブジェクトを返してもアシスタントの変数は設定されません。値が到着すると、すぐにプロンプト内で利用可能になります。つまり、ランタイムで解決されるという点で、設定にハードコードされるのとは異なります。
アシスタントがアプリケーションを必要とするとき
動的変数は問題の前半を解決します。後半は会話中に発生します。発信者が「今週の金曜日に来てほしい」と言ったとします。この時点でアシスタントはまだ存在しないもの(予約)を作成する必要があります。これはLLMが捏造すべきではなく、アプリケーションに問い合わせる必要があります。ここでWebhookツールが活躍します。アシスタントはアクションを実行するのに十分な情報を持っていると認識し、schedule_estimateというツールを呼び出します。リクエストは、動的変数のコールバックを処理したのと同じEdge Compute関数に送信されます。関数は必要なビジネスロジックを実行します(内部スケジューリングAPIの呼び出し、技術者の空き状況の確認、CRMレコードの作成など)。サンプルは意図的にシンプルに保ち、次のように返します。
{ "scheduled_date": "2025-04-10", "scheduled_time": "10:00", "confirmation_number": "CONF-1715234567", "estimate_id": "EST-1715234567" }
重要なのは偽の確認番号ではなく、パターンです。アシスタントはビジネスデータを生成せず、アプリケーションが生成します。アシスタントはいつ情報が必要かを知っているだけで、バックエンドがその情報を決定します。これは、LLMに識別子、予約スロット、顧客レコードを捏造させるよりもはるかに健全な分離です。
1つの関数、2つのまったく異なるリクエスト
このサンプルで特に気に入っている点は、必要なインフラが非常に少ないことです。多くのWebhookベースのシステムは、次のようなエンドポイントを持つことになります:/dynamic-variables、/schedule、/orders、/customers、/transfers、/authentication... やがてアシスタントをサポートするためだけにAPIをデプロイすることになります。このサンプルは異なるアプローチを取ります。両方のコールバックタイプがまったく同じURLを指します。関数は受信リクエストを調べ、data.event_typeが含まれていれば動的変数の処理と判断し、フラットなJSONペイロードを受信した場合はWebhookツールの呼び出しとして扱います。実装は数分で理解できるほど小さく、それでいて本番環境に適したアーキテクチャを示しています。アシスタントの機能が増えるにつれて、同じ関数を通じてさまざまなコールバックタイプをルーティングし続けることも、後でアプリケーションがそのモデルを超えて成長した場合に別のサービスに分割することもできます。シンプルに始めるのが通常は正しい判断です。
なぜバックエンドをエッジに置くのか?
「なぜ普通のWebhookサーバーをデプロイしないのか?」と疑問に思うかもしれません。それは完全に有効な選択肢であり、多くのチームがそうしています。問題は従来のバックエンドが機能するかどうかではなく、この特定のワークロードにとって最もシンプルなアーキテクチャかどうかです。音声AIアプリケーションは、ほとんどのWebアプリケーションとは異なるレイテンシプロファイルを持ちます。動的変数はアシスタントが挨拶する前に解決され、ツールの呼び出しは誰かが電話で待っている間に発生します。バックグラウンドのAPIリクエストとは異なり、追加のネットワークホップはすべて会話体験に直接影響します。それはミリ秒単位が成功か失敗かを決定するという意味ではありませんが、コールバックのレイテンシはユーザー体験の一部であるということです。
アシスタントのバックエンドをEdge Computeで実行すると、コールバックロジックが通話を処理している通信インフラに近くなります。また、運用作業の一部が不要になります。別のWebサービスをプロビジョニングし、公開し、デプロイを管理し、別のインフラを維持する代わりに、1つのEdge Compute関数をデプロイし、アシスタントをその呼び出しURLに向けるだけです。多くの音声アプリケーションにとって、それで十分です。アシスタントは依然として自社のデータベース、CRM、スケジューリングシステム、内部APIと通信します。違いは、アシスタントのコールバックを受け取るために別のスタンドアロンバックエンドを必要としないことです。そしておそらくもっと重要なのは、アーキテクチャが理解しやすいままであることです。明確なメンタルモデルがあります。AIアシスタントが会話を所有し、Edge Computeがビジネスロジックを所有します。この分離は、アプリケーションが大幅に洗練されても維持されます。
本番バックエンドの構築
このリポジトリのスケジュール例は意図的にシンプルですが、アーキテクチャは予約のスケジュールをはるかに超えてスケールします。バックエンドをAIアシスタントの背後に配置すると、会話とアプリケーションの残りの部分の間の橋渡し役になります。アシスタントが通話中に実行する必要があるアクションの種類を考えてみてください。CRMからの顧客情報の取得、注文ステータスの確認、サポートチケットの作成、予約のスケジュール、内部ワークフローのトリガー、通話を人間に転送するかどうかの判断などです。アシスタントの観点からは、これらはすべて同じ操作です。つまり、アシスタントは自分が持っていない情報が必要であると認識し、ツールを呼び出し、応答を待ち、会話を続けます。バックエンドはそれらすべての違いが存在する場所です。あるツールはSalesforceを呼び出し、別のツールはPostgreSQLにクエリを実行し、別のツールは内部REST APIを呼び出し、4つ目のツールは単にRedisでデータを検索するかもしれません。アシスタントはそれらがどのように機能するかを知る必要はありません。その仕事はビジネスロジックが必要なときに認識することです。バックエンドがそのロジックをどのように実行するか、どのデータを返すべきかを決定します。会話ロジックをアプリケーションロジックから分離しておくことは、有用なアーキテクチャ上の境界であることがわかります。プロンプトはアシスタントの動作に焦点を当てたままで、バックエンドはビジネスシステムの成長に合わせて独立して進化します。
セキュリティは本番のTODOにすべきでない
このサンプルで評価できる点の1つは、セキュリティを読者の課題にしていないことです。GitHubリポジトリを十分に閲覧してきたなら、「TODO: Verify webhook signature」のようなコメントを見たことがあるでしょう。それはデモには有効ですが、実際の電話に参加するアプリケーションには適しません。Telnyxから送信されるすべてのWebhookリクエストはEd25519を使用して署名されています。関数はリクエストを処理する前に、telnyx-signature-ed25519とtelnyx-timestampヘッダーを読み取り、タイムスタンプと生のリクエストボディを組み合わせて署名ペイロードを再構築し、タイムスタンプが許容範囲内にあることを検証し、最後にTelnyxの公開鍵を使用して署名を検証します。これらのチェックが成功した後にのみ、関数はビジネスロジックを続行します。このエンドポイントは単にJSONを返すだけでなく、予約を作成し、顧客固有の情報を解決し、内部システムとやり取りする可能性があるため、これは重要です。関数は、これらの操作を実行する前にすべてのリクエストが本当にTelnyxから発信されたことを確信すべきです。
シークレットの管理
署名検証にはTelnyxの公開鍵が必要であり、サンプルではアプリケーションにハードコードする代わりにEdge Computeのシークレットとして保存しています。鍵の取得は簡単です:
PUBLIC_KEY=$(curl -s \ -H "Authorization: Bearer $TELNYX_API_KEY" \ https://api.telnyx.com/v2/public_key \ | jq -r '.data.public')
取得後、CLIを使用して安全に保存します: telnyx-edge secrets add TELNYX_PUBLIC_KEY "$PUBLIC_KEY" 同じメカニズムは、バックエンドが依存する他のもの(APIキー、OAuth認証情報、データベースパスワード、下流サービスの認証トークンなど)にも機能します。設定をアプリケーションコードの外部に保持することで、デプロイが管理しやすくなり、秘密をソース管理に焼き付ける誘惑を避けられます。
なぜすべてが1つの関数を通るのか?
このサンプルでおそらく最も興味深い設計上の決定は、すべてが単一のEdge Compute関数を通ることです。一見すると、これは珍しいように思えるかもしれません。ほとんどのWebアプリケーションは、異なるリソースに対して複数のルートを公開します。顧客、予約、注文などに対して別々のエンドポイントを期待するかもしれません。しかし、このサンプルは異なるアプローチを取ります。単一の関数がすべてのコールバックを処理します。これにより、デプロイメントが1つ、エンドポイントが1つ、シークレット管理が1箇所、リクエスト検証が1箇所、ビジネスロジックへの接続が1箇所になります。アシスタントが成長するにつれて、同じ関数を通じてすべてのコールバックをルーティングし続けることも、後で分割することもできます。シンプルに始めることが通常は正しい判断です。