Node.js+LLM实现NLP流程:从模块化到Prompt工程指南
摘要
借助大语言模型与Prompt工程,通过Node js模块化架构和ES6语法,高效完成了情感分类、信息
从模块化到Prompt工程:用 Node.js + LLM 复刻传统 NLP 流程
传统自然语言处理(NLP)开发,哪怕只是一个简单的二分类任务,也得走完数据清洗、特征工程、模型选型、训练调优、部署上线这一整套流程。一个经验丰富的算法团队,交付一个可用的情感分析接口,通常需要数天甚至数周的时间。

但现在,借助大语言模型(LLM)和 Prompt Engineering,同样功能的 NLP 系统,几分钟就能搭起来。下面以一个真实的 Node.js 项目为例,完整展示如何通过模块化的代码结构、ES6 现代语法和精心设计的 Prompt,高效完成一系列任务:
- 情感分类(正面 / 负面 / 愤怒检测)
- 信息提取(商品名、品牌名、缺失零件等)
- 主题推断(从长文本中自动归纳话题)
- 文本总结(控制长度、聚焦特定方面)
所有代码和注释都来自这个项目,接下来逐段分析其设计思想与实现细节。
一、项目架构:三文件模块化,高内聚低耦合
一个优秀的 AI 应用,首先应该是一个结构清晰、易于维护和复用的 Node.js 模块。项目分为三个核心文件:
| 文件 | 职责 | 导出内容 |
|---|---|---|
client.mjs | 封装 LLM 客户端(OpenAI 兼容接口) | 默认导出 client 对象 |
completions.mjs | 定义具体的 LLM 调用函数 | 命名导出 getCompletion、getImage |
main.mjs / main2.mjs | 程序入口,编排 Prompt 并执行 | 不导出,直接运行 |
这种分层设计带来了三个直接好处:
- 可读性与维护性:每一层的职责单一,修改 LLM 配置只改
client.mjs,新增 Prompt 任务只改main文件。 - 复用性:
getCompletion可以在任何地方被引入,无需重复编写chat.completions.create逻辑。 - 企业级协作:不同开发者可以同时修改不同模块,减少合并冲突。
1.1 客户端封装 (client.mjs) 解析
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;
几个关键设计点:
- 通过
dotenv从.env文件读取 API 密钥和 Base URL,避免硬编码。 - 创建
OpenAI实例时,baseURL指向了 DeepSeek 或其他兼容服务——说明这套代码不局限于 OpenAI 官方,可以替换任意提供相同接口的 LLM。 - 默认导出,把
client作为整个模块的核心资源暴露出去。
1.2 任务函数封装 (completions.mjs) 解析
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) {
// 预留:图像生成接口
}
设计亮点:
getCompletion是一个通用的 LLM 调用入口,接收任意 Prompt 字符串,返回模型的文本回复。- 模型名称也通过环境变量
DEEPSEEK_MODEL控制,方便切换不同模型(如从 DeepSeek-V3 换到 GPT-4)。 - 预留了
getImage函数,体现了可扩展性——未来可以轻松加入图像生成能力。 - 使用命名导出(
export async function),允许多个任务函数共存。
二、ES6 语法:让 Ja vaScript 真正成为企业级语言
在 main2.mjs 的开头,有一段被注释掉的代码,正是对 ES6 核心特性的精彩演示。我们来深入分析一下。
2.1 解构赋值(Destructuring)
let { name, age } = { "name": "詹姆斯", "age": 20 };
console.log(name, age); // 詹姆斯 20
let obj = { "name": "姚明", "city": "上海" };
let { name, city } = obj;
传统写法需要 obj.name、obj.city 逐个访问,解构赋值让代码更简洁,同时性能更好(因为引擎可以直接绑定属性到变量)。在大型项目中,这种写法极大地提升了代码的可读性。
2.2 Rest 运算符与 Spread 运算符
let [coach, ...players] = ['范甘迪', '姚明', '麦迪', '穆托姆博', '弗朗西斯'];
console.log(coach); // '范甘迪'
console.log(players); // ['姚明', '麦迪', '穆托姆博', '弗朗西斯']
let [hrCoach, ...hrPlayers] = ['杰克逊', '科比', '费舍尔', '加索尔'];
let allPlayers = [...players, ...hrPlayers];
console.log(allPlayers); // ['姚明', '麦迪', '穆托姆博', '弗朗西斯', '科比', '费舍尔', '加索尔']
...在赋值左边时是 Rest 运算符,用于收集剩余元素到一个数组。...在表达式右边时是 Spread 运算符,用于展开数组或对象。- 这个例子非常生动:通过 Rest 把教练和球员分开,再用 Spread 合并两支球队的球员名单。这种操作在处理动态数据时极为高效。
2.3 模块化导入导出
// 同时导入默认导出和命名导出
import client, { a, b } from "./client.mjs";
// 默认导出
export default client;
// 命名导出
export const a = 2;
export const b = 3;
这些注释清晰地解释了默认导出只能有一个,而命名导出可以有多个。在大型项目中,这种区分可以让模块的公共接口更加明确。
三、Prompt 实战:从情感分类到信息提取
main.mjs 和 main2.mjs 中包含了大量被注释掉但非常有价值的 Prompt 示例。下面按任务类型重新组织,每个示例都配有完整代码和输出说明。
3.1 情感分类(Sentiment Analysis)
3.1.1 基础二分类:正面 / 负面
const lamp_review_zh = `我需要一盏漂亮的卧室灯,这款灯具有额外的储物功能,价格也不算太高。我很快就收到了它。在运输过程中,我们的灯绳断了,但是公司很乐意寄送了一个新的。几天后就收到了。这款灯很容易组装。我发现少了一个零件,于是联系了他们的客服,他们很快就给我寄来了缺失的零件!在我看来,Lumina 是一家非常关心顾客和产品的优秀公司!`;
const prompt = `以下用三个反引号分隔的产品评论的情感是什么?用一个单词回答:正面或负面
评论文本:
\`\`\`${lamp_review_zh}\`\`\``;
const response = await getCompletion(prompt);
console.log(response);
// 正面
分析:
- 指令非常直接:“用一个单词回答:正面或负面”——严格限定输出格式,避免 LLM 输出冗长的解释。
- 使用三个反引号包裹评论文本,这是常见的分隔技巧,可以避免评论中的特殊字符(如换行、引号)干扰 Prompt 的解析。
- 结果返回“正面”,符合预期。
3.1.2 愤怒检测(是否表达愤怒)
const prompt = `以下用三个反引号分割的产品评论是否表达了愤怒?给出是或否的回答。
评论文本:
\`\`\`${lamp_review_zh}\`\`\``;
// 输出:否
这是一个更细粒度的情感分析。传统方法需要额外标注“愤怒”类别并重新训练模型。而这里只需修改一句指令,同一个 LLM 就能完成新任务。
3.1.3 提取多个情感词(逗号分隔)
const prompt = `识别以下用三个反引号分隔的产品评论的作者表达的情感。包含不超过5个项目。将答案格式化为逗号分隔的单词列表。
评论文本:
\`\`\`${lamp_review_zh}\`\`\``;
// 可能输出:满意, 感激, 高兴, 放心, 惊喜
不限制情感类型(正面/负面),而是让 LLM 自由提取评论中蕴含的情感词。这对于客户反馈的深度分析非常有用。“不超过5个项目” 可以防止输出过长,也迫使 LLM 聚焦于最显著的情感。“逗号分隔的单词列表”是一种简单的结构化输出,便于后续用 split(',') 解析。
3.1.4 Few-Shot 示例
虽然文件中没有完整写出,但根据注释提示,典型写法如下:
const prompt = `将评论的情感分类为:正面、负面、中性。
示例:
评论:"非常好用" -> 正面
评论:"垃圾产品" -> 负面
评论:"一般般" -> 中性
现在请分类:
评论:"${someReview}"`;
Few-Shot 是指给 LLM 提供少量示例(通常是 1~5 个),让它通过模仿来完成任务。当任务边界模糊或输出格式复杂时,Few-Shot 可以显著提高准确率。示例覆盖了正面、负面、中性三类,LLM 会学习这种映射关系,然后对未知评论进行分类。
3.2 信息提取(Information Extraction)
3.2.1 提取商品名和品牌名(JSON 格式)
const prompt = `从评论文本中识别以下项目:
- 评论者购买的商品
- 制造该商品的公司
将你的响应格式以"物品(product)"和"品牌(brand)"为键的JSON对象。如果信息不存在,请使用**未知**作为值。
评论文本:
\`\`\`${lamp_review_zh}\`\`\``;
// 输出:{"product": "卧室灯", "brand": "Lumina"}
强制要求输出 JSON 对象,这是让 LLM 输出的结果可以被程序直接 JSON.parse() 的关键。增加“如果信息不存在,请使用未知”可以避免 LLM 胡乱编造(幻觉问题)。例如,如果评论中没有提到品牌,LLM 不会瞎猜一个,而是输出 "brand": "未知"。这种技术可以用于自动抽取电商评论中的产品属性、价格、物流信息、售后评价等,实现非结构化数据到结构化数据的转换。
3.2.2 从长文本中提取主题(主题推断)
const story_zh = `在政府最近进行的一项调查中,要求公共部门的员工对他们所在部门的满意度进行评分。调查结果显示,NASA 是最受欢迎的部门,满意度为 95%。一位 NASA 员工 John Smith 对这一发现发表了评论,他表示:"我对 NASA 排名第一并不感到惊讶。这是一个与了不起的人们和令人难以置信的机会共事的好地方。我为成为这样一个创新组织的一员感到自豪。"NASA 的管理团队也对这一结果表示欢迎,主管 Tom Johnson 表示:"我们很高兴听到我们的员工对 NASA 的工作感到满意。我们拥有一支才华横溢、忠诚敬业的团队,他们为实现我们的目标不懈努力,看到他们的辛勤工作得到回报是太棒了。"调查还显示,社会保障管理局的满意度最低,只有 45%的员工表示他们对工作满意。政府承诺解决调查中员工提出的问题,并努力提高所有部门的工作满意度。`;
const prompt = `确定以下给定文本中讨论的五个主题。每个主题用一到两个单词概括。输出时用逗号分隔。
给定文本:${story_zh}`;
// 输出:NASA, 员工满意度, 政府调查, 管理团队, 社会保障局
主题推断是传统无监督学习(如 LDA)的典型任务,需要调参(主题数量、超参数)且输出是一堆概率分布词,可读性差。而这里直接用 Prompt 完成,输出可读性极高,并且可以指定主题数量和词长限制。注意:LLM 在这里不仅做了聚类,还做了命名实体识别(NASA、社会保障局)和抽象概念归纳(员工满意度、管理团队)。
3.3 文本总结(Summarization)
3.3.1 基础总结:限制词汇数量
const prod_review_zh = `这个熊猫公仔是我给女儿的生日礼物,她很喜欢,去哪都带着。公仔很软,超级可爱,面部表情也很和善。但是相比于价钱来说,它有点小,我感觉在别的地方用同样的价钱能买到更大的。快递比预期提前了一天到货,所以在送给女儿之前,我自己玩了会。`;
const prompt = `您的任务是从电子商务网站上生成一个产品评论的简短摘要。请对三个反引号之间的评论文本进行概括,最多30个词汇。
评论文本:
\`\`\`${prod_review_zh}\`\`\``;
“最多30个词汇”是常见的长度控制技巧。LLM 会严格遵守,因为这是明确的数值约束。相比之下,如果说“简短一点”,LLM 的“简短”可能因人而异。这种摘要可以直接用于商品页面的短评展示、客服工单的自动摘要等。
3.3.2 聚焦特定方面(产品运输)
const prompt = `您的任务是从电子商务网站上生成一个产品评论的简短摘要。请对三个反引号之间的评论文本进行概括,最多30个词汇。并且聚焦在产品运输上。
评论文本:
\`\`\`${prod_review_zh}\`\`\``;
// 可能的输出:快递提前一天到货,买家在送给女儿前自己先玩了。
通过添加“并且聚焦在产品运输上”,我们要求 LLM 忽略其他信息(如价格、质量),只关注物流体验。这是可控文本生成的典型应用。在电商客服分析中,可以批量抽取“物流差评”的共同原因,而不被其他内容干扰。
3.3.3 多维度聚焦(运输 + 价格 + 质量)
const prompt = `您的任务是从电子商务网站上生成一个产品评论的简短摘要。请对三个反引号之间的评论文本进行概括,最多30个词汇。并且聚焦在产品运输上,以及产品的价格和质量上。
评论文本:
\`\`\`${prod_review_zh}\`\`\``;
// 输出:快递提前一天到货,价格偏高且公仔偏小,但柔软可爱。
可以同时聚焦多个方面,用“以及”或逗号连接即可。LLM 能够综合这些指令,输出一个融合了运输、价格、质量的摘要。这种多维度控制非常灵活,可用于生成不同视角的摘要,供不同部门使用(物流部看运输摘要,产品部看质量摘要)。
3.3.4 批量处理四篇评论
const reviews = [review_1, review_2, review_3, review_4];
for(let review of reviews){
const prompt = `你的任务是从电子商务网站上的产品评论提取相关信息。请对三个反引号之间的评论文本进行概括,最多20个字符。
评论文本:
\`\`\`${review}\`\`\``
const response = await getCompletion(prompt);
console.log(response, 'n');
}
四篇评论分别是:熊猫公仔(软但价格偏高)、台灯(灯绳断裂但客服好)、电动牙刷(电池续航好但刷头太小)、搅拌机(价格波动大、质量下降)。每篇评论都独立调用 getCompletion,输出极短的摘要(最多20个字符)。这个循环展示了如何将 Prompt 工程应用到批量数据处理场景,例如分析过去一周的所有用户评论。
四、深度分析:为什么 Prompt 工程能碘伏传统 NLP?
传统 NLP 任务需要“熟练的机器学习人员数天到数周的时间”,而 Prompt 方法只需要几分钟。这种差距的本质是什么?
4.1 从“算法设计”到“指令设计”
| 传统 ML 流程 | Prompt 流程 |
|---|---|
| 标注大规模数据集 | 无需标注 |
| 设计特征工程 | 无需特征 |
| 选择模型架构(CNN/RNN/Transformer) | 无需选型 |
| 训练、调参、验证 | 无需训练 |
| 部署模型服务 | 调用 LLM API |
| 处理 OOV(未登录词)问题 | LLM 泛化能力强 |
传统方法中,模型的知识完全来自训练数据,一旦遇到训练集外的表达方式就会失效。而 LLM 在海量语料上预训练后,已经具备了语言理解与推理的通用能力。我们只需要通过 Prompt 将这种能力引导到特定任务上。
4.2 Prompt 的本质:上下文学习(In-Context Learning)
LLM 并不是真正“理解”了任务,而是通过注意力机制从 Prompt 中提取模式。例如,当我们给出“评论:xxx -> 正面”的示例时,模型会在内部表示空间中调整权重分配,从而对下一个输入产生类似输出。这种能力称为 In-Context Learning。
4.3 局限性与应对策略
Prompt 方法并非万能:
- 输出格式不稳定:解决方案如文中所示,强制要求 JSON 格式并加入“如果信息不存在,使用未知”等兜底指令。
- Token 成本:长文本或 Few-Shot 示例会消耗更多 Token。对策是使用更小的模型或缓存常用结果。
- 确定性不足:相同 Prompt 可能产生不同结果。可以通过设置
temperature=0提高确定性。
五、总结:已具备构建企业级 LLM 应用的基础能力
通过分析这个项目,可以梳理出几个关键收获:
- 模块化架构:用
client.mjs封装 LLM 连接,用completions.mjs定义通用调用函数,用main.mjs编排业务逻辑。 - ES6 现代语法:解构赋值、Rest/Spread、模块化导入导出,让代码更简洁、更安全、更易于协作。
- Prompt 工程实战:
- 情感分类(二分类 + 愤怒检测 + 多情感词提取 + Few-Shot)
- 信息提取(JSON 结构化输出)
- 主题推断(无监督话题发现)
- 文本总结(长度控制 + 多维度聚焦 + 批量处理)
- 深度理解:Prompt 的本质是上下文学习,它以极低的成本替代了传统 ML 的繁琐流程。
现在,可以回到编辑器中,尝试修改 main.mjs 里的 review 数组,加入自己的产品评论,看看 LLM 会给出怎样的摘要和情感分析。通往 AI 原生应用开发的路,已经铺好了。
来源:互联网
本网站新闻资讯均来自公开渠道,力求准确但不保证绝对无误,内容观点仅代表作者本人,与本站无关。若涉及侵权,请联系我们处理。本站保留对声明的修改权,最终解释权归本站所有。