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 场景中,路由大致是这样运作的(大幅简化,但对开发很有用):
- ChatGPT 有一份可用 App 及其 tools 的清单与元数据(名字、描述、参数的 JSON Schema、注解与 _meta)。
- 用户发送消息。
- 模型建立内部的意图(intent)表示,并对应用及其 tools 的 descriptions 做语义搜索,以判断哪些工具合适。
- 如果命中条件——就调用某个 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) | 如何填写参数、可接受的值 |
|
模型(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. 工具注解:readOnlyHint、destructiveHint、openWorldHint
在 Multi‑App 世界里,重要的不仅是“何时调用”,还有调用的安全性。为此,Apps SDK 在工具描述里引入了一组注解。
理念是:注解为模型提供关于操作性质的柔性提示。它们不能替代服务端鉴权,但会显著影响 ChatGPT 在链路中的行为。
简要概念性总结:
| 注解 | 含义 | 模型的典型行为 |
|---|---|---|
|
不改变数据 | 可频繁调用,无需额外确认 |
|
会改变状态(购买、删除) | 调用前征求用户确认 |
|
访问“外部世界”(搜索、Web) | 在结果的体量与质量上更谨慎 |
这些注解(readOnlyHint、destructiveHint、openWorldHint)是工具描述的标准部分,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 的隔离相当严格:应用不会彼此直接调用,通信通过共享的文本上下文进行。
可以将基础模式概括为:
- App A 在聊天中返回文本或 JSON(常见于 structuredContent/小部件中)。
- 模型读取该输出。
- 下一步中,它可能调用 App B,并将 A 的输出细节填入 B 的工具参数。
这被称为 text/context handoff:“App A 的输出 → 模型 → App B 的输入”。
示例:CalendarApp + GiftGenius + CommerceApp
来看一个具体场景。
用户:“老板明天过生日,帮我挑个礼物并直接下单。”
分步说明:
-
模型先要弄清日期与对象。它调用日历 App 的工具,假设为 corporate_calendar.list_upcoming_birthdays,并获得如下结构:
[ { "name": "Alexey Bykov", "date": "2025-11-22", "relation": "manager" } ] -
接着模型决定该调用 GiftGenius。它用从日历得到的参数来调用你的 suggest_gifts:
{ "recipientName": "Alexey", "occasion": "birthday", "budget": 150, "relationship": "manager" }GiftGenius 小部件展示礼物轮播,文本回答解释这些想法为何合适。
-
用户在小部件中选择一两个候选(按钮 → 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 后,可以直接拾取 id 与 purchaseUrl 继续结账。
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_data、process_request 这种名字配上“处理用户数据”的解释,只会让模型更迷惑。在多个 App 共存的世界里,你很容易被调用到完全无关的领域。正确做法是将领域与动作绑定(giftgenius_get_gift_catalog、calendar.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(如 id、price、currency、purchaseUrl、occasion)能同时给 UI 与组合带来好处:模型可以不必做自然语言解析,直接把这些数据填入其他工具的参数。
错误 №6:试图在一个 App 内实现用户可能需要的一切。
有时会忍不住想:“既然我做了 GiftGenius,让它也管日历、群发邮件、做预算吧。” 在孤立的世界里或许还能接受,但在 Multi‑App 语境下你会变成一个与其他聚焦型 App 冲突的“全家桶”。更好的做法是与自己达成一致:我的 App 只做 X,并把它做到极致;其他是别人的职责。这样的设计会极大简化 UX 与生态发展。
错误 №7:没有在其他 App 的环境中测试行为。
开发者经常只在 Dev Mode 的“干净”聊天里测试自己的应用,那里没有其他 App。而在 Store 里,用户很可能连了十来个应用,其中有些在概念上与你重叠。别偷懒,搭建一个包含邻近 App(如日历、通用购物、理财)的测试场景,跑一遍 golden 样例:模型是否会在“礼物类请求”中正确选择 GiftGenius,是否会与其他参与者混淆?
GO TO FULL VERSION