はじめて GPT-Live-1 のブラウザセッションを開いたとき、従来の音声ループ——話す、待つ、返答を聞く——を想像していました。ところが実際には、アシスタントが返答している間もマイクは開いたまま。会話は堅苦しさが和らいだ一方で、アプリは裏側の処理を引き続き管理する必要がありました。
OpenAIはまず7月に ChatGPTでGPT-Liveを紹介 し、その後 今週初めにGPT-Live-1をAPIへ提供 しました。これはちょうど本記事の制作直前でした。弊社の GPT-Realtime-2.1チュートリアル は単一モデルのアプローチを扱い、 GPT Live Transcribeガイド はライブ字幕に焦点を当てています。本稿では、実際のDataCampリソースを検索し、確認後にのみプランを保存する音声学習アシスタントを構築します。
私はこれをDataCamp Voice Learning Assistantと呼んでいます。これはチュートリアル用のプロトタイプであり、製品版の DataCamp AI Assistant ではありません。本プロジェクトは、学習者が音声で目標を述べてからプランを保存するまでの流れを追います。
具体的なポイント
GPT-Live-1は、音声でのやり取りとバックエンド作業を切り離します。以下の4点が学習アシスタントの設計に影響しました。
WebRTC とバックエンド作業は別経路: メディアトラックは音声を運び、 Responsesのデリゲーション が検索やツール呼び出しを処理します。
話し言葉での割り込みはバックエンド作業をキャンセルしない: タスクバージョンはアプリのアクションを保護しますが、Responsesのデリゲーションは古い結果のすべてを次の返答から排除できるわけではありません。
トランスクリプトの差分は会話の最終ターンではない: ネットワークのタイミングは変動し、ユーザーとアシスタントの区間が重なることがあり、権威的な完了ターンを示すトランスクリプトイベントは存在しません。
関数呼び出し は保存の許可ではない: アプリはプランを書き込む前に、アプリ側での二度目の確認を待ちます。
これらは本稿の学習プランフローに当てはまる知見です。プロンプトやネットワークが異なれば挙動も変わり、クライアントデリゲーションでは制御境界も変わります。
GPT-Live-1とは?
GPT-Live-1は、OpenAIの全二重音声モデルです。発話のターンや割り込み(その間のポーズを含む)を扱い、検索やツール呼び出しのような長い処理はバックエンドに送ります。
学習者にとって最初に目に見える違いは、まさにそのポーズにあります。
全二重会話の仕組み
全二重はターンテイキングを変えます。考えるために沈黙したり、アシスタントにかぶせて話したりでき、アシスタント側も話すのを止めて訂正を聞けます。OpenAIのプロンプトガイドには、短い相槌や割り込み用のセクションがあります。
学習アシスタントではこれは重要です。キャリア目標を説明する人は、途中で間を置いたり、言い直したり、制約を追加したりします。「そうですね、週5時間くらいで」といった逡巡を待てるモデルなら、学習者は考えながら話せます。
音声とバックエンド作業の分離
デリゲーションはタスクをバックエンドへ渡しますが、アプリケーション制御そのものは譲りません。誰が実行できるか、保存を許可するかはアプリが決め、保存するタスク状態の所有権もアプリにあります。
GPT-Live-1 vs. GPT-Realtime-2.1
すでにGPT-Realtime-2.1を使っているなら、GPT-Live-1で置き換わるのかと思うかもしれません。答えはノーです。
GPT-Realtime-2.1は v1/realtimeで、リッスン・推論・ツール選択を単一モデルで担い、音声とテキストのトークンで課金されます。GPT-Live-1は v1/live/sessionsを使用し、音声レイヤーは秒単位課金、推論は別のバックエンドに送られます。
Realtime-2.1は古い・劣る選択肢ではありません。設計が異なるのです。
GPT-Live-1音声学習アシスタントの構築
アプリは話された目標を受け取り、実在するDataCampリソースの優先度付きリストに変換します。バックエンドの処理中も音声セッションは開いたままです。リクエストが変わったら、バックエンドの動作を実行する前にタスクバージョンを更新します。
学習者がアプリ内で再確認するまでは、何も書き込みません。
GPT-Live-1アプリケーションアーキテクチャ
ブラウザページはWebRTC接続とマイクを保持し、サーバーがGPT-Live-1セッションを作成しAPIキーを保持します。Responsesのバックエンド(gpt-5.6-sol)はウェブ検索と save_learning_plan 関数を使います。現在のタスクバージョンと確定済みプランはアプリ状態に保持されます。
検索中にリクエストが変化した際、どのバックエンドアクションを受け付けるかはタスクバージョンが決めます。完全な実行可能アプリは GitHubリポジトリ を参照してください。以降のセクションではGPT-Liveの経路に絞って解説します。
ブラウザ、GPT-Live-1、バックエンドモデルが連携。画像:筆者作成。
GPT-Live-1をPythonでセットアップする方法
GPT-Live-1にアクセスできるOpenAIプロジェクト(無料枠は非対応)、Python、そしてマイク許可ダイアログのためHTTPSまたはlocalhostで動くブラウザが必要です。私はPython 3.11と openai 3.13.0を使用しました。Live APIには少なくともopenai 3.12.0が必要で、それ以前にはクライアントに.live属性がありません。
同時セッション上限は利用プランに依存します。多数のタブを開く前にプロジェクトの制限を確認してください。
python -m venv .venv
.venv\ Scripts\ Activate.ps1
pip install openai fastapi uvicorn python-dotenv streamlit requests提供:
macOSやLinuxでは、source .venv/bin/activate で環境を有効化してください。プロジェクトルートに .envファイルを作成し、次の値を追加します。
OPENAI_API_KEY = sk-.. .提供:
python-dotenvはサーバーがインポートされると自動的にそのファイルを読み込むため、キーをコード内に記載する必要はありません。
OpenAI()クライアントは、キーを渡さない場合に同じ環境変数を参照します。
APIキーをサーバーに保持する
ブラウザはプロジェクトキーを参照しません。WebRTCのオファーをサーバーへPOSTし、サーバーがキーを用いてセッションを作成します。SDP交換後、ブラウザはキーを受け取ることなくWebRTC経由でOpenAIに音声を送ります。
内部の/api/session のGPT-Live呼び出しは、SDPオファーからセッションを作成します。音声向けの指示、バックエンドモデル、ウェブ検索、保存関数を同一リクエストで渡します。
result = client. live. create(
session= {
"model" : "gpt-live-1" ,
"instructions" : LIVE_INSTRUCTIONS,
"delegation" : {
"type" : "responses" ,
"responses" : {
"model" : "gpt-5.6-sol" ,
"instructions" : BACKEND_INSTRUCTIONS,
"tools" : [
{
"type" : "web_search" ,
"filters" : {
"allowed_domains" : [ "datacamp.com" , "www.datacamp.com" ]
} ,
} ,
SAVE_LEARNING_PLAN_TOOL,
] ,
"tool_choice" : "auto" ,
} ,
} ,
} ,
transport= { "type" : "webrtc" , "sdp" : sdp} ,
) 提供:
この呼び出しはPOST /v1/live/sessions へのリクエストを送り、セッションIDとSDPアンサーを返します。HTTPリクエストがセッションを開始するため、その後に別途session.start イベントを送らないでください。
サンプルサーバーは、ブラウザからのリクエストを localhost:8501と 127.0.0.1:8501のみ受け付けます。このルールはローカル用です。
デプロイ時は、これらのオリジンを置き換え、 /api/sessionと /api/save-planの双方を認証してください。各リクエストはコストと同時実行枠を消費し得るため、セッション作成はレート制限をかけてください。クライアントは自ら confirmed: trueを送れるので、公開サーバーはその値を本人確認の証拠として扱えません。
WebRTCでGPT-Live-1セッションを作成する方法
OpenAIのWebRTCガイドに従い、ブラウザはマイクアクセスを要求し、 RTCPeerConnectionを開きます。ドキュメントどおりの oai-events というデータチャネルのラベルを使い、SDPオファー生成前に作成します。セッション開始後は、そのチャネルで双方向にJSONイベントが流れます。
WebRTCは開始し、音声をストリームし、正常終了します。画像:筆者作成。
マイクと音声出力の接続
メディアのセットアップ自体は通常のWebRTCです。GPT-Liveのイベントは最後の行で作成したデータチャネルを使います。
const connection = new RTCPeerConnection ( ) ;
connection. addEventListener ( "track" , ( event ) => {
audio. srcObject = new MediaStream ( [ event. track] ) ;
audio. play ( ) ;
} ) ;
const microphone = await navigator. mediaDevices. getUserMedia ( { audio : true } ) ;
for ( const track of microphone. getAudioTracks ( ) ) {
connection. addTrack ( track, microphone) ;
}
const events = connection. createDataChannel ( "oai-events" ) ; 提供:
オファーを作成したら、ブラウザはsetLocalDescription() を呼び、ICE収集の完了を待ちます。ローカルSDPを/api/session に送り、OpenAIのアンサーをsetRemoteDescription() で適用します。マイクの音声とアシスタントの音声はメディアトラックで運ばれるため、別途音声認識と音声合成 のリクエストは不要です。
音声はoai-events に載せるものではありません。WebRTCのデータチャネルでsession.input_audio.append を送ったり、session.output_audio.delta を待ったりしないでください。
データチャネルは別のタイミング規則に従います。 session.started を待ってから oai-events経由でイベントを送ってください。最初の試行では早すぎるタイミングで送ってしまい、接続に無視されました。
有用なエラーは出ず、小さな順序ミスの追跡に手こずりました。
GPT-Liveのトランスクリプトイベントをストリームする
字幕表示が不要であれば、このサブセクションは飛ばして構いません。音声接続はすでに完成しています。
session.input_transcript.delta と session.output_transcript.delta は、ライブ字幕用にミリ秒オフセット付きのテキスト断片を返します。OpenAIのドキュメントでは、これらの断片が会話ターンの完了を意味しないことに注意喚起があります。配信は不均一で、ユーザーとアシスタントの区間が重なることがあります。
トランスクリプト断片は届き次第画面に追加しますが、そこからバックエンド作業を開始しないでください。どのタイミングでデリゲートするかはモデルが決めます。
自然な会話のためにGPT-Live-1をプロンプトする方法
Liveモデルの指示は短く保ちます。OpenAIのガイドでは、詳細な手順はバックエンドのプロンプトに置くとしています。私は手順をバックエンド側に収め、Liveのプロンプトは音声に集中させました。
以下の抜粋は、音声のふるまいを学習プランのタスクから切り離しています。発話ルールはデリゲーションを引き起こす条件より上に置いています。
You are Sage, a warm, encouraging voice learning coach for DataCamp learners.
Speak naturally at an unhurried pace. Be clear and direct, not overly cheerful.
Backchannel policy: Use moderate backchannels without competing with the response.
Interruption policy: Stop speaking when the learner interrupts, and listen.
Delegation policy:
Backend tools:
- learning_plan_research: search DataCamp resources and assemble a personalized learning plan.
- save_learning_plan: propose the current plan for app confirmation when the learner asks to save.
Delegate to the backend when:
- The learner states or changes a goal, skill level, or weekly time.
- A correction changes the plan already requested.
- The learner asks to save the plan.
Do not delegate for greetings, small clarifications, or a result already given.
Saving: a proposed save only asks the app to confirm. Do not say the plan is saved until the app reports a saved result.
After a save, keep the conversation open and ask what the learner wants next.提供:
これらのルールにより、挨拶はLive層に残し、調査や保存リクエストはバックエンドに送られます。最終的な確認は依然としてアプリ側の責務です。
ポーズ、相槌、割り込みへの対応
相槌と割り込みの指示は、ポーズ周りの応答方法を定めます。「ほどよい相槌」は、「うんうん」のような控えめな合図をときどき入れつつ、相手の発話を妨げないことを意味します。学習者が考える余白を残すため、その程度に設定しました。ポーズが長いレッスンでは、さらに相槌を減らす選択もあります。
アプリに別のふるまいが必要であれば、その一文を変更してください。「ユーザーが話している間は決して話さない」を加えると、相槌もなくなります。
音声の指示とタスクの指示を分ける
2つのプロンプトは役割が異なります。Liveのプロンプトは発話と引き継ぎを制御し、バックエンドのプロンプトは調査と回答形式を制御します。OpenAIのガイドは、詳細な検索手順を音声の指示に入れないよう助言しています。
GPT-Liveのバックエンドデリゲーションを追加する方法
前述の分離は、セッションのdelegation フィールドに現れます。学習者が目標を述べると、GPT-Liveはコースカタログを検索してプランを作成できるモデルにタスクを送ります。
GPT-Live-1はResponsesのデリゲーションとクライアントデリゲーションを提供します。ResponsesのデリゲーションではOpenAIがバックエンド呼び出しを管理し、クライアントデリゲーションではそれを自前コードに委ねます。本アプリでは追加のバックエンドループを避けるため、Responsesのデリゲーションを採用しました。
バックエンドモデルの設定
使用したのは gpt-5.6-solです。OpenAIのデリゲーションガイドは例として gpt-5.6-terra を起点に挙げ、低コスト用途には gpt-5.6-lunaを記載しています。Solでは、バックエンドは要求したプラン構造を返しました。
バックエンドがウェブ検索や保存関数を選べるよう、 tool_choice は auto に設定します。デリゲーションの方式は起動時に固定されるため、クライアントデリゲーションへ切り替えるには、現在のセッションを閉じて新たに作成してください。
アシスタントがデリゲートすべきタイミングの判断
Liveプロンプトのルールは単純です:挨拶や短い質問はLiveモデルに留め、学習プランやその変更はバックエンドへ送ります。APIがその境界を強制するわけではありません。アプリで想定されるリクエストでテストしてください。判断はモデルが行います。
DataCampリソース向けのウェブ検索を追加する方法
デリゲート後、バックエンドのタスクは一つ:学習者の目標を、リンク付きの簡潔なDataCampリソースリストに変換します。そのために web_search ツールを与え、 filters.allowed_domains を datacamp.comと www.datacamp.comに設定しました。これは検索の指示であって、各リンクの正しさを保証するものではないと考えてください。
サンプルの目標は、週5時間、Pythonは少し知っていて、SQLは未経験のデータエンジニアリング学習パスです。応答はまず How to Learn Data Engineering From Scratch in 2026 と、 Associate Data Engineer in SQL トラックから始まります。
残りの項目は、プロジェクト、Pythonのデータベースコース、別のトラック、最後にパイプラインプロジェクトを組み合わせています。掲載されたURLはすべて既存のDataCampページを開きます。
検索結果を学習プランにまとめる
バックエンドのプロンプトでは、4~7件の順序付き項目を要求します。各項目には、タイトル、URL、簡潔な理由、 course、project、track、article のいずれかのタイプがあります。構成は、学習者の希望する形式や週の学習時間に合わせます。
ページに記載がない場合にコースの所要時間を推測するようには求めていません。その場合に正確な数値を出すのは、出典以上の主張になってしまうためです。
バックエンドが動作中でも会話を続ける方法
GPT-Liveは、Responsesのバックエンドが処理している間も音声セッションを維持できます。最初のプランが返る前に学習者がハンズオン必須という制約を追加しても、元のバックエンド処理は自動ではキャンセルされません。
実行中のリクエストを更新する
話し言葉での訂正は、すでにバックエンドで始まっている作業を自動でキャンセル・書き換えしません。アシスタントの発話を中断することと、タスクを変更することは別の行為です。過去の結果をどう扱うかはアプリケーションが決めます。
サーバーはtask_version カウンタを保持し、新たなデリゲーションが始まるたびにインクリメントします。結果が届いたとき、アプリはバージョンを確認してから処理します。自前のハンドラは古い結果をログに残し、実行しません。
Responsesのデリゲーションには限界があります:Liveモデルはバックエンド結果を直接受け取るため、バージョンチェックで次の発話内容を完全には制御できません。クライアントデリゲーションなら、モデルに届く前に古い結果を破棄できます。したがってタスクバージョンは、アプリのアクションを保護するものであり、アシスタントの発話のすべてを保護するわけではありません。
タスクバージョンにより新しい制約が有効に。画像:筆者作成。
最初のバックエンド応答が完了した後、私は「ハンズオンのプロジェクト中心で、初級Pythonは除外」という追記をしました。改訂された7件のプランは Introduction to SQL から始まり、1件のトラック、2件のコース、4件のプロジェクト( Exploring London's Travel Network や Building a Retail Data Pipeline を含む)を組み合わせていました。これは完了済みターン間の改訂を示すもので、進行中の応答停止については何も示しません。
バックエンドの更新を音声モデルへ送る
バックエンド作業中、3種類のappendイベントでLiveモデルを更新できます。 session.thinking.append は発話しない文脈を追加し、 session.commentary.append はモデルが自分の言葉で話すべきテキストを追加し、 session.instructions.appendは指示を変更します。
各appendは最大500トークンのプレーン文字列を運びます。これらはLiveモデルの文脈やふるまいを更新しますが、進行中のResponsesタスクを変更・キャンセルするものではありません。instructionsは現在のLiveのふるまいを方向付け、commentaryはモデルが音声で伝えるべき情報を与えます。
ダッシュボードはバックエンドの進行を記録しますが、これらのappendイベントは送信しません。Responsesのデリゲーションでも、アプリからの更新を oai-events経由で送れますが、その際はdelegation_id: nullを使います。非nullのdelegation IDはクライアントデリゲートされたタスクに使われます。
アプリケーション状態には task_id と task_version を保持し、どちらにも delegation_id を使わないでください。
確定保存のための関数呼び出しを追加する方法
本アプリでは、モデルの返答だけでは何も保存されません。バックエンドは save_learning_plan で保留中のアクションを提案し、実際の書き込みは /api/save-planが担います。
バックエンドの関数呼び出しはresponse.event 内に届きます。ハンドラは入れ子のresponse.output_item.done 項目を待ち、そこから call_id、name、argumentsを読み取ります。
完了項目を待つことが重要です。より前のイベントは呼び出しの一部しか含まないことがあるためです。アプリは引数を解析しますが、まだ関数は実行しません。
SAVE_LEARNING_PLAN_TOOL = {
"type" : "function" ,
"name" : "save_learning_plan" ,
"description" : "Propose the current learning plan for confirmation when the learner asks to save." ,
"parameters" : {
"type" : "object" ,
"properties" : {
"goal" : { "type" : "string" } ,
"weekly_hours" : { "type" : "number" } ,
"items" : {
"type" : "array" ,
"items" : {
"type" : "object" ,
"properties" : {
"title" : { "type" : "string" } ,
"url" : { "type" : "string" } ,
"reason" : { "type" : "string" } ,
"type" : {
"type" : "string" ,
"enum" : [ "course" , "project" , "track" , "article" ] ,
} ,
} ,
"required" : [ "title" , "url" , "reason" , "type" ] ,
"additionalProperties" : False ,
} ,
} ,
} ,
"required" : [ "goal" , "weekly_hours" , "items" ] ,
"additionalProperties" : False ,
} ,
"strict" : True ,
} 提供:
このスキーマにより、アプリは確認前に表示する項目を固定のフィールドセットで扱えます。type フィールドで、コース・プロジェクト・トラック・記事の別を保存データ上も明確にします。
ターミナルに型付きの保存関数引数が表示。画像:筆者作成。
アクション前の確認を必須にする
学習者が保存を求めると、バックエンドはsave_learning_plan をプラン全体で呼びます。ウィジェットはその引数を保持し、確認ダイアログを表示しますが、呼び出しはあくまで提案のままです。
その関数呼び出しを未応答のままにすると、委譲した応答や後続のバックエンドターンがブロックされます。ウィジェットは直ちに「確認待ち」の結果で応答し、その後response.create を送って会話を継続させます。
events. send ( JSON . stringify ( {
type : "response.item.create" ,
item : {
type : "function_call_output" ,
call_id : callId,
output : JSON . stringify ( {
status : "awaiting_user_confirmation" ,
saved : false ,
} ) ,
} ,
} ) ) ;
events. send ( JSON . stringify ( { type : "response.create" } ) ) ; 提供:
この時点では何も書き込まれません。アシスタントは「確認して保存」ボタンへ学習者を誘導しつつ、以降の委譲処理を妨げません。
/api/save-plan エンドポイントは、confirmed がtrueでなければ書き込みを拒否します。トランスクリプトは誤りや不完全さを含み得るため、音声での依頼だけでは保存しません。
確認が、リクエストと保存済みアクションを分けます。画像:筆者作成。
確定保存を会話に戻す
Confirmのクリックで /api/save-plan へ保留中のプランと confirmed: trueを送ります。サーバーからプランIDが返ったら、ウィジェットは session.commentary.append を delegation_id: null で送ります。元の関数呼び出しにはすでに応答済みだからです。
const saveResponse = await fetch ( ${ SERVER } / api/ save- plan, {
method : "POST" ,
headers : { "Content-Type" : "application/json" } ,
body : JSON . stringify ( {
confirmed : true ,
plan : pendingFunctionCall. args,
} ) ,
} ) ;
const saveResult = await saveResponse. json ( ) ;
events. send ( JSON . stringify ( {
type : "session.commentary.append" ,
delegation_id : null ,
content : The plan was saved as ${ saveResult. plan_id} . ,
} ) ) ; 提供:
commentaryの更新は、書き込み完了をLiveモデルへ伝え、保存を音声で確認させます。先の関数呼び出しはクローズされたままで、音声セッションは学習者の次のリクエストに引き続き利用できます。
GPT-Live-1音声アシスタントを実行する方法
前述のGitHubリポジトリには、FastAPIサーバー、Streamlit インターフェース、そしてapp/ 配下のWebRTCウィジェットが含まれています。クローン後、そのフォルダで2つのターミナルを開き、一方でuvicorn server:app --host 127.0.0.1 --port 8000、もう一方でstreamlit run streamlit_app.py を実行します。
Streamlitインターフェースは、本稿全体で用いたサーバーとウィジェットを同梱しています。ライブの会話を、学習プランやバックエンドの活動状況の隣に配置し、ダッシュボードは通話をリセットせずに更新します。
以下のビデオは、音声での目標、バックエンド検索、改訂プラン、確定保存までを追っています。保存後も通話は開いたままで、学習者は続けられます。
はじめてGPT-Live-1のブラウザセッションを開いたとき、従来の音声ループ——話す、待つ、返答を聞く——を想像していました。ところが実際には、アシスタントが返答している間もマイクは開いたまま。会話は堅苦しさが和らいだ一方で、アプリは裏側の処理を引き続き管理する必要がありました。
OpenAIはまず7月にChatGPTでGPT-Liveを紹介し、その後今週初めにGPT-Live-1をAPIへ提供しました。これはちょうど本記事の制作直前でした。弊社のGPT-Realtime-2.1チュートリアルは単一モデルのアプローチを扱い、GPT Live Transcribeガイドはライブ字幕に焦点を当てています。本稿では、実際のDataCampリソースを検索し、確認後にのみプランを保存する音声学習アシスタントを構築します。
私はこれをDataCamp Voice Learning Assistantと呼んでいます。これはチュートリアル用のプロトタイプであり、製品版のDataCamp AI Assistantではありません。本プロジェクトは、学習者が音声で目標を述べてからプランを保存するまでの流れを追います。
具体的なポイント
GPT-Live-1は、音声でのやり取りとバックエンド作業を切り離します。以下の4点が学習アシスタントの設計に影響しました。
これらは本稿の学習プランフローに当てはまる知見です。プロンプトやネットワークが異なれば挙動も変わり、クライアントデリゲーションでは制御境界も変わります。
GPT-Live-1とは?
GPT-Live-1は、OpenAIの全二重音声モデルです。発話のターンや割り込み(その間のポーズを含む)を扱い、検索やツール呼び出しのような長い処理はバックエンドに送ります。
学習者にとって最初に目に見える違いは、まさにそのポーズにあります。
全二重会話の仕組み
全二重はターンテイキングを変えます。考えるために沈黙したり、アシスタントにかぶせて話したりでき、アシスタント側も話すのを止めて訂正を聞けます。OpenAIのプロンプトガイドには、短い相槌や割り込み用のセクションがあります。
学習アシスタントではこれは重要です。キャリア目標を説明する人は、途中で間を置いたり、言い直したり、制約を追加したりします。「そうですね、週5時間くらいで」といった逡巡を待てるモデルなら、学習者は考えながら話せます。
音声とバックエンド作業の分離
デリゲーションはタスクをバックエンドへ渡しますが、アプリケーション制御そのものは譲りません。誰が実行できるか、保存を許可するかはアプリが決め、保存するタスク状態の所有権もアプリにあります。
GPT-Live-1 vs. GPT-Realtime-2.1
すでにGPT-Realtime-2.1を使っているなら、GPT-Live-1で置き換わるのかと思うかもしれません。答えはノーです。
GPT-Realtime-2.1は
v1/realtimeで、リッスン・推論・ツール選択を単一モデルで担い、音声とテキストのトークンで課金されます。GPT-Live-1はv1/live/sessionsを使用し、音声レイヤーは秒単位課金、推論は別のバックエンドに送られます。Realtime-2.1は古い・劣る選択肢ではありません。設計が異なるのです。
GPT-Live-1音声学習アシスタントの構築
アプリは話された目標を受け取り、実在するDataCampリソースの優先度付きリストに変換します。バックエンドの処理中も音声セッションは開いたままです。リクエストが変わったら、バックエンドの動作を実行する前にタスクバージョンを更新します。
学習者がアプリ内で再確認するまでは、何も書き込みません。
GPT-Live-1アプリケーションアーキテクチャ
ブラウザページはWebRTC接続とマイクを保持し、サーバーがGPT-Live-1セッションを作成しAPIキーを保持します。Responsesのバックエンド(
gpt-5.6-sol)はウェブ検索とsave_learning_plan関数を使います。現在のタスクバージョンと確定済みプランはアプリ状態に保持されます。検索中にリクエストが変化した際、どのバックエンドアクションを受け付けるかはタスクバージョンが決めます。完全な実行可能アプリはGitHubリポジトリを参照してください。以降のセクションではGPT-Liveの経路に絞って解説します。
ブラウザ、GPT-Live-1、バックエンドモデルが連携。画像:筆者作成。
GPT-Live-1をPythonでセットアップする方法
GPT-Live-1にアクセスできるOpenAIプロジェクト(無料枠は非対応)、Python、そしてマイク許可ダイアログのためHTTPSまたはlocalhostで動くブラウザが必要です。私はPython 3.11と
openai3.13.0を使用しました。Live APIには少なくともopenai3.12.0が必要で、それ以前にはクライアントに.live属性がありません。同時セッション上限は利用プランに依存します。多数のタブを開く前にプロジェクトの制限を確認してください。
macOSやLinuxでは、
source .venv/bin/activateで環境を有効化してください。プロジェクトルートに.envファイルを作成し、次の値を追加します。python-dotenvはサーバーがインポートされると自動的にそのファイルを読み込むため、キーをコード内に記載する必要はありません。OpenAI()クライアントは、キーを渡さない場合に同じ環境変数を参照します。APIキーをサーバーに保持する
ブラウザはプロジェクトキーを参照しません。WebRTCのオファーをサーバーへPOSTし、サーバーがキーを用いてセッションを作成します。SDP交換後、ブラウザはキーを受け取ることなくWebRTC経由でOpenAIに音声を送ります。
内部の
/api/sessionのGPT-Live呼び出しは、SDPオファーからセッションを作成します。音声向けの指示、バックエンドモデル、ウェブ検索、保存関数を同一リクエストで渡します。この呼び出しは
POST /v1/live/sessionsへのリクエストを送り、セッションIDとSDPアンサーを返します。HTTPリクエストがセッションを開始するため、その後に別途session.startイベントを送らないでください。サンプルサーバーは、ブラウザからのリクエストを
localhost:8501と127.0.0.1:8501のみ受け付けます。このルールはローカル用です。デプロイ時は、これらのオリジンを置き換え、
/api/sessionと/api/save-planの双方を認証してください。各リクエストはコストと同時実行枠を消費し得るため、セッション作成はレート制限をかけてください。クライアントは自らconfirmed: trueを送れるので、公開サーバーはその値を本人確認の証拠として扱えません。WebRTCでGPT-Live-1セッションを作成する方法
OpenAIのWebRTCガイドに従い、ブラウザはマイクアクセスを要求し、
RTCPeerConnectionを開きます。ドキュメントどおりのoai-eventsというデータチャネルのラベルを使い、SDPオファー生成前に作成します。セッション開始後は、そのチャネルで双方向にJSONイベントが流れます。WebRTCは開始し、音声をストリームし、正常終了します。画像:筆者作成。
マイクと音声出力の接続
メディアのセットアップ自体は通常のWebRTCです。GPT-Liveのイベントは最後の行で作成したデータチャネルを使います。
オファーを作成したら、ブラウザは
setLocalDescription()を呼び、ICE収集の完了を待ちます。ローカルSDPを/api/sessionに送り、OpenAIのアンサーをsetRemoteDescription()で適用します。マイクの音声とアシスタントの音声はメディアトラックで運ばれるため、別途音声認識と音声合成のリクエストは不要です。音声は
oai-eventsに載せるものではありません。WebRTCのデータチャネルでsession.input_audio.appendを送ったり、session.output_audio.deltaを待ったりしないでください。データチャネルは別のタイミング規則に従います。
session.startedを待ってからoai-events経由でイベントを送ってください。最初の試行では早すぎるタイミングで送ってしまい、接続に無視されました。有用なエラーは出ず、小さな順序ミスの追跡に手こずりました。
GPT-Liveのトランスクリプトイベントをストリームする
字幕表示が不要であれば、このサブセクションは飛ばして構いません。音声接続はすでに完成しています。
session.input_transcript.deltaとsession.output_transcript.deltaは、ライブ字幕用にミリ秒オフセット付きのテキスト断片を返します。OpenAIのドキュメントでは、これらの断片が会話ターンの完了を意味しないことに注意喚起があります。配信は不均一で、ユーザーとアシスタントの区間が重なることがあります。トランスクリプト断片は届き次第画面に追加しますが、そこからバックエンド作業を開始しないでください。どのタイミングでデリゲートするかはモデルが決めます。
自然な会話のためにGPT-Live-1をプロンプトする方法
Liveモデルの指示は短く保ちます。OpenAIのガイドでは、詳細な手順はバックエンドのプロンプトに置くとしています。私は手順をバックエンド側に収め、Liveのプロンプトは音声に集中させました。
以下の抜粋は、音声のふるまいを学習プランのタスクから切り離しています。発話ルールはデリゲーションを引き起こす条件より上に置いています。
これらのルールにより、挨拶はLive層に残し、調査や保存リクエストはバックエンドに送られます。最終的な確認は依然としてアプリ側の責務です。
ポーズ、相槌、割り込みへの対応
相槌と割り込みの指示は、ポーズ周りの応答方法を定めます。「ほどよい相槌」は、「うんうん」のような控えめな合図をときどき入れつつ、相手の発話を妨げないことを意味します。学習者が考える余白を残すため、その程度に設定しました。ポーズが長いレッスンでは、さらに相槌を減らす選択もあります。
アプリに別のふるまいが必要であれば、その一文を変更してください。「ユーザーが話している間は決して話さない」を加えると、相槌もなくなります。
音声の指示とタスクの指示を分ける
2つのプロンプトは役割が異なります。Liveのプロンプトは発話と引き継ぎを制御し、バックエンドのプロンプトは調査と回答形式を制御します。OpenAIのガイドは、詳細な検索手順を音声の指示に入れないよう助言しています。
GPT-Liveのバックエンドデリゲーションを追加する方法
前述の分離は、セッションの
delegationフィールドに現れます。学習者が目標を述べると、GPT-Liveはコースカタログを検索してプランを作成できるモデルにタスクを送ります。GPT-Live-1はResponsesのデリゲーションとクライアントデリゲーションを提供します。ResponsesのデリゲーションではOpenAIがバックエンド呼び出しを管理し、クライアントデリゲーションではそれを自前コードに委ねます。本アプリでは追加のバックエンドループを避けるため、Responsesのデリゲーションを採用しました。
バックエンドモデルの設定
使用したのは
gpt-5.6-solです。OpenAIのデリゲーションガイドは例としてgpt-5.6-terraを起点に挙げ、低コスト用途にはgpt-5.6-lunaを記載しています。Solでは、バックエンドは要求したプラン構造を返しました。バックエンドがウェブ検索や保存関数を選べるよう、
tool_choiceはautoに設定します。デリゲーションの方式は起動時に固定されるため、クライアントデリゲーションへ切り替えるには、現在のセッションを閉じて新たに作成してください。アシスタントがデリゲートすべきタイミングの判断
Liveプロンプトのルールは単純です:挨拶や短い質問はLiveモデルに留め、学習プランやその変更はバックエンドへ送ります。APIがその境界を強制するわけではありません。アプリで想定されるリクエストでテストしてください。判断はモデルが行います。
DataCampリソース向けのウェブ検索を追加する方法
デリゲート後、バックエンドのタスクは一つ:学習者の目標を、リンク付きの簡潔なDataCampリソースリストに変換します。そのために
web_searchツールを与え、filters.allowed_domainsをdatacamp.comとwww.datacamp.comに設定しました。これは検索の指示であって、各リンクの正しさを保証するものではないと考えてください。サンプルの目標は、週5時間、Pythonは少し知っていて、SQLは未経験のデータエンジニアリング学習パスです。応答はまずHow to Learn Data Engineering From Scratch in 2026と、Associate Data Engineer in SQLトラックから始まります。
残りの項目は、プロジェクト、Pythonのデータベースコース、別のトラック、最後にパイプラインプロジェクトを組み合わせています。掲載されたURLはすべて既存のDataCampページを開きます。
検索結果を学習プランにまとめる
バックエンドのプロンプトでは、4~7件の順序付き項目を要求します。各項目には、タイトル、URL、簡潔な理由、
course、project、track、articleのいずれかのタイプがあります。構成は、学習者の希望する形式や週の学習時間に合わせます。ページに記載がない場合にコースの所要時間を推測するようには求めていません。その場合に正確な数値を出すのは、出典以上の主張になってしまうためです。
バックエンドが動作中でも会話を続ける方法
GPT-Liveは、Responsesのバックエンドが処理している間も音声セッションを維持できます。最初のプランが返る前に学習者がハンズオン必須という制約を追加しても、元のバックエンド処理は自動ではキャンセルされません。
実行中のリクエストを更新する
話し言葉での訂正は、すでにバックエンドで始まっている作業を自動でキャンセル・書き換えしません。アシスタントの発話を中断することと、タスクを変更することは別の行為です。過去の結果をどう扱うかはアプリケーションが決めます。
サーバーは
task_versionカウンタを保持し、新たなデリゲーションが始まるたびにインクリメントします。結果が届いたとき、アプリはバージョンを確認してから処理します。自前のハンドラは古い結果をログに残し、実行しません。Responsesのデリゲーションには限界があります:Liveモデルはバックエンド結果を直接受け取るため、バージョンチェックで次の発話内容を完全には制御できません。クライアントデリゲーションなら、モデルに届く前に古い結果を破棄できます。したがってタスクバージョンは、アプリのアクションを保護するものであり、アシスタントの発話のすべてを保護するわけではありません。
タスクバージョンにより新しい制約が有効に。画像:筆者作成。
最初のバックエンド応答が完了した後、私は「ハンズオンのプロジェクト中心で、初級Pythonは除外」という追記をしました。改訂された7件のプランはIntroduction to SQLから始まり、1件のトラック、2件のコース、4件のプロジェクト(Exploring London's Travel NetworkやBuilding a Retail Data Pipelineを含む)を組み合わせていました。これは完了済みターン間の改訂を示すもので、進行中の応答停止については何も示しません。
バックエンドの更新を音声モデルへ送る
バックエンド作業中、3種類のappendイベントでLiveモデルを更新できます。
session.thinking.appendは発話しない文脈を追加し、session.commentary.appendはモデルが自分の言葉で話すべきテキストを追加し、session.instructions.appendは指示を変更します。各appendは最大500トークンのプレーン文字列を運びます。これらはLiveモデルの文脈やふるまいを更新しますが、進行中のResponsesタスクを変更・キャンセルするものではありません。instructionsは現在のLiveのふるまいを方向付け、commentaryはモデルが音声で伝えるべき情報を与えます。
ダッシュボードはバックエンドの進行を記録しますが、これらのappendイベントは送信しません。Responsesのデリゲーションでも、アプリからの更新を
oai-events経由で送れますが、その際はdelegation_id: nullを使います。非nullのdelegation IDはクライアントデリゲートされたタスクに使われます。アプリケーション状態には
task_idとtask_versionを保持し、どちらにもdelegation_idを使わないでください。確定保存のための関数呼び出しを追加する方法
本アプリでは、モデルの返答だけでは何も保存されません。バックエンドは
save_learning_planで保留中のアクションを提案し、実際の書き込みは/api/save-planが担います。バックエンドの関数呼び出しは
response.event内に届きます。ハンドラは入れ子のresponse.output_item.done項目を待ち、そこからcall_id、name、argumentsを読み取ります。完了項目を待つことが重要です。より前のイベントは呼び出しの一部しか含まないことがあるためです。アプリは引数を解析しますが、まだ関数は実行しません。
このスキーマにより、アプリは確認前に表示する項目を固定のフィールドセットで扱えます。
typeフィールドで、コース・プロジェクト・トラック・記事の別を保存データ上も明確にします。ターミナルに型付きの保存関数引数が表示。画像:筆者作成。
アクション前の確認を必須にする
学習者が保存を求めると、バックエンドは
save_learning_planをプラン全体で呼びます。ウィジェットはその引数を保持し、確認ダイアログを表示しますが、呼び出しはあくまで提案のままです。その関数呼び出しを未応答のままにすると、委譲した応答や後続のバックエンドターンがブロックされます。ウィジェットは直ちに「確認待ち」の結果で応答し、その後
response.createを送って会話を継続させます。この時点では何も書き込まれません。アシスタントは「確認して保存」ボタンへ学習者を誘導しつつ、以降の委譲処理を妨げません。
/api/save-planエンドポイントは、confirmedがtrueでなければ書き込みを拒否します。トランスクリプトは誤りや不完全さを含み得るため、音声での依頼だけでは保存しません。確認が、リクエストと保存済みアクションを分けます。画像:筆者作成。
確定保存を会話に戻す
Confirmのクリックで
/api/save-planへ保留中のプランとconfirmed: trueを送ります。サーバーからプランIDが返ったら、ウィジェットはsession.commentary.appendをdelegation_id: nullで送ります。元の関数呼び出しにはすでに応答済みだからです。commentaryの更新は、書き込み完了をLiveモデルへ伝え、保存を音声で確認させます。先の関数呼び出しはクローズされたままで、音声セッションは学習者の次のリクエストに引き続き利用できます。
GPT-Live-1音声アシスタントを実行する方法
前述のGitHubリポジトリには、FastAPIサーバー、Streamlitインターフェース、そして
app/配下のWebRTCウィジェットが含まれています。クローン後、そのフォルダで2つのターミナルを開き、一方でuvicorn server:app --host 127.0.0.1 --port 8000、もう一方でstreamlit run streamlit_app.pyを実行します。Streamlitインターフェースは、本稿全体で用いたサーバーとウィジェットを同梱しています。ライブの会話を、学習プランやバックエンドの活動状況の隣に配置し、ダッシュボードは通話をリセットせずに更新します。
以下のビデオは、音声での目標、バックエンド検索、改訂プラン、確定保存までを追っています。保存後も通話は開いたままで、学習者は続けられます。