一、问题背景:为什么我又回头啃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

几个观察:

  1. P1到P2提升最大(+15.2%)。中性类F1从0.12飙到0.41,说明"标签定义"是刚需,模型不定义就不知道你要的中性是什么粒度。
  2. P3输出约束把completion tokens从6降到3,但准确率还涨了。因为不用再解析"我认为这条评论是正面的"这种废话了。
  3. P4 Few-shot提升明显但token暴涨,412 token/条,成本是P3的2.5倍,准确率只多8个点。
  4. P5 CoT性价比最高,只多了18 token的输入,但completion涨到68——不过准确率+12个点,值得。
  5. 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小时,电费忽略不计。

七、总结

这轮实验下来,我的几条经验:

  1. 标签定义 > Few-shot > CoT,在预算有限时按这个优先级加料。
  2. 输出格式约束几乎零成本,能加就加,省token还提准确率。
  3. Few-shot示例顺序要调,尤其是类别不平衡时,别偷懒。
  4. CoT不是银弹,推理任务用,纯分类任务慎用。
  5. 评测要跑多次,temperature=0也有波动,单次结果不可信。

Prompt Engineering不是玄学,本质是把"任务定义"翻译成模型能稳定执行的形式。定义清楚、约束明确、例子到位,7B小模型也能干出不错的活。下一步我打算把这套prompt固化成模板,接入线上做A/B测试,看看真实流量下的表现。

有在做类似任务的同学欢迎评论区交流,尤其是中性类的处理,我这边F1到0.91就上不去了,还在琢磨怎么再挤一挤。