从模块化到 Prompt 工程:我用 Node.js + LLM 复刻了传统 NLP 的流程

从模块化到 Prompt 工程:我用 Node.js + LLM 复刻了传统 NLP 的流程

情感分类、信息提取、主题推断、文本总结------不到 100 行代码全部搞定

没有训练、没有标注、没有模型部署,只有写好一段自然语言指令

在传统的自然语言处理(NLP)开发中,即便是最简单的二分类任务,也需要经过数据清洗、特征工程、模型选型、训练调优、部署上线等一系列漫长流程。一个熟练的算法工程师团队,通常需要数天到数周才能交付一个可用的情感分析接口。

但今天,借助大语言模型(LLM)和 Prompt Engineering,我们可以在几分钟内构建出功能同样完整的 NLP 系统。本文基于一个真实的 Node.js 项目,完整展示如何用模块化的代码结构、ES6 现代语法以及精心设计的 Prompt,高效完成以下任务:

  • 情感分类(正面 / 负面 / 愤怒检测)
  • 信息提取(商品名、品牌名、缺失零件等)
  • 主题推断(从长文本中自动归纳话题)
  • 文本总结(控制长度、聚焦特定方面)

所有的代码和注释都来自你手中的文件,我会逐段分析其设计思想与实现细节。


一、项目架构:三文件模块化,高内聚低耦合

一个优秀的 AI 应用,首先应该是一个结构清晰、易于维护和复用的 Node.js 模块。项目分为三个核心文件:

文件 职责 导出内容
client.mjs 封装 LLM 客户端(OpenAI 兼容接口) 默认导出 client 对象
completions.mjs 定义具体的 LLM 调用函数 命名导出 getCompletiongetImage
main.mjs / main2.mjs 程序入口,编排 Prompt 并执行 不导出,直接运行

这种分层的模块化设计带来了三个直接好处:

  • 可读性与维护性 :每一层的职责单一,修改 LLM 配置只改 client.mjs,新增 Prompt 任务只改 main 文件。
  • 复用性getCompletion 可以在任何地方被引入,无需重复编写 chat.completions.create 逻辑。
  • 企业级协作:不同开发者可以同时修改不同模块,减少合并冲突。

1.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;

关键设计点

  • 使用 dotenv.env 文件读取 API 密钥和 Base URL,避免硬编码。
  • 创建 OpenAI 实例时,baseURL 指向了 DeepSeek 或其他兼容服务,说明这套代码不局限于 OpenAI 官方,可以替换任意提供相同接口的 LLM。
  • 默认导出 :一个模块只能有一个默认导出,这里把 client 作为整个模块的核心资源暴露出去。

1.2 任务函数封装 (completions.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) {
    // 预留:图像生成接口
}

设计亮点

  • getCompletion 是一个通用的 LLM 调用入口,接收任意 Prompt 字符串,返回模型的文本回复。
  • 模型名称也通过环境变量 DEEPSEEK_MODEL 控制,方便切换不同模型(如从 DeepSeek-V3 换到 GPT-4)。
  • 预留了 getImage 函数,体现了可扩展性------未来可以轻松加入图像生成能力。
  • 使用命名导出export async function),允许多个任务函数共存。

二、ES6 语法:让 JavaScript 真正成为企业级语言

main2.mjs 的开头,有一段被注释掉的代码,正是对 ES6 核心特性的精彩演示。我们把它整理出来并深入分析。

2.1 解构赋值(Destructuring)

javascript 复制代码
let { name, age } = { "name": "詹姆斯", "age": 20 };
console.log(name, age);  // 詹姆斯 20

let obj = { "name": "姚明", "city": "上海" };
let { name, city } = obj;

传统写法需要 obj.nameobj.city 逐个访问,解构赋值让代码更简洁,同时性能更好(因为引擎可以直接绑定属性到变量)。在大型项目中,这种写法极大地提升了代码的可读性。

2.2 Rest 运算符与 Spread 运算符

javascript 复制代码
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 模块化导入导出

javascript 复制代码
// 同时导入默认导出和命名导出
import client, { a, b } from "./client.mjs";

// 默认导出
export default client;

// 命名导出
export const a = 2;
export const b = 3;

这些注释清晰地解释了默认导出只能有一个,而命名导出可以有多个。在大型项目中,这种区分可以让模块的公共接口更加明确。


三、Prompt 实战:从情感分类到信息提取

main.mjsmain2.mjs 中包含了大量被注释掉但非常有价值的 Prompt 示例。下面按任务类型重新组织,每个示例都配有完整代码和输出说明。

3.1 情感分类(Sentiment Analysis)

3.1.1 基础二分类:正面 / 负面
javascript 复制代码
const lamp_review_zh = `
我需要一盏漂亮的卧室灯,这款灯具有额外的储物功能,价格也不算太高。
我很快就收到了它。在运输过程中,我们的灯绳断了,但是公司很乐意寄送了一个新的。
几天后就收到了。这款灯很容易组装。我发现少了一个零件,于是联系了他们的客服,
他们很快就给我寄来了缺失的零件!在我看来,Lumina 是一家非常关心顾客和产品的优秀公司!
`;

const prompt = `
以下用三个反引号分隔的产品评论的情感是什么?
用一个单词回答:正面或负面
评论文本:\`\`\`${lamp_review_zh}\`\`\`
`;

const response = await getCompletion(prompt);
console.log(response);  // 正面

分析

  • 指令非常直接:"用一个单词回答:正面或负面" 严格限制了输出格式,避免 LLM 输出冗长的解释。
  • 使用三个反引号包裹评论文本,这是常见的分隔技巧,可以避免评论中的特殊字符(如换行、引号)干扰 Prompt 的解析。
  • 结果返回 "正面",符合预期。
3.1.2 愤怒检测(是否表达愤怒)
javascript 复制代码
const prompt = `
以下用三个反引号分割的产品评论是否表达了愤怒?
给出是或否的回答。
评论文本:\`\`\`${lamp_review_zh}\`\`\`
`;
// 输出:否

分析

  • 这是一个更细粒度的情感分析。传统方法需要额外标注"愤怒"类别并重新训练模型。而这里只需修改一句指令,同一个 LLM 就能完成新任务。
  • 输出限制为"是或否",同样简洁。
3.1.3 提取多个情感词(逗号分隔)
javascript 复制代码
const prompt = `
识别以下用三个反引号分隔的产品评论的作者表达的情感。
包含不超过5个项目。
将答案格式化为逗号分隔的单词列表。
评论文本:\`\`\`${lamp_review_zh}\`\`\`
`;
// 可能输出:满意, 感激, 高兴, 放心, 惊喜

分析

  • 不限制情感类型(正面/负面),而是让 LLM 自由提取评论中蕴含的情感词。这对于客户反馈的深度分析非常有用。
  • "不超过5个项目" 可以防止输出过长,也迫使 LLM 聚焦于最显著的情感。
  • "逗号分隔的单词列表"是一种简单的结构化输出,便于后续用 split(',') 解析。
3.1.4 Few‑Shot 示例(注释中提到的"few shot")

虽然文件中没有完整写出,但根据注释提示,典型写法如下:

javascript 复制代码
const prompt = `
将评论的情感分类为:正面、负面、中性。

示例:
评论:"非常好用" -> 正面
评论:"垃圾产品" -> 负面
评论:"一般般" -> 中性

现在请分类:
评论:"${someReview}"
`;

分析

  • Few‑Shot 是指给 LLM 提供少量示例(通常是 1~5 个),让它通过模仿来完成任务。当任务边界模糊或输出格式复杂时,Few‑Shot 可以显著提高准确率。
  • 示例覆盖了正面、负面、中性三类,LLM 会学习这种映射关系,然后对未知评论进行分类。

3.2 信息提取(Information Extraction)

3.2.1 提取商品名和品牌名(JSON 格式)
javascript 复制代码
const prompt = `
从评论文本中识别以下项目:
- 评论者购买的商品
- 制造该商品的公司
将你的响应格式以"物品(product)"和"品牌(brand)"为键的JSON对象。
如果信息不存在,请使用**未知**作为值。
评论文本:\`\`\`${lamp_review_zh}\`\`\`
`;
// 输出:{"product": "卧室灯", "brand": "Lumina"}

分析

  • 强制要求输出 JSON 对象,这是让 LLM 输出的结果可以被程序直接 JSON.parse() 的关键。没有这一步,你将得到一段自然语言,需要再用正则表达式提取,既不稳定又低效。
  • 增加 "如果信息不存在,请使用未知" 可以避免 LLM 胡乱编造(幻觉问题)。例如,如果评论中没有提到品牌,LLM 不会瞎猜一个,而是输出 "brand": "未知"
  • 这种技术可以用于自动抽取电商评论中的产品属性、价格、物流信息、售后评价等,实现非结构化数据到结构化数据的转换
3.2.2 从长文本中提取主题(主题推断)

main2.mjs 中有一个 story_zh 长文本,描述了政府调查 NASA 和社会保障局的员工满意度。对应的 Prompt 如下:

javascript 复制代码
const story_zh = `
在政府最近进行的一项调查中,要求公共部门的员工对他们所在部门的满意度进行评分。
调查结果显示,NASA 是最受欢迎的部门,满意度为 95%。
一位 NASA 员工 John Smith 对这一发现发表了评论,他表示:
"我对 NASA 排名第一并不感到惊讶。这是一个与了不起的人们和令人难以置信的机会共事的好地方。我为成为这样一个创新组织的一员感到自豪。"
NASA 的管理团队也对这一结果表示欢迎,主管 Tom Johnson 表示:
"我们很高兴听到我们的员工对 NASA 的工作感到满意。我们拥有一支才华横溢、忠诚敬业的团队,他们为实现我们的目标不懈努力,看到他们的辛勤工作得到回报是太棒了。"
调查还显示,社会保障管理局的满意度最低,只有 45%的员工表示他们对工作满意。
政府承诺解决调查中员工提出的问题,并努力提高所有部门的工作满意度。
`;

const prompt = `
确定以下给定文本中讨论的五个主题。
每个主题用一到两个单词概括。
输出时用逗号分隔。
给定文本:${story_zh}
`;
// 输出:NASA, 员工满意度, 政府调查, 管理团队, 社会保障局

分析

  • 主题推断是传统无监督学习(如 LDA - Latent Dirichlet Allocation)的典型任务,需要调参(主题数量、超参数)且输出是一堆概率分布词,可读性差。而这里直接用 Prompt 完成,输出可读性极高,并且可以指定主题数量("五个主题")和词长限制("一到两个单词")。
  • 注意:LLM 在这里不仅做了聚类,还做了命名实体识别 (NASA、社会保障局)和抽象概念归纳(员工满意度、管理团队)。这展示了 LLM 强大的语言理解能力。

3.3 文本总结(Summarization)

3.3.1 基础总结:限制词汇数量
javascript 复制代码
const prod_review_zh = `
这个熊猫公仔是我给女儿的生日礼物,她很喜欢,去哪都带着。
公仔很软,超级可爱,面部表情也很和善。但是相比于价钱来说,
它有点小,我感觉在别的地方用同样的价钱能买到更大的。
快递比预期提前了一天到货,所以在送给女儿之前,我自己玩了会。
`;

const prompt = `
您的任务是从电子商务网站上生成一个产品评论的简短摘要。
请对三个反引号之间的评论文本进行概括,最多30个词汇。
评论文本:\`\`\`${prod_review_zh}\`\`\`
`;

分析

  • "最多30个词汇" 是常见的长度控制技巧。LLM 会严格遵守,因为它是明确的数值约束。相比之下,如果说"简短一点",LLM 的"简短"可能因人而异。
  • 这种摘要可以直接用于商品页面的短评展示、客服工单的自动摘要等。
3.3.2 聚焦特定方面(产品运输)
javascript 复制代码
const prompt = `
您的任务是从电子商务网站上生成一个产品评论的简短摘要。
请对三个反引号之间的评论文本进行概括,最多30个词汇。
并且聚焦在产品运输上。
评论文本:\`\`\`${prod_review_zh}\`\`\`
`;
// 可能的输出:快递提前一天到货,买家在送给女儿前自己先玩了。

分析

  • 通过添加 "并且聚焦在产品运输上" ,我们要求 LLM 忽略其他信息(如价格、质量),只关注物流体验。这是可控文本生成的典型应用。
  • 在电商客服分析中,可以批量抽取"物流差评"的共同原因(如"运输过程中灯绳断了"),而不被其他内容干扰。
3.3.3 多维度聚焦(运输 + 价格 + 质量)
javascript 复制代码
const prompt = `
您的任务是从电子商务网站上生成一个产品评论的简短摘要。
请对三个反引号之间的评论文本进行概括,最多30个词汇。
并且聚焦在产品运输上,以及产品的价格和质量上。
评论文本:\`\`\`${prod_review_zh}\`\`\`
`;
// 输出:快递提前一天到货,价格偏高且公仔偏小,但柔软可爱。

分析

  • 可以同时聚焦多个方面,用"以及"或逗号连接即可。LLM 能够综合这些指令,输出一个融合了运输、价格、质量的摘要。
  • 这种多维度控制非常灵活,可用于生成不同视角的摘要,供不同部门使用(物流部看运输摘要,产品部看质量摘要)。
3.3.4 批量处理四篇评论

main.mjs 末尾,有一个 for 循环:

javascript 复制代码
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个字符)。20个字符大约相当于"快递提前到,价格贵"这样的长度,适合作为标题或卡片提示。
  • 这个循环展示了如何将 Prompt 工程应用到批量数据处理场景,例如分析过去一周的所有用户评论。

四、深度分析:为什么 Prompt 工程能颠覆传统 NLP?

根据 readme.md 中的笔记,传统 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 应用的基础能力

通过分析这个项目,我们学到了:

  1. 模块化架构 :用 client.mjs 封装 LLM 连接,用 completions.mjs 定义通用调用函数,用 main.mjs 编排业务逻辑。
  2. ES6 现代语法:解构赋值、Rest/Spread、模块化导入导出,让代码更简洁、更安全、更易于协作。
  3. Prompt 工程实战
    • 情感分类(二分类 + 愤怒检测 + 多情感词提取 + Few-Shot)
    • 信息提取(JSON 结构化输出)
    • 主题推断(无监督话题发现)
    • 文本总结(长度控制 + 多维度聚焦 + 批量处理)
  4. 深度理解:Prompt 的本质是上下文学习,它以极低的成本替代了传统 ML 的繁琐流程。

未来,"写 Prompt" 将和"写 SQL"一样,成为每一位开发者的基础技能。你不需要成为算法专家,也能利用 LLM 解决复杂的 NLP 问题。

现在,回到你的编辑器中,尝试修改 main.mjs 里的 review 数组,加入你自己的产品评论,看看 LLM 会给出怎样的摘要和情感分析。你已经在通往 AI 原生应用开发的路上了。

相关推荐
xixingzhe25 小时前
AI 自然语言转SQL
人工智能
love530love5 小时前
【笔记】AutoClaw NSIS 安装器卡死、进程杀不掉、目录删不了?我是这么解决的
人工智能·windows·笔记·agent
胡萝卜术5 小时前
当大模型遇上浏览器:用 React + WebGPU 在前端跑通 DeepSeek-R1 的实战笔记
前端·javascript·面试
Mandy的名字被占用了5 小时前
晨风AI+知识付费系统|学练考全闭环,重构教育变现新模式
人工智能·后端
人间凡尔赛5 小时前
Next.js 16 生产级实战:Cache Components + View Transitions 完整指南
开发语言·javascript·ecmascript
步步为营DotNet5 小时前
Avalonia 11.3 本地离线AI图像识别绑定Minimal API AI推理网关
人工智能
满怀冰雪5 小时前
06-自动微分入门:用 Paddle 计算梯度
人工智能·python·深度学习·paddle
架构源启5 小时前
文档接入与智能解析:基于 Spring AI 1.1.x 的多格式解析、版面理解与结构化抽取
java·人工智能·spring
李昊哲小课6 小时前
GLM 多技术栈集成完整教程
人工智能·智能体
朦胧之6 小时前
AI应用-消费流式输出
前端·javascript·ai编程