一、问题背景:为什么我要重新做一遍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

几个关键结论:

  1. F1从0.684到0.821,提升13.7个百分点,但输入Token从62涨到458,翻了7.4倍。如果按GPT-4o-mini输入价算,单条成本从约 $0.0000093 涨到 $0.0000687,贵了约7.4倍。
  2. P2角色设定是负收益。加了"资深电商分析师"反而比裸Prompt低0.4个点,说明角色描述在简单分类任务上纯属浪费Token。
  3. P3规则版性价比最高。相比P1,F1提升7.7个点,Token只涨2.4倍;相比P6,F1只差6个点,但Token只有P6的1/3。
  4. 延迟和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方案,欢迎评论区交流。