CodeGym /课程 /ChatGPT Apps /与 ChatGPT 的交互:Follow‑ups 与“围绕小部件的对话”

与 ChatGPT 的交互:Follow‑ups 与“围绕小部件的对话”

ChatGPT Apps
第 3 级 , 课程 3
可用

1. 什么是 ChatGPT App 中的 follow‑up,它有什么用

先用“人话”来定义,而不是营销术语。在 ChatGPT App 的上下文中,follow‑up 是程序发起的对话下一步。它通常是一个简短提示,以按钮形式呈现:当用户点击时,这个提示会变成聊天中一条新的来自用户的文本消息,从而让人与 AI 的对话继续推进。

从模型的角度看,follow‑up 就是另一条用户消息。没有魔法:当用户点击你提供的“显示更便宜的选项”按钮时,聊天历史会新增类似“请展示更便宜的礼物”的内容。模型把它视为普通的用户话语,应用 system‑prompt 和工具(tools)的描述,决定调用哪个工具,并可能再次以不同数据渲染你的小部件。

为什么这很重要:

  • 模型以文本为思维单位。 如果你在点击后直接调用 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: Pre-text: "我现在打开 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. 你的后端/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 里有 Hook:useWidgetPropsuseSendMessage。名称只是示意,但概念与参考实现一致:

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。保留从小部件直接调用 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. 用户:“我需要一份送给当程序员的妹妹的礼物,预算不超过 100 美元”。
  2. 模型:给出文本性开场(pre‑text)——解释将要打开 GiftGenius 以及它会做什么。
  3. 小部件:显示备选点子,并提供 follow‑up
  4. 用户:要么自己输入消息,要么点击 follow‑up 按钮(例如“仅显示数字礼物”)。
  5. 模型:将其视为文本,调用所需 tool,必要时给出注释性文本(post‑text),或再次显示小部件。
  6. 如此循环,直到任务解决——包括确认选择、下单等。

follow‑up 是串连每一轮循环的“胶水”。没有它,用户在看完小部件后会“悬着”:虽好看,却不知道“接下来怎么办”。

在更复杂的场景(工作流、代理)中,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