跳到正文
北京时间
原文
OpenAI Developers:Blog(网页)·· 2026-02-04精选AI 评分65

Alpic 总结构建 ChatGPT Apps 的 15 条经验,并开源 Skybridge 框架与 Codex Skill

15 lessons learned building ChatGPT Apps

AI 导读

Alpic 基于 Apps SDK 在三个月内开发二十多个 ChatGPT Apps,总结出 15 条经验,核心包括用户、UI 与模型三方之间的上下文不对称问题。

推荐理由

作者基于三个月开发二十多个 ChatGPT Apps 的一手经验,总结上下文流、UI 模式与 CSP 等实操教训,并开源配套框架。

正文 · AI 翻译

在 Alpic,我们相信下一代产品和服务将围绕AI 优先体验构建,即用户与模型协作的界面,而非浏览传统的、预先确定的 UI 工作流。

当 OpenAI 发布 Apps SDK 时,我们立即开始用它进行构建。在三个月的时间里,我们开发了两打 ChatGPT 应用,既供内部使用,也服务于我们在 B2B 和 B2C 领域的客户,如旅游、零售和 SaaS。

我们很早就发现,构建 ChatGPT 应用与构建传统 Web 或移动应用有着根本性的不同。在 Web 上行之有效的模式(即时数据获取、UI 驱动的状态、显式用户配置等)在智能体环境中往往会失效,甚至主动损害体验。

本文提炼了我们在构建真实世界 ChatGPT 应用过程中学到的15 条最重要的经验,随后介绍了我们如何将这些经验融入一个面向社区的开源框架 Skybridge,以及一个 Codex Skill,帮助开发者显著更快地构思、构建、测试和发布应用。

三体问题

对于传统 Web 应用,事情很简单:你只有一个用户和一个UI。而在 ChatGPT 应用中,第三个天体进入了系统:模型。

为 ChatGPT 构建应用最困难的部分之一,就是管理信息在这三者之间如何流动。如果用户在你的小组件中点击“选择”按钮,UI 会在视觉上更新,但模型——对话的大脑——仍然不知情,除非你显式地将该上下文呈现出来。如果用户随后问,“给我更多关于这个产品的细节,”模型根本不知道用户实际在看什么。

我们称之为上下文不对称,即每个天体对系统只有部分了解,没有任何一个拥有完整图景。构建优秀的 ChatGPT 应用不在于让一切保持同步,而在于决定什么信息应该被共享、何时共享,以及谁需要看到它。解决这个问题,就是笨拙应用与无缝智能体体验之间的区别。

1. 并非所有上下文都应共享

我们最初的本能是“把所有东西都共享到所有地方”。结果这成了我们最早犯下的错误之一。

在实践中,ChatGPT 应用的不同部分往往需要对同一状态持有有意不同的视图。为什么?

  • 为了性能:UI 小组件通常需要远比模型所需更多的数据:例如,在一个旅行预订应用中,这可能包括图片、价格变体、预加载选项。将所有这些发送给模型会增加 token 用量、延迟和认知噪音。
  • 为了逻辑:有些信息在设计上就必须保持不对称。在我们最早的一个应用——Murder in the Valleys 悬疑游戏中,模型需要知道凶手是谁才能正确地进行角色扮演,而 UI 和用户则不能知道。在一个 Time's Up 风格的游戏中,情况则相反:UI 向用户展示秘密词语,而模型必须保持不知情。

教训不是“始终同步一切”,而是:明确决定谁需要知道什么。我们通过不同的工具输出字段将这一点形式化:

字段 用途 可见对象
structuredContent 用于 widget 和模型的结构化数据 同时用于 widget 和模型(通过 toolOutput 和 callTool 函数)
_meta 响应元数据 仅用于 widget,对模型隐藏

例如,在 Time's Up 游戏中,我们只把秘密单词通过 _meta 字段传给 widget,让模型根据用户的提示来猜词。

2. 懒加载在 AI 应用中效果不佳

来自 Web 开发的我们默认采用懒加载:用户点击时才获取数据;按需加载详情;优化初始负载使其最小化。

在 ChatGPT 中,这一范式被反转了:工具调用意味着延迟,由于安全沙箱和模型推理,往往需要数秒。

在实践中,我们学会了激进地前置加载:在初始工具响应中尽可能多地发送数据,并通过 window.openai.toolOutput 来填充 widget。这几乎总能带来更快、更灵敏的体验。

当然,如果 widget 可以安全地从公共 API 端点获取数据,并且不需要与模型共享信息,那么始终可以在 widget 内使用经典的 XHR 调用,但大多数情况下,你希望模型能够自主调用工具,以保持对话式的体验。

3. 模型需要可见性

一个微妙但关键的问题出现在用户与 widget 交互时(例如,在列表中选择某个特定产品),然后在聊天中提问。如果模型不知道用户指的是 UI 的哪一部分,它就无法正确回答。

为此我们使用了 window.openai.setWidgetState(state),它允许你存储特定的状态数据,这些数据会在下一次用户-模型交互时添加到模型的上下文中。

随着应用复杂度的增长,我们发现自己在很多地方添加 setWidgetState,以便让模型跟踪导航状态。因此我们决定引入一种声明式的方式来描述 UI 上下文。我们不再在每次交互时以命令式方式更新模型,而是直接将 data-llm 属性附加到组件上:

<div
  data-llm={
    selectedTab === "details"
      ? "User is viewing product details"
      : "User is viewing reviews"
  }
>

为了在幕后实现这一点,我们构建了一个 Vite 插件,它会抓取这些属性并自动更新 widgetState。从模型的角度来看,它只是在合适的时机收到相关的 UI 上下文,而开发者无需手动同步每一次交互。

你可以在我们创建的开源框架中找到这个 Vite 插件(以及本文中分享的许多其他技巧),我们创建它是为了与社区分享我们的经验。

4. 不同的交互需要不同的 API

ChatGPT Apps 涉及 widget、服务器和模型之间的多条交互路径。这些路径不可互换:每一条都用于支持不同类型的交互。

构建 ChatGPT Apps 的关键经验之一,是让这些通信路径变得明确,并有意识地为体验的每个部分指定由哪种机制负责。

梳理出这条路径大致如下:

Diagram of the different interactions between the widget, the server, and the model

这些经验奠定了 ChatGPT App 的基础:上下文如何共享、模型如何获得可见性,以及不同的交互如何在系统中传播。下一节将在此基础上展开,重点讨论对 UI 设计的影响。

为 AI 重新发明 UI

ChatGPT Apps 是一个全新的环境,因此我们很快学会了放下对 UI 的先入之见,充分利用新的能力。本节涵盖了为了打造有效的应用,我们需要学习(以及摒弃)的界面设计假设。

5. UI 必须适配多种显示模式及其约束

ChatGPT Apps 并不只存在于单一布局中。根据其被调用的方式和时机,同一个 widget 可以在三种不同的显示模式下渲染。

Apps 可以内联出现在对话中,以画中画(PiP)形式悬浮于其上,或在需要更多空间时以全屏呈现。虽然 PiP 和全屏能实现更丰富的界面,但它们也会引入 widget 无法控制的 UI 覆盖层。考虑设备特定的安全区域(例如移动端上常驻的关闭按钮)对于避免内容被裁剪和优化交互至关重要。

随着时间推移,我们总结出了关于显示模式以及何时使用它们的规律:

它看起来是什么样 何时使用它
内联 默认显示模式。widget 保留在对话历史中。 用于快速交互
全屏 widget 占据整个屏幕,聊天栏位于底部。 如果你的 widget 很复杂且需要大量空间(例如地图)
画中画 与内联尺寸相同,但 widget 悬浮在对话之上 如果你的 widget 在生成后的对话追问过程中仍然相关

6. 在嵌入式环境中,UI 一致性很重要

早期,我们遇到的一个不确定之处是 ChatGPT App 应该拥有多大的视觉自由度。作为面向用户的新界面,它需要让人感到熟悉且一致,无论是在我们自己的应用内部,还是与周围的 ChatGPT 生态系统之间。与独立产品不同,widget 存在于一个现有界面之中,视觉上的不一致会立刻凸显出来。

幸运的是,OpenAI Apps SDK UI Kit 为我们提供了清晰的基准。

它基于 Tailwind CSS 构建,提供了与 ChatGPT 设计系统一致的即用型组件、图标和设计令牌。使用它让我们能够快速推进,同时确保我们的 widget 感觉原生,并在视觉上与周围界面保持一致,即使在构建自定义组件时也是如此(例如我们的 Mapbox 集成)。

7. 语言优先的筛选

传统仪表盘建立在满是复选框和范围滑块的侧边栏之上。在智能体 UI 中,这往往是一种倒退。当用户可以用自然语言直接表达意图时,例如“欧洲阳光充足且价格低于 200 美元的目的地”,强迫他们通过多个 UI 控件操作会增加摩擦。他们应该能够直接说出来。

因此,我们决定在大多数应用中走“无筛选器”的路线。我们没有提供带有筛选和排序选项的侧边栏,而是为工具参数向模型提供值列表(LOV)。

这允许模型直接将用户的消息作为输入,防止它“猜测”有哪些可用选项。换句话说,它允许模型将自然语言直接映射到我们后端的 API 需求。如果用户说“sunny”,模型就知道应以 weather=“sunny” 调用工具。

8. 文件可以解锁更丰富的交互

随着我们构建更复杂的应用,浮现出的一个经验是:文件不应被视为次要输入。在 ChatGPT Apps 中,文件可以解锁新的交互。体验不必从表单或筛选器开始,而可以从用户已经拥有的东西开始。

例如,在一个电商应用中,用户可以在聊天中上传一张产品照片,让模型识别它,然后直接在 widget 中继续进行产品匹配或发现。

这是通过让文件在系统的两侧流动来实现的。在模型侧,工具可以通过 openai/fileParams 直接使用聊天中上传的文件,使模型能够对图像或其他用户提供的资源进行推理。在 UI 侧,小部件也可以使用 window.openai.uploadFile 和 window.openai.getFileDownloadUrl 直接处理文件,从而可以在 UI 流程中请求上传,或生成用户可以下载和复用的文件。

投入生产

接下来,随着应用超越本地开发,围绕安全性、配置和工具的另一组考量开始发挥作用。这就是第三组课程所涵盖的内容。

9. CSP 是新的 CORS

出于安全原因,OpenAI 将应用渲染在双重嵌套的 iframe 中。内容安全策略(CSP)是 iframe 隔离的原生机制,这种设置会严格执行它们,通常表现为经典的“本地能跑,生产环境就崩”综合征。

与传统的 Web 开发中你可能可以容忍宽松的策略不同,Apps SDK 要求你做到精准。

在应用清单中,这意味着要仔细声明每种交互类型允许哪些域名:

字段 用途 示例 常见错误
connectDomains API 和 XHR 请求 https://api.weather.com 忘记区分预发布 API 与生产 API。
resourceDomains 图片、字体、脚本 https://cdn.jsdelivr.net 使用像 delivr.net 这样的通用 CDN 却没有将其加入白名单
frameDomains 嵌入 iframe https://www.youtube.com 嵌入 YouTube 视频或 Mapbox 实例却没有将其加入白名单。
redirectDomains 无警告打开的外部链接 https://app.alpic.ai 忘记结账或 OAuth 回调域名。

从一开始就将 CSP 配置视为一等要务,为我们后来节省了大量生产环境调试时间。

10. 小部件的小标志有巨大影响

除了 CSP 之外,一小组小部件级别的设置决定了控制权如何在小部件、模型和宿主环境之间共享。这些标志很容易被忽视,但它们定义了导航、工具访问和发布的关键边界。

宿主和导航边界

  • widgetDomain 是提交所必需的。它定义了全屏模式下“在 <App> 中打开”按钮指向的默认位置,并参与来源白名单,因为小部件是在 <widgetDomain>.web-sandbox.oaiusercontent.com 下渲染的。我们使用 setOpenInAppUrl 根据上下文将用户路由到合适的路径。

模型和工具边界

  • 工具注解必须遵循发布指南。像 readOnly、destructiveHint 和 openWorldHint 这样的标志是必需的,并会在提交期间进行验证。
  • 工具可见性很重要:不应被模型调用的工具必须显式标记为私有。

小部件执行边界

  • widgetAccessible 控制小部件是否可以自行使用 callTool 调用工具。

单独来看,这些设置都很小,但它们共同决定了应用在发布后是否能正确运行。

为快速迭代而优化

Apps SDK 正在快速发展,我们很高兴能与之共同构建。为了支持顺畅高效的开发工作流,我们决定开发自己的开源框架并与社区分享。以下是一些经验教训,可帮助避免我们一开始遇到的一些开发者体验问题。

11. 快速迭代需要热重载

我们最先着手解决的问题之一是迭代速度。长 TTL 资源缓存与使用 JSON-RPC 转发资源的组合,使得标准的热模块替换(如 Vite 或 Next.js 中的那样)无法在 ChatGPT Apps 中开箱即用。

在花了大量时间理解 Vite 的内部机制后,我们构建了一个 Vite 插件,可以直接在 ChatGPT 内部实现小组件的实时重载。该插件拦截发往 MCP 服务器的资源请求,并将实时更新注入到 ChatGPT 的 iframe 中。在 IDE 中看到改动立即反映到 ChatGPT 内部,极大地缩短了我们的反馈循环。

Gif of the hot reload in action

12. 并非所有测试都适合放在 ChatGPT 中进行

在 ChatGPT 上测试是黄金标准,但在最初的迭代阶段,本地模拟器可以帮助你更快地推进,尤其是在你处理需要在开发者模式下重新加载应用的工具有定义时。

为了加速早期迭代,我们构建了一个轻量级的本地模拟器,模拟 ChatGPT 宿主环境,并配备了调试工具和针对应用的日志。这使我们能够在毫秒级内迭代 React 状态和布局,将真实的 ChatGPT 测试保留用于验证模型交互和边缘情况。

13. 移动端测试需要显式支持

移动端测试带来了另一个挑战:虽然通过隧道暴露本地服务器是在 ChatGPT 中测试所必需的,但 Vite 默认使用 localhost,导致同一 URL 无法从其他设备访问。

我们通过扩展 Vite 插件来支持隧道端口上的域名转发解决了这个问题,这打通了 iOS 和 Android 设备上的测试,使移动端验证成为我们常规工作流程的一部分。

14. 熟悉的抽象(如 React hooks)能加速前端工作

Apps SDK 暴露了强大的能力,但主要是通过底层 JavaScript API。作为长期的 React 用户,我们希望更接近我们已经掌握的概念。

因此我们引入了一些对 React 友好的抽象——如 useCallTool、useWidgetState 和 useLocale 这样的 hooks,以及基于 Zustand 构建的、用于复杂数据流的更高级状态管理如 createStore。重新引入熟悉的前端模式减少了样板代码,让小组件开发感觉更接近现代 Web 工作流程。

将经验教训转化为 Codex Skill

15. 将经验教训转化为可复用的工具

随着这些模式在多个应用中涌现,很明显反复重新发现它们拖慢了我们的速度。为了让 ChatGPT App 开发更快、更可预测,我们决定将这些经验教训直接编码到我们的工具中,不仅为我们自己,也为社区。

这带来了两项互补的工作:

  1. Skybridge Framework:一个开源 React 框架,将本文中描述的许多模式打包成可复用的构建块,包括我们的 hooks(useCallTool、useToolInfo)、开发工具(HMR 和本地模拟器)以及 data-llm 属性。
  2. The chatgpt-apps-builder Codex Skill: on top of the framework, we built a dedicated Codex Skill to support the full app lifecycle:
    • 构思:头脑风暴如何让应用变得“智能体化”,而不仅仅是 Web 移植。
    • 代码生成:同时编写 React 前端和 MCP 服务器后端,预先配置好所有正确的 UX 和 UI 模式。
    • 本地测试:启动开发服务器并将本地应用连接到 ChatGPT,通过热重载进行实时迭代。
    • QA 与发布:针对 OpenAI 的提交指南运行结构化检查,包括 CSP 验证、安全区域考量以及生产环境测试。
    • 应用部署:协助完成应用发布和迭代所需的最后步骤。

要安装并使用该 Skill,只需使用以下命令:

npx skills add alpic-ai/skybridge

您的浏览器不支持视频标签。

结论

构建 ChatGPT 应用需要重新思考上下文如何流动、界面如何表现,以及用户与模型如何协作。本文中的许多经验都来自熟悉的 Web 模式与智能体系统现实之间的差距。

通过分享这些经验,并将其融入我们的开源框架和 Codex skill 中,我们希望帮助团队减少重复发现相同问题的时间,把更多时间用于探索这种全新交互模式所带来的可能性。最引人注目的 ChatGPT 应用不会是现有产品的简单移植,而是围绕这种全新的 AI 优先体验刻意设计的体验。

来源:OpenAI Developers:Blog(网页) · developers.openai.com