Node.js 调用大模型 API 排错实录

你在 Node.js 里第一次调用大模型 API,终端直接返回 401 Unauthorized,或者流式输出只打出半句话就停住。这类问题看起来像网络抖动,实际往往是参数填错。读完这篇,你能拿到一个依赖最少的最小示例,并知道每个关键参数填错的后果。

场景与前置条件

这篇解决的是 Node.js 调用大模型 API 时最常见的两类问题:认证头配错,以及流式响应解析不完整。不涉及 Python,不涉及框架集成。你需要先有一个可用的大模型 API Key,并且已经在千木控制台创建过密钥。这个 Key 前缀是 sk_live_,只在创建时显示一次,丢了只能重新生成。

先说环境限制,避免你做到一半发现版本不对。示例基于 Node.js 18 以上版本,因为代码直接使用全局 fetch。18 以下需要额外安装 node-fetch 或 axios,行为略有差异。模型调用名使用 deepseek-v3.2,这个名称必须从模型广场详情页复制,不要根据展示名自己拼版本号。OpenAI 兼容端点是 /chat/completions,后文所有示例都围绕它展开。

环境准备

不需要安装任何 npm 包。创建一个新目录,在目录里放一个 package.json,将 type 设置为 module,这样可以使用 top-level await。示例文件命名为 index.mjs 也可以直接运行。如果你坚持用 CommonJS,把 import 改成 require 就行,但本教程不这么写。

API Key 要放进环境变量,不要在代码里硬编码。硬编码会让密钥跟着代码一起进入 Git 历史,排查问题时也容易在日志里泄露。Linux 或 macOS 终端里执行 export,Windows PowerShell 里用 $env: 赋值。代码里通过 process.env 读取环境变量,这是唯一推荐做法。Base URL 只有一个,示例里已经写死,你不需要修改,尤其注意不要写成带 api 子域名的形式。这个地址的根路径是 /api/v1,后面接 /chat/completions 才是对话端点。

最小可跑示例

先跑一个非流式请求,拿到完整回复就退出。把下面代码保存为 index.mjs,然后 node index.mjs。

javascript 复制代码
const baseUrl = 'https://www.silvamux.com/api/v1';
const apiKey = process.env.SILVAMUX_API_KEY;

const response = await fetch(`${baseUrl}/chat/completions`, {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': `Bearer ${apiKey}`
  },
  body: JSON.stringify({
    model: 'deepseek-v3.2',
    messages: [
      { role: 'user', content: '用一句话解释 Node.js 事件循环' }
    ],
    stream: false
  })
});

if (!response.ok) {
  const errText = await response.text();
  console.error('请求失败', response.status, errText);
  process.exit(1);
}

const data = await response.json();
console.log(data.choices[0].message.content);

代码不长,但里面几个参数不能填错。model 必须使用平台规范后的调用名,这里填 deepseek-v3.2。如果你按上游厂商的命名习惯写成别的版本号,会收到 400 或 gateway_error。stream 填 false 表示这次要一次性拿完整响应,不要传字符串 'false',布尔值和字符串在类型检查时会不一样。messages 数组的第一个元素必须带 role,role 只能是 user、assistant、system 其中之一。Authorization 头部的 Bearer 后面有一个空格,少了这个空格,服务端会认为没有认证信息。

关键在 baseUrl 和 apiKey 两行。baseUrl 已经是完整的根路径,后面拼接 /chat/completions 时不要重复加 /api/v1。apiKey 从环境变量读取,如果读到 undefined,fetch 也会发出请求,但那会变成 401。跑之前先 echo 一下环境变量,确认不是空值。

逐步扩展

真实业务里很少用非流式请求,因为长回复会让人等很久。流式输出能边生成边显示,体验好得多。在最小示例上加一个流式版本,顺便加 30 秒超时控制。超时用 AbortController,超时后 fetch 会被中止,而不是一直挂着。

javascript 复制代码
const baseUrl = 'https://www.silvamux.com/api/v1';
const apiKey = process.env.SILVAMUX_API_KEY;

const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), 30000);

const response = await fetch(`${baseUrl}/chat/completions`, {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': `Bearer ${apiKey}`
  },
  body: JSON.stringify({
    model: 'deepseek-v3.2',
    messages: [{ role: 'user', content: '写一段关于初秋傍晚的场景描写' }],
    stream: true
  }),
  signal: controller.signal
});

if (!response.ok || !response.body) {
  clearTimeout(timer);
  console.error('请求失败', response.status);
  process.exit(1);
}

const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = '';

while (true) {
  const { done, value } = await reader.read();
  if (done) break;
  buffer += decoder.decode(value, { stream: true });
  const lines = buffer.split('\n');
  buffer = lines.pop() ?? '';
  for (const line of lines) {
    const trimmed = line.trim();
    if (!trimmed.startsWith('data:')) continue;
    const json = trimmed.slice(5).trim();
    if (json === '[DONE]') continue;
    try {
      const chunk = JSON.parse(json);
      const delta = chunk.choices?.[0]?.delta?.content;
      if (delta) process.stdout.write(delta);
    } catch {
      // 忽略半行数据
    }
  }
}

clearTimeout(timer);
console.log('\n流结束');

这个版本和非流式的差别集中在三点。第一,stream 变成 true,服务端会返回 SSE 格式的增量数据。第二,用 response.body.getReader() 逐块读取,而不是 response.json() 一次性解析。第三,每一行 SSE 都可能包含空 choices,甚至最后可能没有 data: DONE。所以代码没有用 DONE 作为唯一结束条件,而是循环到 done 为 true 才跳出。这个细节在 Node.js 里特别容易错。

坑在这里:如果你只判断 DONE 就退出循环,可能会漏掉最后一条内容,或者在一个只带 usage 的 chunk 上卡住。正确做法是继续读取直到流结束。TextDecoder 的 { stream: true } 参数也不要省,它能避免多字节字符被截断。超时时间 30000 毫秒可以按需调整,但建议保留,否则请求一旦挂起,Node 进程不会自动退出。

排错

下面三个报错是 Node.js 调用大模型 API 时最高频的,按状态码区分。

401 Unauthorized

报错文本:HTTP 状态码 401,终端通常会打印 401 Unauthorized,响应体里 error.type 多为 gateway_error,code 字段按网关策略变化。

直接原因:请求头里的 Authorization 值不对。最常见的是拼成了 Authorization: apiKey,缺少 Bearer 前缀,或者 Bearer 后面没有空格,又或者 API Key 本身少了 sk_live_ 前缀。

修法:先检查环境变量是否有值,用 console.log(process.env.环境变量名) 打出来看是否为 undefined。再看 fetch 头部是不是写成了 Bearer ${apiKey}。最后去密钥页重新生成一个,因为旧 Key 可能已经丢失。

400 Bad Request

报错文本:状态码 400,响应体同样包含 error.message 和 type 字段,message 会因模型名不同而变化。

直接原因:model 参数填错,或者 stream 传了字符串 'false' 或 'true'。模型调用名必须来自平台模型广场,不能自己根据展示名拼接版本号。比如写 deepseek 或 deepseek-chat 都可能不是平台规范名。

修法:打开模型广场详情页,复制稳定的 alias,也就是调用名。示例里用的 deepseek-v3.2 是可直接复制的名称。stream 改成布尔值 true 或 false。

流式输出中断

报错文本:有时没有报错,只是终端只显示了半句话,进程就退出了;有时一直挂住不结束。

直接原因:客户端用 data: DONE 判断结束,但服务端可能在末尾返回一个只带 usage、choices 为空的 chunk,甚至省略 DONE。Node.js 读到这种 chunk 后如果不继续读,就会提前结束或卡死。

修法:把结束条件从"收到 DONE"改成"read() 返回 done 为 true"。在循环里忽略空 choices 和 DONE,但不要 break。这样无论服务端是否发送 DONE,客户端都能等到真正的流结束。这个修法也适用于其他 OpenAI 兼容端点。

常见问题

问:Node.js 版本必须是 18 吗?

不一定。Node.js 18 以上内置 fetch,所以示例能直接跑。18 以下可以安装 node-fetch@3 或 axios,但你需要把 fetch 调用替换成对应库的请求方法,响应体的流式读取方式也不同。如果只是做最小验证,建议用 Node.js 18 或 20,避免工具链干扰。

问:API Key 放在环境变量里,重启终端后就不见了?

是的,用 export 设置的环境变量只在当前终端会话有效。Windows 上重启后也会消失。如果要持久化,可以把 export 写进 .bashrc 或 .zshrc,但不要把这些文件提交到 Git。生产环境建议用密钥管理服务,而不是写在项目的 .env 文件里再硬编码进前端。

问:为什么模型名复制过来还是报 400?

因为展示名和调用名可能不是一回事。有些模型展示为"DeepSeek V3.2",但调用名是 deepseek-v3.2。必须从模型广场详情页的"调用名"或"模型 ID"字段复制,不要根据页面标题自己拼。拼错任何一个字符,请求都会在网关层被拦截。

以上示例在千木SilvaMux的 OpenAI 兼容端点上验证,模型调用名为 deepseek-v3.2。模型调用名和错误码字段以开发者文档为准。

相关推荐
Amumu121384 小时前
NodeJs介绍
前端·node.js
孟陬5 小时前
利用 TypeScript `asserts` function 语法定制一个类型缩窄的 `assertIncludes`
typescript·node.js
damoluomu5 小时前
静态站点生成器怎么选:Node.js 生态里常用的五个(番外一)
javascript·css·typescript·node.js·html
濮水大叔6 小时前
Cabloy/Vona 如何避开 NestJS 的循环依赖问题:懒属性注入机制源码解析
typescript·node.js·nestjs
月走乂山13 小时前
DSH 桌面版用 DeepSeek 报 REQUEST_EXTENSION 的根因定位与修复
electron·node.js·插件系统·deepseek·bug排查·dsh·deepseek harness
斯晨的AI笔记1 天前
在服务器上跑 npm ci 四十分钟没装完,我打包把 node_modules 扔了过去
服务器·node.js
cpolar技术支持1 天前
第三方平台回调总说收不到?把本机 Webhook 收单页跑通,用 cpolar 固定二级子域名给对面看实时请求
node.js·内网穿透·webhook·cpolar·回调对接
damoluomu1 天前
Node.js CMS 的官网和仓库,22 个一次收齐(五)
node.js