CodeGym /课程 /ChatGPT Apps /system‑prompt、工具...

system‑prompt、工具描述和 follow‑ups 串联起来:对抗幻觉

ChatGPT Apps
第 5 级 , 课程 2
可用

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 一样。借此可以把应用的“脑内固件”放回去。

创建一个服务型工具,例如 aboutabout_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‑prompttoolsfollow‑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 契约,包含 segmentsbudgetlocaleoccasion):

// 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 与场景。为字段提供明确的结构与约束(如 minmax、固定长度的 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_giftssearch_giftssearch_tariffscalculate_quote 等)的描述,重写 description,使其:

  • 明确声明工具作用于你的数据源(礼品目录、资费等);
  • 说明何时需要它,何时不需要;
  • 包含显式的否定约束:“不要编造……”,“不要用于……”。

第三,在工具响应结构层,若尚无明确描述状态的字段(如 statusresultTypehasMore),请在后端类型与 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 而言,这些是提示的重要组成部分:模型据此理解应从请求中提取哪些参数、以及正确的响应该长什么样。薄弱或不一致的字段描述(descriptionenum)会提高出错概率,并间接诱发幻觉。

错误 7:只有“禁止”,没有“替代路径”。
有时提示变成一串“不要这样、不要那样”,却没说在复杂情形下该做什么。比如我们禁止编造礼物、把自己限制在目录内,但没有对理论性问题给出任何指引。结果模型有时就只会说“我不知道”,明明它本可以解释选礼的一般原则。务必同时提供可行路径:例如“如果目录中找不到礼物——请坦诚说明并建议如何调整请求”;或“如果问题是理论性的——请直接回答,无需调用工具”。

评论
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION