以前觉得调 AI 接口就是把 key 直接贴进去,
后来发现那样做会被 Git 历史"出卖"。
一、为什么要"工程化"?
最开始调大模型 API,我是这样写的:
js
php
const client = new OpenAI({
apiKey: "sk-1234567890abcdef"
});
代码能跑,但有个致命问题:我把钥匙贴在门上了 。
一旦 git push,全世界都能看到你的 key。别人拿去用,账单算你的。
所以需要一套"工程化"流程:
- key 不写死在代码里
- 每个开发者用自己的 key(或者运维统一管理)
- 代码仓库里只有配置模板,没有真实 key
这套流程在 Node.js 里怎么实现?
二、初始化项目:从 npm init 开始
新建一个目录,打开终端:
bash
perl
mkdir my-ai-project
cd my-ai-project
npm init -y
-y 表示所有选项都取默认值。执行后会生成 package.json,内容大致如下:
json
bash
{
"name": "my-ai-project",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo "Error: no test specified" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC"
}
这个文件是项目的"身份证",记录了项目名称、版本、依赖、脚本等信息。
2.1 为什么需要 "type": "module"
现代 JavaScript 有两种模块规范:
- CommonJS (Node.js 默认):用
require()和module.exports - ES Module (ES6 标准):用
import和export
我们想写 import { OpenAI } from 'openai',而不是 const { OpenAI } = require('openai')。
所以需要在 package.json 里加一行:
json
json
{
"type": "module"
}
这样所有 .js 文件默认就是 ES 模块。你也可以用 .mjs 后缀(不写 "type": "module" 也行)
三、安装依赖:openai 和 dotenv
bash
npm install openai dotenv
- openai:OpenAI 官方 SDK,DeepSeek 兼容它的接口格式,所以可以直接用。
- dotenv :读取
.env文件,把里面的变量加载到process.env。
3.1 关于包管理器:npm vs pnpm
pnpm和 npm 的区别是:
npm每个项目都会把依赖包复制一份到node_modules,占用磁盘空间。pnpm使用全局存储 + 硬链接,多个项目共享同一份包,省空间且安装更快。
如果你经常做多个 AI 项目,可以试试全局安装 pnpm:
bash
npm install -g pnpm
pnpm install openai dotenv
用法和 npm 基本一样。
四、藏好你的 API Key
4.1 创建 .env 文件
在项目根目录新建 .env:
text
ini
DEEPSEEK_API_KEY=sk-你的真实key
DEEPSEEK_BASE_URL=https://api.deepseek.com/v1
变量名通常用大写 + 下划线,这是行业惯例。
注意:等号两边不要有空格,值也不要加引号(除非值本身包含空格)。
4.2 创建 .gitignore
确保 .env 不会被提交到 Git:
text
bash
.env
node_modules/
如果有 pnpm-lock.yaml 或 package-lock.json,这些应该提交到仓库,用来锁定依赖版本。
4.3 用 dotenv 读取
js
arduino
import dotenv from 'dotenv';
dotenv.config();
console.log(process.env.DEEPSEEK_API_KEY); // 能读到,但别真打印
dotenv.config() 会做几件事:
- 在当前目录找
.env文件 - 按行解析(
KEY=VALUE) - 把键值对挂到
process.env对象上
如果文件不存在或读取失败,不会报错,只是 process.env 里没有对应的值。
4.4 process 是什么?
process 是 Node.js 的全局对象 ,代表当前运行的进程。
进程是操作系统分配资源(内存、CPU、文件句柄)的最小单位。你执行 node index.js 时,操作系统就启动了一个进程。
process 对象有很多属性:
process.env:环境变量字典process.argv:命令行参数数组process.cwd():当前工作目录process.exit():退出进程
process.env 里除了你从 .env 加载的变量,还有一些系统默认的(比如 PATH、HOME 等)。
注意 :
.env文件只在本地开发时用。在生产环境(比如部署到服务器),通常直接设置系统的环境变量,而不是放.env文件。
五、创建客户端并调用 API
5.1 实例化 OpenAI 客户端
js
arduino
import { OpenAI } from 'openai';
const client = new OpenAI({
apiKey: process.env.DEEPSEEK_API_KEY,
baseURL: process.env.DEEPSEEK_BASE_URL
});
如果不传 baseURL,SDK 默认指向 https://api.openai.com/v1。DeepSeek 提供了兼容的端点,所以只需要改 baseURL 即可。
5.2 写一个异步入口函数
因为调用 API 是异步操作(发送 HTTP 请求,等网络返回),需要用 async/await 来"卡住"执行流程:
js
javascript
const main = async () => {
console.log('程序开始运行');
const result = await client.chat.completions.create({
model: 'deepseek-chat',
messages: [{ role: 'user', content: 'hello' }]
});
console.log(result.choices[0].message.content);
console.log('程序结束');
};
main();
如果不加 await 会怎样?
js
arduino
// 错误示例
const result = client.chat.completions.create(...); // 返回 Promise,不是结果
console.log(result.choices); // TypeError: Cannot read property 'choices' of undefined
client.chat.completions.create() 返回的是一个 Promise 对象,不是最终数据。await 会等待 Promise 完成,然后把结果解包出来。
为什么程序不会在 await 那行卡死?
await 只是让当前 async 函数暂停,并不阻塞整个进程。Node.js 的事件循环会去处理其他任务(比如其他定时器、I/O),等 API 响应回来了,再继续执行后面的代码。
5.3 运行
bash
node index.mjs
或者如果你用了 nodemon(自动重启),可以安装:
bash
npm install -g nodemon
nodemon index.mjs
每次保存文件,nodemon 会自动重启进程,省得手动重跑。
六、关于模块后缀:.js vs .mjs
笔记里用了 index.mjs 后缀。这是 Node.js 早期区分模块类型的方式:
.js→ CommonJS(除非package.json里有"type": "module").mjs→ ES Module(无论package.json如何)
现在更推荐的做法是:在 package.json 里写 "type": "module" ,然后所有 .js 文件默认就是 ES 模块。这样你不需要改后缀,也符合大多数人的直觉。
两种方式选一种就行,不要混用。
七、异步编程深入:为什么需要 async/await
7.1 同步 vs 异步
js
javascript
console.log('1');
setTimeout(() => console.log('2'), 1000);
console.log('3');
// 输出顺序:1, 3, 2
setTimeout 是异步的:它告诉浏览器/Node"1 秒后执行这个函数",然后立即继续执行后面的代码,不会卡住。
API 请求也是这样:发送请求后,网络传输需要时间,程序不能干等着。所以设计成异步的------发出去,等结果回来了再处理。
7.2 回调地狱 vs Promise vs async/await
早期的异步写法是用回调:
js
javascript
client.chat.completions.create({ ... }, (err, result) => {
if (err) throw err;
console.log(result);
});
如果多个请求有先后顺序,就会层层嵌套,变成"回调地狱"。
Promise 解决了嵌套问题,但需要链式调用:
js
ini
client.chat.completions.create({ ... })
.then(result => {
return client.chat.completions.create({ ... });
})
.then(result2 => {
console.log(result2);
});
async/await 让代码看起来像同步的,更容易理解:
js
ini
const result1 = await client.chat.completions.create({ ... });
const result2 = await client.chat.completions.create({ ... });
console.log(result2);
7.3 main 函数为什么是 async?
因为函数内部使用了 await。await 只能在 async 函数内部使用。
调用 main() 时,它返回一个 Promise。如果你不关心它的完成时机,直接调用就行。如果想等它完成再退出程序,可以:
js
javascript
main().then(() => console.log('全部完成'));
八、完整的代码示例
index.js(或 index.mjs)
js
javascript
import dotenv from 'dotenv';
import { OpenAI } from 'openai';
dotenv.config();
const client = new OpenAI({
apiKey: process.env.DEEPSEEK_API_KEY,
baseURL: process.env.DEEPSEEK_BASE_URL
});
const main = async () => {
console.log('程序开始运行');
try {
const result = await client.chat.completions.create({
model: 'deepseek-chat',
messages: [{ role: 'user', content: '用一句话解释什么是异步编程' }],
temperature: 0.3,
max_tokens: 100
});
console.log('AI 回答:', result.choices[0].message.content);
} catch (error) {
console.error('调用失败:', error.message);
}
console.log('程序结束');
};
main();
.env
text
ini
DEEPSEEK_API_KEY=sk-你的真实key
DEEPSEEK_BASE_URL=https://api.deepseek.com/v1
.gitignore
text
bash
.env
node_modules/
package.json 关键部分
json
json
{
"type": "module",
"dependencies": {
"dotenv": "^17.4.2",
"openai": "^6.39.1"
}
}
九、AIGC 工程化开发流程总结
| 步骤 | 做什么 | 为什么 |
|---|---|---|
| 1 | npm init -y |
初始化项目,生成 package.json |
| 2 | "type": "module" |
支持 import 语法 |
| 3 | npm i openai dotenv |
安装 SDK 和环境变量加载库 |
| 4 | 创建 .env + .gitignore |
安全存放 API Key,不提交到仓库 |
| 5 | dotenv.config() |
读取 .env 到 process.env |
| 6 | 创建 client 实例 |
配置 apiKey 和 baseURL |
| 7 | 写 async 入口函数 |
用 await 等待 API 响应 |
| 8 | 调用 client.chat.completions.create |
发送请求并处理返回 |