CodeGym /課程 /ChatGPT Apps /串接 system‑prompt、t...

串接 system‑prompt、tool descriptions 與 follow‑ups:對抗幻覺

ChatGPT Apps
等級 5 , 課堂 2
開放

1. 前言

在 prompt 工程的課程裡,有時會展示一個魔法咒語:

「不要編造事實,若沒有資訊就說『我不知道』。」

可惜(或許也幸運的是),對 ChatGPT App 來說,這並不是銀彈。原因很簡單:模型看到的不只是你的 system‑prompt,還包括:

  • 工具清單及其描述與結構;
  • 這些工具的結果;
  • 對話歷史,包括先前的 follow‑ups。

如果這三層——system‑prompt、工具描述、follow‑up 模式——彼此矛盾或彼此脫節,模型就會像典型的 junior 一樣:很勤奮,但容易編故事。

在本課程中我們使用「三層防護」(defense in depth)這個想法:

  • 1 級——在 system‑prompt 中的全域規則;
  • 2 級——在每個工具定義中的區域性限制;
  • 3 級——錯誤/空結果的處理與 follow‑ups。

在本講我們會把三層整合為一份一致的契約,套用到我們的教學應用 GiftGenius

Insight

在 ChatGPT Apps 的新架構中,system‑prompt 欄位已消失——形式上不復存在。但這不代表你要放棄對模型的全域指示。有個技巧:對 ChatGPT 而言,工具的描述就是提示文字,和傳統的 system 一樣。正是透過它,你可以把應用的「腦內韌體」放回去。

建立一個服務型工具,例如 aboutabout_app。它不需要經常被呼叫,但它的 description 會與其他工具一樣被模型閱讀。因此可以把完整的 system‑prompt 嵌入描述中。最好在工具本身的簡短技術說明之後再放它。結果是:模型在一開始就能取得你的 system‑prompt——並把它套用到後續的對話與工具呼叫。

為了便於維護,建議在 system‑prompt 開頭加入明確版本,例如 SYSTEM_PROMPT_VERSION: v3。這讓你能快速定位模型為何行為不同:你會立刻看到它當下使用的是哪一版指示。如果在正式環境出現怪異行為,也能判斷是舊版 prompt 還是新版 prompt 導致。

範例:

tool description 

### Global assistant behavior for the entire GiftGenius App (ver 3.01)
*(system-level guidelines, not user-facing text)*

system prompt text

2. ChatGPT App 會出現哪些幻覺(以禮物目錄為例)

要對症下藥,先得把病名說清楚。在目錄情境(禮物、商品、資費)中,最常見的幻覺類型如下。

其一,憑空捏造的目錄項目。使用者要求「特定服務的一年期數位訂閱禮券」或非常冷門的禮物,這些都不在你的 feed 裡,而模型為了「有用」,愉快地杜撰了「Super Space Flight 3000 — 太空飛行」,而這在 GiftGenius 的資料庫裡根本不存在。

其二,虛構屬性。目錄裡確實有該禮物,但模型會「美化」現實:更改價格、禮物類型(數位 vs 實體)、是否能運送到用戶所在國,或是禮券有效期,因為這樣「比較合理」或「聽起來更好」。

其三,幻覺式動作。模型聲稱「我已經幫你買了這個禮物,並把代碼寄到你的 e‑mail,已從卡片扣款 $49」,但你的 GiftGenius backend 根本沒有嘗試購買,也沒有啟動任何 ACP/Stripe 流程。

最後是組合情況:工具回傳空清單(在這些篩選與預算下沒有禮物),而模型為了不讓使用者失望,編了幾個「大概的選項」,卻沒說明它們其實不在目錄中。

我們的目標是讓模型在這些情境下:

  • 坦承地表示目錄裡沒有精確匹配
  • 不要捏造新禮物,也不要更改欄位值;
  • 向使用者解釋發生了什麼,並提出清楚的下一步。

而且不是靠在任意地方貼上「咒語」,而是透過一份連貫的契約:system‑prompttoolsfollow‑ups

3. 第 1 層:強化 system‑prompt 對抗幻覺

要阻止上一節提到的捏造禮物、屬性與動作,我們先強化最上層——system‑prompt:為模型設定在目錄情境中的整體「行為哲學」。

你在本模組的第一堂課已經寫過一個基礎的 system‑prompt,例如:「你是 GiftGenius,你幫忙挑選禮物……」,並明確界定了助理的責任邊界與工具的使用方式。

現在我們加入明確的反幻覺規則

邏輯是這樣:system‑prompt設定的是整體哲學。它不清楚每個工具的細節,但可以:

  • 禁止捏造不在目錄內的禮物/價格/存貨;
  • 規定當工具回傳空結果或錯誤時的行為;
  • 要求清楚區分禮物目錄的資料與任何「模型的一般知識」。

以下是 system‑prompt 片段範例(我們的 Next.js 專案中的 TypeScript 常數,例如 config/systemPrompt.ts):

// config/systemPrompt.ts
export const SYSTEM_PROMPT = `
# 角色
你是 GiftGenius,一名根據我們應用的禮物目錄
來協助挑選禮物的助理。

# 資料與限制
- 不要捏造目錄或工具回傳中沒有的禮物。
- 不要虛構價格、存貨、禮物類型(數位/實體)、
  配送地區或其他屬性。
- 若工具沒有回傳所需資料或發生錯誤,
  要誠實告知,不要嘗試猜測。

# 工具使用
- 任何事關事實的資訊(禮物清單、價格、類型、可用性、SKU)
  都要使用禮物目錄相關的工具。
- 若工具回傳空結果,就說在目前條件下找不到合適的禮物,
  並提出放寬條件的選項(調整預算、分類、禮物類型)。
`;

這裡有幾個重點。

首先,我們分離「模型的一般知識」與「應用資料」。模型仍可解釋數位與實體禮物的差異或常見送禮場合,但任何具體的禮物、價格與 SKU 都只能來自 GiftGenius 的工具。

其次,我們明確描述遇到錯誤/空結果時要怎麼做:不要沉默、不要「創作」,而是坦白告知使用者沒找到,並建議調整參數。

第三,我們聚焦於與我們領域相關的具體行為(禮物目錄與購買數位/實體 SKU),而不是抽象的「不要出現幻覺」。

這一層本身就很有幫助,但也很容易被不嚴謹的工具描述「蓋過」。接下來談工具描述。

4. 第 2 層:工具描述(tool descriptions)與結構作為契約的正式部分

模型決定何時以及如何呼叫你的工具,主要取決於:

  • 工具名稱;
  • 工具的 description
  • inputSchema / outputSchema(描述欄位的 JSON Schema)。

也就是說,工具描述(tool description)不是「給人看的文件」,而是同樣屬於提示的一部分,且更形式化。許多幻覺正是源於此。

想像我們的工具 recommend_gifts,後端實作為從 GiftGenius 目錄裡挑選禮物。

糟糕的描述可能長這樣:

// 不佳:過於模糊
const recommendGiftsTool = {
  name: "recommend_gifts",
  description: "為使用者推薦禮物",
  inputSchema: {
    type: "object",
    properties: {
      profile: { type: "string" }
    }
  }
};

形式上看似正確,但模型無法從中理解:

  • 工具的邊界在哪;
  • 若找不到禮物該怎麼辦;
  • 不可以捏造目錄之外的禮物與價格。

好的描述要一次做到幾件事:明確界定領域、說清楚何時呼叫工具、嚴格規定不能杜撰結果。

範例(配合 GiftGenius 契約中的 segmentsbudgetlocaleoccasion):

// config/tools.ts
export const recommendGiftsTool = {
  name: "recommend_gifts",
  description: `
在 GiftGenius 目錄中挑選禮物。

當需要根據收禮者的個人輪廓分段、預算、在地設定與送禮場合
取得「真實存在」的禮物清單時,使用本工具。
本工具只會回傳實際存在於 GiftGenius 目錄中的禮物。

不要捏造本工具結果之外的禮物與其屬性。
如果工具回傳空清單,請不要臆造替代方案,
而是回到對話:建議調整預算、禮物類型、
送禮場合或其他參數。
  `.trim(),
  inputSchema: {
    type: "object",
    properties: {
      segments: {
        type: "array",
        description:
          "收禮者個人輪廓的分段,例如 ['tech', 'fitness']。",
        items: { type: "string" }
      },
      budget: {
        type: "object",
        description: "禮物預算範圍。",
        properties: {
          min: {
            type: "number",
            description: "最低金額,不能為負。",
            minimum: 0
          },
          max: {
            type: "number",
            description: "最高金額,大於 0。",
            exclusiveMinimum: 0
          },
          currency: {
            type: "string",
            description: "三字母貨幣代碼,例如 'USD' 或 'RUB'。",
            minLength: 3,
            maxLength: 3
          }
        },
        required: ["min", "max", "currency"]
      },
      locale: {
        type: "string",
        description:
          "使用者在地設定(語言/地區格式),例如 'ru-RU' 或 'en-US'。",
        minLength: 2
      },
      occasion: {
        type: "string",
        description:
          "送禮場合:例如 'birthday'、'anniversary'、'new_year'。"
      }
    },
    required: ["segments", "budget", "locale", "occasion"]
  }
};

這段 description 做了幾件有用的事。

它明確指出工具只處理GiftGenius 禮物目錄,且任何具體的禮物與價格都必須僅來自工具結果。

它解釋了何時該使用工具:當需要針對收禮者參數挑選具體禮物,而不是討論送禮的通則時。

它規定了空結果時的行為:不要杜撰,而是回到對話(稍後我們會在 follow‑ups 進一步固定)。

inputSchema 則幫助模型更可靠地從使用者請求中抽取實體:分段、預算、在地設定與場合。對欄位給出明確結構與限制(minmax、長度固定的 currency)也能降低奇怪組合與解析錯誤的機率。

還可以補上反面條件——不只寫「何時要呼叫」,也寫何時不要呼叫。例如,當請求明顯是理論性的:

description: `
...
如果使用者提出的是關於送禮的通則問題,
而不是要為特定對象挑選禮物
(例如:「一般來說新年有哪些受歡迎的禮物?」),
就不要使用本工具。
這種情況請直接在聊天中回答。
`.trim()

如此你就讓工具描述與 system‑prompt 裡關於「理論型 vs 實務型」請求的規則對齊。

5. 第 3 層:follow‑ups 作為 UX 與安全層

即便 system‑prompt 與工具描述寫得再好,現實仍不完美:

  • 後端可能回傳錯誤;
  • 目錄可能是空清單;
  • 結果可能模稜兩可或數量過多。

若不明確規定呼叫工具後要說什麼,模型就會即興發揮:有時不錯,有時就會杜撰。

在第 2 講你已經看過基本的 UX 指示:模型如何宣告 App 啟動、如何收尾、以及在「完成」時對使用者說什麼。現在我們加入可降低幻覺的 follow‑up 模式

這些模式通常直接寫在 system‑prompt 的獨立區塊,例如「工具呼叫後的對話」。

片段範例:

// SYSTEM_PROMPT 延續
export const SYSTEM_PROMPT = `
# ... 前述章節 ...

# 工具呼叫後的對話

- 若禮物挑選工具回傳空清單:
  1) 坦誠說明在目前的篩選條件下沒有找到合適禮物;
  2) 建議使用者調整 1–2 個關鍵參數
     (預算、禮物類型、收禮者興趣、送禮場合)。

- 若工具回傳的選項過多:
  1) 挑出 3–7 個最相關的;
  2) 清楚說明你的篩選依據
     (興趣匹配、符合預算、評分)。

- 若工具發生錯誤:
  1) 不要捏造資料;
  2) 說明發生技術性錯誤,並建議
     稍後再試或簡化請求。
`.trim();

如此我們把示例性的 follow‑up 台詞「燒」進模型。它會用自己的語句來表達,但結構是一致的:

  • 先陳述事實(是空/太多/發生錯誤);
  • 坦承限制;
  • 給出合宜的下一步建議。

在禮物目錄情境中特別重要:當結果為空時,模型不會說「一切都好,這裡有三個禮物」,而會改說:

「依你目前的條件(與太空相關的數位禮物,預算 $5,只限美國配送),我們的目錄裡沒有相符的項目。要不要我試著提高預算,或改推薦其他分類?」

請注意:這已經不是工具程式碼,而是提示中的指示,用來設定期望的 UX。

6. 串起來:我們的 GiftGenius 演進

我們來看看應用的小演進,逐步降低幻覺程度。

初始版本:問題在哪裡

假設我們的 system‑prompt 非常極簡:

export const SYSTEM_PROMPT = `
你是禮物挑選助理。
幫助使用者找到合適的靈感。
`;

工具的描述是這樣:

export const recommendGiftsTool = {
  name: "recommend_gifts",
  description: "為使用者推薦禮物",
  inputSchema: { type: "object" }
};

沒有任何 follow‑up 指示。

實際上會發生什麼:

  • 若使用者要求「送給玩家朋友的數位禮物,預算不超過 $10」,而資料庫在這些條件下找不到任何東西,模型可能:
    • 乾脆不呼叫工具,直接從腦海中編幾個禮物;
    • 或呼叫了工具、得到空清單,卻不說明,仍然想像出幾個選項;
  • 若後端回傳錯誤,模型可能自認「總該有點什麼」,開始猜測。

這就形成經典狀況:聊天裡答案很漂亮,資料庫裡卻沒有半點影子

新版 system‑prompt

system‑prompt 重寫,納入三層防護。部分內容你在上文已看到,這裡把它完整整理:

// config/systemPrompt.ts
export const SYSTEM_PROMPT = `
# 角色
你是 GiftGenius,一名根據我們應用的禮物目錄
來協助挑選禮物的助理。

# 責任範圍
- 你的任務是:從目錄中幫使用者挑選合適的禮物,
  並解釋各選項的優缺點。
- 不要承諾實際購買或寄送禮物——
  你只負責協助挑選與比較。
  當使用者明確同意後,購買與代碼/連結的發放由 backend 負責。

# 資料與限制
- 不要捏造在目錄或工具回傳中不存在的禮物。
- 不要虛構價格、禮物類型、存貨或配送地區。
- 若工具沒有回傳資料或發生錯誤,
  不要猜測,要據實以告。

# 工具使用
- 任何事關事實的資訊(例如 profile_to_segments、
  recommend_gifts、get_gift;以及禮物清單、價格、類型、SKU、描述)
  都請使用相關工具。
- 若問題是理論性的且不需要為特定對象挑選禮物,
  請直接在聊天中回答(不必呼叫工具)。

# 工具呼叫後的對話
- 若結果為空:坦誠說明目前條件下沒有結果,
  並建議調整 1–2 個參數。
- 若結果過多:挑出 3–7 個最合適的,
  並解釋你的挑選依據。
- 若工具錯誤:不要捏造資料,為中斷致歉,
  建議重試或簡化請求。
`.trim();

現在模型會更清楚地知道:

  • 它在送禮諮詢與「後台」之間的分工(後台在我們的工具裡);
  • 哪些資料絕對不能捏造;
  • 在典型的非理想情況中該如何行動。

recommend_gifts 的新版 description 與結構

我們已加強系統提示,接著把工具也打磨好,根據第 4 節的想法組裝最終版描述。

// config/tools.ts
export const recommendGiftsTool = {
  name: "recommend_gifts",
  description: `
在 GiftGenius 目錄中挑選禮物。

當你需要:
- 取得真實存在的禮物清單與即時價格、類型
  (digital/physical)與標籤;
- 根據興趣分段、預算、在地設定與送禮場合縮小選擇範圍,
就使用本工具。

不要使用本工具的情況:
- 使用者只是在問送禮的通則問題,
  而非要替特定人選擇禮物;
- 用於捏造目錄中不存在的禮物。

若結果為空,請不要自行捏造禮物,
而是把控制權交回對話(遵循 system‑prompt 的指示)。
  `.trim(),
  inputSchema: {
    type: "object",
    properties: {
      segments: {
        type: "array",
        description:
          "收禮者的興趣分段:例如 'tech'、'sport'、'books'。",
        items: { type: "string" }
      },
      budget: {
        type: "object",
        description:
          "以使用者貨幣表示的禮物預算區間(min/max)。",
        properties: {
          min: { type: "number", minimum: 0 },
          max: { type: "number", exclusiveMinimum: 0 },
          currency: {
            type: "string",
            minLength: 3,
            maxLength: 3,
            description: "ISO 4217 貨幣代碼,例如 'USD' 或 'RUB'。"
          }
        },
        required: ["min", "max", "currency"]
      },
      locale: {
        type: "string",
        description: "使用者在地設定,例如 'ru-RU' 或 'en-US'。"
      },
      occasion: {
        type: "string",
        description:
          "送禮場合:'birthday'、'anniversary'、'new_year' 等。"
      }
    },
    required: ["segments", "budget", "locale", "occasion"]
  }
};

這裡有幾個細節。

我們把工具行為與 system‑prompt 明確連結起來:「遵循 system‑prompt 的指示」這句話會提醒模型,「空結果 = 誠實對話,而不是創作」。

我們加入了負面條件(「不要使用……」、「不要捏造……」),實務上這往往與正面描述同等重要。

我們讓 inputSchema 更有語意:清楚的描述與限制有助模型把請求正確對齊到欄位,甚至在呼叫工具之前就減少「犯錯」的機率。

在元件與回傳格式中的 follow‑up 模式

除了文字指示,還有另一個槓桿——工具回傳格式本身。透過格式也能提示模型剛發生了什麼,進一步縮小發揮空間。

形式上,follow‑ups 是寫在 system‑prompt 的文字,但在你的 Next.js 元件中,還能另外正規化 ToolOutput,讓模型更省事,也更不容易胡亂發揮。

例如,約定後端對 recommend_gifts 一律回傳:

// 後端的工具回傳型別
export type RecommendGiftsResult = {
  items: Array<{
    id: string;
    title: string;
    price: number;
    currency: "USD" | "EUR" | "RUB";
    tags: ("digital" | "physical" | "education" | "fitness" | "tech")[];
  }>;
  // 由後端填寫,用來明確告知模型實際情況
  status: "ok" | "empty" | "error";
  errorMessage?: string;
};

元件可以把這些結果好好呈現,而模型在組織回答時也能依據 status。在 Apps SDK 中,你通常會把 ToolOutput 以 JSON 物件回傳給模型,它能看到這個欄位。

system‑prompt 中可以再加上一個小區塊:

# 工具狀態的解讀

- 當 status = "empty":請參照「工具呼叫後的對話」章節,
  不要捏造禮物。
- 當 status = "error":請說明技術性錯誤,不要嘗試猜測
  目錄內容。

是的,模型或許也能自己猜到,但明確的指示能降低它憑感覺「亂猜」的機率。

7. 實作:打磨你的 App

為了避免「看起來只是在投影片上很漂亮」的感覺,以下是一個你可以在現有 App(我們的例子是 GiftGenius)裡立刻動手做的練習。我們針對三層防護做一點小重構:system‑prompt、工具描述與結果處理。

首先,system‑prompt,把第一講的 system‑prompt 打開,找到描述責任範圍與工具使用的部分,加入:

  • 禁止在目錄/資料庫之外捏造實體(禮物、SKU、價格);
  • 面對空結果與工具錯誤時的規則;
  • 「工具呼叫後的對話」章節,含 2–3 種情境(空、太多、錯誤)。

其次,在工具描述層,打開你的關鍵工具描述(你這裡可能是 recommend_giftssearch_giftssearch_tariffscalculate_quote,不拘)。重寫 description,讓它:

  • 明說工具使用你的資料來源(禮物目錄、資費等);
  • 解釋何時需要它,何時不需要;
  • 包含明確的負面限制:「不要捏造……」、「不要用於……」。

第三,在工具回傳結構層,若你的工具回傳尚未包含描述狀態的欄位(statusresultTypehasMore)——請在後端型別與 ToolOutput 中加入。接著在 system‑prompt 中寫明模型應如何在對話中解讀這些狀態。

最後,在 Dev Mode 跑幾個請求,其中包含明知為空或邊界情境的案例。留意模型是否不再捏造實體,以及它向使用者誠實說明限制的程度。

下一講你會把這些請求形式化為所謂的 golden prompt set,並把它變成可重複的測試產物。不過現在對我們而言,更重要的是你能親手感受到差異。

8. 透過提示與工具對抗幻覺時的常見錯誤

錯誤 №1:在 system‑prompt 中只放一句「不要產生幻覺」。
開發者在 prompt 結尾寫了「不要捏造資訊」,就覺得任務完成。實務上模型仍會亂編,因為工具描述與 follow‑ups 並沒有告訴它「不捏造時該怎麼辦」。缺少具體替代行為(承認空結果、建議調整篩選、告知錯誤)時,這句話幾乎幫不上忙。

錯誤 №2:system‑prompt 與工具 description 互相矛盾。
你在 system‑prompt 裡說:「不要捏造目錄裡沒有的禮物」,但在工具描述裡卻寫:「為使用者挑選合適禮物,若找不到——可以自行推薦類似選項」。最後模型在兩套規則間搖擺,而較具體的一方(通常是工具 description)會勝出。兩層必須說同一件事:若允許提供類似選項,也要形式化,並且務必向使用者說明那不是精確匹配。

錯誤 №3:工具描述過於寬泛。
像「協助使用者解決問題的工具」這樣的描述幾乎沒有提供工具邊界資訊。模型可能根本不會用它,或逮到機會就亂叫——當工具回傳很少或沒有時,再「補上想像」。好的 description 應具辨識力:清楚說明工具做什麼以及何時不該呼叫。

錯誤 №4:沒有處理空結果與錯誤的策略。
開發者很貼心地讓後端回傳 { items: [], status: "empty" },卻沒在任何地方解釋模型該如何理解它。於是模型看到空陣列就認定「那我用一般知識補幾個吧」。system‑prompt 缺少解釋這類狀態與告知使用者的章節。其實只要加幾條關於空/錯誤結果的清楚規則,品質就會大幅提升。

錯誤 №5:只想靠前端元件程式碼「醫治」幻覺。
有時會想把一切都丟給前端:「如果清單為空——我就顯示占位畫面,不讓使用者看到模型的文字回覆」。這或許能稍微改善 UX,但模型本身仍然相信那些捏造的實體,也會在後續對話中延續這種行為。正確做法是先修改指示system‑prompt、工具描述、follow‑ups),再用 UI 防護來補強。

錯誤 №6:忽略中繼資料與結構對模型行為的影響。
有些開發者把 JSON Schema 與欄位描述當成「只為了表單與驗證」。事實上,對 ChatGPT 而言它們是提示的重要部分:模型會依此理解需要從請求中抽取哪些參數,以及正確的回覆長什麼樣。薄弱或不一致的欄位描述(descriptionenum)會提高出錯機率,並間接引發幻覺。

錯誤 №7:只有嚴格禁止,沒有替代路徑。
有時 prompt 會變成一長串「不要做這個、不要做那個」,但沒有說在困難情況下該如何處理。比如,我們禁止捏造、只允許使用目錄,卻沒提理論性問題該怎麼辦。結果模型有時只會回答「我不知道」,明明它可以提供有用的送禮原則。務必同時給出允許的路徑:例如「若找不到目錄中的禮物——坦誠說明並提議如何調整請求」,或「若是理論性問題——不必用工具,直接在聊天中回答」。

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