メインコンテンツへスキップ
見出し画像

マジでxAIのVoiceAgentが電話オペレーターになる2分前!


    xAIのVoice Agentを使うと、 050番号にかかってきた電話を AIオペレータに応答させるところまで、 コンソール上の作業だけなら 文字通り2分で組める。

    エージェントを1体作って SIP URIを発行し、 既存のPBXから中継先に指定するだけだ。

    ……というのが理想の話で、 現実には 「SIP/TLSは200 OKまで通るのに 音が一切流れない」 という無音問題に 一週間はまった。

    原因は単一ではなかった。

    ・TLSポートの誤り ・SDPの私設IP ・外部×外部ブリッジのRTPデッドロック ・MAP-E回線のポート制限

    これらが積み重なっていた。

    この記事は、 その2分の裏で何を切り分け、 どのコマンドで確定し、 最終的にどの設定で開通させたかを 再現可能な形で残すものである。


    注意書き(2026年7月10日時点)

    本記事は2026年7月10日時点の情報にもとづく。 xAIはこのVoice Agentを 現時点で Beta版 として提供しており、 エンドポイントや仕様は 予告なく変わりうる。 設定値やポート・証明書の情報は あくまで執筆時点のものとして 参照してほしい。

    前提条件(=ここがいちばん高いハードル)

    「2分で作れる」の前提として、 次が揃っている必要がある。

    一家に一台はあるFreePBXが すでに立ち上がっていて、 携帯電話などから 実際にコールできる着信番号(トランク)が 準備済みであること。

    ……もちろん一家に一台も FreePBXは無いし、 この「携帯から着信できるトランクが動いている」 状態を作るところまでが 本当は9割の労力である。

    本記事はそこは済んでいる前提で、 xAIへ中継する部分に絞る。

    PBX自体の構築や キャリアトランクの開通は 範囲外とする。

    番号は何でもよい。

    筆者はBrastelのMy050番号で 運用しているが、 050でも0ABJの固定番号でも、 PBXに着信さえできれば 同じ手順で通る。

    以降の説明では、 この「着信させたい自分の番号」を 読者それぞれの番号に 読み替えてほしい。

    筆者の場合、 これは新規構築ではない。

    もともとFreePBXを 普段の電話として そのまま運用していて、 その環境にAIエージェントを 取り込んだだけである。

    だから「すでに構築済み」の人にとっては、 本当に2分の話になる。

    逆に言えば、 この記事の価値は 既存のPBX資産を そのまま活かせる点にある。

    なお本記事はFreePBXで書いているが、 要点は 「TLSのSIPトランクを1本張り、 着信をそこへ流し、 RTPメディアの経路を確保する」 ことに尽きる。

    クラウドPBXや 他社のPBX製品でも、 この3つが設定できるなら 同じことは十分できると推測している。


    xAIのエージェントを作る(ここは本当に2分)

    先にxAI側から作る。

    ここだけは誇張なしで 2分で終わる。

    xAIコンソールの Voice Agents(Beta) で 新規エージェントを1体作成する (名前は「〇〇株式会社 サポート窓口」のように、 用途が分かるものにしておけばよい)。

    エージェントには 応答方針や口調を指示する instructions を書く。

    これは日本語でよく、 「どんな会社の何の窓口で、 何を聞かれたらどう答えるか」を 素直に書けば、 その通りに喋るオペレータになる。

    作成するとエージェントに対して Direct SIP URI が発行される。

    形式は sip:<自分の電話番号>@8.228.120.166:5060;transport=tls のようなもので、 @ の左側に入るのは xAIが採番した番号ではなく、 こちらが着信させたい自分の番号だ。

    ホスト側(8.228.120.166 以降)が xAIの接続先になる。

    この1本のURIへ 着信を流し込めば、 あとはxAIが音声応答 (音声認識・応答生成・音声合成)を 全部ホストしてくれる。

    webhookを受けるバックエンドも WebSocketに参加するコードも 要らない。

    つまりAIオペレータ本体は 本当にこれで完成している。

    残りの記事は全部、 「既存のPBXから このURIへ、 電話の音をちゃんと届ける」 ための配管の話である。


    PBXに設定する

    やることは1つ。

    既存のPBXに 「xAI宛のトランク」を1本足し、 着信をそこへ流すだけだ。

    通話経路は 次の一方向になる。

    外部発信者 → Brastel(050着信) → FreePBX 17 / Asterisk 20.18.2
      (NAT内 <PBX私設IP> / 公開 <PBX公開IP>)
      → TLSトランク → xAI Voice Agent
         (SIP: sip.voice.x.ai:5061 / メディア: LiveKit)
    

    設定は2箇所。

    1つ目は、 xAIへ発呼するPJSIPトランク (筆者は xai-voiceagent と命名)を作り、 宛先を sip.voice.x.ai:5061(transport=tls) にする。

    2つ目は、 自分の番号の着信ルートから このトランクへDialする カスタムコンテキストを extensions_custom.conf に置く。

    ; /etc/asterisk/extensions_custom.conf
    [xai-agent-out]
    exten => s,1,Dial(PJSIP/<自分の電話番号>@xai-voiceagent)
     same => n,Hangup()
    

    追記後に dialplan reload。

    FreePBXの役割は Brastelから受けた着信を xAIへ中継するだけで、 AI応答はxAI側がホストする。

    webhookもWebSocketも 要らない。

    ……理屈のうえでは これで終わりのはずだった。

    実際には、 ここから音が出ずに 一週間溶けた。


    ここからが苦労話:SIP/TLSの罠

    まずSIP/TLSが繋がらない。

    この層のつまずきは、 半分がxAI Beta版特有の罠、 半分は環境依存だった。

    TLSポートの誤り(xAI側の罠)。

    xAIが発行したURIは sip:...@8.228.120.166:5060;transport=tls だが、 実際のTLSリスナーは 標準の5061だった。

    5060は無応答で、 5061でハンドシェイクが成立する。

    トランクの宛先を sip.voice.x.ai:5061 に直した。

    発行URIのポート表記を 鵜呑みにせず、 openssl s_client -connect host:5061 -servername sip.voice.x.ai で実リスナーを叩いて 確認するのが早い。

    TLSバージョン。

    Asterisk側トランスポートの SSL Methodが古い版に固定され、 error:0A0000BF ... no protocols available で失敗していた。

    xAIはTLS 1.2/1.3対応のため、 SSL Methodを TLSv1.2 に設定して 通った。

    ここまでで 100 Processing → 180 Ringing → 200 OK を受信し、 xAIのAPI利用履歴にも セッションが記録された。

    SIP/TLS層は成功している。

    だが音は出なかった。


    苦労話の本題:信号ではなくメディアの問題

    200 OKが成立した通話のSDPを pjsip set logger on で確認すると、 Asteriskが提示した接続アドレスが c=IN IP4 <PBX私設IP>、 つまり私設IPだった。

    xAIのメディアサーバ(LiveKit)は インターネット側にあり、 私設IP宛にRTPは届けられない。

    信号は届くが音は届かない、 という典型的な NAT越えメディアの失敗である。

    ここで一度、 対策のつもりで FreePBX全体のExternal Addressを 設定したところ、 事故を起こした。

    この設定は 全トランク共通に効くため、 UDPトランスポートを使うBrastelを 巻き込む。

    5060を転送していない環境では Brastelのqualify応答が戻らず、 BrastelがUnavail化して 発着信ごと破綻した。

    External Addressを撤去し fwconsole restart で復旧させた。

    教訓は、 メディアの公開IP設定は トランスポート単位に限定する こと。

    xAIはTLSトランスポート(0.0.0.0-tls)、 BrastelはUDPトランスポート(0.0.0.0-udp) で分かれているので、 これを利用する。


    RTPが1パケットも出ていない:外部×外部のデッドロック

    SDPを公開IPに直す前に、 そもそもRTPが流れているのかを 実測した。

    テスト通話中に

    ・pjsip set logger on ・tcpdump -n -i any udp portrange 10000-20000 ・pjsip show channelstats

    を同時採取したところ、 想定より深い問題が見えた。

    tcpdumpは0パケット。

    Asterisk自身が RTPを送出すらしていなかった。

    channelstatsも 両レグでCount 0/0。

    一方、BrastelのBYEには X-RTP-Statistics: 1052 packets とあり、 キャリア側は送信していたが 全損していた。

    メカニズムはこうだ。

    Asteriskのbridgeは 受信フレームを転送するだけで、 自分から音を生成しない。

    内線宛の通話では 内線電話が先にRTPを出すため、 その起点で キャリア側のラッチが成立して 音が通る。

    ところが 外部(Brastel)×外部(xAI)のブリッジでは、 どちらも 「相手が送ってきたら送り返す」側で、 誰も最初の一発を撃たない。

    両側受信ゼロなら 送信もゼロになる 相互デッドロックである。

    起点を作るために、 xAIとBrastelの両エンドポイントに rtp_keepalive=1 を設定し、 Asteriskから先に 小さなパケットを撃たせた。

    結果、 Asteriskが13バイトのkeepaliveを送出し、 これを起点に Brastel側SBCがラッチして 発信者音声がPBXに到達、 Asteriskがg722に変換して LiveKitへ送出した。

    発信者→xAI方向は成立した。

    しかし逆方向、 LiveKitからの着信パケットは ゼロのままだった。

    LiveKitは 受信パケットの送信元へ打ち返す ラッチングをしない。

    SDPに書かれたアドレス(公開IP)宛に だけ返送する。

    つまりxAI→PBXの入口を 物理的に開けない限り 音は返ってこない。

    無音の残り原因は 「入口が無い」の一点に 絞られた。


    最後の壁:RTPの入口をルーターで開ける

    残る作業は、 RTPのUDPポート帯を PBXへ転送して 入口を作ることだ。

    FreePBX の Settings → Asterisk SIP Settings → RTP Port Ranges で使うポート範囲を確認し (既定は 10000-20000)、 その範囲をルーターで PBX(<PBX私設IP>)へ UDPポート転送すればよい。

    転送後のテスト通話で LiveKitからのRTPがPBXに着信し、 双方向の音声が成立した。

    これで xAI Voice Agentとの通話が 開通した。

    普通のグローバルIP+ 一般的なルーターなら、 ここは 「RTPレンジをまるごと ポート転送する」だけで 終わる話である。

    余談:MAP-E回線だと広範囲のポート転送ができない

    ここは環境依存の話なので 余談として書く。

    筆者の回線が OCNのIPoE (OCNバーチャルコネクト=MAP-E方式)で、 これが最後に ひと悶着あった。

    MAP-Eは 1つのIPv4アドレスを 複数契約者で共有する方式で、 外向き通信に使えるのは 割り当てられた 「16ポート×64ブロック」の 飛び飛びのポート番号だけになる。

    10000-20000 のような 広範囲転送は 仕組み上できない。

    伏線はログにもあった。

    応答の rport=48613 (送信元5060が外側で48613に変換) というポート非保存NATの挙動は、 この割当ポートしか 外側で使えないことの帰結だった。

    対処は、 割当ブロックの連続した1ブロックに AsteriskのRTPを閉じ込め、 そのブロックだけを転送する方式にした。

    RTP Port Ranges を 10720-10735 (ルーターの利用可能ポート番号を示す項目で 確認した割当ブロックの一つ。 表示名や場所は機種で異なる) に絞り、 ルーターのポート転送にあたる項目で UDP 10720-10735 → <PBX私設IP> を追加する。

    割当内ポートなので 変換が成立する。

    ただし16ポートは RTP 8セッション分で、 xAI通話1本が 2レグ×(RTP+RTCP)=4ポートを 消費するため、 同時通話は2〜3本が上限になる。

    増やすには別ブロックを足せない (rtpstart/rtpendは連続範囲のみ)ので、 PPPoE併用や VPS中継が要る。

    IPoE系の回線で メディア着信を伴うSIPを組むなら、 この制約を先に確認しておくと 楽になる。

    余談:FQDNが社内ドメインで誤解決された

    これも環境依存の話。

    sip.voice.x.ai を 名前解決させたら、 社内LANの検索ドメイン(<社内ドメイン>)が 勝手に付与され、 sip.voice.x.ai.<社内ドメイン> として Cloudflareのワイルドカードへ 化けていた。

    別物のIPに 繋ぎにいっていたわけだ。

    /etc/hosts に 8.228.120.166 sip.voice.x.ai を固定して、 正しいIP解決とSNI付与を 両立させた。

    社内でsearch domainを配っている環境だと 踏みやすい罠なので、 外部FQDNへ繋ぐ前に dig +search sip.voice.x.ai で 化けていないか 見ておくとよい。


    診断の決め手になった5つの観測

    無音の切り分けは、 次の5点で確定させた。

    推測を挟まず 事実で潰していけたのは これらのおかげである。

    • pjsip set logger on — SDPの接続アドレスが私設IPだと 現物で確認できる。

    • tcpdump -n -i any udp portrange 10000-20000 — 「Asteriskが送信すらしていない」という 事実を証明する。

    • pjsip show channelstats — 両レグのRTP Countが0/0だと分かる。

    • BrastelのBYEに付く X-RTP-Statistics — キャリアは送っていたのに 届いていない、を裏付ける。

    • 応答の rport= 書き換え — ポート非保存NAT(≒MAP-E)の 判定材料になる。


    最終構成(設定ファイル全文)

    再現用に、 最終的に投入した設定を 全文で残す。

    /etc/asterisk/pjsip.transports_custom_post.conf(新規作成):

    ; TLSトランスポート(xAI向け): メディア・シグナリング両方を公開IPで広告する
    [0.0.0.0-tls](+)
    external_media_address=<PBX公開IP>
    external_signaling_address=<PBX公開IP>
    local_net=<LANサブネット>/24
    
    ; UDPトランスポート(Brastel等): メディアのみ公開IP。
    ; ここに external_signaling_address を書くと Contact/Via が公開IPになり、
    ; 5060未転送のため Brastel が再び Unavail 化する(過去の事故の再発防止)。
    [0.0.0.0-udp](+)
    external_media_address=<PBX公開IP>
    local_net=<LANサブネット>/24
    

    /etc/asterisk/pjsip.endpoint_custom_post.conf(新規作成):

    ; 外部×外部ブリッジのRTPデッドロック対策。
    ; Asteriskから先にkeepaliveを撃たせ、対向SBCのラッチを起動する。
    [xai-voiceagent](+)
    rtp_keepalive=1
    
    [Brastel-in](+)
    rtp_keepalive=1
    

    トランスポートの変更は リロードでは反映されないため、 投入後は fwconsole restart を実行する (endpointだけなら dialplan reload / pjsip reload でよいが、 transportsを触ったら restart)。

    反映は pjsip show transport で 確認する。

    その他の確定設定:

    • RTPポート範囲: ルーターでPBXへUDPポート転送 (一般には既定の 10000-20000 で可)。 筆者環境はMAP-Eのため 10720-10735 に絞って転送している。

    • /etc/hosts: 8.228.120.166 sip.voice.x.ai。

    • xAIトランクはNonQual化 (Qualify Frequency=0)。 OPTIONS無応答でUnavailになり 発呼がブロックされるのを避けるため。

    • SSL Method: TLSv1.2。


    まとめ

    AIオペレータ自体は xAIコンソールで2分で完成する。

    難しかったのは全部、 既存PBXから その電話へ音を届ける 配管だった。

    SIP/TLSの罠 (TLSポート5061・TLSバージョン)を 潰した後に、 NAT越えメディアの問題が 順番に現れる。

    ・SDPの私設IP ・外部×外部のRTPデッドロック ・RTPの入口となるポート転送

    各層で 「どの観測がその原因を確定させたか」を 押さえておけば、 別のSIPトランク×クラウドVoice Agentを 繋ぐときも 同じ手順で切り分けられる。

    ポート転送が普通にできる回線なら、 ここまでの大半は 素直に片づく。

    筆者のようにMAP-Eなど IPoE系の回線や、 社内search domainのある環境だと、 末尾の余談で書いた一手間が 余分に乗る。

    逆に言えば、 そこさえ環境依存として 切り分けておけば、 この構成は誰でも再現できる。


    使ってみた感想

    開通して 実際に喋らせてみて、 正直 「こりゃオペレーターの仕事 なくなるよ……」 と思った。

    もちろん粗はある。

    Beta版だからなのか 日本語だからなのか 切り分けられていないが、 漢字の読みを よく間違える。

    プロンプトで読み方を 指定しておいても 読み違えることがある。

    URLを口頭で案内させると、 ドットやハイフンが 微妙に聞き取りにくい日本語で 読み上げられて、 伝わりづらい。

    一方で 良かった点のほうが 印象に残った。

    変に待たされる間がなく、 応対のテンポと品質は 決して悪くない。

    xAI側のダッシュボードで 通話が録音され、 チャットの音声入力の要領で 文字にも起こされるので、 管理や品質チェックが 楽そうだと感じた。

    ランニングコストも安い。

    API利用料は 2026年7月10日時点で、 おおよそ1USDあたり 20分ほどの応対ができる感覚だった (Beta版であり、 料金は今後変わりうる)。

    1件数分の問い合わせなら 1コール十数円で、 電話番をひとり置くコストとは 桁が違う。

    何より、 細かいカスタムを別にすれば、 極論、 会社ホームページのURLを 1本渡すだけで、 そこに書いてある内容を ひととおり答えてくれる。

    この気楽さは大きい。

    中小企業なら 少額のコストで、 そこそこどや顔ができる。


    IPアドレス表記について

    本文中の <PBX私設IP> <PBX公開IP> <LANサブネット> は、 筆者環境の実アドレスを プレースホルダに置き換えたものである。

    読者は 自分の環境の値に 読み替えてほしい。

    xAI側の sip.voice.x.ai(= 8.228.120.166)は 直書きにしている。

    これは公開ドキュメント(下記リンク)に 記載されたホスト名の 公開DNS解決結果であり、 誰でも dig sip.voice.x.ai で得られる 公開情報のため、 掲載に問題は無いと判断した。

    一方、 通話中に観測した LiveKit(メディアサーバ)の配信IPは、 セッションごとに変わる 動的なGCP上のアドレスで 固定エンドポイントとして 公開されたものではないため、 本文では具体値を出さず 「LiveKitからのRTP」と 一般化している。

    参考: xAI SIP ドキュメント

     
     
    元ポッポヤ。未経験でIT転職。最初に覚えたJavaはどこへやら、古のCOBOLとアセンブラを経て、PHPだのPythonだの今はAWSで遊んでる。業務で生成AIをフル活用している。Ai依存症で広く浅くのエンジニア気取り。アイコンは偽物です。中身はただの太ったオッサン。

    あなたへのおすすめ