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 一樣。正是透過它,你可以把應用的「腦內韌體」放回去。
建立一個服務型工具,例如 about 或 about_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‑prompt ↔ tools ↔ follow‑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 契約中的 segments、budget、locale、occasion):
// 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 則幫助模型更可靠地從使用者請求中抽取實體:分段、預算、在地設定與場合。對欄位給出明確結構與限制(min、max、長度固定的 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_gifts、search_gifts、search_tariffs、calculate_quote,不拘)。重寫 description,讓它:
- 明說工具只使用你的資料來源(禮物目錄、資費等);
- 解釋何時需要它,何時不需要;
- 包含明確的負面限制:「不要捏造……」、「不要用於……」。
第三,在工具回傳結構層,若你的工具回傳尚未包含描述狀態的欄位(status、resultType、hasMore)——請在後端型別與 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 而言它們是提示的重要部分:模型會依此理解需要從請求中抽取哪些參數,以及正確的回覆長什麼樣。薄弱或不一致的欄位描述(description、enum)會提高出錯機率,並間接引發幻覺。
錯誤 №7:只有嚴格禁止,沒有替代路徑。
有時 prompt 會變成一長串「不要做這個、不要做那個」,但沒有說在困難情況下該如何處理。比如,我們禁止捏造、只允許使用目錄,卻沒提理論性問題該怎麼辦。結果模型有時只會回答「我不知道」,明明它可以提供有用的送禮原則。務必同時給出允許的路徑:例如「若找不到目錄中的禮物——坦誠說明並提議如何調整請求」,或「若是理論性問題——不必用工具,直接在聊天中回答」。
GO TO FULL VERSION