1. 部署解剖:到底可能在哪里出错
先把整条链路看清楚。可以将你架构中的 ChatGPT App 部署在脑海里展开成这样的一条线:
flowchart TD A[你的笔记本
git commit] --> B[Git 仓库
GitHub/GitLab] B --> C[Vercel Build
npm run build] C --> D[Vercel Deploy
Preview/Prod] D --> E[HTTP endpoint
/mcp, /api/...] E --> F[ChatGPT / Dev Mode
tool calls, widgets]
任何一个步骤都可能出错,但在 ChatGPT 里的症状看起来大多相似: "Error talking to app"、"Network error",或者干脆没有任何响应。 你的任务不是胡乱试,而是先弄清楚:是构建阶段挂了、运行时挂了,还是 ChatGPT 指向了错误的目标。
把问题按三大类来划分会更容易定位:
- 构建错误(Build):Vercel 根本没能构建你的项目。生产环境没有更新——这“还算好”,但你会看到红色的构建失败。
- 运行时错误(Runtime):构建通过了,但请求返回 500/502、超时或出现奇怪行为。
- 配置漂移(Config drift):本地一切正常,Vercel 日志也正常,但 ChatGPT 访问了旧的 URL,使用旧的清单,或拿到的是空的 env 变量。
我们将沿着这三个层面逐步展开,同时形成一套通用的调试策略。
2. 构建错误:项目无法编译时
这是引言中的第一类问题——构建错误:项目根本无法编译,因为 Vercel 没能成功构建你的 Next.js 项目。
Node 与 Next.js:环境不同,要求不同
在本地你也许(不幸地)还在用过期的 Node,而 Vercel 会尝试用受支持的 Node 版本去构建你的 Next.js 16 项目 (最低版本 18.18.0)。 如果你在 package.json 里显式声明了不兼容的版本,那么生产构建就可能失败,尽管你的本地 dev 服务器还能跑。
一个简单的防护方法——在 package.json 中显式指定 "engines":
{
"engines": {
"node": ">=18.18.0"
}
}
这样无论本地还是 CI/Vercel,都会提前提示 Node 版本过旧。
“在我机器上能跑!”与遗漏的依赖
典型情形:你通过 npm install some-lib 安装了一个库,却没有提交更新后的 package-lock.json,或者你把部分依赖装成了全局依赖。 在 Vercel 上应用是“从零开始”构建的,它会根据清单老老实实执行 npm install, 而你钟爱的 some-lib 根本不存在——于是得到构建失败。
应对方法是严格的纪律:
- 新增任何依赖后立刻提交;
- 在 push 到 main/production 之前,本地先跑一遍 npm run build。如果本地构建都失败了,Vercel 只会更糟。
区分大小写的文件系统
本地很多人用的是 macOS 或 Windows,这些系统的文件系统默认不区分文件名大小写。 而在 Vercel 的 Linux 环境中,Widget.tsx 与 widget.tsx 是两个不同的文件。
常见漏洞:
// 代码中的导入
import { AppWidget } from "@/components/Widget";
// 而仓库里实际的文件是 components/widget.tsx
在你的电脑上能跑,但在 Vercel 上就会出现模块错误 “Cannot find module '@/components/Widget'”。 解决方法是统一命名并对大小写保持严格约束。
构建阶段的环境变量(env)
另一个“惊喜”来源——在构建阶段执行的代码里使用 process.env.* (例如在 next.config.mjs 或构建时被导入的模块中)。 如果你本地加载了 .env.local,却忘了在 Vercel 的构建环境中设置这些变量, 构建要么直接失败,要么(更糟的是)以 undefined 通过,然后把不合法的值“烘进”产物包里。
对于 ChatGPT App,若你在构建阶段就生成 MCP 端点的 baseURL 或外部 API 的 URL,这一点更为关键。
一个好的实践是:在应用启动前就显式校验关键 env 变量(我们会在单独部分讨论), 让构建因缺失而“响亮地、可预期地”失败。
3. 运行时错误:构建成功但运行失败
现在进入引言中的第二层——运行时错误:构建通过了,但一运行就出问题。
构建通过,Vercel 高高兴兴显示绿色部署,你把 ChatGPT App 切到生产 URL——结果在对话里收到 "Error talking to app"。 这意味着问题已经到了运行时层面。
空或未设置的环境变量
在 ChatGPT App 的世界里,生产事故往往以 undefined 开头。 你的本地 .env.local 里有 OPENAI_API_KEY、MCP_BASE_URL 等等, 但你在 Vercel 上要么没设置这些变量,要么名字写错了。
例如你读取的是:
const apiKey = process.env.OPENAI_API_KEY;
但在 Vercel 上你创建的是 OPENAI_APIKEY 或 OPENAI_API_KEY_PROD。 结果是,一旦调用 MCP 工具,你的路由处理器会因认证失败而崩溃。
更舒服的做法是:让应用尽早、清晰地失败。一个好模式是在 Next.js 项目中做一个单独模块, 在导入时就校验环境变量:
// app/lib/env.ts
const required = ["OPENAI_API_KEY", "MCP_BASE_URL"] as const;
type RequiredKey = (typeof required)[number];
function getEnv(key: RequiredKey): string {
const value = process.env[key];
if (!value) {
throw new Error(`Missing required env var: ${key}`);
}
return value;
}
export const env = {
OPENAI_API_KEY: getEnv("OPENAI_API_KEY"),
MCP_BASE_URL: getEnv("MCP_BASE_URL"),
};
现在,如果你在 Vercel 上忘了配置变量,Next.js 会在首次导入 env 时就失败, 日志里也会出现易懂的信息 "Missing required env var: ..."。
务必记住,在 Vercel 上修改 env 变量不会自动被运行时拾取。 改完值之后必须进行一次新部署(redeploy),否则运行时仍然使用旧值。
路由处理器与 MCP 端点中的错误
在官方的 ChatGPT App 模板中,MCP 服务器通常实现为 app/mcp/route.ts。 你的代码会解析 JSON‑RPC 请求,将其路由到具体工具并返回结果。 如果链路中某处出现了未处理的 throw——用户在 ChatGPT 里就会收到 500。
建议总是用 try/catch 包裹 MCP 路由的最外层, 记录错误日志,并返回结构化的响应:
// app/mcp/route.ts
import { NextRequest, NextResponse } from "next/server";
export const dynamic = "force-dynamic";
export const maxDuration = 30; // 秒
export async function POST(req: NextRequest) {
try {
const body = await req.json();
// 这里处理 MCP 请求
const result = await handleMcpRequest(body);
return NextResponse.json(result);
} catch (error) {
console.error("MCP route error", error);
return NextResponse.json(
{ error: "Internal MCP error" },
{ status: 500 }
);
}
}
几点说明:
- dynamic = "force-dynamic" 有助于避免 Next.js 16 对 MCP 路由意外进行静态化与缓存。
- maxDuration = 30 明确告诉 Vercel:该路由处理器最多可运行 30 秒,这对耗时较长的 LLM 请求很重要。
超时与 ChatGPT 中的“Network error”
Vercel 会限制 serverless 函数的执行时间:免费套餐通常约 10 秒, 付费套餐可能更长(可达数分钟)。 如果你的 MCP 工具要对数据库或外部 API 发起较长请求,可能会来不及返回, ChatGPT 就会收到 "Network error" 或被截断的流。
如果你使用流(SSE)返回部分结果,尤其应该在超时之前尽早发送首字节响应。 这样后续传输可以继续,但平台不会把函数判定为“卡住”。
小技巧:为每个工具调用测量耗时,并把耗时与工具名一起记录到日志。 这样会很直观地看到,例如 search_flights 稳定需要 12 秒, 刚好超出限制一点点。
export async function safeToolCall<TInput, TOutput>(
name: string,
handler: (input: TInput) => Promise<TOutput>,
input: TInput
): Promise<TOutput> {
const started = Date.now();
try {
const result = await handler(input);
console.log("[tool] ok", name, { ms: Date.now() - started });
return result;
} catch (error) {
console.error("[tool] fail", name, {
ms: Date.now() - started,
error,
});
throw error;
}
}
然后把 handler(args) 换成 safeToolCall("search_flights", handler, args) 即可。
网络与外部服务
有时问题仅仅是 https:// 与 http:// 混用,或 baseURL 过期。 尤其是你先在本地用一个 URL 测试,而生产上域名或端口已经变了。
把基础 URL 抽到与环境相关的配置中,不要把它们硬编码在工具代码里。 这样切换环境只需改一个 env,而不是回忆你在代码的五个角落写了 http://localhost:3001。
4. 配置与环境漂移
最后来看第三类——不同环境之间的配置漂移。
即便构建成功、运行时日志也健康,ChatGPT 仍可能表现为“像是在跑旧版本应用”。 这时问题通常不在代码,而在配置与环境之间的不一致。
Dev Mode 与生产的区别
在 Dev Mode 中,ChatGPT 访问你手动设置的 Connector URL:通常是某个隧道 URL (https://myapp-dev.ngrok-free.app/mcp 或类似) 或者 Vercel 的 staging URL。 在生产(通过 Store 或组织设置)中,App 应该指向稳定的生产端点, 如 https://myapp.vercel.app/mcp。
几乎每个人都会犯的错误:你已经在 Vercel 上完成了部署,但在 ChatGPT App 的设置里仍然保留着旧的隧道 URL。 本地服务器停了、隧道也早就断了,而 ChatGPT 还在那儿敲门,收到的自然是 502。 界面上显示 "Error talking to app",学生就开始修 MCP 代码——可这些代码根本没被执行到。
解决方法是靠纪律:每次变更环境(隧道 → staging、staging → prod)后,都要检查 Dev Mode 和生产配置中到底写的是什么 URL。
旧清单与 ChatGPT 的缓存
ChatGPT 会缓存你的 App 信息: 工具列表、描述、元数据等。 因此“我改了工具 schema,但模型仍然以为参数名是旧的”——确实会发生。
在对工具做重大修改时,建议:
- 确认你确实部署了新版本(在日志里查看 commit hash,或把它打印到启动日志);
- 在 Dev Mode 中重新创建或重新连接 App,强制平台重新读取清单;
- 调试阶段尽量通过 MCP Inspector 工作,这样你能明确看到当前工具列表与 schema。
环境配置:dev/staging/prod
我们已经提过 env 变量可能把构建与运行时都搞崩。 这里从更高的层面看 dev/staging/prod 三种环境的取值一致性。
常见痛点:你的 .env.local 完美无缺,但 Vercel 的各环境是一片“动物园”。结果:
- 本地使用了一套 API 密钥与外部服务 URL;
- staging 又是完全不同的一套;
- prod 上有一半变量根本没配。
一个很有用的做法是在仓库里放一个简单的文本文件 docs/env.md,列清楚: 需要哪些变量、在哪些环境是必填、示例值长什么样。 这看似“官僚”,但一旦出事故,它能帮你省下好几个小时。
5. 从 ChatGPT 一侧看错误是什么样的
现在换位思考一下,从 ChatGPT 用户的视角看问题。 他只能看见界面,对 Vercel、Node 与 MCP 一无所知。 而你此刻也还不知道到底是哪里坏了。
ChatGPT 里的典型症状:
- 尝试使用后立刻出现 "Error talking to [App Name]";
- 无休止的加载动画,没有可见错误;
- 红色文字 "I encountered an error while running the tool";
- 组件(widget)没有出现,或空白出现。
这些症状通常对应不同层级的问题:
- 如果 App 完全不可达(URL 填错、隧道中断、SSL 错误),ChatGPT 就联系不到你的 MCP 端点—— 先用浏览器检查域名可达性,再看 Vercel 日志中的 4xx/5xx;
- 如果 MCP 返回的是带有 error 字段的合法 JSON‑RPC, ChatGPT 会如实显示工具返回了错误——这通常是业务逻辑或参数校验的问题;
- 如果 MCP 返回成功,但响应里是损坏的 widget HTML 或出现 JS 错误,那么在组件的控制台 (DevTools → widget 的 iframe)里能看到具体是哪里报错。
因此一个好习惯是:一旦在对话中看到异常行为,立刻记下时间戳(精确到分钟), 然后去 Vercel 日志里按时间定位请求。
6. 调试策略:不慌,按步骤来
把上面内容汇总一下,做一个小小的“作战手册”——当事情出岔子时如何行动。 目标是用冷静的算法替代无头苍蝇式乱跑。
步骤 1:先判断问题类型
如果 Vercel 上构建是红的——值得庆幸:错误没进生产。 打开构建日志,找到第一个真正的错误(而不是 200 行 warning), 并用 npm run build 在本地复现。
如果构建是绿的,而 ChatGPT 在抱怨——那就是运行时或配置问题。检查:
- 你的生产 App URL 是否能通过浏览器访问(https://myapp.vercel.app/mcp 是否有任何响应);
- MCP 端点是返回 200/500,还是根本无法解析;
- App 设置里的 URL 是否与刚才你检查的一致。
步骤 2:看日志,不要猜
下一站——Vercel 日志:查看对应部署、对应环境(Preview/Production)的服务端日志。
重点关注:
- Error: Missing required env var ...——说明是配置问题;
- 来自 MCP 路由处理器的 stack trace——说明是业务逻辑或入参解析在崩;
- 关于超时或函数运行时长超限的消息。
同时别忘了 MCP Inspector。 用它连到同一个 MCP 端点手动调用工具, 很快就能判断问题在 MCP 本身,还是在 ChatGPT ↔ MCP 这条链路上。
步骤 3:快速回滚还是直接热修?
如果你看到生产部署明显是坏的(例如 MCP 路由对每个请求都抛同一个错误), 而上一版是健康的,那就该回滚了。 Vercel 允许你快速切回上一个成功部署而无需重新构建——本质上是切换“活动版本”。
这比在“上线中”硬修要安全,尤其是在你还没完全搞清楚事故原因的时候。
稳定住局面后,再从容定位原因、补测试、修代码, 最后再发布新版本。
步骤 4:把经验写进文档
任何一次较严重的事故,都是更新内部 README 的好时机:
- 把某个必需的 env 变量加入清单,否则整个应用会崩;
- 记录导致错误的具体情况(例如“导入语句大小写与文件名不一致”);
- 写下这次快速修复的简要步骤,方便下次直接照做。
这事看着枯燥,但过两个月你会感谢当初的自己。
7. 代码中的一些实用做法
现在把“作战手册”里的若干步骤,固化成我们教学应用(ChatGPT App)里的小技巧。
统一的配置模块
我们已经写了一个简单的环境变量校验器。可以再扩展一下,让它区分不同环境:
// app/lib/config.ts
type NodeEnv = "development" | "test" | "production";
const nodeEnv = (process.env.NODE_ENV || "development") as NodeEnv;
const requiredBase = ["OPENAI_API_KEY"] as const;
const requiredProd = ["MCP_BASE_URL"] as const;
function ensure(keys: readonly string[]) {
for (const key of keys) {
if (!process.env[key]) {
throw new Error(`Missing env var ${key} for NODE_ENV=${nodeEnv}`);
}
}
}
ensure(requiredBase);
if (nodeEnv === "production") {
ensure(requiredProd);
}
export const config = {
nodeEnv,
openaiApiKey: process.env.OPENAI_API_KEY!,
mcpBaseUrl: process.env.MCP_BASE_URL ?? "http://localhost:3000/mcp",
};
这样的模块可以在生产环境缺少必要变量时第一时间把你“叫醒”。
记录传入的 MCP 请求
给 MCP 路由加一层简单但很有用的日志封装:
// app/lib/mcp-logger.ts
export function logMcpRequest(body: unknown) {
console.log("[mcp] request", {
time: new Date().toISOString(),
// 不记录敏感数据
keys: typeof body === "object" && body !== null
? Object.keys(body as Record<string, unknown>)
: typeof body,
});
}
然后在 app/mcp/route.ts 中使用:
import { logMcpRequest } from "@/app/lib/mcp-logger";
export async function POST(req: NextRequest) {
try {
const body = await req.json();
logMcpRequest(body);
const result = await handleMcpRequest(body);
return NextResponse.json(result);
} catch (error) {
console.error("MCP route error", error);
return NextResponse.json({ error: "Internal error" }, { status: 500 });
}
}
这样你能在日志中看到 ChatGPT 实际发来了什么:至少能看到键名 ("jsonrpc"、"method"、"params"), 更容易判断到底是哪类调用在报错。
一个简单的 MCP 端点健康检查
有时可以为 MCP 服务器新增一个像“healthcheck”的小路由, ChatGPT 不会直接调用,但你可以快速在浏览器里打开, 判断服务是否存活以及是否能看到自己的 env 变量:
// app/api/health/route.ts
import { NextResponse } from "next/server";
import { config } from "@/app/lib/config";
export async function GET() {
return NextResponse.json({
status: "ok",
env: config.nodeEnv,
hasOpenAiKey: !!config.openaiApiKey,
});
}
如果 https://myapp.vercel.app/api/health 返回 status: "ok", 说明至少从入口到你的 Node 代码这条管线是活的。
8. 部署与排查中的常见错误
错误 1:未先在本地执行 npm run build。
如果开发者从不在本地跑构建,他会直到上了 Vercel 才知道 Node 版本不兼容、路径问题或 TS 错误。 这会拉长“破坏 → 修复”的周期,因为每次尝试都是一次新部署。 在 push 到 main 之前养成先跑 npm run build 的习惯能省很多时间(另见第 2 节以及第 6.1 步关于本地 npm run build)。
错误 2:机密只放在 .env.local 里。
项目在作者的机器上完美运行,但生产直接崩,因为 process.env.OPENAI_API_KEY === undefined。 原因太普通:忘了在 Vercel 的设置里添加 env(有时甚至用了不同的名字)。 尤其容易忽略 Development/Preview/Production 的区分,导致 staging 与 prod 行为不一致(详见 3.1、4.3 与 7.1 节)。
错误 3:把 NEXT_PUBLIC_* 用于机密。
在 Next.js 中,所有以 NEXT_PUBLIC_ 开头的变量都会进入浏览器端 bundle。 如果你不小心把 API 密钥命名为 NEXT_PUBLIC_OPENAI_API_KEY, 它就会被送到用户的浏览器,能从 devtools 中被拿走。千万别这么做。 放到前端的只能是安全的公开值(比如特性开关的标识,不是 token)。
错误 4:忽视 Vercel 日志,试图“让 ChatGPT 来修”。
有时开发者在聊天里看到 "Error talking to app" 就狂改 prompt、工具描述, 在 Dev Mode 里到处折腾,却一次都不看 serverless 日志。 而日志里其实写得很清楚:"Missing env var"、"Cannot find module", 或者是某个工具的 stack trace。优秀工程师先看日志,再和模型“争论”。
错误 5:混淆 Dev Mode 与生产 App。
在 Vercel 完成第一次成功部署后,很容易忘了 Dev Mode 可能仍指向旧的隧道或 preview URL。 结果你以为自己在测试生产版,其实是在和一条早该删掉的本地分支对话。 反过来也可能发生:你以为在测试草稿改动,ChatGPT 却在敲生产端点。 要定期检查 App 设置与 Dev Mode 中到底写的是哪个 URL(另见 4.1 节关于 Dev Mode 与生产的说明)。
错误 6:以为在 Vercel 改了 env 会“即时生效”。
有些同学在 Vercel 面板改了变量值,就立刻去 ChatGPT 里验证。 但运行时仍使用旧值,因为没有 redeploy。 任何 env 变量的变更都需要一次新部署,否则函数看不到更新(详见 3.1 节)。
错误 7:缺少简单的回滚策略。
事故当下很容易冲动地“直接往 main 推一个快速修复”。 但这只会增加另一个可能失败的部署,而用户一直在受影响。 更稳妥的习惯是:遇到严重错误先回滚到上一个成功部署, 在单独的分支修复问题,确认后再发布新版本。 Vercel 已经提供了很顺手的回滚能力,完全值得用起来。
GO TO FULL VERSION