一、问题背景
上个月接手了一个电商评论情感分析的小项目。需求很明确:给定一条用户评论,判断它是正面、负面还是中性。数据是某平台的真实评论,大概5000条,标注好的测试集有500条。
一开始想的是直接微调一个小模型,但标注数据不够,而且业务方希望快速上线。于是转向用LLM做零样本/少样本推理。模型选了 gpt-4o-mini(2024-07-18版本),原因是便宜、快,情感分类这种任务不需要GPT-4级别的推理能力。
但第一次跑完测试集,准确率只有78.3%,而且有大量"中性"被误判成"负面"。这个结果没法交付。于是我决定系统地做一轮Prompt Engineering,把每次迭代的效果记录下来。
二、环境与版本
- Python 3.11.6
- openai==1.35.0
- 模型:gpt-4o-mini(快照 2024-07-18)
- temperature=0,top_p=1,max_tokens=64
- 测试集:500条电商评论,人工标注,分布为 正面280 / 负面160 / 中性60
- 评估指标:整体准确率 + 每类F1
调用代码骨架大概是这样:
import os, json, time
from openai import OpenAI
client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
def classify(prompt: str, comment: str) -> str:
resp = client.chat.completions.create(
model="gpt-4o-mini",
temperature=0,
top_p=1,
max_tokens=64,
messages=[
{"role": "system", "content": prompt},
{"role": "user", "content": comment},
],
)
return resp.choices[0].message.content.strip(), resp.usage.total_tokens
评估脚本就是遍历测试集,比对预测和标注,统计混淆矩阵。
三、方案设计:四个版本的Prompt
我的迭代思路是:从最朴素的零样本开始,逐步加约束,每一步只改一个变量,看效果变化。
V1:零样本,一句话指令
判断下面这条评论的情感倾向。
V2:明确类别 + 输出格式约束
你是一个情感分析助手。请判断用户评论的情感倾向,只能是以下三种之一:正面、负面、中性。
只输出这三个词中的一个,不要输出其他内容。
V3:加入角色设定 + 判定标准
你是一名资深的电商评论分析师,擅长从用户措辞中识别真实情感。
判定标准:
- 正面:明确表达满意、推荐、复购意愿
- 负面:明确表达不满、投诉、退货意愿
- 中性:仅陈述事实、提问、无明显情感倾向,或正负情感同时出现且强度相当
请只输出"正面"、"负面"或"中性"。
V4:V3 + 少样本示例 + JSON结构化输出
在system里塞了6条示例(每类2条,包含一些边界case),并要求输出JSON:
输出格式(严格JSON):
{"label": "正面|负面|中性", "confidence": 0.0-1.0}
示例里特意放了两条容易误判的:
- "东西收到了,还没用,包装完好" → 中性
- "质量还行吧,就是发货太慢了,等了一周" → 负面
四、核心实现
跑评估的核心代码:
import json
from collections import Counter
from sklearn.metrics import f1_score, accuracy_score
def evaluate(prompt: str, testset: list[dict]) -> dict:
preds, golds, tokens = [], [], 0
for item in testset:
out, tk = classify(prompt, item["comment"])
tokens += tk
# V4 是JSON,需要解析;V1-V3直接取文本
try:
label = json.loads(out)["label"]
except Exception:
label = out.strip()
preds.append(label)
golds.append(item["label"])
return {
"acc": round(accuracy_score(golds, preds), 4),
"f1_macro": round(f1_score(golds, preds, average="macro"), 4),
"avg_tokens": round(tokens / len(testset), 1),
"dist": dict(Counter(preds)),
}
V4的Prompt里示例部分我写成了一个常量,方便复用:
FEW_SHOT = """
示例:
评论:这手机续航太顶了,一天重度使用还剩30% -> {"label": "正面", "confidence": 0.95}
评论:收到货就发现有划痕,客服还不给退 -> {"label": "负面", "confidence": 0.97}
评论:东西收到了,还没用,包装完好 -> {"label": "中性", "confidence": 0.8}
评论:质量还行吧,就是发货太慢了,等了一周 -> {"label": "负面", "confidence": 0.75}
评论:请问这个支持无线充电吗 -> {"label": "中性", "confidence": 0.9}
评论:第二次回购了,一如既往的好 -> {"label": "正面", "confidence": 0.96}
"""
五、踩坑与优化
坑1:V2的"只输出三个词"被无视。 大概有3%的样本模型会输出"这条评论是正面的"这种完整句子。加"不要输出其他内容"能缓解,但没根治。V4用JSON格式后彻底解决——因为JSON结构本身强制了输出形态。
坑2:中性类被大量误判为负面。 V1时中性类的F1只有0.41。原因是"还行""一般""凑合"这类词,模型倾向于往负面靠。V3加了判定标准后提升到0.68,V4的示例里放了"质量还行吧,就是发货太慢"这种混合情感case后,涨到0.81。
坑3:token消耗比想象中涨得快。 V1平均180 token,V4平均720 token,翻了4倍。500条测试集跑下来,V1花了约$0.02,V4花了约$0.09。如果按每天10万条评论算,V4一天就是$18,V1只要$4。这个差距在真实业务里是要算账的。
优化尝试: 我试过把V4的示例从6条减到3条(每类1条),准确率掉到91.2%,token降到540。也试过用"正面/负面/中性"改成数字标签1/2/3,token几乎没变,准确率也没变——说明token主要吃在示例和判定标准上,不在输出格式。
六、效果数据
| 版本 | 准确率 | macro-F1 | 中性类F1 | 平均token | 500条成本 |
|---|---|---|---|---|---|
| V1 零样本 | 78.3% | 0.71 | 0.41 | 180 | ~$0.02 |
| V2 格式约束 | 82.6% | 0.76 | 0.52 | 210 | ~$0.02 |
| V3 角色+标准 | 89.4% | 0.85 | 0.68 | 380 | ~$0.04 |
| V4 少样本+JSON | 94.1% | 0.92 | 0.81 | 720 | ~$0.09 |
| V4精简版(3示例) | 91.2% | 0.88 | 0.74 | 540 | ~$0.07 |
最终上线用的是V4,因为94%的准确率刚好卡在业务方要求的93%以上。如果哪天量级涨到成本扛不住,我会退到V4精简版,或者考虑用V4的输出去蒸馏一个小模型。
七、总结
几个真实感受:
第一,Prompt Engineering的收益曲线不是线性的。从V1到V3,每加一点约束都能看到明显提升;从V3到V4,靠的是示例的质量而不是数量。那6条示例我改了3轮才定下来,边界case比典型case重要得多。
第二,结构化输出是刚需。JSON不仅解决了格式问题,还顺带让下游解析代码不用写一堆正则。多花的token绝对值不大,但省下的工程时间很值。
第三,不要迷信"更长=更好"。V4精简版掉了3个点准确率但省了25%的token,这个权衡在不同业务阶段答案不一样。早期冲指标就上V4,稳定期要算ROI。
第四,一定要建评估集。我一开始凭感觉调Prompt,改完觉得"好像好点了",一跑测试集发现是错觉。500条标注数据花了我一个下午,但后面每次迭代都靠它做决策,非常值。
最后,Prompt Engineering不是玄学,本质是在用自然语言给模型做"软微调"。理解了这一点,迭代方向就清晰了:你在补的是模型缺的先验知识,用示例补,用判定标准补,用输出格式约束补。补到位了,效果自然上来。