Node.js 接入 OpenAI API 完整教程
Node.js 和 TypeScript 项目接大模型,官方 openai SDK 十几行代码就能跑通。这篇覆盖安装配置、baseURL 指向兼容网关、流式输出、超时重试和常见报错排查,附 Next.js 与 Express 两种落地写法。
Node.js 是大模型应用最常见的技术栈,官方 openai npm 包对 TypeScript 支持完整,十几行代码就能跑通一次对话。真正容易出问题的不是调用本身,而是三件事:baseURL 该不该带 /v1、流式在框架里被缓冲、以及超时和重试没配置导致线上挂起。这篇按可直接复制的最小实现讲清楚。
先说结论
- 用官方
openai包(v4+),不要手写 fetch,也不要用社区停更的封装库 - 构造时传
baseURL: 'https://api.zaimod.com/v1',要带/v1,这一点与部分框架(如 Spring AI)相反 - API Key 一律走环境变量
OPENAI_API_KEY或自定义变量,不要写进代码提交到仓库 - 流式输出用
stream: true配合for await消费,Web 框架里注意关闭响应缓冲
安装与最小可运行示例
Node 18 及以上自带 fetch,SDK 开箱即用:
npm install openai
// chat.mjs
import OpenAI from 'openai';
const client = new OpenAI({
apiKey: process.env.ZAIMOD_API_KEY,
baseURL: 'https://api.zaimod.com/v1',
});
const res = await client.chat.completions.create({
model: 'gpt-4o-mini',
messages: [{ role: 'user', content: '用一句话解释什么是 API 网关' }],
});
console.log(res.choices[0].message.content);
运行:
export ZAIMOD_API_KEY=sk-你的Key # Windows PowerShell 用 $env:ZAIMOD_API_KEY="sk-..."
node chat.mjs
换模型只改 model 字符串。zaimod 这类聚合网关下,Claude、Gemini、DeepSeek 等模型走同一个 baseURL 和同一个 Key,协议原理见 OpenAI 兼容协议详解。
TypeScript 写法
SDK 自带完整类型,消息体用 ChatCompletionMessageParam 类型标注即可获得角色与内容检查:
import OpenAI from 'openai';
import type { ChatCompletionMessageParam } from 'openai/resources/chat/completions';
const client = new OpenAI({
apiKey: process.env.ZAIMOD_API_KEY,
baseURL: 'https://api.zaimod.com/v1',
});
const messages: ChatCompletionMessageParam[] = [
{ role: 'system', content: '你是严谨的技术助手,回答控制在 200 字内。' },
{ role: 'user', content: 'ESM 和 CJS 的区别是什么?' },
];
const res = await client.chat.completions.create({
model: 'gpt-4o-mini',
messages,
max_tokens: 256,
});
const text = res.choices[0]?.message?.content ?? '';
注意 choices[0].message.content 类型是 string | null——模型只返回工具调用时内容为空,必须做空值处理,否则 TS 编译就过不了,这其实帮你挡住了线上的一类空指针。
流式输出
把 stream: true 打开后,返回值变成异步迭代器:
const stream = await client.chat.completions.create({
model: 'gpt-4o-mini',
messages: [{ role: 'user', content: '写一首关于数据中心的短诗' }],
stream: true,
});
for await (const chunk of stream) {
const delta = chunk.choices[0]?.delta?.content;
if (delta) process.stdout.write(delta);
}
在 Web 框架里接入时有两个坑:
Express:用 res.write() 逐段写出,并设置 Content-Type: text/event-stream。如果前面挂了 Nginx,记得 proxy_buffering off;,否则用户看到的是最后一次性吐出全文。
Next.js App Router:Route Handler 直接把 SDK 流转成 ReadableStream 返回即可,不要用 await res.json() 之类把流读完的中间层。SSE 协议细节与前端 EventSource 写法参考 SSE 流式输出从 curl 到前端。
超时、重试与限流
SDK 默认会重试两次(仅对连接错误和 429/5xx),但默认超时 10 分钟对多数业务太长,建议显式设置:
const client = new OpenAI({
apiKey: process.env.ZAIMOD_API_KEY,
baseURL: 'https://api.zaimod.com/v1',
timeout: 60_000, // 单次请求超时
maxRetries: 2, // SDK 内置重试次数
});
捕获限流错误做退避:
import OpenAI from 'openai';
async function chatWithBackoff(params, maxAttempts = 3) {
for (let i = 0; i < maxAttempts; i++) {
try {
return await client.chat.completions.create(params);
} catch (err) {
if (err instanceof OpenAI.APIError && err.status === 429 && i < maxAttempts - 1) {
await new Promise(r => setTimeout(r, 2 ** i * 1000 + Math.random() * 500));
continue;
}
throw err;
}
}
}
限流触发后的完整排查思路(按 RPM/TPM 哪个先超、如何申请提额)见 大模型 API 429 限流怎么解。
常见报错对照
| 报错信息 | 原因 | 处理 |
|---|---|---|
| 404 Not Found | baseURL 少了或多了 /v1 | 官方 SDK 固定写 https://api.zaimod.com/v1 |
| 401 Incorrect API key | Key 错误或环境变量没生效 | echo 检查变量名,注意进程需重启才会读到新变量 |
| 429 Rate limit | 并发或 token 速率超限 | 指数退避 + 检查调用方并发 |
| ESM/CJS 报错 ERR_REQUIRE_ESM | 老项目用 require 引 openai | openai v4 是纯 ESM 包,改 import 或降级方案 |
| 流式一次性返回 | 中间层缓冲了 SSE | 关 Nginx proxy_buffering,去掉多余中间件 |
常见问题
Q:Next.js 服务端组件里能直接调吗?
能,但建议放 Route Handler 或 Server Actions 里,并把 Key 只放在服务端环境变量。NEXT_PUBLIC_ 前缀的变量会打进浏览器 bundle,等于把 Key 公开,绝对不要这样放。
Q:怎么统计每个请求花了多少 token?
非流式响应里有 res.usage(prompt_tokens 与 completion_tokens),流式可请求 stream_options: { include_usage: true },最后一个 chunk 会带 usage。计费单价在 价格速查 按模型列出。
Q:一个 Node 进程该建几个 client 实例?
一个。OpenAI 实例内部维护连接池,做成模块级单例即可,高频 new 会导致连接抖动。若要按模型或按租户区分超时策略,可以建少量命名实例。