最刚开始上手对接 DeepSeek 接口的时候,我图赶速度,把 API 密钥、OpenAI 实例、对话请求逻辑、业务 Prompt 全部揉在一个 JS 文件里。跑通第一条返回时还美滋滋,觉得单文件写起来省事。结果第二天要换模型、调整 API 地址,整段代码翻来覆去改;想复制到另一个新项目,还要手动剪切拆分代码,维护起来一团糟。
意识到不对之后,我重新梳理分层,拆分出了下面这四个核心文件,结构清爽太多:

第一步:先填环境变量,踩了密钥硬编码的大坑
第一版我直接把sk-xxxx密钥写在代码变量里,写完才后背一凉:一旦推到 Git 仓库,API 密钥直接泄露,分分钟被刷额度。立刻新建.env文件隔离敏感配置。
env
# .env 只存配置,绝不提交代码仓库
DEEPSEEK_API_KEY=sk-xxxxxx
DEEPSEEK_API_BASE_URL=https://api.deepseek.com/v1
DEEPSEEK_API_MODEL=deepseek-v4-flash
一定要把
.env写入.gitignore,不然配置会跟着代码上传;我第一次忘记忽略,提交前 git diff 看到配置文件吓出一身汗。
第二步:封装 LLM 客户端 client.mjs
这里是全局唯一的 OpenAI 实例,负责对接 DeepSeek 兼容 OpenAI 格式的接口。这里栽了默认导出的语法坑:一开始手贱写了两个export default,直接报语法错误,翻 ESM 规范才记死一个模块只能有 1 个默认导出。
javascript
// client.mjs
import { OpenAI } from 'openai';
import dotenv from 'dotenv';
// 重点!这行必须放在实例化前面,晚一步process.env拿不到值,坑我半小时
dotenv.config();
// 初始化DeepSeek兼容客户端
const client = new OpenAI({
apiKey: process.env.DEEPSEEK_API_KEY,
baseURL: process.env.DEEPSEEK_API_BASE_URL,
});
// 默认导出整个客户端实例,全局复用单例
export default client;
// 顺带测试命名导出(后面main里解构用)
export const testNumA = 2;
export const testNumB = 3;
第三步:拆分业务请求方法 completions.mjs
把对话生成、图像生成(预留)的请求逻辑抽离,不和入口耦合,后续加流式输出、重试逻辑只改这一个文件就行。用命名导出,可以灵活按需引入多个函数。
javascript
// completions.mjs
import client from './client.mjs';
// 文本对话完成核心方法
export async function getCompletion(prompt) {
const response = await client.chat.completions.create({
model: process.env.DEEPSEEK_API_MODEL,
messages: [{ role: 'user', content: prompt }]
});
return response.choices[0].message.content;
}
// 预留画图接口,后续扩展DeepSeek绘图模型直接补逻辑
export async function getImage(prompt) {
}
第四步:程序唯一入口 main.mjs
单点入口调度所有逻辑,所有 NLP 业务、循环批量推理、测试 demo 全部写在这里。这里刚好能用上 ES6 解构、展开、rest 这些语法,顺便巩固企业级 JS 写法。先贴简化骨架,再放完整 NLP 业务代码:
javascript
// main.mjs 全局唯一启动入口
import { getCompletion } from "./completions.mjs";
async function main(){
// 所有业务代码、批量推理、Prompt调试全写在这
}
// 必须调用执行
main();
完整的业务代码里我测了五类 Prompt 驱动 NLP 能力:情感分类、信息抽取、文本摘要、主题匹配,对比传统机器学习不用标注训练,写提示词就能跑通基础 NLP 任务。
javascript
import { getCompletion } from "./completions.mjs";
async function main(){
// 测试1:电商评论情感+多维度信息提取
const lamp_review_zh = '我需要一盏漂亮的卧室灯,这款灯具有额外的储物功能,价格也不算太高。\
我很快就收到了它。在运输过程中,我们的灯绳断了,但是公司很乐意寄送了一个新的。\
几天后就收到了。这款灯很容易组装。我发现少了一个零件,于是联系了他们的客服,他们很快就给我寄来了缺失的零件!\
在我看来,Lumina 是一家非常关心顾客和产品的优秀公司!';
// 精准结构化输出Prompt,直接返回JSON方便程序解析
const extractPrompt = `
从评论文本中识别以下项目:
- 情绪(正面或负面)
- 是否表达了愤怒?(布尔值true/false)
- 评论商品
- 品牌名称
文本用三个反引号包裹,严格输出JSON对象,键名:sentiment、angry、product、company,无信息填"未知"
评论文本:```${lamp_review_zh}```
`;
const extractRes = await getCompletion(extractPrompt);
console.log('多维度信息提取结果:\n', extractRes, '\n');
// 测试2:批量多商品评论摘要
const prod_review_zh = `这个熊猫公仔是我给女儿的生日礼物,她很喜欢,去哪都带着。公仔很软,超级可爱,面部表情也很和善。但是相比于价钱来说,它有点小,同价位能买更大的,快递提前一天送达`;
const review_3 = `电动牙刷牙医推荐,电池续航强,但刷头尺寸太小,50美元价位性价比高,替换原装刷头贵,通用平替划算`;
const reviewList = [prod_review_zh, review_3];
console.log('批量评论摘要:');
for(let review of reviewList){
const sumPrompt = `总结下面评论,不超过30个字:```${review}````;
const sumRes = await getCompletion(sumPrompt);
console.log('-', sumRes);
}
// 测试3:文本主题匹配判断
const topicList = ['美国国家航空航天局', '地方政府', '员工满意度'];
const newsText = `NASA员工满意度95%排名公共部门第一,社保管理局满意度仅45%`;
const topicPrompt = `给主题列表,判断每个是否属于文本话题,按顺序输出0/1逗号分隔。主题:${topicList.join(',')} 文本:${newsText}`;
const topicRes = await getCompletion(topicPrompt);
console.log('\n主题匹配0/1结果:', topicRes);
}
main();
顺带梳理这套架构用到的 ES6 企业级语法
之前写小脚本习惯用 var,拆模块化之后才发现 ES6 是支撑大型 JS 项目的根基,几个高频用法我整理了实操对比:
- 变量声明
let/const拥有块级作用域,不存在 var 的变量提升污染;const 引用地址锁死,对象 / 数组内部值可以修改,基础类型完全不可改。 - 解构赋值(性能比逐行取值更好)
javascript
// 对象解构
const info = {name:"张三",age:18};
const {name,age} = info;
// 数组解构 + rest剩余运算符
const [coach, ...team] = ['范甘迪','姚明','麦迪'];
// 展开运算符合并数组
const allTeam = [...team, ['科比','加索尔']];
- ESM 模块化核心规则
export default:一个文件仅 1 个,默认导出,导入时可自定义名称export xxx:命名导出,可多个,导入必须花括号匹配名称- 文件后缀
.mjs代表强制 ESM 模式;如果不想写后缀,在package.json加"type":"module"
跑通整套项目的前置依赖
初始化安装两行命令,缺一不可:
bash
# 安装LLM兼容客户端 + 环境变量读取工具
npm install openai dotenv
如果 Node 版本低于 14,顶层不能直接写 await,必须全部包裹在 async function 里调用,我本地 v16 刚好兼容无压力。
踩过的几个致命小坑复盘
- ESM 与 CommonJS 冲突 :一开始没加
"type":"module",node 默认走 require 规范,识别import直接抛错,要么改 package 配置,要么全部文件后缀改为.mjs; - dotenv 执行顺序颠倒 :把
new OpenAI写在dotenv.config()前面,process.env全是 undefined,客户端初始化失败; - 默认导出重复 :同一个文件写两次
export default,JS 直接语法终止; - 异步函数忘记调用 :写了 async main 函数,最后没执行
main(),程序一动不动,无报错无输出。
最后梳理三个核心收获
- 分层模块化的本质就是单一职责:配置、客户端实例、请求方法、业务入口完全隔离,改一处不牵连全局,复用性拉满;
- 兼容 OpenAI 协议的大模型(DeepSeek、通义千问兼容版等),客户端封装逻辑可以 100% 平移,只换环境变量里的 key 和 baseURL;
- Prompt 能搞定简单 NLP 任务(分类、抽取、摘要、匹配),零训练成本快速落地业务;但高并发、万级精准标注场景还是得微调模型。
这套方案适合快速验证 LLM 业务原型,不用搭复杂后端框架就能跑通 NLP 推理。搞懂之后你可以试着加流式输出、请求超时重试,有调整思路可以留条消息聊聊你的改动方案。