一、问题背景:为什么我开始折腾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。