一、问题背景:为什么我要重新做一遍Prompt对比
事情起因很简单。我们团队有个商品评论舆情模块,之前用的是"一句话Prompt + 关键词规则"的土办法,准确率一直在70%上下晃。产品经理天天催:"能不能上大模型?"我一开始很抵触,觉得情感分类这种任务用小模型微调就够了,但实际排期根本来不及标注数据。
于是退而求其次:用Prompt Engineering榨一榨通用大模型的能力。但我发现网上大部分"提示词教程"都是玄学——"你要温柔地对模型说话"、"加一句Let's think step by step就能起飞"。作为一个写代码的,我更想看数字:不同Prompt到底差多少?Token贵多少?值不值得?
所以我自己设计了一套对比实验。任务选的是最典型的:电商商品评论情感三分类。数据用的是一个内部脱敏数据集,共1200条中文评论,正/中/负比例约 5:3:2。
二、环境与版本
实验环境如下,全部可复现:
- Python 3.11.6
- openai==1.51.0(调用 GPT-4o-mini,2024-07-18 版本)
- dashscope==1.20.0(调用 Qwen2.5-7B-Instruct)
- pandas==2.2.2
- scikit-learn==1.5.1(算F1)
- 温度统一设为
temperature=0.0,max_tokens=16,因为只需要输出一个标签 - 每条请求独立发送,无并发,避免限流影响延迟统计
GPT-4o-mini 官方定价:输入 $0.15 / 1M tokens,输出 $0.60 / 1M tokens。Qwen2.5-7B-Instruct 走 DashScope,输入 0.0005元/1K tokens,输出 0.001元/1K tokens。我按人民币折算统一对比。
三、方案设计:7组Prompt
我设计了7个递进式Prompt,覆盖了常见的"提示词技巧":
| 编号 | 方案 | 核心思路 |
|---|---|---|
| P1 | 裸Prompt | 只给任务,无角色、无示例 |
| P2 | 角色设定 | "你是资深电商分析师" |
| P3 | 角色+规则 | 加上判定标准 |
| P4 | 结构化输出 | 强制 JSON |
| P5 | Few-shot 3例 | 每类1个示例 |
| P6 | Few-shot 9例 | 每类3个示例 |
| P7 | CoT + Few-shot | 让模型先思考再输出 |
标签统一约定为:positive / neutral / negative。
四、核心实现
先放数据加载和评测代码,这是复用的骨架:
import os, json, time
import pandas as pd
from openai import OpenAI
from sklearn.metrics import accuracy_score, f1_score
client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
LABELS = ["positive", "neutral", "negative"]
def call_gpt(prompt: str, text: str, model="gpt-4o-mini"):
start = time.time()
resp = client.chat.completions.create(
model=model,
messages=[
{"role": "system", "content": prompt},
{"role": "user", "content": text},
],
temperature=0.0,
max_tokens=16,
)
latency = time.time() - start
content = resp.choices[0].message.content.strip().lower()
usage = resp.usage
return content, usage.prompt_tokens, usage.completion_tokens, latency
def evaluate(df: pd.DataFrame, prompt: str):
preds, in_toks, out_toks, lats = [], 0, 0, 0.0
for text in df["text"]:
pred, it, ot, lt = call_gpt(prompt, text)
# 简单归一化,防止模型输出带标点
for lb in LABELS:
if lb in pred:
pred = lb
break
else:
pred = "neutral" # 兜底
preds.append(pred)
in_toks += it
out_toks += ot
lats += lt
n = len(df)
return {
"accuracy": accuracy_score(df["label"], preds),
"f1_macro": f1_score(df["label"], preds, average="macro"),
"avg_in_tokens": in_toks / n,
"avg_out_tokens": out_toks / n,
"avg_latency": lats / n,
}
下面是7个Prompt的定义,直接内联在代码里方便对照:
PROMPTS = {
"P1_bare": "判断下面这条商品评论的情感倾向,只回答 positive、neutral 或 negative 中的一个词。",
"P2_role": "你是一位资深电商分析师,擅长从用户评论中洞察情绪。"
"请判断下面评论的情感倾向,只回答 positive、neutral 或 negative 中的一个词。",
"P3_role_rule": (
"你是一位资深电商分析师。判定规则:\n"
"- positive:明确表达满意、推荐、夸赞\n"
"- negative:明确表达不满、投诉、劝退\n"
"- neutral:仅陈述事实、无情绪倾向或情绪混合\n"
"只回答 positive、neutral 或 negative 中的一个词。"
),
"P4_json": (
"你是电商分析师。请判断评论情感,并以 JSON 输出,"
"格式为 {\"label\": \"positive|neutral|negative\"}。"
),
"P5_fewshot3": (
"你是电商分析师。请判断评论情感,只回答 positive、neutral 或 negative。\n\n"
"示例:\n"
"评论:质量很好,物流也快,下次还来 -> positive\n"
"评论:东西还行吧,包装一般 -> neutral\n"
"评论:收到就是坏的,客服还不理人 -> negative\n\n"
"现在判断下面的评论:"
),
"P6_fewshot9": (
"你是电商分析师。请判断评论情感,只回答 positive、neutral 或 negative。\n\n"
"示例:\n"
"评论:质量很好,物流也快,下次还来 -> positive\n"
"评论:性价比超高,回购第三次了 -> positive\n"
"评论:客服态度好,问题秒解决 -> positive\n"
"评论:东西还行吧,包装一般 -> neutral\n"
"评论:和描述基本一致,没啥惊喜 -> neutral\n"
"评论:用了一周,暂时没发现问题 -> neutral\n"
"评论:收到就是坏的,客服还不理人 -> negative\n"
"评论:用了两天就坏了,垃圾 -> negative\n"
"评论:色差严重,退货还让我出运费 -> negative\n\n"
"现在判断下面的评论:"
),
"P7_cot_fewshot": (
"你是电商分析师。请先在心里分析评论的情绪词与语义倾向,"
"然后只回答 positive、neutral 或 negative,不要输出分析过程。\n\n"
"示例:\n"
"评论:质量很好,物流也快 -> positive\n"
"评论:东西还行吧,包装一般 -> neutral\n"
"评论:收到就是坏的 -> negative\n\n"
"现在判断下面的评论:"
),
}
跑实验只需一行:
df = pd.read_csv("reviews_1200.csv")
results = {name: evaluate(df, p) for name, p in PROMPTS.items()}
print(pd.DataFrame(results).T)
五、踩坑与优化
踩坑比想象中多,挑三个最典型的讲。
坑1:JSON输出在小样本下不稳定。 P4强制JSON,结果GPT-4o-mini偶尔会输出 {"label": "Positive"} 带大写,或者干脆多吐一段解释,导致解析失败。我的处理是加归一化 + 兜底到neutral。但这也意味着P4的"准确率"里混进了兜底噪声,实际使用建议加 function calling 或 response_format。
坑2:CoT在分类任务上几乎没用。 我在P7里写了"先在心里分析",结果模型并没有真的思考,只是把标签吐出来,反而因为Prompt更长导致输入Token涨了。这印证了一个观点:CoT适合推理型任务,不适合短标签分类。
坑3:Few-shot示例的"顺序"影响很大。 我最初把3个negative示例放最前面,模型明显偏向输出negative。按 positive→neutral→negative 平衡排序后,F1回升了约3个点。这是个容易被忽略的细节。
优化点: 我把P3的判定规则从"描述性"改成"边界性"(比如明确写"仅陈述事实=neutral"),中性类召回率从0.58提升到0.71,这是提升最大的一处改动。
六、效果数据
1200条样本,GPT-4o-mini 结果如下(Qwen2.5-7B 趋势类似,不单独展开):
| Prompt | Accuracy | F1-macro | 平均输入Token | 平均输出Token | 平均延迟(s) |
|---|---|---|---|---|---|
| P1 裸Prompt | 0.712 | 0.684 | 62 | 2.1 | 0.61 |
| P2 角色 | 0.708 | 0.679 | 84 | 2.2 | 0.68 |
| P3 角色+规则 | 0.783 | 0.761 | 148 | 2.3 | 0.79 |
| P4 结构化JSON | 0.769 | 0.742 | 96 | 9.8 | 0.72 |
| P5 Few-shot 3 | 0.812 | 0.794 | 210 | 2.4 | 0.94 |
| P6 Few-shot 9 | 0.834 | 0.821 | 458 | 2.4 | 1.32 |
| P7 CoT+Few-shot | 0.807 | 0.786 | 246 | 2.6 | 1.01 |
几个关键结论:
- F1从0.684到0.821,提升13.7个百分点,但输入Token从62涨到458,翻了7.4倍。如果按GPT-4o-mini输入价算,单条成本从约 $0.0000093 涨到 $0.0000687,贵了约7.4倍。
- P2角色设定是负收益。加了"资深电商分析师"反而比裸Prompt低0.4个点,说明角色描述在简单分类任务上纯属浪费Token。
- P3规则版性价比最高。相比P1,F1提升7.7个点,Token只涨2.4倍;相比P6,F1只差6个点,但Token只有P6的1/3。
- 延迟和Token基本线性。P6平均延迟1.32s,比P1的0.61s慢一倍多,在实时接口场景要慎重。
如果按"每提升1个F1点消耗的Token数"来算性价比:P3约为 (148-62)/7.7 ≈ 11.2 tokens/点,P6约为 (458-62)/13.7 ≈ 28.9 tokens/点。P3的性价比是P6的2.6倍。
七、总结
这次实验给我最大的感受是:Prompt Engineering不是越复杂越好,而是要找性价比拐点。
对于商品评论情感三分类这个任务,我最终的选型是 P3(角色+规则)。它在F1 0.761的水平上,单条成本不到0.00002元,延迟0.79s,完全够业务用。如果业务对准确率极度敏感、且能接受成本翻倍,再上P6 Few-shot 9例。
还有几点想提醒后来者:
- 别迷信"角色设定",它在简单任务上可能是负收益;
- CoT不是万能药,短标签分类别用;
- Few-shot示例的顺序、数量、类别平衡都会影响结果,别随便贴三段就完事;
- 一定要把Token和延迟纳入评估,否则Prompt优化容易变成"炫技"。
代码和数据脱敏脚本我已经整理到本地仓库,感兴趣的同学可以按上面骨架复现。如果有更优的Prompt方案,欢迎评论区交流。