CodeGym /课程 /ChatGPT Apps /Multi‑App 场景与 Apps 组合

Multi‑App 场景与 Apps 组合

ChatGPT Apps
第 20 级 , 课程 4
可用

1. 什么是 Multi‑App 场景,以及为什么你需要它们

到目前为止,我们把 GiftGenius 视为某个对话里的唯一外部应用:用户在列表中选择你的 App,ChatGPT 挂载你的 tools,而你就是这段故事里的“主角”。但在真实的 Store 里并非如此:用户可以同时连接多个 Apps,ChatGPT 会根据具体请求决定要调用哪一个应用。

例如,在同一个对话里可能同时存在:

  • 企业日历 App,知道同事的生日;
  • GiftGenius,负责生成礼物创意;
  • 公司的 commerce‑App,负责下单与支付。

用户写道:“提醒我同事们的生日,并立刻给出礼物建议以及如何购买。” 模型可能会按顺序调用三个不同的 App:一个去查日历,第二个生成礼物创意,第三个完成结账。

重要的是要理解:用户并没有一个“请调用 App №2,并且这是它的 HTTP endpoint”的按钮。用户用自然语言交流,而 ChatGPT 充当路由器——读取所有可用应用的 descriptions 与元数据,并决定何时调用谁。

由此得出三点关键结论:

  • 存在对上下文的竞争。你的 App 需要在几十个应用中凭借名称、描述与行为被选中。
  • 元数据成为你的“LLM 的 SEO”——它们决定了模型是否会在关键时刻想起 GiftGenius,还是将其忽略。
  • 需要考虑互操作性:你的回答不仅要对聊天中的人有用,也要对阅读同一上下文的其他 App 有用。

本质上,我们把一个孤立的 ChatGPT App 转变为更大系统的组件。

2. 模型如何选择 App:路由的心智模型

在 Multi‑App 场景中,路由大致是这样运作的(大幅简化,但对开发很有用):

  1. ChatGPT 有一份可用 App 及其 tools 的清单与元数据(名字、描述、参数的 JSON Schema、注解与 _meta)。
  2. 用户发送消息。
  3. 模型建立内部的意图(intent)表示,并对应用及其 tools 的 descriptions 做语义搜索,以判断哪些工具合适。
  4. 如果命中条件——就调用某个 tool,或建议打开某个 App。

这里有个关键点:descriptions需要足够可区分(具有判别性)。“商品搜索”和“礼物搜索”“图书搜索”并没有显著区别,而“基于 GiftGenius 合作伙伴库的礼物创意搜索”则大幅收窄了域,显著提升你的工具在礼物请求场景下被选中的概率。

第二点——避免命名冲突。一个名为 get_data 的工具在几十个 App 的世界中毫无信息量,而 giftgenius_get_gift_catalog 则清晰得多,尤其当它配合明确的 description 时。

最后,模型会依赖对话上下文:如果聊天里已经出现了“礼物”“生日”甚至 GiftGenius 这个名称,这会在路由器眼中“高亮”你的 App。

3. 元数据与 descriptions 作为 LLM‑SEO

不要把元数据仅仅当作“JSON 里必须填的表格”,更应将其视为产品文案工作。官方建议明确写道:treat metadata like product copy,并按照 “one job per tool” 去设计。

粗略地可以将描述分为几个层级:

层级 谁会读 描述什么
Manifest description 人类 + 模型 整个 App 的任务:为什么要把它接入聊天
Tool description 模型(routing) 何时使用该 tool,以及用于哪些任务
Parameter descriptions 模型(slot fill) 如何填写参数、可接受的值
_meta["openai/widgetDescription"]
模型(UI) 小部件中会展示什么,以及是否需要由模型以文本重复这些信息

在小部件世界里,widgetDescription尤为重要:模型并“不看见”你的 React 代码,它只知道你会提供哪些 props、意图为何。写好这个字段能让模型不必替你“编”,而是配合 UI,对文本回答做相应的取舍与适配。

Apps SDK 的文档强调:ChatGPT 会基于元数据来决定何时、如何调用你的连接器(App)。打磨好的 descriptions 和参数说明能提升 recall(召回率)——即模型能想起你的 App 的场景占比——并减少误触发。

迷你示例:GiftGenius 的旧版 vs 新版 description

假设我们之前是这样的:

export const appDescription = `
GiftGenius — 用于查找和购买礼物的助手。
`;

从人的角度看还不错,但在 Multi‑App 世界的路由里,更好的是强调何时使用该 App,以及它不做什么:

export const appDescription = `
GiftGenius — 礼物创意助手。
当用户请你为特定的人或场合提供礼物点子,并且要控制预算时,使用这个应用。
不要将它用于泛化的网购或个人财务规划。
`;

现在,模型更容易把 GiftGenius 与通用 e‑commerce App 或理财顾问区分开。

4. _meta["openai/widgetDescription"]:向模型“讲清楚”我们的 UI

在上面的表格中我们单独提到了 _meta["openai/widgetDescription"]。现在聚焦在这一层:它帮助模型“想象”你的小部件,理解答案的哪些部分已由 UI 覆盖,哪些需要用文本表达。

假设我们的核心工具 suggest_gifts 返回礼物列表,而小部件将其渲染为水平滚动的卡片轮播。我们已在工具描述中说明了何时使用它,而在 widgetDescription 中则说明结果长什么样

工具描述(descriptor)的片段示例(简化、根据官方推荐改写):

const suggestGiftsTool = {
  name: "suggest_gifts",
  description: "Use this to generate gift ideas within user's budget.",
  inputSchema: { /* ... */ },
  _meta: {
    "openai/widgetDescription":
      "展示一组横向滚动的礼物卡片,包含价格与“购买”按钮。不要在文本回答里重复礼物名称。"
  }
};

我们一下达成了多重目的:

  • 模型知道 UI 已显示名称和价格——因此文本回答可以专注于解释与建议,而不是重复列表。
  • 其他 Apps(通过模型)也能理解 toolOutput 并非一段文本,而是可被“接力”的结构化列表。

是的,坦白讲:写这些描述不如写代码有趣,但它们能为你省下大量排查“模型奇怪行为”的时间。

5. 工具注解:readOnlyHintdestructiveHintopenWorldHint

在 Multi‑App 世界里,重要的不仅是“何时调用”,还有调用的安全性。为此,Apps SDK 在工具描述里引入了一组注解。

理念是:注解为模型提供关于操作性质的柔性提示。它们不能替代服务端鉴权,但会显著影响 ChatGPT 在链路中的行为。

简要概念性总结:

注解 含义 模型的典型行为
readOnlyHint
不改变数据 可频繁调用,无需额外确认
destructiveHint / isConsequential
会改变状态(购买、删除) 调用前征求用户确认
openWorldHint
访问“外部世界”(搜索、Web) 在结果的体量与质量上更谨慎

这些注解(readOnlyHintdestructiveHintopenWorldHint)是工具描述的标准部分,ChatGPT 之外也可使用。字段 _meta["openai/isConsequential"] 则是更窄的、特定于 ChatGPT 的信号,用于进一步帮助模型区分“安全的”与“有后果的”调用。

来看 GiftGenius 的两个工具:

  • suggest_gifts —— 读取目录,安全(只读)。
  • create_checkout_session —— 创建结账,是显式的副作用操作。

工具 suggest_gifts 的描述示例

const suggestGiftsTool = {
  name: "suggest_gifts",
  description:
    "Use this when the user asks for gift ideas for a person or occasion.",
  inputSchema: { /* ... */ },
  annotations: {
    readOnlyHint: true
  },
  _meta: {
    "openai/widgetDescription": "带价格与链接的礼物卡片轮播。",
    "openai/isConsequential": false
  }
};

这样的工具,模型可以连续多次调用,甚至“前置地”准备若干方案,而无需对每一步都向用户确认。

工具 create_checkout_session 的描述示例

const createCheckoutTool = {
  name: "create_checkout_session",
  description:
    "Finalize purchase of selected gifts via Instant Checkout.",
  inputSchema: { /* ... */ },
  annotations: {
    destructiveHint: true
  },
  _meta: {
    "openai/isConsequential": true
  }
};

这里我们明确发出信号:这是写操作,具有后果(扣款、创建订单),在长链路且涉及多个 App 的情况下,模型应当在调用前请求用户确认。

不要高估“魔法”:即使有 destructiveHint,你仍必须在服务端复核输入、令牌与权限,正如我们在安全与授权模块中所讨论的。但从 Multi‑App 编排的角度看,注解能帮助模型避免不必要地“触发”此类工具。

6. 单体 App vs 生态系统:收紧 GiftGenius 的边界

当 GiftGenius 是对话里唯一的 App 时,你可以容忍一个较宽的 scope:挑礼物、包装建议、节日提醒,甚至写几句祝福语。模型反正只会调用你的工具。

在 Multi‑App 场景里,这种“我无所不能”的做法开始有害

  • 路由器更难区分你何时是“最佳人选”,何时该用别的 App;
  • 你会与日历、通用任务管理器、理财规划等发生重叠;
  • 在多个应用并用时,模型可能会选错执行者,并在工具之间产生混淆。

更好的做法——清晰划定职责范围:

  • GiftGenius:只做礼物创意 + 通过 ACP/Checkout 协助购买;
  • CalendarApp:事件与提醒;
  • Finance‑App:用户整体预算与个人财务规划。

在 App 与工具的描述中,明确写出 “Use this when…” 以及 “Do not use when…” 很有帮助。官方的 discovery playbook 也正是这么建议的。

工具描述的迷你示例:

description: `
Use this tool when the user explicitly asks for gift suggestions.
Do not use for generic product discovery or price comparison.
`

这些限制不仅帮助路由,也使你的 App 在产品与 QA 层面更可预测。

7. Apps 的组合模式:pipeline、handoff、shared context

在 Multi‑App 场景中,实践里会浮现三个相关理念:

  • pipeline —— 多个 App 依次执行(日历 → 礼物 → commerce),各司其职;
  • handoff —— 一个 App 的输出成为下一个的输入;
  • shared context —— 整个传递通过共享的聊天上下文进行,而非 Apps 之间的直接 HTTP 调用。

正如我们所暗示的,Multi‑App 场景不是“App A 通过 HTTP 调用 App B”的魔法。在当前实现中,ChatGPT Apps 的隔离相当严格:应用不会彼此直接调用,通信通过共享的文本上下文进行。

可以将基础模式概括为:

  1. App A 在聊天中返回文本或 JSON(常见于 structuredContent/小部件中)。
  2. 模型读取该输出。
  3. 下一步中,它可能调用 App B,并将 A 的输出细节填入 B 的工具参数。

这被称为 text/context handoff:“App A 的输出 → 模型 → App B 的输入”。

示例:CalendarApp + GiftGenius + CommerceApp

来看一个具体场景。

用户:“老板明天过生日,帮我挑个礼物并直接下单。”

分步说明:

  1. 模型先要弄清日期与对象。它调用日历 App 的工具,假设为 corporate_calendar.list_upcoming_birthdays,并获得如下结构:

    [
      { "name": "Alexey Bykov", "date": "2025-11-22", "relation": "manager" }
    ]
    
  2. 接着模型决定该调用 GiftGenius。它用从日历得到的参数来调用你的 suggest_gifts

    {
      "recipientName": "Alexey",
      "occasion": "birthday",
      "budget": 150,
      "relationship": "manager"
    }
    

    GiftGenius 小部件展示礼物轮播,文本回答解释这些想法为何合适。

  3. 用户在小部件中选择一两个候选(按钮 → widgetState),然后模型调用 commerce‑App 的工具,例如 corp_checkout.create_gift_order,携带所选 SKU 的 ID 和收货地址。

在 ChatGPT 看来这是三个不同的应用,但对用户来说是一段连续对话。让它顺畅运作的关键:

  • 每个 App 的工具都要有清晰的 descriptions
  • 严谨的命名(corporate_calendar.list_upcoming_birthdays,而不是 list_events);
  • 一致的结构化数据格式(让礼物创意以 commerce‑App 可理解的方式描述)。

可视化示意

这个 pipeline 可以这样画:

sequenceDiagram
    participant U as 用户
    participant C as ChatGPT (路由器)
    participant Cal as CalendarApp
    participant G as GiftGenius
    participant Com as CommerceApp

    U->>C: 老板明天过生日,挑一个并下单
    C->>Cal: tools.call(list_upcoming_birthdays)
    Cal-->>C: [{ name, date, relation }]
    C->>G: tools.call(suggest_gifts, { recipient, occasion, budget })
    G-->>C: gift suggestions (+ widget)
    C-->>U: 说明 + GiftGenius 小部件
    U->>C: 喜欢第 #2 个,买它
    C->>Com: tools.call(create_gift_order, { skuId, address })
    Com-->>C: Order confirmation
    C-->>U: 完成,订单已创建

作为 GiftGenius 的开发者,你的目标是让你的“声音”在这支合唱中清晰、恰到好处,而不是造成干扰。

8. 互操作性:让回答可被其他 Apps 利用

在 Multi‑App 世界,仅仅“直观地回答用户”不够。更理想的是,你的 toolOutput 也应当可被机器处理——例如 commerce‑App、分析代理或 workflow 编排器等。

这意味着两件实务上的事情:

  • 在工具输出中使用结构化 JSON,而不是“面向人类”的长句文本;
  • 尽量坚持稳定、清晰的字段命名。

例如,可以将 suggest_gifts 的结果类型化为:

export type GiftSuggestion = {
  id: string;
  title: string;
  description: string;
  price: number;
  currency: string;
  forPerson: string;
  occasion: string;
  purchaseUrl: string;
};

并在工具响应中返回此类对象的数组:

{
  "gifts": [
    {
      "id": "sku_123",
      "title": "桌面星空投影仪",
      "description": "迷你星空投影仪...",
      "price": 89.99,
      "currency": "USD",
      "forPerson": "阿列克谢",
      "occasion": "birthday",
      "purchaseUrl": "https://shop.example.com/sku_123"
    }
  ]
}

GiftGenius 小部件会将其作为 props 渲染卡片,而 commerce‑App 在上下文中看到该 JSON 后,可以直接拾取 idpurchaseUrl 继续结账。

Multi‑App 实践表明:一个好的 App 会以“他 App 能吃”的方式返回数据,而不仅是“人眼好看”。

9. 将 GiftGenius 面向 Multi‑App 的实用重构

把上述内容落到我们的教学应用上,归纳为几条具体改动。

明确 manifest‑description

假设我们在 openai-app.json(或 Next.js 模板中的等价文件)里有这样的描述:

{
  "name": "GiftGenius",
  "description": "Gift assistant for finding and buying presents."
}

我们把它改得更利于路由:

{
  "name": "GiftGenius",
  "description": "Assistant for gift ideas and purchase flows. Use this app when the user asks what to gift a specific person or for a specific occasion within a budget. Do not use for generic online shopping or personal finance planning."
}

现在这段文字已经清楚表明:它不是通用购物,不是理财顾问,也不是日历。

重写工具的 descriptions

礼物搜索工具:

const suggestGiftsTool = {
  name: "giftgenius_suggest_gifts",
  description: `
Use this when the user asks for gift ideas for a specific person or group,
optionally with a budget or occasion.
Do not use for non-gift product recommendations or travel booking.
`
};

从你的目录中拉取某个 SKU 详情(只读)的工具:

const getGiftDetailsTool = {
  name: "giftgenius_get_gift_details",
  description: `
Use this to fetch more details for a gift suggested earlier by GiftGenius,
for example when the user asks “tell me more about option #2”.
`,
  annotations: { readOnlyHint: true }
};

购买工具——如前所示,带上 destructiveHint

更新 _meta["openai/widgetDescription"]

假设我们的小部件已经有带 CTA“购买”的卡片。向模型提示这一点:

const giftWidgetMeta = {
  _meta: {
    "openai/widgetDescription": `
展示带描述、价格与“购买”按钮的礼物卡片列表。
模型不应在文本里重复完整清单,而应给出点评,帮助用户做决定。
`
  }
};

这样一来,如果礼物已经在小部件中可见,模型就不太会在聊天里铺陈十个礼物名字,而会把焦点放在解释与取舍上——这对 UX 与 token 成本都更友好。

10. 面向未来产品的 Multi‑App 思维

把思维从“如何击败所有竞争者、成为用户唯一的 App”切换到“如何让自己的 App 成为大型生态中的理想模块”,非常重要。

这种思路带来多项现实收益:

  • 你更容易向用户与 Store 审核者解释为什么需要你的 App、何时使用它;
  • 模型更容易做路由决策:更少混淆,更少“错误调用”;
  • 你可以有意识地设计组合:今天与日历和 commerce 搭配,明天与企业 HR 机器人或内部 CRM 协同。

官方的 discovery 指南强调:坚持 “one job per tool”,把元数据当作需要测试与更新的“活文档”,而不是初次提交后就一劳永逸的静态文本。

这方面,你在第 20 模块之前的内容已打下基础:golden 样例、LLM‑evals、CI 跑批。你可以把测试用例扩展到“聊天中同时存在 GiftGenius 与 CalendarApp”的场景,观察描述的调整如何影响 App 的选择与回答质量。

11. 使用 Multi‑App 与组合时的常见错误

错误 №1:App 描述“我啥都会”。
如果你在 manifest‑description 里写“智能助理,处理各种任务”,你不仅在与其他 App 竞争,也在与基础的 ChatGPT 竞争。路由器很难判断何时必须叫你、何时内建能力就够了。在 Multi‑App 世界里,获胜的是那些职责清晰、范围聚焦的应用:“礼物挑选”“日历管理”“日志分析”。

错误 №2:工具 descriptions 模糊、命名冲突。
get_dataprocess_request 这种名字配上“处理用户数据”的解释,只会让模型更迷惑。在多个 App 共存的世界里,你很容易被调用到完全无关的领域。正确做法是将领域与动作绑定(giftgenius_get_gift_catalogcalendar.list_birthdays),并明确写出 “Use this when… / Do not use when…”。

错误 №3:忽略 _meta["openai/widgetDescription"]
开发者常常只填 description,至多把 _meta 当作本地化字段。结果就是模型不清楚小部件到底展示了什么:要么用文本重复 UI,要么反过来“承诺”一个你的小部件里压根没有的“价格表”。在 widgetDescription 写上几句话,能避免很多这样的误会。

错误 №4:缺少 readOnlyHint/destructiveHint 注解。
如果你的所有工具看起来都“中性”,模型就分不清哪些可以频繁调用而不必担心,哪些需要用户确认。在包含多个 App 的多步场景中,这尤其关键:可能会在没有明确人参与的情况下连续触发多个写操作。记得标注只读工具,并明确标识具有后果/破坏性的操作。

错误 №5:只面向人类的回答,没考虑其他 Apps。
从工具里返回“一行文字的礼物清单”很诱人,但这会让其他 App 难以使用你的结果。带有清晰字段的结构化 JSON(如 idpricecurrencypurchaseUrloccasion)能同时给 UI 与组合带来好处:模型可以不必做自然语言解析,直接把这些数据填入其他工具的参数。

错误 №6:试图在一个 App 内实现用户可能需要的一切。
有时会忍不住想:“既然我做了 GiftGenius,让它也管日历、群发邮件、做预算吧。” 在孤立的世界里或许还能接受,但在 Multi‑App 语境下你会变成一个与其他聚焦型 App 冲突的“全家桶”。更好的做法是与自己达成一致:我的 App 只做 X,并把它做到极致;其他是别人的职责。这样的设计会极大简化 UX 与生态发展。

错误 №7:没有在其他 App 的环境中测试行为。
开发者经常只在 Dev Mode 的“干净”聊天里测试自己的应用,那里没有其他 App。而在 Store 里,用户很可能连了十来个应用,其中有些在概念上与你重叠。别偷懒,搭建一个包含邻近 App(如日历、通用购物、理财)的测试场景,跑一遍 golden 样例:模型是否会在“礼物类请求”中正确选择 GiftGenius,是否会与其他参与者混淆?

1
调查/小测验
LLM-Apps: 新一代第 20 级,课程 4
不可用
LLM-Apps: 新一代
LLM-Apps: 新一代
评论
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION