Alpic 总结构建 ChatGPT Apps 的 15 条经验,并开源 Skybridge 框架与 Codex Skill
15 lessons learned building ChatGPT Apps
Alpic 基于 Apps SDK 在三个月内开发二十多个 ChatGPT Apps,总结出 15 条经验,核心包括用户、UI 与模型三方之间的上下文不对称问题。
作者基于三个月开发二十多个 ChatGPT Apps 的一手经验,总结上下文流、UI 模式与 CSP 等实操教训,并开源配套框架。
在 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 的关键经验之一,是让这些通信路径变得明确,并有意识地为体验的每个部分指定由哪种机制负责。
梳理出这条路径大致如下:

这些经验奠定了 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 内部,极大地缩短了我们的反馈循环。

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 开发更快、更可预测,我们决定将这些经验教训直接编码到我们的工具中,不仅为我们自己,也为社区。
这带来了两项互补的工作:
- Skybridge Framework:一个开源 React 框架,将本文中描述的许多模式打包成可复用的构建块,包括我们的 hooks(
useCallTool、useToolInfo)、开发工具(HMR 和本地模拟器)以及 data-llm 属性。 - 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