从零搭建 NLP Demo:用 ES6 模块化 + DeepSeek API 构建你的第一个 AI 应用

从零搭建 NLP Demo:用 ES6 模块化 + DeepSeek API 构建你的第一个 AI 应用

本文首发于稀土掘金,作者:hushu。未经授权禁止转载。

📖 本文概要

最近在系统学习 NLP 实战开发,从一个极简的 Demo 入手,把「Prompt 驱动大模型做 NLP 任务」这件事拆解清楚。这个项目代码只有 3 个文件、不到 40 行 ,但背后涉及的工程思想------模块化架构、ES6 语法特性、API 调用链路、关注点分离------是每一个 AI 应用开发者都必须掌握的。本文用 CodeGraph 静态分析工具把项目结构完全可视化,带你一窥究竟。

读完你将收获:

  • 一个可运行的 NLP Demo 项目骨架
  • ES6 模块化的核心语法及使用场景
  • OpenAI SDK 调用 DeepSeek API 的完整流程
  • 软件工程中「单一职责」与「关注点分离」的落地实践
  • 用 CodeGraph 理解代码结构的思维方式

一、项目全景:3 个文件,13 个符号,10 条关系边

先把整个项目的骨架摊开来看。启动 CodeGraph 索引后,得到一张精确的结构图:

css 复制代码
nlp-demo/
├── client.mjs      ← 6 个符号,最底层
├── completion.mjs  ← 4 个符号,中间层
└── main.mjs        ← 3 个符号,最上层

模块依赖关系 (由 CodeGraph codegraph_tracecodegraph_callers 给出):

arduino 复制代码
main.mjs  ─── import { getCompletion } ──→  completion.mjs
                                               │
completion.mjs  ─── import client ──→  client.mjs
                                           │
client.mjs  ─── import { OpenAI } ──→  openai (npm 依赖)

这个依赖方向是单向的、自上而下的:上层调用下层,下层不感知上层。这是一条铁律------依赖倒过来就会产生循环引用,项目稍微变大就不可维护。课程开篇第一句话 "有哪些东西可以模块化?" 问的就是这个。


二、逐层拆解:从螺丝钉到发动机

2.1 底层:client.mjs --- 管「连谁」

javascript 复制代码
import { OpenAI } from 'openai'
import dotenv from 'dotenv'
dotenv.config()

const client = new OpenAI({
  apiKey: process.env.DEEPSEEK_API_KEY,
  baseURL: process.env.DEEPSEEK_API_BASE_URL,
})

export default client

这里藏着三个工程实践:

① 配置外部化。 DEEPSEEK_API_KEYDEEPSEEK_API_BASE_URL 不硬编码在代码里,而是从 .env 文件通过 dotenv 注入。这样切换环境(开发 / 测试 / 生产)只改配置文件,不用动代码。更关键的是,API Key 这类敏感信息绝不能提交到 Git------.env 写进 .gitignore,这才是生产级习惯。

export default 语义。 一个模块只能有一个 default export。这里把 client 实例作为默认导出,因为其他模块只需要「用这个客户端发请求」,不需要知道它怎么构造的。这就是封装。

③ SDK 抽象。 用的是 OpenAI 官方的 openai npm 包,但实际调的是 DeepSeek 的 API。SDK 提供统一的接口规范------chat.completions.create() 这个方法签名在 OpenAI、DeepSeek、Moonshot 之间通用,换模型只改 baseURLapiKey,业务代码零修改。这就是「面向接口编程」的威力。

2.2 中间层:completion.mjs --- 管「做什么」

javascript 复制代码
import client from './client.mjs'

export async function getCompletion(prompt) {
  const response = await client.chat.completions.create({
    model: process.env.DEEPSEEK_MODEL,
    messages: [
      { role: 'user', content: prompt }
    ]
  })
  return response.choices[0].message.content
}

export async function getImage(prompt) {
  // 待实现
}

这一层是课程的核心------Prompt 做 NLP 任务的落地代码。

getCompletion 函数做的事情用一句话描述:把人类自然语言的指令(prompt)发给大模型,拿回结构化的回答 。传统 NLP 要做分类、实体识别、摘要等,需要分别训练模型;现在一个 getCompletion(prompt) 全干了------你写什么 prompt,它就做什么任务。这就是「Prompt Engineering」的本质。

注意几个细节:

  • async/await 网络请求是异步的,await 让异步代码写起来像同步代码,极大降低心智负担。这是 ES2017 的特性,课程虽然没展开讲,但已经是现代 JS 基础设施。
  • export vs export default 这里用的是命名导出(export async function),因为一个模块里可能有多个任务函数------getCompletiongetImage,将来还能加 getEmbeddinggetSummary。和 client.mjs 的单一默认导出形成对比,两种导出模式的使用场景一目了然。
  • response.choices[0].message.content 这是 OpenAI 兼容 API 的标准响应格式。choices 是数组(可以一次返回多个候选回答),取第 0 个,拿 message.content 就是 AI 回复的纯文本。

2.3 顶层:main.mjs --- 管「怎么触发」

javascript 复制代码
import { getCompletion } from './completion.mjs'

async function main() {
  const prompt = '用一句话解释什么是模块化编程'
  const response = await getCompletion(prompt)
  console.log(response)
}

main()

入口文件只有 6 行有效代码,无比简洁。这就是课程讲的「单点入口」------程序的启动逻辑集中在一处,不散落。

这里 prompt 是硬编码的示例,真实场景下它可以来自:

  • HTTP 请求的 body(Express/Koa 接口)
  • 命令行参数(CLI 工具)
  • 消息队列消费(后台任务)
  • 用户输入流(聊天机器人)

入口层不关心 prompt 具体怎么来的,它只负责「拿到 prompt → 调用完成任务 → 输出结果」这个编排。这是路由/编排层的职责。


三、ES6 语法:让 JS 成为企业级语言的能力

课程在 readme 里专门列了 ES6(ECMAScript 2015)的四个关键特性。它们不是孤立的知识点------这个项目本身就是 ES6 语法的实战练习场。

3.1 let / const:告别变量提升的噩梦

在 ES6 之前,JS 只有 varvar 有两个致命问题:

javascript 复制代码
// 问题一:变量提升(hoisting)
console.log(x)  // undefined,不报错!
var x = 10

// 问题二:没有块级作用域
if (true) {
  var y = 20
}
console.log(y)  // 20,在块外部依然能访问!

letconst 解决了这两个问题:块级作用域 让变量的生命周期限制在 {} 之内,暂时性死区 让声明前访问直接报错。const 更进一步------简单数据类型(字符串、数字)不可重新赋值,复杂类型(对象、数组)可以修改内部属性但不能指向新的内存地址。这能防止意外覆盖,提高代码安全性。

在本项目中,client 实例用 const 声明,因为程序运行期间不需要更换客户端;promptresponse 也用 const,语义清晰------这些值不会被重新赋值。

3.2 解构赋值:一行代码干三行的事

main.mjs 里虽然没有直接出现解构语法,但注释里给了清晰的示例:

javascript 复制代码
// 传统写法
let name = obj.name
let city = obj.city

// 解构赋值
let { name, city } = { name: '姚明', city: '北京' }

对象解构按属性名 匹配,数组解构按顺序匹配:

javascript 复制代码
// 数组解构 + rest 操作符
let [coach, ...players] = ['范甘迪', '姚明', '麦迪', '穆托姆博', '弗朗西斯']
console.log(coach)   // '范甘迪'
console.log(players) // ['姚明', '麦迪', '穆托姆博', '弗朗西斯']

这不仅仅是语法糖------解构一次提取,后续直接使用变量 ,比每次通过属性链访问(obj.name)性能更好,尤其在循环和频繁访问的场景下差异明显。

3.3 Rest / Spread:... 的双面人生

同一个 ... 运算符,根据上下文有两种含义:

javascript 复制代码
// Rest:收集剩余元素(在赋值左侧 / 函数参数位)
let [first, ...rest] = arr

// Spread:展开可迭代对象(在赋值右侧 / 函数调用位)
let merged = [...teamA, ...teamB]

Pro tip:spread 展开是浅拷贝 ,嵌套对象/数组的内部引用仍然共享。需要深拷贝时得用 structuredClone()JSON.parse(JSON.stringify())

3.4 模块化:import / export 体系

回顾 CodeGraph 的模块依赖图,三种导出方式在这个项目里各司其职:

语法 使用场景 本项目示例
export default 模块只有一个主要输出 client.mjs 导出 client 实例
export(命名导出) 模块输出多个工具函数 completion.mjs 导出 getCompletiongetImage
import { ... } from 按需引入命名导出 main.mjs 只引入 getCompletion
import ... from 引入默认导出 completion.mjs 引入 client

默认导出可以随意命名import x from './client.mjs' 中的 x 叫什么都可以,因为它对应的是那个唯一的 export default。命名导出则必须用花括号按原名引入 (除非用 as 别名),这种约束让模块间的契约更明确。


四、API 调用链:从 Prompt 到 Response 的五步流转

把 CodeGraph codegraph_trace main → getCompletion 的结果展开,可以看到一次 API 调用的完整生命周期:

php 复制代码
┌─────────────────────────────────────────────┐
│  ① dotenv.config()                          │
│     读取 .env → 注入 process.env            │
│     DEEPSEEK_API_KEY / BASE_URL / MODEL     │
└──────────────────┬──────────────────────────┘
                   ↓
┌─────────────────────────────────────────────┐
│  ② new OpenAI({ apiKey, baseURL })          │
│     创建指向 DeepSeek 的客户端实例          │
│     SDK 内部处理鉴权、请求序列化、重试逻辑  │
└──────────────────┬──────────────────────────┘
                   ↓
┌─────────────────────────────────────────────┐
│  ③ main() 构造 prompt 字符串                │
│     '用一句话解释什么是模块化编程'           │
│     → 这就是 NLP 任务的「输入」             │
└──────────────────┬──────────────────────────┘
                   ↓
┌─────────────────────────────────────────────┐
│  ④ client.chat.completions.create({         │
│       model: 'deepseek-v4-flash',           │
│       messages: [{role:'user', content}]    │
│     })                                      │
│     HTTP POST → https://api.deepseek.com    │
└──────────────────┬──────────────────────────┘
                   ↓
┌─────────────────────────────────────────────┐
│  ⑤ response.choices[0].message.content      │
│     从响应 JSON 中提取纯文本回复            │
│     返回给调用方 → console.log 输出         │
└─────────────────────────────────────────────┘

这个流程值得记住的原因在于:市场上一半以上的 AI 应用,都是这个模式的变体。 无论你做的是聊天机器人、文档问答还是智能客服,核心都是「拿用户输入 → 拼 prompt → 调 API → 解析回传」。把握住这条链路,你就理解了 AI 应用的骨架。


五、设计思想:课程里没明说但贯穿始终的东西

5.1 单一职责原则(SRP)

css 复制代码
client.mjs      → 只负责「建立连接」
completion.mjs  → 只负责「定义任务」
main.mjs        → 只负责「编排流程」

每个模块只有一个理由被修改。想换模型?改 client.mjs。想加新任务类型?改 completion.mjs。想改入口逻辑?改 main.mjs。三者独立演化,互不干扰。

5.2 关注点分离(Separation of Concerns)

鉴权、业务、路由------三个关注点分属三个文件。这和 MVC 的 Model-View-Controller 是一个思路,只是在 AI 应用的语境下,分离的维度变成了:

  • 基础设施层(client)--- SDK 配置、鉴权、网络
  • 领域逻辑层(completion)--- Prompt 构造、API 调用、响应处理
  • 应用编排层(main)--- 路由、参数校验、结果输出

5.3 可复用性

getCompletion(prompt) 是一个高度可复用的「积木」。今天在 main.mjs 里调用它,明天可以在 Express 路由里调用,后天可以在定时任务脚本里调用------函数签名不依赖运行环境,只依赖一个 string 入参,这就是好的抽象。

5.4 配置外部化

.env 文件的存在,让这个项目具备了「开发环境」和「生产环境」自由切换的能力。这是 12-Factor App 的第三因子------「配置存储在环境变量中」------在这个小 Demo 里就已经落地了。


六、动手实战:复现这个项目的 5 个步骤

如果你也想跑起来这个 Demo,按以下步骤操作:

Step 1:初始化项目

bash 复制代码
mkdir nlp-demo && cd nlp-demo
npm init -y

Step 2:安装依赖

bash 复制代码
npm install openai dotenv

Step 3:配置环境变量

创建 .env 文件(记得加入 .gitignore):

env 复制代码
DEEPSEEK_API_KEY=你的DeepSeek-API-Key
DEEPSEEK_API_BASE_URL=https://api.deepseek.com
DEEPSEEK_MODEL=deepseek-v4-flash

Step 4:创建三个模块文件

按上面的代码分别创建 client.mjscompletion.mjsmain.mjs

Step 5:运行

bash 复制代码
node main.mjs

如果一切顺利,控制台会输出 DeepSeek 对「什么是模块化编程」的一句话解释。

⚠️ 注意package.json 中如果 "type": "commonjs".mjs 扩展名仍然可以让 Node.js 以 ES Module 模式处理这些文件。这是课程的一个教学细节------.mjs 后缀显式声明模块类型,避免和 CommonJS 混淆。


七、用 CodeGraph 理解代码:一种新的学习方式

传统学代码的方式是「打开文件 → 按顺序读 → 在脑子里拼关系」。CodeGraph 给了另一种路径:先看关系,再看细节

bash 复制代码
# 初始化索引
codegraph init -i

# 看整体结构
codegraph_files        # → 3 个文件,清晰的分层

# 查符号定义位置
codegraph_search getCompletion  # → completion.mjs:3

# 追踪完整调用链
codegraph_trace main → getCompletion  # → 2 跳,完整路径

这套工具把代码变成了可查询的知识图谱------谁调谁、谁依赖谁、改一个符号会影响哪些文件------全部结构化地展现在眼前。建议你在学习任何新项目时都先用它扫一遍,建立全局认知再深入细节,效率会高很多。


八、总结

这篇文章围绕一个不到 40 行的 NLP Demo,展开了七个维度的深度分析:

维度 核心收获
项目结构 3 层架构:client → completion → main
ES6 语法 let/const、解构、rest/spread、模块化
API 链路 5 步流转:配置 → 客户端 → prompt → 调用 → 解析
设计原则 单一职责、关注点分离、可复用、配置外部化
工程实践 .env 管理敏感信息、SDK 抽象、async/await
工具方法 CodeGraph 先看关系再看细节的学习路径
实战步骤 5 步跑通第一个 NLP Demo

这个 Demo 很小,但它是一颗种子。Prompt 驱动大模型做 NLP 任务这个范式,从这里出发,可以长出聊天机器人、智能客服、文档分析、代码生成------所有 AI 应用的本质,都是「拿输入 → 拼 prompt → 调 API → 输出结果」这个循环的不同包装。

真正的模块化思想,不是把代码拆成多个文件就完事了------而是让每一层都对变化保持开放,对修改保持封闭。这个 3 文件的 NLP Demo,就是这种思想的微型范本。


参考资源


如果这篇文章对你有帮助,欢迎点赞、收藏、转发。有问题欢迎在评论区交流讨论!

相关推荐
颂love1 小时前
TypeScript速学
前端·javascript·typescript
Raink老师1 小时前
【AI面试临阵磨枪-99】纯浏览器 Agent:记忆、工具、RAG、流式、安全如何实现?
人工智能·安全·面试
凌涘1 小时前
深入理解 JavaScript 执行机制:从执行上下文到调用栈全解析
前端·javascript
用户938515635071 小时前
从模块化到 Prompt 工程:我用 Node.js + LLM 复刻了传统 NLP 的流程
javascript·人工智能·node.js
YAwu111 小时前
手写一个符合 Promise/A+ 规范的 Promise(附完整代码)
前端·javascript
bonechips1 小时前
用 Prompt 做 NLP:从零搭建一个情感分析与信息提取系统
javascript
暗不需求1 小时前
从路虎汽车小程序看微信小程序开发的最佳实践
前端·javascript·微信小程序
东风破_1 小时前
用原型实现一个队列:JS 面向对象的"不走寻常路"
javascript
向日的葵0061 小时前
vue路由(二)
前端·javascript·vue.js·vue