藏好Key的小妙招,从搭建node.js+大模型项目开始

以前觉得调 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 标准):用 importexport

我们想写 import { OpenAI } from 'openai',而不是 const { OpenAI } = require('openai')

所以需要在 package.json 里加一行:

json

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

这样所有 .js 文件默认就是 ES 模块。你也可以用 .mjs 后缀(不写 "type": "module" 也行)


三、安装依赖:openaidotenv

bash

复制代码
npm install openai dotenv
  • openai:OpenAI 官方 SDK,DeepSeek 兼容它的接口格式,所以可以直接用。
  • dotenv :读取 .env 文件,把里面的变量加载到 process.env

3.1 关于包管理器:npm vs pnpm

pnpmnpm 的区别是:

  • 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.yamlpackage-lock.json,这些应该提交到仓库,用来锁定依赖版本。

4.3 用 dotenv 读取

js

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

console.log(process.env.DEEPSEEK_API_KEY);  // 能读到,但别真打印

dotenv.config() 会做几件事:

  1. 在当前目录找 .env 文件
  2. 按行解析(KEY=VALUE
  3. 把键值对挂到 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 加载的变量,还有一些系统默认的(比如 PATHHOME 等)。

注意.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

因为函数内部使用了 awaitawait 只能在 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() 读取 .envprocess.env
6 创建 client 实例 配置 apiKeybaseURL
7 async 入口函数 await 等待 API 响应
8 调用 client.chat.completions.create 发送请求并处理返回

相关推荐
扫地僧9851 小时前
基于改进版YOLOv11的海洋垃圾检测系统设计与实现
人工智能·深度学习·yolo
ZHW_AI课题组1 小时前
基于XGBoost的鸢尾花花瓣长度回归预测
人工智能·数据挖掘·回归
前端摸鱼匠1 小时前
YOLOv11 深入 Ultralytics 框架的源码目录,解析 ultralytics/cfg/models/11/ 下的模型配置文件,以及 ultralytics/nn/modules/下的模块
人工智能·yolo·目标检测·计算机视觉·目标跟踪
KaMeidebaby1 小时前
卡梅德生物技术快报|组蛋白乙酰化修饰调控动脉粥样硬化的分子机制及中药表观干预研究
网络·人工智能·网络协议·tcp/ip·算法
SEO_juper1 小时前
搜索进入 Agentic 智能体时代,内容要能 “被 AI 直接用”
人工智能·ai·seo·跨境电商·geo·谷歌优化·2026
装不满的克莱因瓶1 小时前
机器学习和数据科学的基石:NumPy详解与实战技巧
人工智能·线性代数·机器学习·ai·矩阵·numpy
好好风格1 小时前
微软这个 14 万星工具,把 PDF、PPT、Excel 都变成大模型爱读的 Markdown
人工智能·python·开源
小糖学代码1 小时前
机器学习:2.线性回归
人工智能·机器学习·线性回归
装不满的克莱因瓶1 小时前
什么是正态分布与标准正态分布?从身高统计到机器学习全面理解
人工智能·深度学习·机器学习·ai·numpy