Prompt 驱动 NLP:从 ES6 模块化到文本推理实战
从 Prompt Engineering 的基础技巧,到真正用代码搭建一个可运行的 NLP 系统,现在让我们往 AI 全栈的方向前进。之前文章中我提到过调优单条 Prompt 的写法,现在让我们直接落地到工程化:用 ES6 模块化组织代码,再围绕情感分析、信息提取、主题推断和文本总结这四个经典 NLP 任务,写出一套真正能跑起来的推理系统。
这篇文章记录了我学习的过程和代码实践,覆盖了 ES6 模块化、Prompt 做 NLP 任务的核心思路,以及多个可运行的代码示例。
从基础的代码素养说起:为什么需要模块化
一个问题:如果所有代码都堆在一个文件里,会发生什么?你想优化一下你们部门常用的一个工具函数,结果发现页面的渲染出现了问题、你们部门要对项目进行优化,n 个人一起修改同一个文件,Conflict 直接爆炸、你完成了一个小功能,接下来你要测试它,没办法,你只能启动整个项目,所以我们应当意识到------当项目里要维护鉴权、路由、多种模型调用时,单文件简直就是灾难。
理解模块化不是为了"装高级",而是为了维护性、可读性和复用性 。import from 引入、export default 导出,让代码随时可以像乐高积木一样拆出来复用。
项目的模块化搭建
接下来我会用一个小项目来进行接下来的内容,项目结构非常清晰,只有三个核心文件:
main.mjs:单点入口,负责业务逻辑(鉴权、路由都在这里)client.mjs:只负责提供 LLM Client 对象completions.mjs:封装调用逻辑,提供getCompletions等方法
client.mjs:只做一件事
javascript
import 'dotenv/config';
import OpenAI from 'openai';
// 负责提供 LLM Client 对象
const client = new OpenAI({
apiKey: process.env.OPENAI_API_KEY,
baseURL: process.env.OPENAI_BASE_URL,
model: process.env.OPENAI_MODEL
});
// export const a = 2; // 直接导出
// export const b = 3;
export default client; // 默认导出 一个文件只能有一个默认导出
这里注意一下:注释里留了 export const a = 2 这种直接导出的写法,和 export default client 形成对比。默认导出和直接导出的不同是------默认导出每个文件只能有一个 ,而直接导出可以有多个。在 main.mjs 里引入时,直接导出是一个对象 {key:value},所以可以用 key 来访问。
completions.mjs:封装调用逻辑
javascript
import client from './client.mjs';
export async function getCompletions(prompt) {
const response = await client.chat.completions.create({
model: process.env.OPENAI_MODEL,
messages: [
{
role: "user",
content: prompt
}
]
});
return response.choices[0].message.content;
}
export async function getImages(prompt) {
return await client.images.create({
});
}
这里 getImages 还只是一个空壳,后续可以扩展到文生图能力。
main.mjs:简洁的入口
javascript
// 让我们的入口文件简洁
import {getCompletions} from './completions.mjs';
async function main() {
// AI 全栈
// 企业里 LLM 接入 NLP 能力
// 情感推理与信息提取
// ...
}
main();
理解了这套分层之后,我们可以发现入口文件确实可以变得很干净。因此我们要注意,AI 全栈开发不只是会调 API,更重要的是代码组织能力。
ES6 语法特性:企业级开发的基石
在搭项目的过程中,我们顺带把 ES6 的几个核心语法过一下。ES6 是 JS 在 2015 年推出的重大更新,目标是让 JS 成为一个适合企业级项目开发的语言。
let 与 const:告别声明提升的坑
- 解决了声明提升 Bug
- 支持块级作用域
let与const都不能重复声明const对于简单数据类型不能赋值,复杂数据类型的属性可以重新赋值,但是不能改变复杂数据类型指向的内存地址(类型)
解构赋值:性能与优雅兼得
我用了一个具体的例子来解释解构赋值的意义:
javascript
// 解构赋值
// 如果只是需要变量值,是不需要解构的
// 需要一个额外的变量来进行接收的时候,才需要解构
let {name,age} = {"name":"詹姆斯","age":20};
console.log(name,age);
let obj = {"name":"詹姆斯","age":20};
console.log(obj.name);
console.log(obj.age);
// name obj.name 两者的查找效率与性能有差异
// 如果要查找 obj.name 需要先查找 obj 对象,再查找 name 属性
// 如果要查找 name 变量,直接查找 name 变量即可
这里有一个小点:name 和 obj.name 的查找效率不一样 。如果要查找 obj.name,需要先查找 obj 对象,再查找 name 属性;而直接查找 name 变量就一步到位。这让我意识到,解构赋值不只是代码好看,性能也更好。而作为解释性语言本身性能就不好的 JS ,我们更应该注意一下类似的可以提升效率的地方。
还有数组解构和 ... reset(收集)运算符的用法:
javascript
// 数组解构,按照顺序解构,reset(...) 操作符 将余下的收集起来
let [coach,...players] = ['詹姆斯','麦迪','姚明'];
console.log(coach,players);
let [hrCoach,...hrPlayers] = ['詹姆斯','麦迪','姚明'];
let allPlayers = [...players,...hrPlayers];
console.log(allPlayers);
... 在这里既可以做 rest(收集余下元素),也可以做 spread(展开数组)。这种灵活性在后续的代码拼接和参数传递中非常实用。
一句话总结:ES6 让 JS 从一个脚本语言变成了能扛大型项目的企业级开发语言。
NLP 任务:Prompt 即系统
理解了代码结构之后,接下来我们进入核心主题------用 Prompt 做 NLP 任务开发。现在和之前相比构建一个 NLP 系统已经十分简单了:
仅用几分钟,我们就可以构建多个用于对文本进行推理的系统,而以前需要熟练的机器学习人员数天到数周的时间。
这就是 Prompt Engineering 带来的平等化------不需要训练模型,不需要调参,只需要写好 Prompt,就能完成以下四类经典 NLP 任务:
- 情感分类 Sentiment Analysis:正面/负面/中性,在电商等行业非常重要,可用于客户服务、预警、产品质检
- 信息提取 Information Extraction:从文本中抽取出结构化的关键信息
- 主题推断:识别文本讨论的核心主题
- 文本总结 Summarization:对长文本进行总结,提取关键信息,减少工作量------老板、行政岗、小编都需要
接下来,我按照顺序逐渐介绍这些任务。
实战一:情感分类与信息提取
我们用的测试文本是一段关于卧室灯的中文评论:
javascript
const lamp_review_zh = '我需要一盏漂亮的卧室灯,这款灯具有额外的储物功能,价格也不算太高。\
我很快就收到了它。在运输过程中,我们的灯绳断了,但是公司很乐意寄送了一个新的。\
几天后就收到了。这款灯很容易组装。我发现少了一个零件,于是联系了他们的客服,他们很快就给我寄来了缺失的零件!\
在我看来,Lumina 是一家非常关心顾客和产品的优秀公司!'
1. 基础情感分类
最开始写的是一个直接的分类 Prompt:
javascript
// 写一个 prompt 来分类这个评论的情感是正面还是负面
const prompt = `
以下用三个反引号分隔的产品评论的情感是什么?
评论文本\`\`\`${lamp_review_zh}\`\`\`
`;
2. Few-shot 引导模型输出固定格式
但模型可能会输出一大段解释,而我们只想要一个词。这时候可以用 Few-shot,通过示例来引导模型:
javascript
// few shot 通过示例来引导模型分类情感
const prompt = `
以下用三个反引号分隔的产品评论的情感是什么?
用一个单词回答(正面/负面/中性)
评论文本\`\`\`${lamp_review_zh}\`\`\`
`;
3. 多情感项识别
如果不止要一个标签,而是想要更细粒度的情感列表:
javascript
const prompt = `
识别以下用三个反引号分隔的产品评论的作者表达的情感
包含不超过五个项目。
将答案格式化为以逗号分隔的单词列表
评论文本\`\`\`${lamp_review_zh}\`\`\`
`;
4. 愤怒检测
在客服预警场景中,判断用户是否表达愤怒非常重要:
javascript
const prompt = `
识别以下用三个反引号分隔的产品评论是否表达了愤怒
给出是或否的答案
评论文本\`\`\`${lamp_review_zh}\`\`\`
`;
5. 信息提取:商品与品牌
从非结构化评论中提取结构化信息,直接输出 JSON:
javascript
const prompt = `
从评论文本中识别以下项目
- 评论者购买的商品
- 制造该商品的公司
评论文本用三个反引号分隔。将你的响应格式以"物品(product)"和"品牌(brand)"为键的 JSON 对象。
如果信息不存在,请使用"未知"作为值。
评论文本\`\`\`${lamp_review_zh}\`\`\`
`;
6. 多任务合并:一次提取所有字段
最让我惊讶的是,可以把多个提取任务合并成一条 Prompt:
javascript
const prompt = `
从评论文本中识别以下项目
- 情绪(正面/负面)
- 是否表达了愤怒(是/否)
- 评论者购买的商品
- 制造该商品的公司
评论文本用三个反引号分隔。
将您的响应格式化为 JSON 对象,以"情感(sentiment)"、"愤怒(anger)"、"物品(product)"、"品牌(brand)"为键。
如果信息不存在,请使用"未知"作为值。
让你的回应尽可能简短。
将 anger 值格式化为布尔值
评论文本\`\`\`${lamp_review_zh}\`\`\`
`;
这里有两个技巧:一是用 JSON 格式约束模型输出,方便后续代码解析;二是在 Prompt 里显式声明数据类型(比如 anger 要布尔值),能显著提升输出稳定性。
实战二:主题推断
测试文本是一段关于 NASA 员工满意度调查的新闻:
javascript
const story_zh = `
在政府最近进行的一项调查中,要求公共部门的员工对他们所在部门的满意度进行评分。
调查结果显示,NASA 是最受欢迎的部门,满意度为 95%。
一位 NASA 员工 John Smith 对这一发现发表了评论,他表示:
"我对 NASA 排名第一并不感到惊讶。这是一个与了不起的人们和令人难以置信的机会共事的好地方。我为成为这样一个创新组织的一员感到自豪。"
NASA 的管理团队也对这一结果表示欢迎,主管 Tom Johnson 表示:
"我们很高兴听到我们的员工对 NASA 的工作感到满意。
我们拥有一支才华横溢、忠诚敬业的团队,他们为实现我们的目标不懈努力,看到他们的辛勤工作得到回报是太棒了。"
调查还显示,社会保障管理局的满意度最低,只有 45%的员工表示他们对工作满意。
政府承诺解决调查中员工提出的问题,并努力提高所有部门的工作满意度。
`;
主题推断的 Prompt 设计思路很清晰------要求模型给出五个主题,每个主题用 1-2 个单词概括,输出时用逗号分隔:
javascript
const prompt = `
确定一下给定文本中讨论的五个主题
每个主题用1-2个单词概括.
输出时用逗号分隔.
给定文本\`\`\`${story_zh}\`\`\`
`;
这种输出格式非常适合直接入库或者做标签系统。
除此之外,这里还留了一个更细粒度的主题判断版本(在 main2.mjs 的注释中):给定一个主题列表,让模型判断每个主题是否在文本中出现,输出 0 或 1:
javascript
// const topicList = [
// '美国国家航空航天局',
// '地方政府',
// '工程',
// '员工满意度',
// '联邦政府'
// ]
// const prompt = `
// 判断主题列表中的每一项是否是给定文本中的一个话题,
// 以列表的形式给出答案,每个主题用 0 或 1 。
// 主题列表:\`\`\`${topicList.join(',')}}\`\`\`
// 给定文本:\`\`\`${story_zh}\`\`\`
// `;
这个版本就是为了提出:同样的推断任务,输出格式可以根据业务需求灵活调整。 要标签就用 0/1,要概览就用关键词列表。
实战三:文本总结与批量处理
如果说前面的任务是在理解文本"里面有什么",那么文本总结就是在回答"这段话说了什么"。
单条评论的多种总结策略
这是一段关于熊猫公仔的中文评论:
javascript
const prod_review_zh = `
这个熊猫公仔是我给女儿的生日礼物,她很喜欢,去哪都带着。
公仔很软,超级可爱,面部表情也很和善。但是相比于价钱来说,
它有点小,我感觉在别的地方用同样的价钱能买到更大的。
快递比预期提前了一天到货,所以在送给女儿之前,我自己玩了会。
`
我来展示三种不同的总结策略,每种聚焦点不同,输出就会不同。
第一种是通用摘要:
javascript
// const prompt = `
// 您的任务是从电子商务网站上生成一个产品评论的简短摘要。
// 请对三个反引号之间的文本进行概括,最多30个词汇。
// 评论文本:\`\`\`${prod_review_zh}\`\`\`
// `;
第二种是聚焦运输:
javascript
// const prompt = `
// 您的任务是从电子商务网站上生成一个产品评论的简短摘要。
// 请对三个反引号之间的文本进行概括,最多30个词汇。
// 并且聚焦在产品运输上。
// 评论文本:\`\`\`${prod_review_zh}\`\`\`
// `;
第三种是聚焦价格和质量:
javascript
const prompt = `
您的任务是从电子商务网站上生成一个产品评论的简短摘要。
请对三个反引号之间的文本进行概括,最多30个词汇。
并且聚焦在产品价格和质量上。
评论文本:\`\`\`${prod_review_zh}\`\`\`
`;
关键点:总结不是越少越好,而是越"对症"越好。同样的评论,物流部门关心快递,产品部门关心质量,运营部门关心整体口碑------Prompt 里的聚焦指令,直接决定了总结的服务对象。
批量处理多条评论
最后展示一下如何用 for...of 循环批量处理多条评论:
javascript
const review_2 = `
我想为我的卧室找一个漂亮的灯,这款灯还有额外的存储空间,价格也不太高。\
购买后很快就收到了,两天就送到了。但在运输过程中,灯的拉链断了,公司态度\
很好,发来了一条新的。新的拉链也在几天内就到了。这个灯非常容易装配。后来,我\
发现缺少一个部分,所以我联系了他们的客户支持,他们很快就给我寄来了缺失的部件\
!我觉得这是一家非常关心他们的客户和产品的好公司。
`
// review for an electric toothbrush
const review_3 = `
我的牙科卫生师推荐我使用电动牙刷,这就是我购买这款牙刷的原因。目前为止,我发现电池的\
续航时间颇为令人印象深刻。在初次充电并在第一周保持充电器插头插入以调节电池状态之后,我\
已经将充电器拔掉,并在过去的3周里,每天两次刷牙都使用同一次充电。然而,这款牙刷的刷头实\
在太小了。我见过的婴儿牙刷都比这个大。我希望牙刷头能做得更大一些,搭配不同长度的刷毛更好\
地清洁牙齿间缝,因为现有的无法做到这一点。总的来说,如果你能以大约50美元的价格购入这款电动\
牙刷,那它就物超所值。厂家配套的替换刷头价格相当昂贵,但你可以买到价格更为合理的通用款。\
使用这款牙刷让我感觉像每天都去看了牙医一样,我的牙齿感觉洁净如新!
`
// review for a blender
const review_4 = `
他们还在11月把17件套系统以大约$49的优惠价格销售,几乎是五折。但不明原因(轻易就可以归咎于价格欺诈)\
在到了12月第二周,同一套系统的价格一下儿飙升到了$70-$89之间。11件套系统的价格也从之前的优惠价$29上\
升了大概$10。看上去还算公道,但如果你仔细观察底部,会发现刀片锁定的部位相比几年前的版本要略逊一筹,所\
以我打算非常小心翼翼地使用(例如,我会将像豆子、冰块、大米之类的硬质食材先用搅拌机压碎,然后调到我需要\
的份量,再用打发刀片研磨成更细的粉状,制作冰沙时我首选交叉刀片,如果需要更细腻些或者少些浆糊状,我会换成\
平刀)。在制作果昔时,把将要用的水果和蔬菜切片冷冻是个小技巧(如果你打算用菠菜,要先稍微焖炖软,再冷冻,\
制作雪葩时,用一个小到中号的食品加工器就行)这样就不用或者很少加冰块到你的果昔了。大约一年后,电机开始发出\
一些可疑的声音。我联系了客服,但保修期已经过期,所以我只好另购一台。友情提示:这类产品的整体质量都在下滑,\
所以他们更多的是利用品牌知名度和消费者的忠诚度来保持销售。我在两天之后就收到了它。
`
javascript
const reviews = [prod_review_zh, review_2, review_3, review_4];
for(let review of reviews){
const prompt = `
你的任务是从电子商务网站上的产品评论中获取相关信息。
请对三个反引号之间的文本进行概括,最多20个词汇。
评论文本:\`\`\`${review}\`\`\`
`
const response = await getCompletions(prompt);
console.log(response , '\n');
}
这四条评论各有特点:熊猫公仔是标准短评,灯具评论涉及售后,电动牙刷是长文的优缺点分析,搅拌机则是一篇接近小作文的深度体验。用同一套 Prompt 批量处理时,我发现评论越长,20个词的压缩率就越高,信息损失也越明显。这让我意识到,实际业务中可能需要根据文本长度动态调整摘要词数限制。
我现在怎么理解它
实践之后,我对 AI 全栈开发有了更具体的感知。以前我觉得调 OpenAI API 就是写个 fetch,但现在我发现,真正的工程化是从代码结构开始的 :client.mjs 隔离配置,completions.mjs 封装调用,main.mjs 专注业务------这种分层思维是从小项目走向企业级开发的必经之路。
在 NLP 任务层面,我最大的收获是:Prompt 本身就是一种"轻量级模型训练"。不需要标注几千条数据,不需要微调参数,只需要在 Prompt 里把任务描述清楚、输出格式约束好,就能完成情感分类、信息提取、主题推断和文本总结。这背后的本质,是 LLM 已经把语言理解的能力预训练好了,我们只是在调用它的推理能力。
当然,我也清楚这种方式的边界------如果领域非常垂直、准确率要求极高,微调或者 RAG 仍然是必要的。但在大多数通用文本处理场景中,Prompt 驱动已经足够解决问题。
学习 Prompt 做 NLP,不只是学习怎么写 Prompt,更重要的是学习如何把 LLM 的推理能力嵌入到工程流程中,让它真正为业务服务。