我见过最浪费时间的排查,是一上来就把 Vercel、OpenAI、代理层和前端全改一遍。改完能跑,但没人知道是哪一刀生效。下一次流量上来,问题还会回来。
这类超时要按时间线拆。你需要的不是更多日志,而是位置对的日志。
30 秒自检
看四个时间点:
| 检查项 | 你要看到什么 | 常见结论 |
|---|---|---|
| Vercel Function 日志 | 请求是否进入 Edge Function | 没进入,多半是路由或部署问题 |
| 发起 OpenAI 请求时间 | fetch 前后是否有记录 | 卡在上游调用或 DNS/TLS |
| 首个 chunk 时间 | streaming 是否很晚才开始 | 模型排队、提示词过长、供应商延迟 |
| 前端结束时间 | 浏览器是否收到完整流 | 前端 reader、代理缓存、连接中断 |
我一般先加这几行日志,别写太花:
const startedAt = Date.now();
console.info("edge:start", startedAt);
const upstreamStartedAt = Date.now();
const response = await fetch("https://api.openai.com/v1/responses", requestOptions);
console.info("openai:headers", Date.now() - upstreamStartedAt, response.status);
如果是流式响应,再在 stream transform 里记录第一段:
let firstChunk = true;
const stream = new TransformStream({
transform(chunk, controller) {
if (firstChunk) {
console.info("openai:first_chunk_ms", Date.now() - startedAt);
firstChunk = false;
}
controller.enqueue(chunk);
},
});
别在日志里打印完整 prompt、用户输入或 key。只记时间、状态码、模型名和 request id。
最短处理路径
按概率从高到低处理:
- 把非流式改成流式。长回答不要等完整 JSON 回来后再返回给浏览器。OpenAI 文档支持 streaming responses,Vercel 也有 Streaming Functions 文档。
- 给上游请求设置总超时预算。比如交互页 25 秒,后台任务 90 秒,不要让用户界面一直转圈。
- 降低提示词体积。系统 prompt、检索上下文、历史消息全塞进去,首字节会变慢。先截断历史,再考虑缓存。
- 限制重试。429 或 5xx 可以短重试一次,网络断开可以提示用户重试。不要在 Edge 里做长轮询式补救。
- 把长任务挪走。报告生成、批量总结、文件分析这类任务,放到队列、Node Runtime 或后台 worker。Edge 只负责创建任务和回传任务 id。
只改一处的话先改 streaming。它不一定让总生成时间变短,但能让用户和平台更早看到响应开始。
为什么 Edge Runtime 容易暴露 LLM 延迟?
Edge Runtime 的好处是离用户近、冷启动轻,适合短逻辑。LLM 调用正好相反:上游不可控、生成时长和 prompt 长度强相关,还会遇到模型队列、限速和供应商错误。
这里有两个时间很容易混在一起:
- 函数执行时间:Vercel 关心你的函数跑了多久。
- 用户等待时间:浏览器关心什么时候看到第一个字。
非流式调用会把这两个时间绑死。模型没有完整返回前,你的函数看起来像「没动」。改成流式后,第一段内容回来,函数就可以往浏览器写。即使后面还要生成,用户至少知道请求活着。
还有一个坑是 SDK。部分 SDK 在 Edge Runtime 下支持不如 Node 环境完整,尤其是依赖 Node API、连接复用或复杂拦截器时。能用 fetch 就先用 fetch,少一层不确定性。
进阶排查要看哪些日志?
我会建一个最小表,写到 Log Drain 或你自己的事件表里:
| 字段 | 示例 | 用途 |
|---|---|---|
| requestId | req_abc | 串起前后端日志 |
| runtime | edge / node | 对比不同运行时表现 |
| model | gpt-x | 找出慢模型或异常供应商 |
| promptTokens | 12000 | 判断输入是否过大 |
| firstChunkMs | 6800 | 判断首字节延迟 |
| totalMs | 23100 | 判断总生成耗时 |
| retryCount | 1 | 看重试是否放大等待 |
| upstreamStatus | 200 / 429 / 5xx | 判断供应商或限速问题 |
如果 firstChunkMs 很高,优先查模型、prompt、上游网络。如果 firstChunkMs 正常但 totalMs 高,查输出长度和前端消费。如果服务端 totalMs 正常但浏览器一直 pending,查代理缓存、Response header 和 reader 实现。
一个我会保留的兜底逻辑:
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), 25_000);
try {
const upstream = await fetch(url, { ...options, signal: controller.signal });
return new Response(upstream.body, {
headers: {
"Content-Type": "text/event-stream; charset=utf-8",
"Cache-Control": "no-cache, no-transform",
},
});
} finally {
clearTimeout(timer);
}
no-transform 很有用。某些中间层会尝试缓冲或改写响应,流式体验会被拖没。
还没恢复时,单独查 Vercel Edge Runtime OpenAI
做一个 A/B:同一段 prompt,同一模型,同一参数,分别跑 Vercel Edge、Vercel Node Runtime、本地直连脚本。只测 20 次也比猜强。
如果本地也慢,问题在模型或供应商。如果只有 Edge 慢,迁到 Node Runtime 或减少 Edge 里的编排。如果只有某些地区慢,就把 API 层独立出来,别让用户请求链路直接押在单一上游连接上。
独立开发者最怕的是「用户以为产品坏了,其实只是模型供应商慢」。这时可以准备一个兼容 OpenAI 协议的备用通道,文中我只放一个选择:稳定调用 Claude / OpenAI 的中转服务。接入前先用压测脚本看首字节和错误码,不要只看宣传页。