先週まで、Gemini Live API で簡単な音声デモを作るのに半日かかっていたのに、気づけば「ユーザーの顔が見える画面共有 + 双方向音声 + 社内 API 呼び出し」を同居させたエージェントを、1 セッションで動かしているお客さまが出てきました。Live API が GA になってから、現場の期待値は一段階上がっています。
とはいえ、サンプルコードをそのまま本番に流すと、ほぼ確実に詰まります。WebSocket が 5 分に 1 回切れる、音声が途中で先頭に戻る、Function Calling が暴走してコストが数万円単位で飛ぶ — 私自身が実装で踏んできた地雷です。ここではAntigravity を母艦にして Gemini Live API を本番投入するときの設計判断を、コードと運用の両面から整理します。教科書には載っていない、実装してみて初めて分かる境界線の話が中心です。
Live API が従来の Gemini 呼び出しと決定的に違う 3 つのこと
Live API の API 仕様を読む前に、「従来の generateContent と何が違うのか」を体で掴んでおくのが重要です。私の現場感覚では、次の 3 つを押さえないと設計を何度も引き直すことになります。
第一に、Live API は ステートフルな WebSocket セッション です。リクエスト/レスポンスではなく、setup → realtimeInput → serverContent → toolCall → toolResponse のメッセージ群が双方向に流れ続けます。従来の感覚で「1 回の呼び出しで完結」と考えると、接続維持・エラー回復・中断処理の設計がごっそり抜け落ちます。
第二に、課金単位が「入力トークン」ではなく「時間」に近い ことです。公式には入出力トークンの消費として計算されますが、音声ストリーミングと動画フレームは常時消費されるため、セッション 1 分あたりのコストがほぼ線形に積み上がります。ここを甘く見ると、検証段階で想定の 3 倍以上請求される事故が起こります。
第三に、中断(バージイン)をサーバー側で処理する ことです。ユーザーが話し始めた瞬間、Live API はモデルの発話を打ち切り、次の応答に移行します。この挙動に合わせてクライアント側の音声再生キューを設計しないと、「ユーザーの声が被った瞬間に AI の声が残り続ける」体験になります。
この 3 点を踏まえずに「とりあえず動くサンプル」から拡張すると、本番手前で設計をやり直す羽目になります。Antigravity のマルチエージェントで複数サービスに繋ぎ込む前に、Live API そのものの性質を固めておきたいところです。
Antigravity で組む本番アーキテクチャ:3 層で責務を分離する
私が実案件で採用しているのは、Antigravity の Manager Surface を中心に、次の 3 層に役割を切ったアーキテクチャです。シンプルに見えて、これが遅延・障害・コストの 3 つを同時に抑える最短経路でした。
Edge 層 : WebRTC / WebSocket でブラウザと繋がり、PCM16 音声のバッファリングと VAD(発話検出)だけを担当します。Cloudflare Workers Durable Objects または Hono on Workers を採用します。音声伝送層の設計は Antigravity で学ぶ WebRTC リアルタイム通信の実装ガイド が具体的に参考になります。
Orchestrator 層 : Live API との WebSocket セッションを所有し、Function Calling を社内 API へ橋渡しします。Antigravity の「Live Agent」として実装し、Manager Surface からセッション状態を監視できるようにします。
Tools 層 : 実業務 API(予約、検索、決済)や RAG(Vertex AI Vector Search)を束ねます。Orchestrator 層から Function Calling 経由で呼び出されます。
この 3 層分離が効くのは、Live API の「ステートフルさ」を Orchestrator 層に閉じ込められるからです。Edge 層は純粋に音声データを流すだけ、Tools 層は普通の HTTP API。Live API 固有の複雑性を持ち込まないので、障害時の切り分けが一気に楽になります。Antigravity で作業する場合、.agent/ 配下に edge.md、orchestrator.md、tools.md の 3 ファイルでエージェント定義を分け、Manager Surface から並列に走らせる構成を私は好みます。
なぜ Durable Objects なのか
Cloudflare Workers 単体だと、Live API の長命セッションを維持するのが難しい場面があります。Durable Objects は WebSocket のハイバネーションを扱えるため、アイドル中のメモリコストをほぼゼロにできます。本番で数千セッションを並行させるなら、Durable Objects + Hibernation API はほぼ必須です。逆に、1 分未満のワンショット接続なら普通の Workers でも十分で、Durable Objects のコストを払う意味は薄くなります。
コード例 1:Live API セッションの堅牢な確立と破棄
最初のコード例は、Live API セッションを「落ちても自動で復旧する」形で立ち上げる最小実装です。Antigravity の Orchestrator エージェント内で動かすことを想定しています。エラーを無視せず、再接続ロジックと状態遷移を入れ込んであるのがポイントです。
// orchestrator/live-session.ts
// 目的: Gemini Live API との WebSocket セッションを、ネットワーク切断や
// モデル側の強制切断から自動復旧させる。
import { GoogleGenAI, LiveSession, Modality } from "@google/genai" ;
type SessionState = "idle" | "connecting" | "live" | "retrying" | "closed" ;
export class LiveVoiceSession {
private ai : GoogleGenAI ;
private session : LiveSession | null = null ;
private state : SessionState = "idle" ;
private retries = 0 ;
private readonly maxRetries = 3 ;
constructor ( apiKey : string , private onAudio : ( pcm : ArrayBuffer ) => void ) {
this .ai = new GoogleGenAI ({ apiKey });
}
async start () : Promise < void > {
if ( this .state \ !== "idle" ) {
throw new Error ( `cannot start in state: ${ this . state }` );
}
this .state = "connecting" ;
try {
this .session = await this .ai.live. connect ({
model: "gemini-2.5-flash-live" ,
config: {
responseModalities: [Modality. AUDIO ],
speechConfig: {
voiceConfig: { prebuiltVoiceConfig: { voiceName: "Aoede" } },
},
systemInstruction:
"あなたは落ち着いた口調のカスタマーサポートです。ユーザーが話し始めたら即座に応答を止めてください。" ,
},
callbacks: {
onmessage : ( msg ) => this . handleMessage (msg),
onerror : ( err ) => this . handleError (err),
onclose : ( ev ) => this . handleClose (ev),
},
});
this .state = "live" ;
this .retries = 0 ;
} catch (err) {
this .state = "closed" ;
throw err; // 呼び出し元でログと監視通知
}
}
private handleMessage ( msg : unknown ) {
// serverContent.modelTurn.parts[].inlineData.data (base64 PCM16)
const data = extractAudioChunk (msg);
if (data) this . onAudio (data);
}
private async handleError ( err : Error ) {
console. error ( "[LiveSession] error" , err.message);
if ( this .retries < this .maxRetries && this .state === "live" ) {
this .state = "retrying" ;
this .retries ++ ;
const backoff = Math. min ( 1000 * 2 ** this .retries, 8000 );
await new Promise (( r ) => setTimeout (r, backoff));
this .state = "idle" ;
await this . start ();
} else {
this .state = "closed" ;
}
}
private handleClose ( _ev : CloseEvent ) {
if ( this .state === "live" ) {
// サーバー側からの強制切断。リトライ可能なら再接続。
this . handleError ( new Error ( "server closed" ));
}
}
async stop () : Promise < void > {
this .state = "closed" ;
await this .session?. close ();
this .session = null ;
}
}
function extractAudioChunk ( msg : unknown ) : ArrayBuffer | null {
// 型は @google/genai の LiveServerMessage に準ずる。
const parts = (msg as any )?.serverContent?.modelTurn?.parts ?? [];
for ( const p of parts) {
if (p?.inlineData?.mimeType?. startsWith ( "audio/" )) {
return base64ToArrayBuffer (p.inlineData.data);
}
}
return null ;
}
function base64ToArrayBuffer ( b64 : string ) : ArrayBuffer {
const bin = atob (b64);
const buf = new Uint8Array (bin. length );
for ( let i = 0 ; i < bin. length ; i ++ ) buf[i] = bin. charCodeAt (i);
return buf.buffer;
}
このコードで意図的に入れているのは、状態機械の強制 と 指数バックオフによる再接続 、そして サーバー側切断の自己認識 の 3 点です。公式サンプルは「状態」をコメントで示しているだけで、実装は開発者任せになっています。本番では状態遷移を型として表さないと、onclose と onerror のレースで二重接続が発生します。
期待する出力としては、start() 成功時にサーバーから setupComplete が返り、ユーザー音声を送り始めると 600ms 前後の初回レイテンシで音声チャンク(20ms/chunk の PCM16)がコールバックに流れ込みます。切断が起きた場合、最大 3 回まで 2s / 4s / 8s のバックオフで再接続を試みます。
コード例 2:音声再生キューとバージイン処理
2 つ目は、Edge 層での音声再生キューです。初心者が一番詰まるのがここで、AudioBufferSourceNode を素朴に並べると、ユーザーが話し始めた瞬間にモデル音声を切れません。
// edge/audio-queue.ts
// 目的: モデルからの音声チャンクをシームレスに再生し、
// ユーザーが発話を始めた瞬間に即時停止(バージイン)できるキュー。
export class AudioQueue {
private ctx : AudioContext ;
private sources : AudioBufferSourceNode [] = [];
private nextStartTime = 0 ;
constructor ( private sampleRate = 24000 ) {
this .ctx = new AudioContext ({ sampleRate });
}
enqueue ( pcm16 : ArrayBuffer ) : void {
const float32 = pcm16ToFloat32 (pcm16);
const buffer = this .ctx. createBuffer ( 1 , float32. length , this .sampleRate);
buffer. copyToChannel (float32, 0 );
const src = this .ctx. createBufferSource ();
src.buffer = buffer;
src. connect ( this .ctx.destination);
const startAt = Math. max ( this .ctx.currentTime, this .nextStartTime);
src. start (startAt);
this .nextStartTime = startAt + buffer.duration;
this .sources. push (src);
src. onended = () => {
this .sources = this .sources. filter (( s ) => s \ !== src);
};
}
interrupt () : void {
// バージイン: 再生中の全音源を即停止。
for ( const s of this .sources) {
try {
s. stop ();
} catch {
// すでに終了している場合は無視
}
}
this .sources = [];
this .nextStartTime = this .ctx.currentTime;
}
}
function pcm16ToFloat32 ( buf : ArrayBuffer ) : Float32Array {
const view = new DataView (buf);
const out = new Float32Array (buf.byteLength / 2 );
for ( let i = 0 ; i < out. length ; i ++ ) {
out[i] = view. getInt16 (i * 2 , true ) / 0x8000 ;
}
return out;
}
なぜ nextStartTime を明示的に持つのか : 単に src.start(0) にするとチャンク境界でプチッとノイズが入ります。AudioContext.currentTime 基準で次の開始時刻を予約することで、ギャップレス再生になります。
なぜ try/catch でサイレントにするのか : AudioBufferSourceNode.stop() は既に終了した音源に対して例外を投げます。バージインは速度勝負なので、個別の音源状態を確認するコストより、例外を握りつぶす方が UX 的にも計算量的にも合理的でした。
VAD は Edge 層で、ブラウザの onaudioprocess + RMS しきい値で実装すると Live API 往復を待たずに interrupt() を呼べます。結果として、ユーザー発話検知から AI 音声停止まで 50ms 以内 に収まります。
コード例 3:Function Calling を型安全に実装する
3 つ目は、Live API の Function Calling を社内 API に接続する部分です。ここはコストと安全性の両面で最も神経を使います。Function Calling のスキーマ設計を単体で深掘りしたい場合は Antigravity で Gemini Function Calling を使いこなす Python 実装ガイド も併せてご覧ください。
// orchestrator/tool-router.ts
// 目的: Live API からの toolCall を型安全にルーティングし、
// タイムアウトとコスト上限を強制する。
import type { LiveSession } from "@google/genai" ;
import { z } from "zod" ;
type ToolHandler < T > = ( args : T ) => Promise < Record < string , unknown >>;
interface Tool < T = unknown > {
name : string ;
schema : z . ZodType < T >;
handler : ToolHandler < T >;
timeoutMs : number ;
costLimit : number ; // 1 呼び出しあたりの想定コスト(円)
}
export class ToolRouter {
private tools = new Map < string , Tool < any >>();
private totalCost = 0 ;
private readonly sessionCostCap = 500 ; // セッションごとの上限(円)
register < T >( tool : Tool < T >) : void {
this .tools. set (tool.name, tool);
}
async dispatch (
session : LiveSession ,
toolCalls : Array <{ id : string ; name : string ; args : unknown }>
) : Promise < void > {
const responses = await Promise . all (
toolCalls. map (( c ) => this . runOne (c))
);
await session. sendToolResponse ({ functionResponses: responses });
}
private async runOne ( call : {
id : string ;
name : string ;
args : unknown ;
}) : Promise <{ id : string ; name : string ; response : Record < string , unknown > }> {
const tool = this .tools. get (call.name);
if (\ ! tool) {
return {
id: call.id,
name: call.name,
response: { error: `unknown tool: ${ call . name }` },
};
}
if ( this .totalCost + tool.costLimit > this .sessionCostCap) {
return {
id: call.id,
name: call.name,
response: { error: "session_cost_cap_exceeded" },
};
}
const parsed = tool.schema. safeParse (call.args);
if (\ ! parsed.success) {
return {
id: call.id,
name: call.name,
response: { error: "invalid_args" , detail: parsed.error. flatten () },
};
}
try {
const result = await withTimeout (
tool. handler (parsed.data),
tool.timeoutMs
);
this .totalCost += tool.costLimit;
return { id: call.id, name: call.name, response: result };
} catch (err) {
return {
id: call.id,
name: call.name,
response: {
error: "tool_failed" ,
message: err instanceof Error ? err.message : "unknown" ,
},
};
}
}
}
function withTimeout < T >( p : Promise < T >, ms : number ) : Promise < T > {
return new Promise (( resolve , reject ) => {
const t = setTimeout (() => reject ( new Error ( "timeout" )), ms);
p. then (( v ) => {
clearTimeout (t);
resolve (v);
}). catch (( e ) => {
clearTimeout (t);
reject (e);
});
});
}
意図して入れているのは次の 3 点です。
Zod によるスキーマ検証 : Live API は時折、args に文字列化された JSON を返してきます。z.preprocess で文字列もパースできるようにしておくと、本番での「たまに失敗する」系のバグがほぼ消えます。
セッション単位のコスト上限 : Live API の Function Calling は無料ではありません。1 回の呼び出しで外部 API 料金が発生することも多く、セッション全体での支出をガードしないと、敵対的なユーザーに 1 セッションで数千円使われます。
Promise.all による並列実行 : Live API は複数 toolCall を 1 メッセージで送ることがあります。直列処理するとレイテンシが跳ね上がるので、必ず並列化します。
よくある間違い・落とし穴
ここからは、私自身と、お客さまの現場で実際に踏んだ落とし穴を 5 つ共有します。ドキュメントには書かれていないものばかりです。
1. サンプルレートの不一致で音声が「早口」になる
Live API のデフォルト入力サンプルレートは 16kHz、出力は 24kHz です。ブラウザの getUserMedia は環境によって 44.1kHz / 48kHz を返します。単純に ArrayBuffer を転送すると、サーバー側で「早口の甲高い声」になって音声認識が失敗します。
対処 : Edge 層で AudioContext を { sampleRate: 16000 } で作り、OfflineAudioContext でリサンプリングしてから送ります。ブラウザが 16kHz を許可しない場合は、BiquadFilter + 3 倍ダウンサンプリングで明示的に落とします。
2. responseModalities: [AUDIO, TEXT] で両方送ると遅くなる
「音声とテキストを同時に欲しい」と思って両方指定すると、モデルがテキスト生成を待ってから音声を流すため、最初の音声チャンクまでのレイテンシが 1.5 倍〜 2 倍になります。
対処 : 画面に字幕を出したい場合でも、音声モダリティのみで受け、クライアント側で別途 transcription を outputAudioTranscription: { } で有効化します。これでテキストは音声と並行に流れてきます。
3. systemInstruction を毎回切り替える設計で課金が倍になる
Live API は setup メッセージで送る systemInstruction をセッション中ずっと保持します。セッション内で役割を切り替えたいからと新しいセッションを張り直すと、その都度セットアップ分のトークンを払うことになります。
対処 : 役割切り替えは clientContent で追加コンテキストを注入する方式に変えるか、そもそも「専門家エージェントへのルーティング」を Function Calling で実装して Live セッションを 1 本に保ちます。
4. 画面共有フレームを 30fps で送り続けるとコストが破綻する
realtimeInput で送れる動画フレームは、デフォルトでは「送った分だけ」課金されます。画面全体を 30fps で送ると、1 分で数百円単位のコストになります。
対処 : 変化検出(前フレームとの diff)で送信を間引き、0.5fps〜2fps に抑えます。ユーザーが「ここ見て」と口頭で指示した瞬間だけ高頻度化する「オンデマンド画面共有」が本番では現実解でした。
5. セッション切断時に音声キューが残り、次のセッションで古い音声が再生される
stop() で Live API セッションを閉じても、ブラウザ側の AudioBufferSourceNode は次回再生まで残っています。新しいセッションで最初のチャンクが届くと、古い音声の末尾が被ります。
対処 : stop() 内で必ず audioQueue.interrupt() を呼び、AudioContext を明示的に close() → 再生成します。体感ノイズが完全に消えます。
Antigravity のマルチエージェントで本番運用する設計
ここまでのピースを Antigravity 上で組み上げます。私が推す構成は、Manager Surface に次の 3 エージェントを常駐させる形です。
Live Orchestrator エージェント : Live API セッションを所有し、音声 I/O の仲介と toolCall のディスパッチを行います。.agent/live-orchestrator.md に状態機械の仕様と、再接続ポリシーを書き込んでおきます。
Business Tools エージェント : 予約、検索、RAG などの社内 API 呼び出し群。Live Orchestrator からの Function Calling を型付きで受けます。.agent/business-tools.md には、各 API のタイムアウトと失敗時のフォールバックを定義します。
Guardian エージェント : セッション長、コスト、エラー率を監視し、しきい値を超えたら Live Orchestrator に terminate を送ります。Antigravity の Background Agent として動かし、Manager Surface のダッシュボードから常時可視化します。
3 エージェント構成にした理由は、Live API の「長時間・リアルタイム・不確実性」と、ビジネスロジックの「短時間・リクエスト応答・決定論」を混ぜない ためです。混ぜると、どちらかの特性に引きずられて設計が歪みます。Antigravity の並列エージェント機能は、この種の責務分離と相性が非常に良いです。
本番で効くモニタリング指標と閾値
運用を回す上で私が Dashboard に置いている指標は次のとおりです。Grafana + Prometheus、または Cloudflare Workers Analytics Engine で十分実装できます。エージェント全体のトレースを体系化するなら Antigravity で組む AI 可観測性パイプライン:OpenTelemetry 実戦ガイド と組み合わせると、Live API 単体の監視から一段上の網羅性になります。
セッション継続時間(P50 / P95) : P95 が 10 分を超え始めたら、ユーザーが長すぎる雑談に誘導されているサイン。Guardian で強制終了の設計を見直します。
音声初回レイテンシ(TTFT-audio) : 1 秒を超えたら劣化中。リージョンまたはモデル(flash-live / pro-live)の切り替えを検討します。
再接続率 : 1 時間内の再接続イベントがセッション数の 5% を超えたら、クライアント側 WebSocket の品質を疑います。
toolCall 成功率 : 95% を下回ったら、関数スキーマが曖昧で Gemini が誤呼び出しを起こしています。Zod スキーマ + 説明文をチューニングします。
セッション単価(円 / min) : 仮説値から 1.5 倍を超えたら、音声以外のモダリティ(動画、テキスト)の送信量を疑います。
これらを Guardian エージェント経由で自動で監視させ、閾値超過時には Antigravity 側で auto-downgrade ポリシーを発火させます。具体的には、pro-live から flash-live への切り替え、動画フレーム頻度の自動引き下げ、セッション継続時間の強制短縮などです。
flash-live と pro-live の使い分け
Guardian で基礎メトリクスが安定してきたら、次にレバレッジが効くのがモデル階層の選択です。以前は反射的に pro-live から入っていましたが、直近 3 プロジェクトで既定方針を変えました。
flash-live が勝つ場面 : トランザクショナルなサポートフロー、構造化データ収集、「事実を集めて tool を叩く」タイプの対話。私の計測だと TTFT が 200〜400ms 速く、Function Calling の精度も 2026 年 3 月のアップデート以降、pro-live との差がかなり縮まりました。予約や決済のエージェントなら、今は flash-live が既定です。
pro-live が勝つ場面 : オープンエンドな推論、前半の文脈を強く引きずる長いトラブルシューティング、セッション中に言語が切り替わる多言語応対。画面共有の推論にも明確に強く、「画面で何が起きているかを解説する」要件なら追加コストを払う価値があります。
ハイブリッドパターン : セッションを flash-live で開始し、Orchestrator がエスカレーション条件(ユーザーの明示要求、トランスクリプトからのフラストレーション兆候、tool 失敗の閾値超え)を検知したときだけ、いったん切断して pro-live に張り直します。コストプロファイルが均一に高いのではなく二峰性になり、ユーザーは 1〜2 秒のハンドオフを「有人エスカレーションの間」として自然に受け止めます。
ひとつ注意点があって、同一接続上でモデル階層を動的に切り替えるのは避けてください 。API はホットスワップをサポートしておらず、setup を再投入して偽装すると微妙な状態ズレが残ります。一度綺麗に切って、再接続するのが正解です。
実用シナリオ:カスタマーサポート × 画面共有 × RAG
最後に、実際に動いている構成の一例を共有します。EC サイトのサポート窓口で、ユーザーが購入履歴画面を開いたまま音声で質問できる体験を作りました。
ユーザーはサイト内のヘルプページで「音声サポートを開始」ボタンを押します。WebRTC で Edge 層に接続し、同時に画面の「購入履歴」DOM を 1fps でキャプチャして送信します。
Live Orchestrator は画面情報を見ながら、ユーザーの質問を Business Tools エージェント経由で社内 API(注文 DB、配送 API)に照会します。
RAG には過去 FAQ を Vertex AI Vector Search で前処理済み。Function Calling search_knowledge として呼び出します。
Guardian は 10 分でセッション自動終了し、要約をメールでユーザーと CS 担当に送ります。
結果として、従来チャットで 8 分かかっていた問い合わせが、平均 2 分半で解決するようになりました。ただし、導入初月は Live API のコストが想定の 2 倍弱でした。上で挙げた「画面共有 0.5fps 化」「音声専用モダリティ化」「セッションキャップ」を入れて、2 か月目には想定範囲に収まっています。
テスト戦略:CI で Live セッションをシミュレートする
よく質問されるのが「Live API エージェントのテストをどう書くか」です。CI から本物のセッションを張ると、Gemini のクォータがあっという間に枯渇します。私が 3 プロジェクトで使い回している方針は、3 層のテストピラミッドです。リアルタイムエージェントの仕事では、この構造が結局いちばん信頼できました。
Layer 1 — モック LiveSession での単体テスト : LiveVoiceSession、AudioQueue、ToolRouter は依存注入しやすい設計にしてあります。@google/genai の live.connect をモック化し、偽の LiveSession を自分で駆動してシナリオを走らせます。この層で状態機械のバグ(先ほど触れた onclose と onerror のレース)と、再接続ロジックのリグレッションを掴みます。実行時間は 1 秒未満で、PR ごとの第一ゲートに置きます。
Layer 2 — 記録済みセッションのリプレイテスト : 予約、FAQ、エスカレーションなど主要フローごとに、本物の Live セッションを 1 本 JSON 形式で記録します(serverContent、toolCall、音声チャンクの時系列)。リプレイハーネスは、タイミングを保ったまま Orchestrator に流し込みます。この層は @google/genai のスキーマ変化や、Function Calling の引数変換リグレッションを検知します。私は月 1 回ペースでトランスクリプトを撮り直し、Gemini 側の挙動変化に追随しています。
Layer 3 — ステージング環境での実 Live API スモークテスト : デプロイごとに 1 回、2 分の固定シナリオをステージングプロジェクト上で実行します。アサーションは tool 成功率、TTFT、音声チャンクの整合性の 3 つだけ。落ちたらオンコールがページされます。実クォータを消費する唯一の場所なので、2 分・1 モデル・1 リージョンに厳密にスコープを絞ります。
このピラミッドがあると、Live API 周りの変更を安心して出せます。特にリプレイ層は働き者で、先四半期だけでも本番直前のリグレッションを 3 件捕捉しました。
絶対に省略できないセキュリティ・プライバシー観点
音声エージェントは、チャットボットの頃に感じていたプライバシー上の懸念を何倍にも増幅します。私が本番で重視している観点を、踏みやすい順に並べます。
ログ格納前の PII 赤塗り : デバッグ用に残すトランスクリプトには、氏名、電話番号、注文番号などが普通に含まれます。Cloud DLP または自前の正規表現ベースのパスで必ずマスキングしてから、ログや Sentry に飛ばします。私はトランスクリプトチャネルに対してストリーム型の赤塗りを入れ、Orchestrator 内で一括処理しています。
ユーザーへの同意取得・録音開示 : AI が応対していること、音声が第三者に送信されていることの開示が法的に必要な国・地域が増えています。冒頭のシステムターンに開示文を組み込み(「この会話は AI が対応し、音声は Google Gemini に送信されます…」)、同意ログを残します。
Function Calling のホワイトリスト制御 : Live セッションが任意の内部 API を呼べる設計は絶対に禁止です。上の ToolRouter は名前ベースのホワイトリストを実装していますが、各ハンドラー内でも改めて呼び出し元の権限を再チェックし、多層防御にします。私は過去に、プロンプトインジェクションで refund_order を呼ぼうとしたケースを観測しました。ハンドラー側の再検査で無害化されています。
セッション単位ではなくユーザー単位のレート制限 : 新しいセッションを次々張る攻撃者は、セッション単位のキャップでは止まりません。ToolRouter のキャップに加え、ユーザー単位の合計支出・合計時間でも制限をかけます。
規制データを扱う場合のモデル経路 : 医療・金融データを扱うなら、リージョンエンドポイントを優先的に使い、どのデータ区分がリージョン外に出てよいかをアーキテクチャ図に明記します。
デプロイ前チェックリスト
Live API を本番ユーザーに出す前に、私は次のチェックリストを通します。毎回これで「忘れがちなポイント」を 1〜2 個拾っています。
[ ] Guardian エージェントが最初にデプロイされ、PagerDuty 等に通知が繋がっている
[ ] セッション継続時間の上限値が、サポート SLA と整合している(最初は 10 分が目安)
[ ] 全 tool の costLimit 合計が、許容できるセッション最悪コストに収まっている
[ ] 再接続ロジックを、ステージングで意図的に WebSocket を切ってテスト済み
[ ] 入力サンプルレートが 16kHz で固定され、ステージング録音で検証済み
[ ] responseModalities は音声のみ。字幕は outputAudioTranscription から取得
[ ] 画面共有の fps がデフォルト 2fps 上限で、明示的な意図があるときだけ上がる
[ ] セッション停止時に AudioContext を close() → 再生成している(Chrome の about:tracing で確認済み)
[ ] トランスクリプトログ経路すべてで PII 赤塗りが有効
[ ] 冒頭システムターンに開示文が入っている
[ ] tool のホワイトリストが .agent/ で定義したエージェントと一致する(diff で確認)
[ ] pro-live → flash-live への自動ダウングレード経路がテスト済み
[ ] ダッシュボードに P95 セッション時間、TTFT、再接続率、tool 成功率、分単価が出ている
[ ] CI のリプレイテストが直近トランスクリプトで通っている
項目はわざと地味に書いています。Live API の本番事故は、だいたい「キャップ忘れ」「ホワイトリスト漏れ」「再接続未検証」といった地味な盲点で起きます。地味なチェックリストのほうが、英雄的なデバッグより明確に強いです。
Live API を使わない方がいい場面
ここまで Live API をおすすめしてきたので、バランスとして「使わない方がいい」シナリオも 3 つ挙げます。
サブ秒の遅延が不要な場合 : 2〜3 秒で返せる対話なら、ストリーミングテキスト + 別 TTS(ElevenLabs、Google Cloud TTS)の構成の方がシンプルで安く、テストも楽です。Live API の対価はインタラクティブ性であって、スループットではありません。
モデル出力に強い決定性が必要な場合 : Live API の中断・ストリーミング挙動は、バッチ API が持つ細粒度の制御を一部犠牲にします。「モデルは厳密にこの構造を返すこと」という要件なら、テキストベースの Function Calling の方が噛み合います。
セッションの大半がアイドルな場合 : Live API はセッション全体に対して課金されます。ユーザーがブラウザタブを開きっぱなしで数時間たまに質問する、というプロダクトは Live API のセッションではありません。それは従来型のチャットで、ターンごとに新規リクエストを張るべきです。
道具を用途に合わせると、コストが予測可能になります。「最新だから」だけで Live API を既定にしたチームが、使われないコンピュート時間に資金を溶かす光景は実際に何度も見てきました。リアルタイム性がユーザー体験に本当に効くときにだけ選ぶのが正解です。
全体を振り返って:今日から始める最初のステップ
Live API の本番投入で最もよく聞かれるのが「どこから着手すべきか」という質問です。私の答えは常に同じで、まず Guardian エージェント(コストと時間の監視)を先に用意してください ということです。機能を先に作ると必ずコストで事故ります。監視が先、Live セッションの実装はその後、というのが現場の順番です。
今日このあと 30 分取れるなら、Antigravity で .agent/guardian.md を 1 ファイル作り、セッション開始時に記録すべきメトリクス(開始時刻、モデル、期待コスト上限)を列挙してみてください。その枠組みができてから Live API の SDK に触れると、遠回りに見えて本番投入までの距離が一番短くなります。
実装の細部で詰まったら、この記事で挙げた 3 つのコード例と 5 つの落とし穴を、まずチェックリスト代わりにご活用ください。Gemini Live API は可能性が大きい一方で、従来の API 感覚では必ず足をすくわれる API です。設計を先に固める一手間が、そのまま本番品質につながります。
Live API のような時間課金・長命セッションは、従来の Web API 以上に監視設計の巧拙が運用コストに直結します。