主張:語音 agent 十次有八次出問題,出在音訊格式跟模型架構選錯,而不是 ADK 本身有 bug。
讀完能做到:分清楚 Native Audio 跟 Half-Cascade 兩種模型的取捨,並知道你的多 agent 語音應用有一個藏起來、你關不掉的行為。

如果你昨天已經跑起了語音 agent,今天大概率會遇到這個場景:你把麥克風接上,講了一句話,結果 agent 完全聽不懂,或者回應變成一串電流雜音。八成不是你的程式邏輯錯了——是音訊格式不對。
官方文件講得很直白:ADK 不做音訊格式轉換。送錯格式進去,結果就是爛品質或直接報錯。所以在寫任何邏輯之前,先把格式釘死。
本篇延續 Day 22〈RunConfig 深潛:BIDI、Session Resumption 與 Streaming Tools〉的環境:LiveRequestQueue(往模型送即時資料的佇列)、RunConfig(控制串流行為的設定物件)、Runner.run_live()(啟動雙向串流的入口)都在那篇介紹過,今天直接拿來用。下面的程式碼會用到這幾個 import:
from google.genai import types
from google.adk.agents import Agent
from google.adk.agents.run_config import RunConfig
from google.adk.models.google_llm import Gemini
送進去的音訊要求:16-bit PCM(有號整數)、取樣率 16,000 Hz、單聲道。呼叫 send_realtime() 之前,資料必須已經是這個格式:
from google.genai import types
audio_blob = types.Blob(
mime_type="audio/pcm;rate=16000",
data=audio_data
)
live_request_queue.send_realtime(audio_blob)
模型回傳的音訊格式不一樣——16-bit PCM、24,000 Hz(native audio 模型)、單聲道,MIME type 是 audio/pcm;rate=24000。輸入輸出取樣率不同,這是瀏覽器端最容易漏掉的一個細節:錄音跟播放要用兩個不同取樣率的 AudioContext,如果你複製貼上同一份設定給兩邊,播放出來的聲音會是變速的。
送音訊的最佳實踐有四條:
LiveRequestQueue 會立即轉發每個分塊,不做合併或批次處理。send_activity_start() / send_activity_end() 只有在你手動關閉 VAD 時才需要。瀏覽器端要用 Web Audio API 的 AudioWorklet 處理器捕捉麥克風輸入,核心步驟是:用 16kHz 的 AudioContext 存取麥克風、AudioWorklet 在獨立執行緒即時捕捉音框、把 Float32 樣本轉成 16-bit PCM、透過 WebSocket 二進位訊框傳送(比 base64 編碼省約 33% 頻寬)。其中轉換公式值得記下來——Web Audio API 給的 Float32 樣本範圍是 [-1.0, 1.0],乘上 0x7fff(32767)就轉成 16-bit 有號整數:
function convertFloat32ToPCM(inputData) {
const pcm16 = new Int16Array(inputData.length);
for (let i = 0; i < inputData.length; i++) {
pcm16[i] = inputData[i] * 0x7fff;
}
return pcm16.buffer;
}
播放端反過來,收到 24kHz 的 16-bit PCM,用**環形緩衝區(ring buffer)**吸收網路抖動,除以 32768 轉回 Float32。官方 demo 用一個 24000×180(三分鐘)的緩衝區,並在收到中斷事件時把 readIndex 直接跳到 writeIndex 位置清空緩衝——這是處理「使用者打斷 agent 說話」時,防止播放到過期音訊的關鍵技巧。
ADK 處理影像跟視訊的方式,跟你想的可能不太一樣——不是用 HLS、mp4、H.264 那套典型的視訊串流,而是把兩者都當成逐張 JPEG 影格處理。規格是:JPEG 格式、建議最高 1 FPS、建議解析度 768×768。前端把影格 base64 編碼後用 JSON 送上來,伺服器端解碼再丟進佇列:
import base64
from google.genai import types
image_data = base64.b64decode(json_message["data"])
mime_type = json_message.get("mimeType", "image/jpeg")
image_blob = types.Blob(
mime_type=mime_type,
data=image_data
)
live_request_queue.send_realtime(image_blob)
1 FPS 這個數字直接劃出了它的能力邊界:不適合做即時動作辨識或運動追蹤——快速移動的畫面在 1 FPS 下等於什麼都沒拍到。它適合的場景是「使用者舉起一件東西問這是什麼」「鏡頭掃過一份文件請 agent 讀取內容」這類靜態或緩慢變化的視覺理解,而不是體感遊戲或運動分析。
Live API 的音訊模型分兩種根本不同的架構,這個選擇會直接影響對話自然度、工具呼叫可靠度、延遲特性,以及功能可用性。
Native Audio——端到端音訊架構,模型直接處理音訊輸入並生成音訊輸出,不經過文字中介。這帶來更自然的語調與抑揚頓挫,支援更廣的聲音庫,能自動偵測對話語言不需明確設定,還有進階的**主動對話(Proactive Audio)跟情緒感知對話(Affective Dialog)**功能。代價是:只支援 AUDIO 回應模式,不支援 TEXT,初始回應速度也較慢。
型號要看你走哪個平台:
gemini-2.5-flash-native-audio-preview-12-2025,公開可用。gemini-live-2.5-flash-native-audio,公開預覽階段。Half-Cascade(也叫 Cascaded)——混合架構,音訊輸入是原生處理,但回應先生成文字再轉語音(TTS)。這個文字中介層讓它在生產環境更可靠,工具呼叫執行更穩健。它支援 TEXT 回應模式(意味著純文字場景可以跳過語音合成,回應快得多),也支援明確的 language_code 語言設定。8 種內建聲音:Puck、Charon、Kore、Fenrir、Aoede、Leda、Orus、Zephyr。注意:gemini-2.0-flash-live-001 已於 2025 年 12 月 9 日棄用——如果你看到的教學還在用這個型號名,那份教學已經過期了。
怎麼選?追求最自然的對話體驗、需要主動對話與情緒感知,選 Native Audio;追求正式環境的可靠性、需要文字回應模式、工具呼叫是核心功能,選 Half-Cascade。官方建議把型號名寫進環境變數而不是寫死在程式碼裡,因為模型的上市與棄用節奏很快:
import os
from google.adk.agents import Agent
agent = Agent(
name="my_agent",
model=os.getenv("DEMO_AGENT_MODEL", "gemini-2.5-flash-native-audio-preview-12-2025"),
tools=[...],
instruction="..."
)
這裡有個 Python import 順序要注意:用 python-dotenv 讀 .env 時,load_dotenv() 必須在 import 任何會讀環境變數的模組之前呼叫。上面的 agent 模組在 import 當下就執行 os.getenv(),如果 load_dotenv() 排在它後面,拿到的只會是預設值,.env 設定完全不生效。
Live API 內建語音轉文字,RunConfig 透過 input_audio_transcription 跟 output_audio_transcription 兩個欄位控制,預設兩者都是開啟的:
from google.adk.agents.run_config import RunConfig
# 明確關閉轉錄
run_config = RunConfig(
response_modalities=["AUDIO"],
input_audio_transcription=None,
output_audio_transcription=None
)
「預設開啟」在程式裡長這樣:print(RunConfig().input_audio_transcription) 印出的不是 None,而是一個所有子欄位都是 None 的 AudioTranscriptionConfig 空物件。看起來像沒設定,其實已經開著了;要關掉就得像上面那樣明確寫 None。
轉錄結果會出現在 Event.input_transcription 跟 Event.output_transcription,各自有 .text(文字內容)跟 .finished(是否已完成,還是部分轉錄)兩個屬性。存取時務必做兩層 null 檢查——先確認轉錄物件存在,再確認文字非空——因為轉錄可能還在進行中,文字可能是空字串。
現在講那個「你關不掉的例外」:只要你的 agent 定義了 sub_agents,run_live() 會自動強制開啟轉錄——即使你把兩個欄位都明確設成 None。原因是 agent 之間的轉移(transfer)需要文字上下文才能運作,轉錄是這個機制的必要條件。細看規則是這樣:
input_audio_transcription:只要有 sub_agents 就一定被打開。output_audio_transcription:有 sub_agents,而且 response_modalities 包含 AUDIO 時被打開。這代表多 agent 語音應用沒辦法關閉轉錄,你的應用一定會收到轉錄事件,必須有邏輯去處理它們——即使你原本完全不想要轉錄功能。這段邏輯在 run_live() 建立執行上下文時就套用了,你在 RunConfig 裡寫什麼都擋不住。
聲音設定可以在兩個層級指定:agent 層級(透過自訂的 Gemini 模型物件)跟 session 層級(透過 RunConfig)。這讓多 agent 場景可以做出「每個 agent 講話聲音不一樣」的效果:
from google.genai import types
from google.adk.agents import Agent
from google.adk.models.google_llm import Gemini
customer_service_llm = Gemini(
model="gemini-2.5-flash-native-audio-preview-12-2025",
speech_config=types.SpeechConfig(
voice_config=types.VoiceConfig(
prebuilt_voice_config=types.PrebuiltVoiceConfig(
voice_name="Aoede" # 友善、溫暖的聲音
)
)
)
)
customer_service_agent = Agent(
name="customer_service",
model=customer_service_llm,
instruction="You are a friendly customer service representative."
)
優先順序很明確:Agent 層級設定優先於 session 層級。如果 Gemini 物件有設 speech_config,不管 RunConfig 寫了什麼都會被蓋掉;如果 agent 沒設,才會用 RunConfig 裡的值;兩邊都沒設,就用 Live API 的預設聲音。
language_code 的行為要注意模型差異:Half-Cascade 模型會照你設的語言代碼合成語音,Native Audio 模型則傾向自動從對話上下文判斷語言,明確設定的 language_code 可能不會被理睬。
VAD 自動偵測使用者何時開始講話、何時講完,讓對話能自然輪替,不需要手動控制。所有 Live API 模型預設開啟 VAD。
什麼時候該關掉它?三種場景:
關掉 VAD 之後,你必須改用手動的 activity 訊號(ActivityStart / ActivityEnd,也就是 Day 22 提過的 send_activity_start() / send_activity_end())來控制對話輪次。
這兩個功能只有 Native Audio 模型才有,都在 RunConfig 裡開:
from google.genai import types
from google.adk.agents.run_config import RunConfig
run_config = RunConfig(
# Model can initiate responses without explicit prompts
proactivity=types.ProactivityConfig(proactive_audio=True),
# Model adapts to user emotions
enable_affective_dialog=True
)
Proactive Audio(proactivity)開啟後,模型能基於上下文主動發起回應:沒被問也會提出建議、主動補充後續資訊、忽略離題的輸入、預判使用者接下來需要什麼。
Affective Dialog(enable_affective_dialog)開啟後,模型會從語氣與內容判讀情緒(挫折、開心、困惑),據此調整回應的風格、語調與正式程度。
放進實際場景,一個需要同理心的客服機器人會把兩者跟串流設定擺在一起:
from google.genai import types
from google.adk.agents.run_config import RunConfig, StreamingMode
# Configure for empathetic customer service
run_config = RunConfig(
response_modalities=["AUDIO"],
streaming_mode=StreamingMode.BIDI,
# Model can proactively offer help
proactivity=types.ProactivityConfig(proactive_audio=True),
# Model adapts to customer emotions
enable_affective_dialog=True
)
客戶抱怨「訂單等了三個禮拜」時,模型可能察覺語氣裡的不耐而先道歉、再主動詢問訂單編號;對話後段,它也可能主動問要不要幫忙開啟出貨通知。要記得這兩種行為都是機率性的,同一句話不保證每次得到一樣的反應,別拿它們當成可以寫測試斷言的確定行為。
今天把 Live API Toolkit 系列收在音訊、影像、視訊的具體實作細節上:格式規格、Native Audio 跟 Half-Cascade 的取捨、轉錄的隱藏行為、Voice Config 優先順序、VAD 的開關時機。三天下來,你手上已經有一個能講話、能斷線重連、能用適合的模型架構跑起來的語音 agent。明天要換一個完全不同的執行模式——不是等使用者開口,而是讓 agent 在沒有人盯著的時候,自己對外部世界的變化做出反應。
Google ADK 官方網站
GitHub - Agent Development Kit (ADK) 2.0
延伸閱讀:ADK Live API Toolkit 開發指南 Part 5
GitHub 開源實作:https://github.com/SeanLinH/adk_tutor