1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Twilio Agent Connect (TAC) を使えば、AI電話アプリがここまでシンプルになる

1
Posted at

2026年5月のTwilio年次イベントにてTwilio Conversationsという新製品群がリリースされました。その中から、一番理解しづらいTwilio Agent Connectという製品を使った実装についてのベースの機能を紹介してみたいと思います。
本来であれば、新製品群を連携するために使う製品ですが、今回はTAC単独で利用した場合について書いています。

はじめに

TwilioでAI音声アプリを作ろうとすると、これまではかなりの量のコードが必要でした。

  • TwiMLを返すHTTPエンドポイント
  • ConversationRelayのWebSocketサーバ
  • WebSocketのメッセージパース・ルーティング
  • TTSやSTTのプロバイダ設定
  • 通話状態の管理
  • コールバックの処理

これらを自前で実装し、それぞれを正しく繋ぎ合わせる必要がありました。

Twilio Agent Connect (TAC) を使うと、こうした「お決まりの下準備コード」がほぼ消えます。開発者が書くのは「AIに何をさせるか」のロジックだけです。

従来のアプローチ

従来、ConversationRelayを使ってAI電話アプリを構築する場合、ざっくりこんな実装が必要でした。

// TwiMLサーバ
app.post("/twiml", (req, res) => {
  const response = new VoiceResponse();
  const connect = response.connect();
  connect.conversationRelay({
    url: `wss://${domain}/ws`,
    ttsProvider: "google",
    voice: "ja-JP-Neural2-B",
    // ...その他設定
  });
  res.type("text/xml").send(response.toString());
});

// WebSocketサーバ
wss.on("connection", (ws) => {
  let conversationId: string;

  ws.on("message", async (data) => {
    const msg = JSON.parse(data.toString());

    switch (msg.type) {
      case "setup":
        conversationId = msg.callSid;
        // 初期化処理...
        break;
      case "prompt":
        // ユーザーの発話を受け取る
        const userText = msg.voicePrompt;
        // AIに投げる
        const response = await callAI(userText);
        // 音声で返す
        ws.send(JSON.stringify({ type: "text", token: response }));
        break;
      case "interrupt":
        // 割り込み処理...
        break;
      // ...他にもハンドリングが必要
    }
  });
});

// コールバックエンドポイント
app.post("/callback", (req, res) => {
  // 通話終了時の処理...
});

WebSocketのメッセージ種別のハンドリング、JSONのパース、状態管理、エラーハンドリング...。AIのロジックにたどり着く前にやることが多すぎます。

TAC を使ったアプローチ

同じことをTACで書くとこうなります。

import "dotenv/config";
import OpenAI from "openai";
import { TAC, TACConfig, TACServer, VoiceChannel } from "twilio-agent-connect";

const openai = new OpenAI();

const history: Record<
  string,
  Array<{ role: "user" | "assistant"; content: string }>
> = {};

const SYSTEM_PROMPT = `あなたはレストランの予約受付AIです。丁寧な日本語で簡潔に応答してください。
予約に必要な情報: お名前、日付、時間、人数。一つずつ確認してください。
営業時間: 11:00-22:00、定休日: 月曜、最大8名。`;

async function main() {
  const tac = await TAC.create({ config: TACConfig.fromEnv() });

  const voiceChannel = new VoiceChannel(tac, { memoryMode: "never" });
  tac.registerChannel(voiceChannel);

  tac.onMessageReady(async ({ conversationId, message, abortSignal }) => {
    if (!history[conversationId]) history[conversationId] = [];
    history[conversationId].push({ role: "user", content: message });

    if (abortSignal?.aborted) return;

    const stream = await openai.chat.completions.create({
      model: "gpt-4o",
      messages: [
        { role: "system", content: SYSTEM_PROMPT },
        ...history[conversationId],
      ],
      stream: true,
    });

    async function* tokens() {
      for await (const chunk of stream) {
        if (chunk.choices?.[0]?.delta?.content) {
          yield chunk.choices[0].delta.content;
        }
      }
    }

    const full = await voiceChannel.sendStreamingResponse(
      conversationId,
      tokens(),
      abortSignal ? { signal: abortSignal } : undefined,
    );

    history[conversationId].push({ role: "assistant", content: full });
  });

  const server = new TACServer(tac, {
    host: "0.0.0.0",
    port: Number(process.env.PORT || 8000),
    conversationRelayConfig: {
      welcomeGreeting:
        "お電話ありがとうございます。ご予約についてお伺いします。",
      ttsProvider: "google",
      voice: "ja-JP-Neural2-B",
      ttsLanguage: "ja-JP",
      transcriptionProvider: "google",
      transcriptionLanguage: "ja-JP",
      speechModel: "telephony",
    },
  });
  await server.start();
  console.log("Server running on port", process.env.PORT || 8000);
}

main().catch(console.error);

これが全コードです。 ファイル1つ、約50行。

何が消えたのか

TACが内部で処理してくれるもの:

従来自分で書いていたもの TACでは
TwiMLエンドポイント TACServer が自動生成
WebSocketサーバの実装 TACServer が自動管理
メッセージのパース・ルーティング onMessageReady に抽象化
割り込み(barge-in)処理 abortSignal として提供
TTS/STTプロバイダ設定 conversationRelayConfig で宣言的に指定
通話ごとの状態分離 conversationId で自動分離

開発者が集中すべきは「ユーザーの発話に対してAIがどう応答するか」だけ。TACはそれ以外のインフラ層を引き受けてくれます。

ポイント解説

ストリーミングレスポンス

const full = await voiceChannel.sendStreamingResponse(
  conversationId,
  tokens(),
  abortSignal ? { signal: abortSignal } : undefined,
);

ジェネレータでトークンを1つずつ yield するだけで、TACがリアルタイムにTTSへ流してくれます。ユーザーはAIの応答を待たされず、生成されたそばから音声が聞こえます。

割り込み対応

if (abortSignal?.aborted) return;

ユーザーがAIの応答中に話し始めた場合、abortSignal が発火します。従来はWebSocketの interrupt メッセージを自分でハンドリングする必要がありましたが、TACでは標準の AbortSignal パターンで扱えます。

環境変数だけで接続設定が完了

TWILIO_ACCOUNT_SID=ACxxxxx
TWILIO_AUTH_TOKEN=xxxxx
TWILIO_API_KEY=SKxxxxx
TWILIO_API_SECRET=xxxxx
TWILIO_PHONE_NUMBER=+81xxxxxxxxxx
TWILIO_VOICE_PUBLIC_DOMAIN=xxxx.ngrok-free.app
PORT=8000

TACConfig.fromEnv() がこれらを読み取り、Twilioとの接続を自動で構成します。TWILIO_VOICE_PUBLIC_DOMAIN にはプロトコル(https://)を含めず、ホスト名だけを指定する点に注意してください。

Voice-Only モード

Conversation Orchestrator(会話の状態管理やハンドオフ機能)を使わない場合、TWILIO_CONVERSATION_CONFIGURATION_ID を設定しなければ voice-only モードで動作します。シンプルなAI電話アプリならこれで十分です。

まとめ

TACを使うことで:

  • コード量が大幅に減る - お決まりの下準備コードが不要に
  • AIロジックに集中できる - onMessageReady の中身だけ書けばいい
  • ストリーミングが簡単 - ジェネレータを渡すだけ
  • 標準的なパターン - AbortSignal、async/await など馴染みのあるAPIで操作できる

さらに、TACを土台にしておけば、Twilioの新機能である Conversations Memory(会話の記憶・文脈保持)、Orchestrator(会話フローの管理・エージェント間ハンドオフ)、Intelligence(通話分析・感情検出)とも設定を追加するだけですぐに連携できます。最小構成から始めて、必要に応じて機能を拡張していける設計です。

TIPS: デプロイリージョンとレイテンシー

本番環境では US-EAST-1(バージニア) リージョンにデプロイするのがおすすめです。TwilioのメディアサーバやConversationRelayの基盤がUS-EAST-1に近いため、音声のやりとりのレイテンシーがかなり良くなります。

ローカル環境(ngrok経由など)での実行は、ネットワークの往復が増える分どうしても遅延が目立ちます。ローカルはあくまで動作確認・検証目的と割り切って、実際のユーザー体験を評価するときはクラウド上のUS-EAST-1環境で試してみてください。

まとめ(再掲)

「電話 x AI」の開発体験が、Webアプリ開発に近い感覚になります。

参考ドキュメント

Twilio Agent Connect Docs

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?