1. 引言
在一些 prompt 工程课程里,常会展示一个“魔法咒语”:
“不要编造事实;若无信息请直接说‘我不知道’。”
可惜(或许也庆幸)的是,这在 ChatGPT App 中并非银弹。原因很简单:模型不仅能看到你的 system‑prompt,还会看到:
- 工具清单及其描述与模式;
- 这些工具的返回结果;
- 对话历史,包括先前的 follow‑ups。
如果这三层——system‑prompt、工具描述、follow‑up 模式——互相矛盾或出现“空档”,模型就会像典型的初级同学:努力却爱发挥想象。
在课程中我们采用“三层反幻觉防护”(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。这样一来,若模型表现异常,你能立刻看到它运行的是哪一版指令,也就容易判断问题出在旧版提示还是新版提示。
示例:
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规定的是一种总体哲学。它不掌握每个工具的细节,却可以:
- 禁止编造目录外的礼物/价格/库存;
- 规定当工具返回空结果或错误时的行为;
- 要求明确区分礼品目录数据与模型的“通用知识”。
示例片段(在我们的 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)。
也就是说,工具描述并非“给人看的文档”,而是同样属于提示的一部分,只不过更形式化。很多幻觉恰恰源于这里。
以我们的工具 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 目录中挑选礼物。
当你需要基于收礼人的画像分段、预算、locale 与送礼场景
获取“真实存在的”礼物清单时,使用本工具。
该工具只返回 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:
"用户的语言/地区(locale),例如 'ru-RU' 或 'en-US'。",
minLength: 2
},
occasion: {
type: "string",
description:
"送礼场景:例如 'birthday'、'anniversary'、'new_year'。"
}
},
required: ["segments", "budget", "locale", "occasion"]
}
};
这里的 description 做了几件有用的事。
它明确指出工具只处理GiftGenius 礼品目录,任何具体礼物与价格必须来自工具返回。
它解释了何时使用该工具:当需要针对收礼人的参数获取具体礼物清单,而不是泛泛而谈。
它规定了空结果时的行为:不要编造,转入对话(稍后我们会在 follow‑ups 中固定此事)。
而 inputSchema 则帮助模型更可靠地从用户请求中提取实体:分段、预算、locale 与场景。为字段提供明确的结构与约束(如 min、max、固定长度的 currency)也能降低解析时奇怪组合与错误的概率。
还可以加入“反面”约束——不仅说明“何时调用”,也说明何时不应调用。比如,当请求明显是理论性的:
description: `
...
当用户只是提出关于礼物的泛化问题,
而不是为具体人选礼(例如“总体上新年有哪些流行礼物?”),
不要使用该工具。
此类情况下请直接在聊天中作答。
`.trim()
这样你就将工具描述与 system‑prompt 中“理论性 vs 实践性请求”的规则对齐了。
5. 第 3 层:作为 UX 与安全层的 follow‑ups
即便 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,一名基于我们应用礼品目录的选礼助手。
# 职责范围
- 你的任务是帮助用户从目录中挑选合适的礼物,
并解释每个选项的优缺点。
- 不要承诺“已经购买或已寄出礼物”——
你只负责挑选与比较。
购买与发码/链接由后端在用户明确同意后执行。
# 数据与限制
- 不要编造目录或工具中不存在的礼物。
- 不要虚构价格、礼物类型、库存或配送地区。
- 若工具未返回数据或发生错误,
不要猜测,要如实说明。
# 工具使用
- 对任何事实性数据(如 profile_to_segments、
recommend_gifts、get_gift 等工具获取的礼物列表、价格、类型、SKU、描述)
都要调用相应工具。
- 若问题是理论性的、并不要求为具体人选礼,
可直接在聊天中作答(无需调用工具)。
# 与工具交互后的对话
- 空结果:坦诚说明当前条件下未找到结果,
并建议调整 1–2 个参数。
- 结果过多:挑选 3–7 个最匹配的,
并解释筛选标准。
- 工具错误:不要编造,告知发生了技术故障,
建议稍后重试或简化请求。
`.trim();
现在模型明白:
- 何处是“礼品顾问”,何处是“后台”(在我们的工具之后);
- 哪些数据绝不可编造;
- 在常见的非理想情形下如何表现。
为 recommend_gifts 重写 description 与 Schema
增强了系统提示后,我们再打磨工具描述,按第 4 节的思路汇总成最终版本。
// config/tools.ts
export const recommendGiftsTool = {
name: "recommend_gifts",
description: `
在 GiftGenius 目录中挑选礼物。
在以下需求时使用本工具:
- 获取“真实存在的”礼物清单及其最新价格、类型
(digital/physical)与标签;
- 按兴趣分段、预算、locale 与送礼场景收窄选择范围。
不要使用本工具于:
- 仅为泛化的理论性提问,
且并未请求为特定人选礼;
- 为编造目录不存在的礼物。
若结果为空,切勿自行编造礼物,
而是将控制权交回对话(遵循 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: "用户 locale,例如 '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‑up 写在 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 里只写一句“不要幻觉”。
开发者在提示结尾写上“不要编造信息”,就觉得完事了。实际中模型依然会编造,因为工具描述与 follow‑ups 并未给出替代行为。缺少“替代行为”的明确规则(承认空结果、建议放宽筛选、提示错误)时,这类口号几乎无效。
错误 2:system‑prompt 与工具 description 自相矛盾。
在 system‑prompt 里你说“不要编造目录之外的礼物”,而在工具描述里却写“为用户挑选礼物,若找不到可自行推荐相似选项”。模型会在两套“真理”之间摇摆,而更具体的一方(通常是工具描述)往往占上风。应确保两层说法一致;如果允许“相似选项”,也要形式化(并向用户明确这不是精确匹配)。
错误 3:工具描述过于模糊。
诸如“该工具帮助用户解决问题”的描述几乎不给模型任何边界信息。结果是要么不使用,要么逮着什么都调用——当工具返回少或无结果时,模型再去“脑补”。一个好的 description 必须具备判别性:明确“做什么”以及“何时不应调用”。
错误 4:没有针对空与错误结果的策略。
开发者在后端细心返回 { items: [], status: "empty" },却没有告诉模型这意味着什么。于是模型看到空数组便想:“那我就给点通用建议吧。”此时 system‑prompt 缺少解释如何解读这些状态、以及应对用户说什么的部分。给空/错结果加上几条明确规则,往往能带来显著质量提升。
错误 5:只想在前端小部件层“治疗”幻觉。
有人会把希望寄托在前端:“如果列表为空,就显示占位,不让用户看到模型的文本回答。”这或许稍微改善 UX,但模型本身仍然“相信”虚构实体,并在后续对话里延续这种行为。正确做法是先改指令(system‑prompt、工具描述、follow‑ups),再在 UI 层补充保护。
错误 6:忽视元数据与模式对模型行为的影响。
一些开发者把 JSON Schema 与字段描述当成“只用于表单与校验”。实际上,对 ChatGPT 而言,这些是提示的重要组成部分:模型据此理解应从请求中提取哪些参数、以及正确的响应该长什么样。薄弱或不一致的字段描述(description、enum)会提高出错概率,并间接诱发幻觉。
错误 7:只有“禁止”,没有“替代路径”。
有时提示变成一串“不要这样、不要那样”,却没说在复杂情形下该做什么。比如我们禁止编造礼物、把自己限制在目录内,但没有对理论性问题给出任何指引。结果模型有时就只会说“我不知道”,明明它本可以解释选礼的一般原则。务必同时提供可行路径:例如“如果目录中找不到礼物——请坦诚说明并建议如何调整请求”;或“如果问题是理论性的——请直接回答,无需调用工具”。
GO TO FULL VERSION