一、问题背景:为什么我开始折腾Prompt
上个月接了个小需求:给一个电商后台做评论自动分类,把用户评价分成“物流”“质量”“客服”“其他”四类,方便运营筛选。听起来很简单,我第一反应是微调一个BERT,但标注数据只有800条,训出来F1只有0.81,而且每次加新类别都要重训,维护成本太高。
于是转向LLM方案。但直接丢一句“请分类这条评论”给模型,效果并不稳定。我统计了200条测试集,零样本Prompt准确率只有78%,而且输出格式五花八门——有的返回“物流”,有的返回“物流问题”,有的还带解释。运营那边要的是能直接入库的结构化字段。
所以我决定系统性地做一轮Prompt Engineering实验,目标有三个:
1. 准确率做到90%以上;
2. 输出必须是严格JSON,字段固定;
3. 单条评论token消耗控制在250以内,控制成本。
二、环境与版本
- 模型:OpenAI GPT-4o-mini(2024-07-18版本)、Anthropic Claude 3.5 Haiku(2024-10-22版本)
- SDK:openai==1.51.0、anthropic==0.34.2
- 测试集:200条人工标注电商评论,四类各50条
- 评估指标:准确率、单条平均token(输入+输出)、单条平均延迟
- 硬件:本地MacBook Pro M2,网络走API
三、方案设计:12组Prompt的变量控制
我把Prompt拆成四个可调维度,做正交对比:
| 维度 | 选项 |
|---|---|
| 角色设定 | 无 / “你是电商运营专家” |
| 少样本示例 | 0-shot / 3-shot / 5-shot |
| 输出约束 | 自由文本 / “只返回类别词” / JSON Schema |
| 思维链 | 无 / “先判断关键词再分类” |
为了控制变量,我固定temperature=0,max_tokens=100,每条评论只跑一次,避免采样波动。最终挑了12组有代表性的组合,下面列出核心几组。
四、核心实现:代码与Prompt模板
先看基础调用封装:
import openai
import json
import time
client = openai.OpenAI(api_key="sk-xxx")
def classify_review(review, prompt_template, model="gpt-4o-mini"):
start = time.time()
resp = client.chat.completions.create(
model=model,
messages=[{"role": "user", "content": prompt_template.format(review=review)}],
temperature=0,
max_tokens=100
)
latency = time.time() - start
content = resp.choices[0].message.content.strip()
usage = resp.usage
return {
"raw": content,
"prompt_tokens": usage.prompt_tokens,
"completion_tokens": usage.completion_tokens,
"latency": round(latency, 2)
}
下面是几组典型Prompt模板。P1 零样本裸Prompt:
P1 = """请对以下电商评论进行分类,类别为:物流、质量、客服、其他。
评论:{review}
类别:"""
P5 角色+3-shot+JSON约束:
P5 = """你是资深电商运营专家,需要对用户评论做精准分类。
分类体系:
- 物流:配送慢、包裹破损、发错货
- 质量:商品瑕疵、材质差、与描述不符
- 客服:态度差、回复慢、不解决问题
- 其他:不属于以上三类
示例:
评论:快递太慢了,等了一周才到。 -> {{"category": "物流"}}
评论:衣服面料很舒服,但线头有点多。 -> {{"category": "质量"}}
评论:客服半天不回消息,气死了。 -> {{"category": "客服"}}
现在请分类下面这条评论,只返回JSON,不要解释:
评论:{review}
输出:"""
P8 加入思维链但限制输出:
P8 = """你是电商评论分类助手。请按以下步骤处理:
第一步:找出评论中与物流、质量、客服相关的关键词。
第二步:根据关键词判断主类别。
第三步:只输出JSON格式结果。
评论:{review}
请直接输出JSON:"""
批量跑测试集的脚本:
import pandas as pd
df = pd.read_csv("test_200.csv") # columns: review, label
results = []
for _, row in df.iterrows():
out = classify_review(row["review"], P5)
# 解析JSON
try:
pred = json.loads(out["raw"])["category"]
except Exception:
pred = "解析失败"
results.append({
"review": row["review"],
"label": row["label"],
"pred": pred,
"prompt_tokens": out["prompt_tokens"],
"completion_tokens": out["completion_tokens"],
"latency": out["latency"]
})
res_df = pd.DataFrame(results)
print("准确率:", (res_df["label"] == res_df["pred"]).mean())
print("平均token:", (res_df["prompt_tokens"] + res_df["completion_tokens"]).mean())
五、踩坑与优化
坑1:JSON输出不稳定。 即使写了“只返回JSON”,GPT-4o-mini仍有约7%概率在JSON前后加“好的,分类结果如下:”。解决办法是加response_format={"type": "json_object"},准确率立刻稳定,但要注意这个参数要求Prompt里必须出现“JSON”字样,否则API会报错。
坑2:少样本示例的类别偏差。 我最初3-shot示例里“物流”给了2条,结果模型对物流类过预测,物流类召回率0.96但精确率只有0.82。改成四类各1条后,F1从0.83升到0.89。
坑3:思维链反而拖累分类。 P8让模型先找关键词,结果有些评论同时出现“质量”和“客服”关键词,模型在推理里绕圈,延迟从1.1s涨到1.9s,准确率还降了2个点。对于这种四分类短文本任务,思维链收益为负。
坑4:Claude 3.5 Haiku对JSON约束更敏感。 同样的P5,Claude输出JSON的合规率99%,但token消耗比GPT-4o-mini高约18%,因为它的system prompt处理方式不同。最终我选GPT-4o-mini做主力。
六、效果数据
12组实验的核心数据如下(200条测试集):
| Prompt | 准确率 | 平均token | 平均延迟 |
|---|---|---|---|
| P1 零样本 | 78.0% | 320 | 1.05s |
| P3 角色设定 | 81.5% | 335 | 1.12s |
| P4 3-shot | 86.0% | 410 | 1.28s |
| P5 角色+3-shot+JSON | 94.0% | 210 | 1.15s |
| P8 思维链 | 84.5% | 480 | 1.90s |
P5相比P1:准确率+16个点,token反而降了34%。原因是结构化约束让输出从平均45 token降到12 token。按每天1万条评论算,GPT-4o-mini输入$0.15/1M、输出$0.60/1M,月成本从$43降到$28。
七、总结
这轮实验最大的体会是:Prompt Engineering不是堆字数,而是做约束设计。角色设定提升有限(+3.5%),少样本示例有效但要防类别偏差(+8%),结构化输出约束是性价比最高的一步——既提准确率又降token。思维链不是万能药,短文本分类任务上要谨慎用。
下一步我打算把这套Prompt模板做成配置化,让运营自己调类别体系,不用改代码。如果你也在做类似任务,建议先跑基线,再逐维度加约束,用数据说话,别凭感觉写Prompt。