CodeGym /課程 /ChatGPT Apps /Voice / Realtime 背景:App 在語音互動時的行為

Voice / Realtime 背景:App 在語音互動時的行為

ChatGPT Apps
等級 8 , 課堂 4
開放

1. 背景:對 ChatGPT App 而言,「語音模式」意味著什麼

首先要釐清,在 ChatGPT Apps SDK 的範圍內,你不需要撰寫自己的音訊用戶端、不會直接控制麥克風,也不會自行串流音訊。這些都由 ChatGPT 用戶端(網頁或行動應用)處理。

假設你已從前面的單元對 widget、callTool 與 GiftGenius 有基本認識——在這裡我們改用語音模式的視角來看同樣的元素。

從你作為 App 開發者的視角,流程大致如下:

  • 使用者對著麥克風說話。ChatGPT 用戶端做語音辨識,並將文字傳給模型。
  • 在資料流中,你「看到」的就像使用者打字一樣的文字,只是到達更快且更口語化。
  • 模型以文字回覆,而用戶端會將其轉成語音播放。
  • 同時,模型可以呼叫你的工具(callTool)、切換 widget 的 displayMode、更新 widgetState,並提出後續建議——就像在文字模式一樣。

關鍵差異在於,使用者可能幾乎不看螢幕,或只偶爾瞥一眼手機。也就是說,你的 UI 不再是主要互動管道,而是成為語音的輔助,而非本位主角。

這帶來兩項後果:

  • 所有真正重要的內容,都必須能「用耳朵」聽懂,由 GPT 的台詞說清楚。
  • Widget 應該做到「一眼即懂」:只要快速掃一眼,就能看出狀態與關鍵選項,而不必閱讀小字。

對我們的 GiftGenius 而言,這馬上給出提示:情境「我在開車,幫我挑給媽媽的禮物」並不只是文字聊天,而是多模態對話——以語音帶領,UI 做保險與補充。

2. 語音情境與文字情境有何不同

為了避免掉入「一切都一樣,只是把打字改成說話」的陷阱,最好從幾個面向比較文字與語音兩種模式。

面向 文字模式 語音模式
使用者注意力 看著螢幕、閱讀、捲動 可能完全不看(hands‑free)
請求形式 較有結構、可編輯 口語、片段語句、像「嗯」、「再來一點」
對停頓的容忍度 1–2 秒的沉默可以接受 長時間沉默會讓人不適
UI 的角色 細節的主要載體 輔助性,像「看板」提供簡短視覺錨點
輸入錯誤 打字錯誤,但可見文字 語音含糊、噪音、誤判的「是/否」

由此得到幾個重點。

  • 不要指望使用者會「去讀卡片」。關鍵事項必須說出來:你理解了什麼、打算做什麼,以及得到了什麼結果。
  • UI 必須支援「只瞄 1 秒」的情境:狀態、進度、主要選擇都要以大字清楚呈現;細節其次。
  • 需要填補空白的停頓。當你的 MCP 伺服器在處理沉重請求時,模型應該口頭說明正在發生什麼,而 widget 顯示進度,避免給人「助理卡住」的感覺。

可以把語音模式想成有插圖的有聲書:你有旁白(GPT)與圖片(widget)。要讓兩者互補,而非重複或相互衝突。

3. 語音模式下 widget 的角色:從「控制面板」到「看板」

在文字情境中,widget 常常是完整的介面:表單、表格、帶過濾器的輪播、動作按鈕。在語音情境中,它的角色改變了。多模態介面與 VUI 的建議顯示:在語音情境下,UI 更像是資訊看板(glanceable UI):它用於快速確認與核對,而不是要求使用者長時間盯著看。

對 GiftGenius 而言,這代表:

當使用者走語音精靈流程時,在 inline widget 或 fullscreen 顯示:

  • 醒目的狀態:「第 2 步(共 3 步):預算與禮物類型」。
  • 文字最少,但標示清楚:「預算上限 50 美元」、「偏好數位禮物」。
  • 如果語音情境允許點擊,提供少數幾個大的 CTA 按鈕:「變更預算」、「繼續」。
  • 使用單一簡潔的 progress‑bar 或步驟條,而不是許多細小指示器。

語音情境下 inline widget 的簡易「看板」範例(TypeScript + React,大幅簡化):

type VoiceUiMode = "default" | "voiceGlance";

interface GiftStepProps {
  step: number;
  totalSteps: number;
  summary: string; // 已蒐集內容的簡短摘要
  uiMode: VoiceUiMode;
}

export function GiftVoiceStep(props: GiftStepProps) {
  const fontSize = props.uiMode === "voiceGlance" ? "text-lg" : "text-sm";

  return (
    <div className="rounded-xl border p-3 flex flex-col gap-2">
      <div className={`${fontSize} font-semibold`}>
        第 {props.step} 步,共 {props.totalSteps} 步
      </div>
      <div className={`${fontSize} text-muted-foreground`}>
        {props.summary}
      </div>
    </div>
  );
}

這裡本身沒有任何「語音」技術,但有清楚的理念:當 uiMode === "voiceGlance" 時,把一切做得更大、更簡潔。語音模式的訊號可能來自多種地方:從間接跡象到由模型在 widgetState 或 tool 回應中設置的顯式旗標。

4. 模態同步:GPT 說了什麼,App 顯示什麼

Apps 的 voice‑UX 核心原則是模態同步:語音與視覺 UI 要講同一個故事,但細節層次不同。

常見錯誤是讓模型把 widget 上顯示的一切逐字念出:冗長的禮物清單、帶過濾器的 JSON 結構等等——這會成為折磨。建議做法:語音給簡短摘要,UI 顯示細節。

GiftGenius 的良好同步範例。

使用者:「幫我挑一個給媽媽的禮物。她喜歡園藝,預算到 50 美元。」

模型(語音):「我挑了幾個選項。我認為最適合的是一組園藝工具,價格 45 美元。我也在畫面上顯示另外兩個類似的選項。要不要我再說詳細一點,或直接進入選擇?」

Widget(inline):顯示三張禮物卡片、簡短描述與 CTA 按鈕「選擇」/「顯示相似」。

以對話‑JSON 的方式描述其中一步(這不是實際協定,只是思考示意):

{
  "user": "幫我挑一個給媽媽的禮物……",
  "assistant_text": "我找到了幾個選項……",
  "widget": {
    "displayMode": "inline",
    "state": {
      "view": "gift_list",
      "items": [
        { "id": "g1", "title": "園藝工具組", "price": 45 },
        { "id": "g2", "title": "園藝圍裙", "price": 30 },
        { "id": "g3", "title": "花卉種子組", "price": 20 }
      ]
    }
  }
}

重點:你可以在 system prompt 中明確規範模型應如何描述 UI,而不是「朗讀 JSON」:例如「如果你在 widget 顯示了選項清單,就不要把每個項目完整念出。請簡短描述最佳選項,並說明其他項目已在畫面上。」

未來當你使用 Realtime API 與自製的語音用戶端時,原則依然相同:UI 與音訊流必須一致。差別只是你會擁有更直接的串流控制。

5. Realtime 與延遲:避免尷尬的沉默

技術上,語音模式下的 tool_calls 與文字模式相同:模型決定呼叫你的工具、你回傳結果、widget 更新。但語音會多出一個新的 UX 問題——延遲。當你的 MCP 伺服器呼叫外部 API 或計算複雜報表時,使用者聽到的……是沒有聲音。這比文字聊天中等待回應更糟。

解法有兩層:語音層與視覺層。

  • 語音層:在 system prompt 中允許(並鼓勵)模型在工具尚在運作時口頭說「我在處理」,並順帶詢問其他條件。例如:「我現在要為你挑選禮物,這會花上幾秒鐘。期間若有其他限制,請先告訴我。」
  • 視覺層:你的 widget 必須非常明確地顯示進度:載入器、狀態「正在尋找選項……」、當前步驟。否則使用者會以為當機,開始重新說話,干擾語音流程。

實務上可以用延遲任務處理:工具立即回傳狀態 "pending"jobId,而實際搜尋在背景執行。Widget 根據 "pending" 顯示進度,語音則說明它正在「工作」。

一個最簡單的伺服器端工具示意,不會阻塞到完整結果,而是先回傳帶 job‑id 的佔位:

// GiftGenius 伺服器端工具的偽代碼
export async function startGiftSearch(params: SearchParams) {
  const jobId = await createBackgroundJob(params); // 將工作放入佇列

  return {
    status: "pending",
    jobId,
    message: "已開始搜尋禮物"
  };
}

Widget 在看到 status: "pending" 後,可以切換到進度模式:

if (toolOutput.status === "pending") {
  return (
    <div className="p-4 rounded-xl border flex items-center gap-3">
      <Spinner />
      <div className="text-base">
        正在為你挑選禮物…… 這會花上幾秒鐘。
      </div>
    </div>
  );
}

而模型也會依據相同的 tool 輸出用語音說出類似資訊,並可能提出進一步的澄清問題。稍後,當背景任務完成,而且例如透過 MCP‑notification 收到 job.completed,widget 會更新為禮物清單,語音則播報摘要。

如此一來,即使後端不是瞬時回應,我們也能獲得最接近即時的體驗。

6. 語音下的安全與確認

一旦涉及關鍵動作(例如付款、刪除資料、變更設定),語音介面就特別棘手。語音辨識並不完美,使用者也常在移動中說話,「嗯」很容易被誤解為「好,買吧」。因此在語音情境裡,確認流程(confirmation flows)尤為重要。

這裡有兩個基本樣式。

  • 明確的語音確認(Explicit Voice Confirmation)。 對危險動作要求具體的語句。例如:「要確認購買,請說:『確認購買』」,並在 system prompt 中禁止用模糊的「嗯」、「OK」、「好啊」來觸發付款。
  • 僅視覺確認(Visual Confirmation Only)。 模型以語音引導使用者到動作點(「我已經建立訂單,畫面上顯示總金額與購物車內容」),但實際觸發是 widget 上的「付款」按鈕。這在電商情境中特別常見,我們會在第 14 單元再談。

對 GiftGenius 而言,可以是這樣。

模型:「我挑到一組很適合的園藝工具,45 美元。我可以透過 ChatGPT 代為下單。畫面上已顯示最終價格與收件地址。若要語音確認,請說『確認購買』,或直接點擊畫面的『付款』按鈕。」

Widget(fullscreen):顯示最終訂單,以粗體標示金額與地址,並提供兩個醒目按鈕:「付款」與「取消」。

你也可以在 widget 內反映確認狀態:

type CheckoutState = "review" | "waiting_voice_confirm" | "confirmed";

if (state.phase === "waiting_voice_confirm") {
  return (
    <div className="space-y-3">
      <h2 className="text-xl font-semibold">快完成了</h2>
      <p className="text-base">
        請以語音說出
        「確認購買」以完成確認,或點擊「付款」按鈕。
      </p>
      <Button variant="primary">付款</Button>
      <Button variant="ghost">取消</Button>
    </div>
  );
}

因此,即使模型仍然誤解了某些語音內容,使用者仍保有視覺層的「安全網」。

7. 簡單語音指令與工具設計

語音使用者不會把指令講得和你工具的參數一模一樣。他們會說「選第一個」、「再便宜一點」、「不要電子產品」。開發者的任務,是把工具與 system prompt 設計得能讓模型把這些自然語句輕鬆對應為你的工具呼叫(callTool)。

對 GiftGenius,可以預設如下動作:

  • 依索引或 id 選取清單中的某個選項。
  • 調整預算:「更便宜」、「最多 30 美元」。
  • 依類型過濾:「只要數位禮物」、「不要需要郵寄的」。

這可以用帶有簡單 enum 參數的工具來表達 action,再配合額外欄位:

// TypeScript 的工具示意
type VoiceActionInput =
  | { action: "select_item"; itemId: string }
  | { action: "refine_budget"; maxPrice: number }
  | { action: "filter_type"; type: "digital" | "physical" };

export function handleVoiceAction(input: VoiceActionInput) {
  switch (input.action) {
    case "select_item":
      // 將該禮物標記為已選
      break;
    case "refine_budget":
      // 依新預算重新計算清單
      break;
    case "filter_type":
      // 篩選現有清單
      break;
  }
}

在 system prompt 中描述這些動作如何對應語音指令:「如果使用者說『選第一個』,呼叫工具 gift.voiceAction,並設定 action="select_item",且帶上畫面上第一個禮物的識別碼」等。

從 UX 角度看,這能降低認知負擔:使用者不需要想出像「請將過濾條件改成只有 30 美元以下的數位禮物」這樣精確的措辭。他們只要自然地說,模型會把它轉換成資料結構。

8. GiftGenius 的語音情境:三個步驟

把上述整合起來,先不深入 low‑level Realtime API,設計一個完整的 GiftGenius 語音情境。

想像一位使用者正在開車,啟動 ChatGPT 的語音模式。他說:「請幫我挑給媽媽的禮物,她喜歡園藝,預算到 50 美元。」

步驟 1. 以語音蒐集資訊

模型:「太好了,我們來挑禮物。我想再確認幾件事:什麼時候需要——這幾天還是之後?還有沒有其他限制,例如不要太重或太占空間的東西?」

Widget(inline):先顯示一個小面板,狀態為「正在挑選禮物給:媽媽、園藝、上限 50 美元」。字體比平常略大,方便一眼看懂。

Widget 狀態代碼可如下:

interface GiftSessionState {
  mode: "voice" | "text";
  step: 1 | 2 | 3;
  recipientSummary: string;
  budget?: number;
}

const [state, setState] = useState<GiftSessionState>({
  mode: "voice",
  step: 1,
  recipientSummary: "媽媽,喜歡園藝"
});

隨著使用者回覆,伺服器端會更新 recipientSummarybudget,widget 也會跟著反應。

步驟 2. 搜尋與等待

當模型蒐集到足夠資訊後,會呼叫你的禮物搜尋工具。若挑選較複雜,工具可以啟動背景任務並回傳 status: "pending"。在背景執行時,模型會說:「我現在要找合適的選項,這會花上幾秒鐘。你也可以先說她偏好實體禮物,還是可以接受數位禮券。」

如果使用者切換到介面的其他區域,widget 會切到「類 PiP」模式;否則保持 inline 並顯示進度:「正在尋找禮物……」與一個小指示器。

步驟 3. 結果與選擇

當結果準備好,模型說:「我找到了三個選項。第一個是一組園藝工具,45 美元。第二個是園藝圍裙,30 美元。我已在畫面上顯示。你可以說『選第一個』或『再便宜一些』。」

Widget 顯示三張大型卡片,附價格與簡短描述。每張卡片都有「選擇」與「相似」兩個 CTA,並另有「顯示更多選項」按鈕。

如果使用者說:「選第二個」,模型會呼叫你的 voiceAction 工具,帶上 action="select_item" 與第二個禮物的 id。Widget 會把它標示為已選,模型則播報:「太好了,已選擇園藝圍裙,30 美元。」

可選的步驟 4. 結帳

如果 App 已整合付款(第 14 單元),則進入結帳。模型會說明條件,並要求以語音或按鈕確認。Widget 進入 fullscreen 精靈流程:「檢查訂單」→「收件地址」→「確認」。

重點是每一步關鍵內容都以語音說清楚,而 widget 提供視覺依據,特別是在使用者停下來看畫面的時候。

9. 實作備註與 Apps SDK 的邊界

上述 GiftGenius 的流程全部都可在一般的 ChatGPT App 內完成——不需要自建音訊用戶端或 WebRTC。記住技術邊界很重要。

人很容易腦中一路飆向 Realtime API、WebRTC、音訊串流,開始打造自己的語音平台。本課另有第 20 單元專講那些主題。此處請記得我們討論的是ChatGPT 用戶端內的 ChatGPT App

在目前的架構中:

  • 音訊串流由 ChatGPT 用戶端管理。你不會在 widget 中傳送或接收音訊位元組。
  • 在後端你仍然看到一般的 tool 呼叫與文字訊息,但模型可能處於語音模式,而它的回覆會被朗讀。
  • 平台可能透過 user-agent 或環境欄位提供語音模式的間接線索。但不要建立硬性依賴:API 可能變動,而你的 App 在純文字模式下也應該同樣有用。

因此一個不錯的實作策略是:先設計一份同時適用於文字語音的 UX——精簡的狀態、清楚的 CTA、明確的進度階段。接著再加入幾個語音專屬強化:在 "voiceGlance" 模式使用較大的字體、更顯眼的進度、強調「第 2 步(共 3 步)」這類狀態與「等待確認」這種清楚的階段。

此外,在 system prompt 中描述模型的語音行為:如何評論 widget 狀態、用哪些語句做確認、避免哪些用詞(例如不念 JSON、不逐條念清單瑣碎內容)。

若日後你用 Realtime API 製作自訂的 Voice Client,這些 UX 決策都能平移過去。差別只在於對事件與串流的存取層級,而非原則。

10. 處理 Voice / Realtime 背景時的常見錯誤

錯誤第 1 條:「把 UI 逐字念出」而不是給摘要。
有時開發者把工具寫成讓模型把 JSON 回應或整份卡片清單全部念出。在語音模式下這會嚴重傷害 UX:使用者會失去重點,你也浪費 token。更好的做法是讓語音播報短摘要,聚焦一兩個重點,其餘放在畫面上。

錯誤第 2 條:語音中完全沒有視覺回饋。
有人會想:「既然使用者在說話,就是在聽,用不到 UI。」實際上,使用者常會瞥螢幕或稍後回到畫面。若此時沒有狀態、進度或清楚的結果,他會以為 App 當機或什麼都沒做。務必顯示「我在思考」、「第 2 步(共 3 步)」、「結果已就緒」等狀態。

錯誤第 3 條:對危險操作缺乏強式確認。
在文字模式用一顆「付款」按鈕就很危險;在語音模式用一句模糊的「嗯」就執行購買更危險。忽視明確的確認流程(語音與/或視覺)會導致錯誤購買與對 App 的不信任。請思考哪些操作需要雙重確認,並在 system prompt 與 UI 中清楚描述。

錯誤第 4 條:只為眼睛設計,而不是為耳朵設計。
有時 App 的設計假設使用者一定會閱讀:措辭太複雜、按鈕文案很長、描述過度冗長。在語音模式下,這些還得「念」出來——變成「語言雜湊」。務必讓關鍵意義能用短而簡的句子說清楚,容易用聽覺吸收。

錯誤第 5 條:混淆 Apps SDK 與自建 Voice 用戶端。
有些同學會在 Apps SDK 裡尋找麥克風事件、音訊串流、WebRTC 等(如同 Realtime API),最後失望「這些都沒有」。請理解:ChatGPT App 活在 ChatGPT 用戶端內,語音由平台管理。你的工作是處理文字、tool 呼叫與 widget 狀態,並把 UX 設計得讓語音模式「自然而然地好用」。若想完全掌控語音,則是另一個更複雜、使用 Realtime API 的專案。

錯誤第 6 條:缺乏處理延遲的策略。
若沒有設計模型在長時間操作時該說什麼、widget 該顯示什麼,使用者就會插話、提出新問題,打亂你的流程。語音中的延遲感受比文字更強。請用中間狀態、背景處理與「我在思考,你先告訴我……」這類語音橋接,避免讓沉默變成 bug。

1
問卷/小測驗
UX 與介面,等級 8,課堂 4
未開放
UX 與介面
UX 與介面(Inline、Fullscreen、Voice)
留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION