从 0 到 1:Node.js 调用 AI API 的完整避坑指南

别急着写代码,先把"钥匙"藏好

你有没有过这种经历:辛辛苦苦写了一个 AI 小工具,兴冲冲地开源到 GitHub,结果第二天收到 AWS 账单------几千美元。点开一看,有人用你的 API Key 挖矿了。

别笑,这种事真真切切地发生在无数开发者身上。API Key 就是你通往 AI 服务的"钥匙",一旦泄露,轻则被薅羊毛,重则倾家荡产。

所以,在写第一行代码之前,我们先来聊聊怎么把这个"钥匙"藏好。

.env 文件:你的秘密保险箱

在项目根目录创建一个 .env 文件,内容大概长这样:

ini 复制代码
DEEPSEEK_API_KEY=sk-xxxxxxxxxxxxxxxxxxxx

注意格式:KEY=VALUE,而且 KEY 一定要大写。这是约定俗成的规矩,dotenv 库就是靠这个约定来解析的。

然后在 .gitignore 文件里加上:

bash 复制代码
.env

这样 Git 就不会把这个文件提交到远程仓库了,你的密钥只属于你自己。

dotenv:让环境变量"活"起来

光有 .env 文件还不够,代码怎么读取它呢?

javascript 复制代码
import dotenv from 'dotenv'
dotenv.config()

就这么两行代码,dotenv 会自动帮你读取 .env 文件,然后把里面的键值对加载到 process.env 对象中。

process.env:Node.js 的"环境字典"

process 是一个全局对象,代表当前 Node.js 进程。而 process.env 就是这个进程的"环境字典",里面存放着各种环境变量。

javascript 复制代码
const apiKey = process.env.DEEPSEEK_API_KEY
console.log(apiKey) // sk-xxxxxxxxxxxxxxxxxxxx

为什么要这么大费周章?直接写死不行吗?

不行。因为不同环境需要不同的配置:

  • 开发环境用测试 Key
  • 生产环境用正式 Key
  • 你的 Key 和我的 Key 不一样

把配置抽离到环境变量里,代码就能"一次编写,多处运行"了。


模块化开发:import 和 export 的艺术

假设你写了一个工具函数,想在多个地方复用。这时候就需要模块化了。

ES6 模块方案 vs 传统 CommonJS

Node.js 最初用的是 CommonJS 方案:

javascript 复制代码
// 导出
module.exports = { add }

// 导入
const { add } = require('./utils')

ES6 推出了更现代的模块方案:

javascript 复制代码
// 导出
export const add = (a, b) => a + b

// 导入
import { add } from './utils'

两者区别在哪?简单来说:

  • CommonJS 是运行时加载
  • ES6 模块是编译时加载

编译时加载意味着什么?意味着性能更好,静态分析更方便,树摇(Tree Shaking)更彻底。所以现在新建项目,强烈推荐用 ES6 模块方案。

.mjs 后缀:让浏览器也能读懂

如果你想把文件命名为 .js 但又想用 ES6 模块,只需要在 package.json 里加一行:

json 复制代码
{
  "type": "module"
}

但如果你的文件叫 index.mjs,那就不需要改 package.json 了------.mjs 后缀天然就是 ES6 模块的标识。

所以你现在看到的代码文件都是 index.mjs,就是这个原因。


async/await:让异步代码"同步"起来

终于到重头戏了。

为什么 API 请求要用 async?

看这段代码:

javascript 复制代码
const result = client.chat.completions.create({
    messages: [{ role: 'user', content: '你好' }]
})
console.log(result) // 打印出来看看?

如果你直接这么写,打印出来的可能不是 AI 的回复,而是一个 Promise 对象。因为 chat.completions.create 是一个异步操作------它需要时间等待服务器响应。

JavaScript 的执行顺序是这样的:

  1. 同步代码:按顺序立即执行
  2. 异步代码:交给 Web APIs / Node.js 处理,不阻塞主线程

API 请求属于异步代码。如果不特殊处理,代码会"跳过"等待,直接执行下一行。

await 到底在"等"什么?

await 关键字的作用就是:暂停当前函数的执行,等待 Promise 完成后再继续

javascript 复制代码
const main = async () => {
    console.log('开始请求')
    const result = await client.chat.completions.create({...})
    console.log('收到回复')
    console.log(result.choices[0].message.content)
}

执行顺序变成了:

css 复制代码
开始请求
[等待 API 响应...]
收到回复
[打印 AI 的回复]

如果不用 await,顺序就变成了:

css 复制代码
开始请求
[立即执行下一行]
收到回复(不对,应该是 Promise)
[打印 Promise 对象]

async 函数:承诺一定会有结果

async 函数有什么特别?它的返回值一定是一个 Promise。

javascript 复制代码
async function getData() {
    return 'Hello'
}

// 等价于:
function getData() {
    return Promise.resolve('Hello')
}

所以 main 函数前面要加 async,是为了告诉 JavaScript:"这个函数里有异步操作,我要用 await 来控制执行顺序。"


实战:调用 AI API 的正确姿势

好了,理论基础打扎实了,现在来写真正的代码。

OpenAI SDK 的初始化

javascript 复制代码
import { OpenAI } from 'openai'
import dotenv from 'dotenv'

dotenv.config()

const client = new OpenAI({
    apiKey: process.env.DEEPSEEK_API_KEY,
    baseURL: 'https://api.deepseek.com/v1',
    model: 'deepseek-chat',
})

注意这里用的是 new OpenAI(),不是 OpenAI()。因为 OpenAI 是一个,必须实例化才能使用。

chat.completions.create 的正确调用方式

javascript 复制代码
const main = async () => {
    const result = await client.chat.completions.create({
        model: 'deepseek-chat',
        messages: [
            {
                role: 'user',
                content: '你好'
            }
        ]
    })
    console.log(result.choices[0].message.content)
}

main()

参数解释:

  • model:指定用哪个模型
  • messages:对话历史,数组格式
    • role:角色,user 表示用户,assistant 表示 AI
    • content:消息内容

常见报错及解决方案

报错 1:Class constructor OpenAI cannot be invoked without 'new'

arduino 复制代码
原因:忘了加 new 关键字
解决:const client = new OpenAI({...})

报错 2:APIConnectionError: Connection error

markdown 复制代码
原因:网络连接问题,可能是:
  - DNS 解析失败
  - 代理配置问题
  - 网络不可达
解决:
  - 检查网络连接
  - 配置代理环境变量
  - 确认 API 地址是否正确

报错 3:Invalid API Key

markdown 复制代码
原因:API Key 无效或未设置
解决:
  - 确认 .env 文件存在且格式正确
  - 确认 KEY 名称匹配(DEEPSEEK_API_KEY)
  - 确认 API Key 没有过期

AIGC 工程化开发流程总结

恭喜你,看到这里说明你已经掌握了 AI 项目开发的核心套路。让我来总结一下:

AI 项目的标准开发套路

  1. 初始化项目

    bash 复制代码
    npm init -y
    npm i openai dotenv
  2. 配置环境变量

    • 创建 .env 文件,存放 API Key
    • .gitignore 中忽略 .env
  3. 初始化 AI 客户端

    • 实例化 OpenAI(或其他 SDK)
    • 配置 baseURL 和 model
  4. 编写入口函数

    • async 声明异步函数
    • await 等待 API 响应
  5. 调用 chat.completions.create

    • 构建 messages 数组
    • 解析返回结果

从"Hello World"到"智能助手"的进化之路

今天你学会的只是一个最简单的例子------给 AI 发一条消息,收到一条回复。

但这只是冰山一角。

真正的 AI 应用可能是:

  • 对话式客服机器人
  • 智能代码助手
  • 自动文案生成器
  • 多轮对话的记忆管理
  • 工具调用和插件系统

每一步进阶都需要在今天的基础上加更多"调料":流式输出、错误重试、上下文管理、Token 优化...

但无论如何,核心永远是这几行代码。你今天学到的,是 AIGC 开发的"内功心法"。


最后留个思考题:如果让你给 AI 加一个"记住对话历史"的功能,你会怎么改这段代码?

欢迎在评论区秀出你的方案!

相关推荐
神明不懂浪漫1 小时前
【第七章】Java中的常用类
java·开发语言·前端·经验分享·笔记
GuWenyue7 小时前
放弃云端API!一套React+WebGPU本地LLM方案,零数据上传、离线可用
前端·人工智能·前端框架
原则猫8 小时前
函数/变量提升
前端
独泪了无痕9 小时前
Vue3 Hooks使用实战解析
前端·vue.js
shawxlee11 小时前
vue3在public下封装config.js自定义配置动态数据,可在打包后直接修改,方便后端部署及后续维护
前端·javascript·经验分享·vue·团队开发·js·项目优化
用户0595401744611 小时前
把记忆存储的回归测试从手工换成 Playwright + GitHub Actions,线上缺陷降低 80%
前端·css
触底反弹11 小时前
🚀 从 DOM0 级到 React 合成事件:前端事件监听的 20 年演进史
前端·react.js·面试
kyriewen11 小时前
我给前端项目的接口请求套了6层保护——才发现以前一直在裸奔
前端·javascript·面试
颜酱11 小时前
04 | 召回前置准备:搭好召回所需的四个数据库
前端·人工智能·后端
郝亚军13 小时前
如何安装webstorm、Node.js和vue CLI
前端·javascript·vue.js