先搞清楚,NLP 到底是个啥
NLP,全称 Natural Language Processing,自然语言处理。
不严谨地说,就是让计算机"读懂"人话。
读到"这熊猫公仔真可爱,但比预期小了点",它能知道:这人情绪总体是正面的,买的是公仔,不满的点是尺寸,跟快递速度没关系。
听起来好像没什么,但你仔细想------"可爱但小",一个"但"字,人类瞬间就知道褒贬在哪边。计算机呢?它看到的是一串数字。从一串数字到"知道褒贬在哪边",这中间要解决的问题就叫 NLP。
传统的做法是:收集这个领域的文本数据 → 人工标注 → 选模型 → 训练 → 调参 → 部署。一个情感分类器,快的一周,慢的一个月。而且换个领域?重新来一遍。
我就想验证一件事:2025 年了,一个普通后端,不走那套 ML 流程,能不能用 Prompt + LLM API 搞定 NLP?
验证结果:能。而且一个下午跑通了情感分类、信息提取、主题推断、文本总结四种任务。代码不到 100 行,模型用的 DeepSeek v4 Flash,全部成本加起来大概几毛钱。
项目怎么搭的
架构很简单,三个文件:
bash
nlp-demo/
├── client.mjs # 初始化 LLM 客户端
├── completion.mjs # 封装通用调用函数
└── main.mjs # 入口 + Prompt 定义
client.mjs 只干一件事------连上 DeepSeek 的 API:
arduino
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
这里有个值得提的工程细节:API Key 写在 .env 文件里,在 .gitignore 里加上 .env,然后用 dotenv 库读进 process.env。看起来简单,但我见过太多人把 Key 推到公开仓库里了------十分钟之内就会被爬虫扫走拿去挖矿。
ini
DEEPSEEK_API_KEY=sk-xxx
DEEPSEEK_API_BASE_URL=https://api.deepseek.com
DEEPSEEK_MODEL=deepseek-v4-flash
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
}
抽出去的价值不在于函数体有多短,而在于:以后换模型、换 SDK、加 retry 逻辑、加日志,只改这一个地方。
四种 NLP 任务,Prompt 怎么写的
1. 情感分类(Sentiment Classification)
这就是最常见的 NLP 任务------判断一段文本是正面、负面还是中性的。电商、客服、舆情监控里到处都是。
拿一条灯具评论当输入:
ini
const lamp_review_zh = '我需要一盏漂亮的卧室灯,这款灯具有额外的储物功能,\
价格也不算太高。我很快就收到了它。在运输过程中,我们的灯绳断了,但是公司很乐意\
寄送了一个新的...在我看来,Lumina 是一家非常关心顾客和产品的优秀公司'
Prompt:
go
以下用三个反引号分隔的产品评论的情感是什么?
用一个单词回答:正面 或 负面
评论文本:```{lamp_review_zh}```
输出:正面
然后我换了几个问法,都跑通了:
- 问"是否表达了愤怒" →
否 - 要求列出最多 5 种情绪 →
满意、感谢、一点不满(灯绳断了)、总体满意 - 加上 few-shot 例子 → 准确率和格式一致性肉眼可见地提升
关键在于 "用一个单词回答" 这句话。你不加,模型给你写一段散文,解析到崩溃。
2. 信息提取(Information Extraction)
这东西传统叫 NER(命名实体识别),就是从一个自然语言段落里捞出你关心的结构化字段。
我的 Prompt:
javascript
从评论文本识别以下项目:
- 评论者购买的商品
- 制造该物品的公司
将你的响应格式以"物品(product)"和"品牌(brand)"为键的 JSON 对象。
如果信息不存在,请以"未知"作为值
评论文本:```{lamp_review_zh}```
输出一把 JSON:
json
{"product": "卧室灯", "brand": "Lumina"}
在代码里 JSON.parse() 一下直接就能用。
然后我把需求升级了一下------同时抽取情绪、是否愤怒、商品、公司,全部打包进一个 JSON,字段格式要求各异(有的取布尔值、有的取字符串):
javascript
评论文本:```{lamp_review_zh}```
从评论文本中识别以下项目:
- 情绪
- 是否表达了愤怒
- 评论者购买的商品
- 制造该物品的公司
将响应格式化为 JSON 对象。
将 anger 值格式化为布尔值。
如果信息不存在,请使用"未知"作为值。
输出照样稳。以前做实体抽取是标注几千条数据 → 训练 NER 模型 → 部署,你想抽的实体类型换一个都得重来。现在 Prompt 里列字段名就行。
3. 主题推断(Topic Inference)
给一段文本,判断它讨论了哪些预设主题。
造了一条关于政府员工满意度调查的新闻,给了五个候选主题:
ini
const topiclist = [
'美国国家航空航天局', '地方政府', '工程', '员工满意度', '联邦政府'
]
Prompt:
ruby
判断主题列表中的每一项是否是给定文本中的一个话题,
以列表的形式给出答案,每个主题用 0 或 1。
主题列表:${topiclist.join(",")}
给定文本:${story_zh}
输出:[1, 0, 0, 1, 1]------NASA、员工满意度、联邦政府命中,其余不相关。
这里我踩了个坑:当任务涉及多个子判断时,必须让模型用结构化方式回答(0/1 列表、JSON)。让它自由文本输出的话,每次措辞不一样,解析代码没法写。
4. 文本总结(Summarization)
对一个熊猫公仔的评论做总结,但加上了不同的聚焦角度。
这条评论是这样的:
"这个熊猫公仔是我给女儿的生日礼物,她很喜欢...公仔很软,超级可爱...但是相比于价钱来说,它有点小...快递比预期提前了一天到货..."
聚焦产品运输:
对评论文本进行概括,最多 30 个词汇,并聚焦在产品运输上。
聚焦价格和质量:
对评论文本进行概括,最多 30 个词汇,并聚焦在产品价格和质量上。
同一条评论,换一个聚焦关键词,输出的侧重点完全不同。传统做法你要为每个维度单独训练一个 summarization 模型,现在改一句话的事。
为什么我觉得这个方向有意思
不是想说"ML 已死"------扯淡。在需要极致吞吐量和低延迟的场景下,训练专用模型仍然是最优解。
我想说的是另一件事:NLP 能力的获取成本被打下来了。
过去一个团队想给产品加一个"自动分类用户反馈情感"的功能,需要招懂 ML 的人,或者走一个冗长的采购流程。现在一个前端或者后端,花一个下午、几十行代码、几毛钱 API 费,就能跑通并且看清楚效果。跑通了再决定要不要做成正式功能、要不要调优。
这是门槛的变化,不是技术路线的谁输谁赢。
几个踩过的坑
- 输出格式比 Prompt 本身更值得花时间。 模型愿意配合,但你不告诉它怎么输出,它就按聊天的方式回复。"用一个单词回答""格式化为 JSON""用逗号分隔"------这种话加上去,省掉后面一大堆正则和容错代码。
- 模块拆分先做好。 文件超不过三个,但
client/completion/main这种拆分是底限,不是过度工程。一个文件写到底的代码,过两周你自己也看不懂。 - LLM 的返回永远要做容错。 再稳的 Prompt 也偶有意外------多一个句号、措辞变了------解析侧要
try-catch。 - few-shot 例子显著提升稳定性。 在 Prompt 里给一两个输入输出示例,特别是格式敏感的任务,准确率和一致性都会好很多。
- 异步调用别舍不得用 async/await。 比
.then()链式调用可读性好太多了,出问题也好定位。
完整代码在 Gitee:ai_doubao_ysh/ai/wnd/nlp-demo
pnpm i 装好依赖,把 .env 里的 API Key 改成你自己的 DeepSeek key,node main.mjs 就能跑。