CodeGym /課程 /ChatGPT Apps /與 ChatGPT 的互動:Follow-ups 與「圍繞小工具的對話」

與 ChatGPT 的互動:Follow-ups 與「圍繞小工具的對話」

ChatGPT Apps
等級 3 , 課堂 3
開放

1. 什麼是 ChatGPT App 中的 follow‑up,以及為什麼需要它

先用貼近人話、而非行銷話術的方式定義。ChatGPT App 中的 follow‑up 是在對話裡由程式發起的下一步。通常是一個以按鈕呈現的短提示:按下後,它會在聊天中變成一則新的文字訊息,並視為由使用者發出,進而推進人機互動的劇本。

從模型的觀點來看,follow‑up 只是另一則使用者訊息。沒有魔法:當使用者點了你的按鈕「顯示更便宜的選項」時,聊天歷史會出現類似「請顯示更便宜的禮物」的內容。模型把它當作一般使用者語句,套用 system‑prompt 與工具描述,決定要呼叫哪個工具,並且可能再次以不同資料重新渲染你的小工具。

為什麼重要:

  • 模型以文字為核心思考。 如果你在點擊時直接呼叫 tool(不送出訊息),等於繞過系統的主要「大腦」,也會遺失部分脈絡。Follow‑up 會把對話進度保留在歷史裡,幫助模型理解當下發生的事。
  • 使用者依然在熟悉的聊天範式中:要嘛自己輸入訊息,要嘛按下提示。Follow‑up 與現代通訊軟體中的「快速回覆」(quick replies)相同。
  • 這是你的 UI 與 ChatGPT 文字部分之間的主要橋樑。沒有 follow‑up,小工具就像無聲的孤島:使用者點了幾下,卻不確定下一步怎麼做。

可以把 follow‑up 視為「下一個要問 AI 的問題」的按鈕,只是由你來擬定措辭。接下來我們不把 follow‑up 當作「又一個 UI 功能」,而是作為圍繞小工具來建立對話的主要方式。

2. 「圍繞小工具的對話」:conversational sandwich

為了更容易看出 follow‑up 如何幫助構建「圍繞小工具的對話」,我們可以把對話想像成三層三明治:

  • 上層:小工具之前的 ChatGPT 文字(pre‑text),
  • 中層:你的小工具(UI),
  • 下層:follow‑up 與後續訊息(post‑interaction)。

示意:

sequenceDiagram
    participant U as 使用者
    participant G as ChatGPT
    participant W as 小工具

    U->>G: "幫我為妹妹挑選不超過 $100 的禮物"
    G->>G: 決定呼叫 App
    G->>U: 前置文字: "現在我會開啟 GiftGenius 並挑選一些點子"
    G->>W: 傳遞 toolOutput 供渲染
    W-->>U: 禮物卡片 + 按鈕 follow-up
    U->>W: 點擊 "顯示更便宜的選項"
    W->>G: sendFollowUpMessage("請顯示更便宜的禮物,不超過 $50")
    G->>G: 模型再次運行,呼叫 tools
    G->>W: 新的 toolOutput,已更新的小工具

Pre‑text 通常完全由模型根據 system‑prompt 與脈絡生成:它會「解釋」接下來會發生什麼(「現在我會開啟能幫你挑禮物的應用」)。我們會在後續「指令」模組中再談如何更好地引導這一層。

小工具就是你熟悉的 React 元件:渲染 toolOutput,使用 widgetState,提供按鈕與選項。

Post‑interaction 層正是我們今天要做的。透過 follow‑up 與發送訊息,你能引導清楚的下一步:「顯示更高價」、「變更預算」、「重新開始挑選」、「前往結帳」。

簡而言之,follow‑up 是收束迴圈的控制台詞:UI → 文字 → 新的 UI。

3. 技術模型:點擊 follow‑up 如何變成新的 tool‑call

來仔細看看「引擎蓋下」的事件鏈。可以把它想成一個混合式互動循環:點擊 → API → 歷史中的文字 → 模型新一輪決策 → 新的工具呼叫 → 更新後的 UI。

大致順序如下:

  1. 使用者在你的小工具裡按下按鈕。
  2. 小工具呼叫 API window.openai.sendFollowUpMessage,或在 React 層使用方便的 hook 包裝 useSendMessage(後續範例我們會使用這個 hook)。參數是一段一般字串:就像使用者寫下的文字。
  3. ChatGPT 將這則訊息加入歷史,作為新的 user_message
  4. 模型進行新一輪處理:考量完整歷史(包含先前的 toolOutput),並決定是否要呼叫工具、呼叫哪個、以及使用哪些參數。
  5. 你的 backend/MCP 執行 tool‑call,並回傳 toolOutput
  6. ChatGPT 顯示新的回應:可能再次是小工具(帶入新資料)、可能是純文字、也可能是兩者的組合。

重點是,你幾乎不會想要在小工具內直接呼叫與模型同一個工具、繞過這個循環。否則模型會「遺失」一步:歷史裡缺了那個觸發重呼叫的請求。之後場景變長時,脈絡就會變得混亂。

可以濃縮成一行公式:

User Click → useSendMessage("...") → ChatGPT (LLM) → Tool Call → New toolOutput → Widget rerender

4. follow‑up 的類型,以及誰來設計它們

我們已經理解點擊 follow‑up 之後「引擎蓋下」發生了什麼。現在看看有哪些 follow‑up 值得提供給使用者,以及它們在流程裡能扮演的角色。

在真實的 App 中,follow‑up 可以分成好幾個「家族」。我會從不同軸線來切:靜態 vs. 動態、以及角色(drill‑down、pivot、commit 等)。

為了實作方便,先看表格。

類型 作用 文字/按鈕範例
提示型(Suggestive) 在使用者不知道接下來要問什麼時提供協助 「顯示更多點子」、「依興趣縮小範圍」
細化型(Drill-down / Parametric) 依參數縮小前一個請求 「更便宜」、「只顯示數位禮物」、「只看 Nike」
轉向型(Pivot) 切換到另一條劇本分支 「重新開始挑選」、「顯示適合小孩的禮物」
導覽型(Navigation) 跳轉到流程的另一個步驟 「前往結帳」、「回到選擇」
確認型(Commit) 確認執行動作 「下單這份禮物」、「儲存這次挑選」

從靈感來源來看,主要有兩大類。

第一類是由應用本身提供的 follow‑up。也就是我們在小工具裡提供的按鈕,與 UI 邏輯與資料綁在一起:例如「顯示與[禮物名稱]相似的項目」或「只用興趣 travel 做篩選」。這些提示可以硬編碼(static),也可以根據 toolOutput 動態生成(dynamic)。

第二類是 ChatGPT 的「原生」提示——訊息下方的小晶片,由模型自行產生。你無法直接控制它們;把它們當作免費加成,而非可靠機制。你的應用就算沒有它們,也必須能良好運作。

在這堂課中,我們更關注第一類:你在 React 元件中繪製的按鈕與提示,使用者點擊後由 sendFollowUpMessage 發送。

5. 在 React 中實作 follow‑up:以 GiftGenius 為例

延續我們的範例 App「GiftGenius」來挑選禮物。在呼叫工具 get_gift_ideas 之後,小工具會取得帶有禮物清單的 toolOutput。在之前的主題裡我們已經做過卡片網格。現在加上 follow‑up 區塊。

假設 SDK 提供了 useWidgetPropsuseSendMessage 兩個 hooks。名稱只是示意,但概念與參考實作相同:

import { useWidgetProps, useSendMessage } from '@/openai-apps';

export const GiftSuggestions: React.FC = () => {
  const { toolOutput } = useWidgetProps();
  const sendMessage = useSendMessage();

  const gifts = toolOutput?.data?.gifts ?? [];

  if (gifts.length === 0) {
    return <div>未找到禮物。請嘗試調整查詢。</div>;
  }

  const handleCheaper = () => {
    sendMessage('請顯示更便宜的禮物,不超過 $50');
  };

  const handleDigital = () => {
    sendMessage('請只顯示數位禮物:禮券、訂閱等。');
  };

  return (
    <div className="flex flex-col gap-4">
      <div className="grid grid-cols-2 gap-2">
        {gifts.map((gift: any) => (
          <GiftCard key={gift.id} item={gift} />
        ))}
      </div>

      <div className="border-t pt-3 text-sm">
        <div className="text-xs text-gray-500 mb-2">接下來要做什麼?</div>
        <div className="flex flex-wrap gap-2">
          <button
            onClick={handleCheaper}
            className="px-3 py-1 rounded-full bg-gray-100 hover:bg-gray-200"
          >
            更便宜
          </button>
          <button
            onClick={handleDigital}
            className="px-3 py-1 rounded-full bg-gray-100 hover:bg-gray-200"
          >
            僅數位
          </button>
        </div>
      </div>
    </div>
  );
};

這裡有幾個重點。

首先,sendMessage 接收的是字串——這段文字會出現在聊天中,就像是使用者自己輸入的。你的 App 不需要模擬「內部」系統協定;只要把句子寫得讓模型看得懂即可。

其次,follow‑up 區塊在視覺上是分隔開的(上框線、細小的「接下來要做什麼?」文字),讓使用者理解:這更像對話的延續,而不是卡片本體的元素。

第三,按鈕不要太多——兩個就好。UX 建議通常落在兩到四個選項:足以提供幫助,但不會造成負擔。

基於資料的動態 follow‑up

假設你想讓使用者針對特定禮物深入:「顯示與這個相似的」。此時 follow‑up 文字應提到該物件,你可以在當下生成。

const handleShowSimilar = (giftTitle: string) => {
  sendMessage(
    `請顯示與 "${giftTitle}" 類似的禮物,可以維持相同預算或稍微高一點`
  );
};

在卡片中:

<button
  onClick={() => handleShowSimilar(gift.title)}
  className="mt-2 text-xs text-blue-600 underline"
>
  顯示類似項目
</button>

這樣你就實作了與 toolOutput 具體資料綁定的動態 follow‑up。正是這類按鈕讓對話變得「聰明」,而不是僅有通用的「下一步 / 上一步」。

6. 為何要送出文字,而不是直接呼叫 tool

前端工程師常見的想法是:「既然有 useCallTool,為什麼還要發送文字?我可以直接用不同參數呼叫 get_gift_ideas。」有時確實需要(我們會在關於從小工具呼叫工具的模組裡談更多),但預設情況下更好的方式是透過文字型的 follow‑up。理由非常務實。

首先,聊天歷史會保持完整。從外觀上看起來就像使用者自己寫了「請顯示更便宜的禮物」。一週後他打開歷史,仍能理解發生了什麼。如果你全部用直接 tool 呼叫,使用者訊息之間會突然冒出各種小工具,卻沒有可見的原因。

其次,模型可以做更多決策。比如它可能先釐清預算,而不是直接重呼叫同一工具:「你確定要把預算降到 $5 嗎?也許至少保留到 $20?」一旦你把「點擊 → 同一個 tool + 不同參數」寫死,這類彈性劇本就無法成立。

第三,模型可能需要呼叫完全不同的工具。假設使用者按了「聯絡客服」,而你的 system‑prompt 教模型在這種情況下要呼叫 create_support_ticket,而不是 get_gift_ideas。把 follow‑up 當作文字,能讓模型自由切換工具。

因此,本模組的實務準則是:在 UI 點擊後,多數情況應發送文字型的 follow‑up,而非直接呼叫 tool。從小工具直接呼叫工具則保留給確定不需要在歷史中新增使用者一步的特殊情況。

7. follow‑up 與狀態的串接:避免 UI 與文字不同步

有個常見問題是:UI「自己在跑」,而聊天文字「另有一套」。比方你在點擊時同時變更了小工具內的篩選器,並送出了 follow‑up。如果你只更新 UI,卻沒有透過 widgetState 固化新狀態,那下次 ChatGPT 重新渲染時會還原舊的 widgetState。結果就是小工具又回到舊篩選,但聊天歷史已經有了「更便宜」這個步驟——體驗會很詭異。

因此好做法是在點擊 follow‑up 時同時:

  1. 更新 widgetState
  2. 發送 follow‑up 訊息。

範例:

import { useWidgetState, useSendMessage } from '@/openai-apps';

type GiftWidgetState = {
  priceFilter?: 'any' | 'cheap' | 'premium';
};

export const GiftFollowups: React.FC = () => {
  const [widgetState, setWidgetState] = useWidgetState<GiftWidgetState>();
  const sendMessage = useSendMessage();

  const handleCheaper = () => {
    setWidgetState({ ...widgetState, priceFilter: 'cheap' });
    sendMessage('請顯示更便宜的禮物,大約 $50 以下');
  };

  // ...
};

現在 ChatGPT 與你的 UI 都知道篩選已經改變。若模型之後重建小工具,它會看到更新後的 widgetState,並能生成新的 pre‑text,例如「以下是更親民預算的選項」。

8. 設計好的 follow‑up

好的 follow‑up 是 UX 成功的一半。它不只是「漂亮」,更能幫使用者省下思考成本。

有幾個實務原則值得貼在螢幕旁。

第一,簡潔。Follow‑up 不是寫詩的地方。一句簡短、脫離 UI 也看得懂的語句通常最理想:「更便宜」、「只有高級」、「更換收禮人」。若需要很長的說明,想想是否該交給 GPT 的一般文字,而不是按鈕。

第二,行動導向。表述要讓人明白接下來會發生什麼。「更多點子」還行;「詳見第 2 點」最好換成「請更詳細介紹第二個選項」(就算沒有 UI,模型也能理解)。

第三,延續劇本,而不是重來。比起「重新挑選禮物」,更好的選項可能是「變更預算」或「更換收禮人的興趣」。Follow‑up 應該讓使用者前進或轉向,而不是沒必要地回到原點。

第四,數量要節制。小工具底下兩到四個按鈕通常就夠了。十個選項的面板會變成「選擇障礙考題」:使用者乾脆不按任何一個。

最後,要注意語氣。如果整個應用的語氣很親切,就不要突然丟一個「確認下單」的全大寫怒吼。Follow‑up 是與模型文字同一段對話的一部分;風格需要一致。

9. 「圍繞小工具的對話」:誰負責什麼

別把小工具當「主角」,而 ChatGPT 只是「外框」。事實上剛好相反:模型持續在帶對話,小工具只是呈現與微調資料的一種方式。

GiftGenius 的典型流程如下:

  1. 使用者:「需要一份送給從事 IT 的妹妹、不超過 $100 的禮物。」
  2. 模型:文字開場(pre‑text)——說明會開啟 GiftGenius 以及它會做什麼。
  3. 小工具:顯示點子清單,並提供 follow‑up
  4. 使用者:要嘛自己輸入訊息,要嘛按下 follow‑up 按鈕(例如「只顯示數位禮物」)。
  5. 模型:將其解讀為文字,呼叫所需的工具,必要時加上後置文字(post‑text)或再次顯示小工具。
  6. 如此循環,直到任務完成——包括確認選擇、下單等。

Follow‑up 是把這個迴圈每一圈都黏合起來的「膠」。沒有它們,小工具之後使用者就會懸在半空中:看起來很漂亮,但「接下來呢」卻不清楚。

在更複雜的場景(workflows、agents)中,follow‑up 有助於建模多步驟的漏斗:「先選擇收禮人」、「現在確認預算」、「接著我們來確認選擇」。但在本模組裡,只要看到:即便是最簡單的一步驟 App,只要加上幾個設計良好的提示,就能大幅改善體驗。

10. 實作:現在就可以做什麼

一個很好的練習是改進你目前的教學用小工具。

如果你已經有結果清單(例如禮物、飯店或文件),就在底下加一個小區塊「接下來要做什麼?」並提供兩到三個按鈕。盡量讓這些按鈕對應上面表格的幾種類型:一個用於細化(drill‑down,例如「更便宜」)、另一個用於轉向(pivot,「更換收禮人」),第三個(如果需要)用於導覽(navigation,「前往結帳」)。

在點擊處理器中,使用 useSendMessage 搭配自然語言的文字來發送訊息,必要時別忘了更新 widgetState。然後在 ChatGPT 中重新跑一次流程,看看對話的樣子:在小工具之後,下一步是否變得更清楚。

也可以刻意做幾個不好的 follow‑up:又長又模糊、一次十個選項——然後比較差異。這是快速體會體驗差距的方式。

11. 使用 follow‑up 時的常見錯誤

錯誤 1:沉默的小工具。
開發者做了很棒的 UI,卻完全沒有提供 follow‑up。使用者看到卡片,覺得「不錯」,但接下來得自己猜能不能要求「更便宜」或「更換收禮人」。大多數人不會主動想到,然後就離開了。至少在小工具底下提供一兩個「接下來」的提示,問題就能大幅緩解。

錯誤 2:按鈕太多。
另一個極端是用十幾個選項轟炸使用者:「變更預算」、「變更興趣」、「切換幣別」、「儲存挑選」、「分享給朋友」、「顯示類似」、「詢問客服」等等。這像是心理學上的吃到飽自助餐:很難做決定。先從兩到三個最常見的動作開始,其餘交給模型與一般文字。

錯誤 3:把對話邏輯放在前端而已。
有時候為了「最佳化」,工程師會不透過 follow‑upuseSendMessage(或較低階的 sendFollowUpMessage),而是在小工具內直接呼叫同一個 tool 來更新 UI。結果是聊天歷史對發生的事情隻字未提。幾步之後模型就開始混亂,你也會跟著混亂。正確作法是把對話邏輯放在文字與工具層,小工具只作為薄 UI 層。

錯誤 4:用詞不明或有歧義。
沒有脈絡的「更多」可能代表任何事:更多禮物、更多文字、更多預算?同樣地,像「重新計算」或「重建」的說法對模型與使用者都不夠清楚。最好的 follow‑up 是具體的:「顯示更多這個預算的選項」、「只顯示數位禮物」。

錯誤 5:UI 與文字不同步。
經典案例:點「更便宜」時你更新了 UI 篩選,卻沒有把 follow‑up 送到聊天,或沒有更新 widgetState。結果聊天歷史不包含變更預算的步驟,而下次渲染時篩選又「跳回去」。使用 setWidgetState + sendMessage 的組合,讓文字與 UI 同步前進。

錯誤 6:試圖控制 ChatGPT 的「原生」提示。
有些團隊指望 ChatGPT 自動產生需要的 follow‑up 晶片,於是不再提供自己的。但這些提示既不保證出現,也不受應用掌控。把它們當愉快的加分項,但務必提供你自己的、對體驗至關重要的 follow‑up 按鈕。

留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION