一、问题背景:为什么我又回头啃Prompt了
上个月接了个小需求:给一批电商评论做情感三分类(正面/负面/中性)。数据量不大,3万条,标注质量还行。我第一反应是上BERT微调,但产品经理说标签体系可能还要调,先别训练模型,用大模型API跑一版看看效果。
行吧,那就Prompt Engineering。我一开始想得很简单,不就是"请判断以下评论的情感"吗?结果第一版跑出来准确率只有58%,比我预期的低太多。于是花了两天时间系统性地做了组对比实验,把过程记录下来,给同样在做落地的人避避坑。
任务定义:
- 输入:一条中文商品评论,长度20~300字
- 输出:正面 / 负面 / 中性 三选一
- 测试集:500条人工标注数据(正面220、负面180、中性100)
- 评价指标:Accuracy + 各类F1
二、环境与版本
- Python 3.11.6
- openai==1.35.0(走兼容接口)
- 模型A:gpt-4o-mini(2024-07-18版本)
- 模型B:Qwen2.5-7B-Instruct(本地vLLM部署,vllm==0.6.2)
- 参数基线:temperature=0.0,max_tokens=16,top_p=1.0
- 硬件:本地A10 24G单卡跑Qwen,GPT走API
统一封装一个调用函数,方便切换prompt:
import time
from openai import OpenAI
client = OpenAI(api_key="sk-xxx", base_url="https://api.xxx.com/v1")
def call_llm(prompt: str, model: str = "gpt-4o-mini",
temperature: float = 0.0, max_tokens: int = 16):
start = time.time()
resp = client.chat.completions.create(
model=model,
messages=[{"role": "user", "content": prompt}],
temperature=temperature,
max_tokens=max_tokens,
)
latency = time.time() - start
content = resp.choices[0].message.content.strip()
usage = resp.usage
return {
"output": content,
"prompt_tokens": usage.prompt_tokens,
"completion_tokens": usage.completion_tokens,
"latency": round(latency, 3),
}
评测脚本核心逻辑(简化版):
import re
LABELS = ["正面", "负面", "中性"]
def parse_label(text: str) -> str:
for lb in LABELS:
if lb in text:
return lb
return "未知"
def evaluate(prompt_template, dataset, model="gpt-4o-mini"):
correct, total_pt, total_ct, total_lat = 0, 0, 0, 0.0
for item in dataset:
prompt = prompt_template.format(review=item["text"])
r = call_llm(prompt, model=model)
pred = parse_label(r["output"])
if pred == item["label"]:
correct += 1
total_pt += r["prompt_tokens"]
total_ct += r["completion_tokens"]
total_lat += r["latency"]
n = len(dataset)
return {
"acc": round(correct / n, 4),
"avg_prompt_tokens": round(total_pt / n, 1),
"avg_completion_tokens": round(total_ct / n, 1),
"avg_latency": round(total_lat / n, 3),
}
三、方案设计:6种Prompt的对比矩阵
我设计了6个版本,从最朴素到逐步加料:
| 编号 | 方案 | 核心思路 |
|---|---|---|
| P1 | Zero-shot基础版 | 直接问情感 |
| P2 | 加标签定义 | 补充三类定义 |
| P3 | 加输出约束 | 限定只输出标签词 |
| P4 | Few-shot 3例 | 每类给1个例子 |
| P5 | CoT思维链 | 先分析理由再给结论 |
| P6 | 角色+CoT+格式约束 | 综合版 |
各版本的prompt长这样(挑3个关键的):
P1 - Zero-shot
请判断以下评论的情感倾向:
{review}
P3 - 加输出约束
请判断以下商品评论的情感倾向,只输出"正面"、"负面"、"中性"三者之一,不要输出任何其他内容。
评论:{review}
P6 - 综合版
你是一位资深的电商运营分析师,擅长从用户评论中识别真实情感。
判断规则:
1. 正面:明确表达满意、推荐、超出预期,或有具体赞美点
2. 负面:明确表达不满、失望、质量/物流/服务问题
3. 中性:仅陈述事实、无明显情绪、或正负参半且无倾向
请按以下格式输出:
分析:
结论:
评论:{review}
四、核心实验结果
500条测试集,gpt-4o-mini,temperature=0:
| 方案 | Accuracy | 正面F1 | 负面F1 | 中性F1 | 平均prompt tokens | 平均completion tokens |
|---|---|---|---|---|---|---|
| P1 Zero-shot | 0.582 | 0.71 | 0.63 | 0.12 | 68 | 5 |
| P2 加定义 | 0.734 | 0.79 | 0.75 | 0.41 | 152 | 6 |
| P3 输出约束 | 0.781 | 0.82 | 0.79 | 0.52 | 168 | 3 |
| P4 Few-shot 3例 | 0.862 | 0.89 | 0.86 | 0.71 | 412 | 3 |
| P5 CoT | 0.901 | 0.92 | 0.90 | 0.83 | 186 | 68 |
| P6 综合版 | 0.941 | 0.95 | 0.94 | 0.91 | 187 | 72 |
几个观察:
- P1到P2提升最大(+15.2%)。中性类F1从0.12飙到0.41,说明"标签定义"是刚需,模型不定义就不知道你要的中性是什么粒度。
- P3输出约束把completion tokens从6降到3,但准确率还涨了。因为不用再解析"我认为这条评论是正面的"这种废话了。
- P4 Few-shot提升明显但token暴涨,412 token/条,成本是P3的2.5倍,准确率只多8个点。
- P5 CoT性价比最高,只多了18 token的输入,但completion涨到68——不过准确率+12个点,值得。
- P6综合版把角色、规则、CoT、格式全叠上,准确率登顶,token还控制在187(比P4便宜一半多)。
P6在Qwen2.5-7B-Instruct上跑的结果:
| 方案 | Accuracy | 平均prompt tokens | 平均latency |
|---|---|---|---|
| P3 | 0.702 | 168 | 0.42s |
| P5 | 0.834 | 186 | 1.15s |
| P6 | 0.876 | 187 | 1.21s |
7B模型差GPT-4o-mini约6.5个点,但延迟可控,本地部署成本几乎为零。对于这个任务,Qwen2.5-7B+P6已经够用了。
五、踩坑与优化
坑1:temperature=0不等于完全确定
我一开始以为temperature=0就稳了,结果同一批数据跑两次有0.6%的差异。原因是vLLM的batch推理和浮点累加顺序会影响argmax。做评测的时候一定要固定batch size,或者跑3次取平均,别拿单次结果下结论。
坑2:Few-shot的例子顺序影响巨大
P4我最初把3个例子按"正-负-中"排列,中性F1只有0.58。后来改成"中-正-负",中性F1涨到0.71。原因是label ordering bias——模型倾向于复现例子中最后一个标签的分布。对于类别不平衡任务,建议把少数类放在最后。
坑3:CoT不是万能的,要看任务
我一开始把CoT用在了一个"判断是否包含手机号"的抽取任务上,结果准确率反而降了3个点,因为模型开始"脑补"理由,把没手机号的也说成有。CoT适合需要推理的任务,纯分类/抽取任务用格式约束+定义就够了。
优化技巧:动态Few-shot
固定Few-shot的token成本高,我后来改成只对"中性"这一类加2个例子(因为中性最难),正面负面靠定义。这样平均token从412降到243,准确率只掉0.8个点,性价比更高。
六、效果数据汇总与成本
按500条测试集折算到3万条生产数据(gpt-4o-mini,$0.15/1M input,$0.60/1M output):
| 方案 | 预估总token | 预估成本 | 预估准确率 |
|---|---|---|---|
| P1 | 2.19M | $0.36 | 58.2% |
| P4 | 12.45M | $1.89 | 86.2% |
| P6 | 7.77M | $1.31 | 94.1% |
P6比P4省了30%成本,准确率还高8个点。如果用Qwen2.5-7B本地跑,3万条在A10上大约2.5小时,电费忽略不计。
七、总结
这轮实验下来,我的几条经验:
- 标签定义 > Few-shot > CoT,在预算有限时按这个优先级加料。
- 输出格式约束几乎零成本,能加就加,省token还提准确率。
- Few-shot示例顺序要调,尤其是类别不平衡时,别偷懒。
- CoT不是银弹,推理任务用,纯分类任务慎用。
- 评测要跑多次,temperature=0也有波动,单次结果不可信。
Prompt Engineering不是玄学,本质是把"任务定义"翻译成模型能稳定执行的形式。定义清楚、约束明确、例子到位,7B小模型也能干出不错的活。下一步我打算把这套prompt固化成模板,接入线上做A/B测试,看看真实流量下的表现。
有在做类似任务的同学欢迎评论区交流,尤其是中性类的处理,我这边F1到0.91就上不去了,还在琢磨怎么再挤一挤。