1. 为什么在 ChatGPT 应用中要考虑缓存与 edge
在传统 Web 应用中你也会关注速度,但用户至少能看到加载指示器。在 ChatGPT 应用里更有意思:用户与模型对话,模型有时会调用你的应用。小部件需要弹出,并且要尽快展示有用内容。
实践非常明确:latency = 金钱。响应越慢,用户流失概率越高;而额外的 LLM/后端调用会直接增加模型与基础设施成本。缓存能同时降低这两者。
另外,ChatGPT 应用有其特性:
- 从 ChatGPT 到你的应用的请求会经过网络和多层中间层。每一步的毫秒延迟都会积累。
- MCP/HTTP 端点有真实的超时(包括 Vercel 无服务器函数与 edge 函数)。如果来不及返回,ChatGPT 会看到错误,甚至可能开始“幻觉”出答案。
- 在 GiftGenius 中,许多数据并非每秒变化:礼物目录结构、面向不同细分的“热门创意”合集、功能配置等。每次都重新访问数据库或外部 API 显然不明智。
这时就该用上:
- CDN 与 edge 缓存,用于快速分发静态资源与可缓存的 JSON。
- 带有 HTTP 缓存的Cache-Control/ETag/SWR,让重复请求更快且更省钱。
- Vercel 的 edge 函数,把轻量逻辑尽可能贴近 ChatGPT 与用户执行,但不要把它们当作“迷你后端”。
2. GiftGenius 的延迟剖析与可缓存点
先画清楚延迟都产生在哪些环节。
sequenceDiagram
participant User as 用户
participant ChatGPT as ChatGPT
participant App as ChatGPT 应用 (Apps SDK)
participant GW as MCP Gateway / Edge
participant GiftAPI as Gift REST API / 礼物微服务
participant DB as 目录/数据库
User->>ChatGPT: "给我弟弟推荐礼物"
ChatGPT->>App: 调用工具 + 渲染小部件
App->>GW: HTTP/MCP 请求(分类、合集)
GW->>GiftAPI: HTTP(REST)
GiftAPI->>DB: 查询目录/推荐
DB-->>GiftAPI: 响应
GiftAPI-->>GW: 响应(JSON)
GW-->>App: 响应(JSON)
App-->>ChatGPT: 带结果的小部件
ChatGPT-->>User: 消息 + UI
哪些地方可以“抄近道”?
- 在 ChatGPT 与你的边界之间——CDN/edge 缓存(Vercel CDN/Edge Network),可直接分发不可变的小部件资源与可缓存的 JSON,而无需访问源站服务器。
- 在 Gateway 与内部 REST/HTTP 服务(Gift REST API、Commerce REST API 等)以及数据库之间——应用层缓存(Redis/内存/数据库缓存),避免对相同请求(例如“礼物分类列表”)重复多次查询。
本讲我们聚焦 HTTP/edge 层,因为它更贴近 ChatGPT 与 Vercel。
3. 架构中的缓存类型
既然我们的架构是“分层蛋糕”,那么缓存也会有多层。
| 缓存类型 | 所在位置 | 适用场景 |
|---|---|---|
| 浏览器缓存 | ChatGPT 客户端内部(浏览器/桌面) | 小部件静态资源、图标、字体(可控性有限) |
| CDN / edge 缓存 | 在 Vercel/Cloudflare 的 edge 节点上 | 静态资源 + 公共 JSON(分类、配置、通用合集) |
| 应用层缓存 | 在 MCP Gateway 或你的后端服务内部(Redis、内存) | 数据库/外部 API 的重查询结果 |
| 数据库缓存/物化 | 在数据库内部(物化视图等) | 预计算聚合、分析 |
现在先专注前两种:HTTP 缓存 + CDN/edge。
4. HTTP 缓存:Cache-Control、max-age 与 s-maxage
HTTP 缓存主要由Cache-Control响应头控制。它决定浏览器/ChatGPT 客户端和/或 CDN 能否缓存你的响应,以及缓存多久。
关键参数:
- max-age —— 浏览器可缓存的秒数。
- s-maxage —— 共享缓存(CDN/代理)可缓存的秒数。
- public —— 允许在共享缓存中缓存。
- private —— 仅供单个客户端使用;CDN 不缓存。
在 GiftGenius 中,例如:
- 小部件的 JS/CSS/字体是版本化文件(文件名带哈希),可以放心返回 Cache-Control: max-age=31536000, immutable。
- 礼物分类列表的 JSON —— 对所有用户相同,适合设置 public、s-maxage=60(或更长)。
一个最简单的 Next.js 处理器(Route Handler)用于 GET /api/gifts/categories,在 CDN 上缓存 60 秒:
// app/api/gifts/categories/route.ts
import { NextResponse } from "next/server";
export const runtime = "nodejs"; // 常规 serverless 函数
export async function GET() {
// 这里本可以去访问数据库/外部 API
const categories = [
{ id: "for_brother", title: "送给弟弟的礼物" },
{ id: "for_mom", title: "送给妈妈的礼物" },
];
return NextResponse.json(categories, {
headers: {
// 允许 CDN 缓存 60 秒
"Cache-Control": "public, s-maxage=60",
},
});
}
Vercel CDN 会将响应保存 60 秒,这个时间窗内 ChatGPT 对该 JSON 的请求根本不会触达你的函数。既快又省钱。
5. ETag:内容指纹与 304 Not Modified
ETag 是资源的“指纹”,通常是内容哈希。工作流程:
- 服务器返回带有 ETag: "v1-abc123" 的响应。
- 下次客户端发送 If-None-Match: "v1-abc123"。
- 如果服务器判断内容未变,更返回 304 Not Modified,且无响应体。
重要提示:ETag节省流量,但未必降低延迟,因为仍需一次到服务器的往返。在 ChatGPT 应用场景,它对大型 JSON 有用,但不要指望仅靠 ETag 获得神速 —— 若要更快,优先使用 SWR 与 edge 缓存。
下面是在 Next.js 处理器中实现一个简单 ETag 的示例(不做复杂的加密哈希计算):
// app/api/gifts/config/route.ts
import { NextRequest, NextResponse } from "next/server";
const CONFIG = { version: 1, showExperimentalIdeas: true };
const ETAG = `"v${CONFIG.version}"`;
export async function GET(req: NextRequest) {
const ifNoneMatch = req.headers.get("if-none-match");
if (ifNoneMatch === ETAG) {
// 内容未变化——返回 304
return new NextResponse(null, { status: 304, headers: { ETag: ETAG } });
}
return NextResponse.json(CONFIG, {
headers: {
ETag: ETAG,
"Cache-Control": "public, s-maxage=300",
},
});
}
在实际项目中,你通常会基于数据哈希或数据库记录的版本号来计算 ETag。
6. Stale‑While‑Revalidate(SWR):更快且足够新鲜
SWR 的理念是“先立即展示旧数据,再在后台拉取新数据”。可在两层实现:
- HTTP 层,通过 Cache-Control 的 stale-while-revalidate 参数。
- UI 层,使用 swr/react-query 等库,它们在本地维护缓存并后台重新获取数据。
HTTP 响应头中的 SWR
典型响应头:
Cache-Control: public, s-maxage=60, stale-while-revalidate=300
含义:
- 前 60 秒,CDN 返回新鲜版本。
- 第 61 秒至第 360 秒,CDN 可以立即返回过期响应, 同时在后台向源站拉取新版本。
- 超过 360 秒,请求将阻塞等待新内容。
用户(和 ChatGPT)即便在高峰期也能秒回,而你在后台温和地更新缓存。对 GiftGenius 而言,这非常适合比如“新年礼物热门合集”这类不需要秒级更新的内容。
示例:
// app/api/gifts/top/route.ts
import { NextResponse } from "next/server";
export async function GET() {
const topGifts = [
{ id: "coffee_mug", title: "带字样的马克杯" },
{ id: "smart_led", title: "智能灯" },
];
return NextResponse.json(topGifts, {
headers: {
"Cache-Control": "public, s-maxage=60, stale-while-revalidate=300",
},
});
}
UI 小部件中的 SWR(React)
GiftGenius 的小部件运行在 ChatGPT 的沙盒中,可以使用任意 React 代码。你已经会通过 window.fetch 调用自己的 API。 现在加上 swr 库,在小部件侧组织缓存:
// widget/GiftTopList.tsx
import useSWR from "swr";
const fetcher = (url: string) => fetch(url).then((r) => r.json());
export function GiftTopList() {
const { data, isLoading } = useSWR(
"https://api.giftgenius.com/api/gifts/top",
fetcher,
{ revalidateOnFocus: false } // 在聊天界面焦点变化较频繁,关闭该行为
);
if (isLoading && !data) return <div>正在加载创意…</div>;
return (
<ul>
{data?.map((gift: any) => (
<li key={gift.id}>{gift.title}</li>
))}
</ul>
);
}
工作方式:
- 首次渲染时向我们的 API 发起请求。
- 结果被放入小部件内部的 swr 缓存。
- 后续的重复渲染(或 ChatGPT 在新的回答中再次插入该小部件且键相同)会直接从缓存取数据。 用户看不到“闪烁”和加载指示器,同时后台可以发起刷新。
因此我们组合了两层 SWR:
- CDN/HTTP 层 —— 以降低对源站的压力。
- UI 层 —— 以减少对用户的打扰。
如果把它们组合起来:
- 基础的 Cache-Control(max-age/s-maxage)—— 赋予 CDN 与客户端缓存权限,降低负载。
- ETag + If-None-Match —— 当需要为大型 JSON 节省流量时加入,但要接受一次网络往返。
- stale-while-revalidate —— 当你需要即刻返回稍微过期的数据(目录、热门合集)时启用。
- UI 层的 SWR(swr/react-query)—— 在 ChatGPT 沙盒中平滑小部件重绘并维护本地缓存。
7. 在 GiftGenius 中该缓存什么、缓存多久
尝试将 GiftGenius 的数据按“可缓存层级”进行划分。
可在 CDN/edge 层缓存
这类内容对所有人(或大范围分群)相同、且变化不频繁:
- 小部件静态资源:JS/CSS、字体、图标 —— 近似“长期”(一年)并使用 immutable。
- 礼物目录结构:分类、分区、筛选 —— 分钟/小时级。
- 通用合集(“适合同事的 50 美元以内最佳创意”)—— 分钟/几十分钟,尤其在高峰季节。
这里非常适合使用 public、s-maxage + stale-while-revalidate。
更适合在应用/Redis 层缓存
更动态、但仍然会重复的内容:
- 外部 API 的重查询结果(例如汇率、外部商店的实时价格)。
- 常用的推荐分群(按性别/年龄/场景)。
这时 CDN 不一定合适,因为数据可能依赖于 token/组织/租户。应在 MCP Gateway 或内部 REST 服务层缓存:完全由你控制,且不会混淆不同用户的数据。
不应在公共缓存中缓存
与具体用户绑定的内容:
- 个人订单及其状态。
- 支付信息、地址、邮箱。
- 基于私有订单历史的个性化推荐(如果较为敏感)。
这些只能在应用层以谨慎语义进行缓存(且必须杜绝用户间数据泄漏),绝不能放在public 的 CDN 缓存中。
8. Edge 层:CDN 与 edge 函数的区别
别混淆两个相似但不同的东西:
- CDN / edge 缓存 —— 保存已经计算好的响应,几乎没有逻辑。
- Edge 函数(Vercel Edge / Cloudflare Workers)—— 在 edge 节点执行的小段代码。
经验表明:Edge ≠ Serverless。许多开发者尝试把重型业务逻辑、LLM 调用与 BLOB 处理塞进去,然后遭遇超时与限制。Edge 函数:
- 启动极快(冷启动几乎为零)。
- 但在 CPU、执行时间和可用 API 上限制很大(往往没有完整的 Node.js、不支持长连接等)。
何时使用 edge 函数是好主意
在 GiftGenius 与 ChatGPT 应用的语境下,edge 函数适合用于:
- 轻量路由:依据请求头 locale、x-openai-user-location 或 tenant ID,决定要访问哪个区域后端集群。
- 添加简单的响应头、功能开关、A/B 路由。
- 快速的只读端点,从 edge-KV 或 CDN 缓存读取,几乎不做计算。
何时不该用 edge 函数
- 对外部 API 的长耗时请求。
- 调用 LLM 模型。
- 复杂的结账流程逻辑。
- 带有重型业务逻辑的 MCP 工具。
这些场景请使用常规的 Next.js 无服务器函数(例如 runtime = "nodejs"),或独立的服务/集群。
Next.js 16 中的 edge 函数示例
我们做一个小路由 GET /api/geo-router,它根据请求头 x-openai-user-location(假设存在)返回要访问的区域集群。
// app/api/geo-router/route.ts
import { NextRequest, NextResponse } from "next/server";
export const runtime = "edge"; // 在 edge 上执行
export function GET(req: NextRequest) {
const userLocation = req.headers.get("x-openai-user-location") ?? "US";
const cluster =
userLocation.startsWith("EU") ? "eu-gift-api" : "us-gift-api";
return NextResponse.json({ cluster }, {
headers: {
"Cache-Control": "public, s-maxage=300",
},
});
}
这样的端点:
- 非常快(edge)。
- 不做复杂计算。
- 可以被 CDN 缓存。
9. GiftGenius 的整体架构中的 Edge 与缓存
把一切放在一张图里。
flowchart TD
ChatGPT[(ChatGPT / User)]
CDN["CDN / Edge Cache(Vercel)"]
EdgeFn["Edge Functions (路由、功能开关)"]
GW[MCP Gateway]
GiftAPI["Gift REST API Cluster"]
CommerceAPI["Commerce REST API Cluster"]
DB[(DB/External APIs)]
ChatGPT --> CDN
CDN -->|缓存命中| ChatGPT
CDN -->|缓存未命中| EdgeFn
EdgeFn --> GW
GW --> GiftAPI
GW --> CommerceAPI
GiftAPI --> DB
CommerceAPI --> DB
典型流程:
- ChatGPT 小部件请求 /api/gifts/categories。
- CDN 检查缓存。如果存在新鲜或“过期但可用”的版本 —— 立即返回,甚至不会触发 EdgeFn/GW。
- 若不存在缓存 —— 请求落到 EdgeFn(若启用)和/或直接到 GW。
- GW 视情况对重操作使用内部 Redis 缓存,或调用内部 REST 服务并进一步访问数据库。
- 响应返回并进入 CDN/edge 缓存,随后服务于其他用户。
这样的架构:
- 降低小部件与 ChatGPT 的延迟。
- 减少 MCP Gateway 与后端集群的负载。
- 降低 LLM/数据库调用成本(减少重复请求)。
10. GiftGenius 的一些实用片段
分类缓存 + Next.js revalidate
前面我们只谈了 API 端点。但 Next.js 也为页面提供了类似机制 —— 通过 ISR(revalidate)。
示例 server component,以 revalidate = 60 获取分类列表:
// app/(widget)/categories/page.tsx
export const revalidate = 60; // ISR:每 60 秒再生成一次
async function fetchCategories() {
const res = await fetch("https://api.giftgenius.com/api/gifts/categories");
return res.json();
}
export default async function CategoriesPage() {
const categories = await fetchCategories();
return (
<ul>
{categories.map((c: any) => (
<li key={c.id}>{c.title}</li>
))}
</ul>
);
}
在生产环境中,Vercel 会生成并缓存该页面的 HTML 输出。当你的小部件/界面不仅通过 ChatGPT 打开,还以普通网页形式使用时(例如调试面板或落地页),这会很有用。
后端服务中的简易应用层缓存
这已经不是 edge 层,而是应用层缓存(Redis/内存,位于你的 Gift REST API 或其他后端服务内部)。 此处用一个极简示例展示它的形态:
// Gift REST API 内的伪代码
const cache = new Map<string, any>();
async function getGiftCategories() {
const key = "gift_categories_v1";
const cached = cache.get(key);
if (cached && Date.now() - cached.ts < 60_000) {
return cached.data; // 缓存 60 秒
}
const data = await fetchRealCategories();
cache.set(key, { ts: Date.now(), data });
return data;
}
在实战中,你当然会用 Map 的替代品(如 Redis/Memcached),但思路相同:尽量减少对数据库/外部 API 的访问。
一句话总结:先明确可以缓存什么、以及缓存的位置(CDN、edge、Redis、数据库),然后再开启平台的“魔法”开关。缓存不是配置文件里的一个勾选项,而是架构的一部分:它影响速度、稳定性与成本。
11. 使用缓存与 edge 层的常见错误
错误 1:“一股脑把一切都缓存,只求更快”。
经典场景:开发者给所有 JSON 响应都加上 Cache-Control: public, s-maxage=3600。几个小时后发现:一个用户能看到另一个用户的订单,而 ChatGPT 还在使用过期的库存数据。对个性化或敏感数据,要么使用private 缓存,要么完全禁用 CDN 缓存,转而在应用层以严格的隔离策略缓存。
错误 2:混淆 max-age 与 s-maxage。
有人只设置 max-age,期待 CDN 也按同样时长缓存。实际上 max-age 主要面向浏览器,而共享缓存需要 s-maxage。结果是浏览器缓存了,CDN 却没有,源站仍然压力山大。正确做法是显式为 CDN 指定 s-maxage。
错误 3:指望 ETag 让一切都变快。
ETag 很适合为大型 JSON 节省流量,但网络往返仍然存在。在 ChatGPT 应用中,这意味着模型依旧要等待来自你的服务器的响应,即便是没有响应体的 304。如果你追求的是真正的延迟优化,应该使用 edge 缓存 + SWR,ETag 只是辅助手段。
错误 4:把重型业务逻辑塞进 edge 函数。
“我们直接在 Vercel Edge 里调用外部 LLM、做复杂筛选、访问三个外部 API —— 反正 edge 很快!” 然后就是痛苦:执行时间限制、没有完整的 Node.js、各种奇怪错误。Edge 适合轻量路由与 A/B,而重活应该放在常规无服务器函数或独立后端集群中。
错误 5:没有缓存失效策略。
“把缓存设成 1 小时”,一开始一切飞快。过会儿业务说:“我们改了价格/分类/限制,为什么 ChatGPT 里还是旧数据?” 开发者开始手动清缓存、重启服务。对重要数据,应事先设计好如何失效缓存(例如由管理后台的 webhook 触发、按版本、按键清理),而不是指望“再等一小时就好了”。
错误 6:忽视缓存与成本的关系。
有时开发者只把缓存当作性能优化。在 LLM 生态里,这还是成本优化:每一次对模型与外部 API 的多余调用都要花钱。没有缓存,MCP 服务器可能会频繁打外部服务/模型,月底账单会很难看。合理的缓存既降低延迟,也减少账单。
错误 7:把不同语言/区域的数据混在同一个缓存键里。
GiftGenius 在多国运营,但缓存里只用一个键 top_gifts。结果:美国用户看到人民币和中国商店,欧洲用户看到美元和美国商店。缓存时务必在键或路由中加入诸如 locale、currency、tenant 等维度(例如 /api/{locale}/gifts/top)。
错误 8:完全依赖 Next.js/平台的“魔法”。
ISR、revalidate、自动 CDN 都很棒。但如果不了解其底层机制,很容易出现意外:页面展示旧内容而 API 返回新内容;ChatGPT 看到的一样,浏览器用户看到的又是另一样。值得花时间弄清 Cache-Control、ETag 与 SWR 模式的原理,把 Next.js 当作便捷封装,而非黑盒。
错误 9:在 dev/staging/production 环境中不区分缓存策略。
在开发环境中,缓存常常妨碍调试(“我已经改了数据,为什么 ChatGPT 还是看到旧合集?”)。建议做环境化配置:在 dev 基本关闭缓存(或将 TTL 降到几秒),在 production 才启用激进缓存。否则要么在开发时被缓存逼疯,要么上线时忘了缓存导致 MCP Gateway 后端集群被请求风暴冲击。
GO TO FULL VERSION