一、问题背景:为什么我决定重写Prompt

上个月接手一个电商评论分析模块,需求很朴素:把用户评论分成「好评 / 中评 / 差评」三类,输出置信度和关键理由。

第一版Prompt我随手写的,大概是这样:

prompt = f"请判断这条评论是好评、中评还是差评:{comment}"

上线后准确率只有78.3%,而且输出格式极其不稳定——有时候返回"好评",有时候返回"这条评论属于好评类别",有时候还会自己加一段解释。后端正则解析直接崩了。

更麻烦的是成本。当时用的是Few-shot,塞了8个示例,单次请求input token接近600,按每天5万次调用算,一个月光这一项就要$18.7。老板问我能不能优化,我说能,于是有了下面这轮实验。

二、环境与版本

  • 模型:gpt-4o-mini-2024-07-18
  • SDK:openai==1.35.0
  • Python:3.11.6
  • 测试集:人工标注的1200条电商评论(好评512 / 中评341 / 差评347)
  • 评测脚本:本地跑,准确率 + 格式合规率 + 平均token
  • 关键参数:temperature=0,max_tokens=200,response_format 按实验分组

为什么选gpt-4o-mini而不是gpt-4o?因为这是要上生产的批量任务,成本敏感。gpt-4o-mini的定价是$0.15/1M input、$0.60/1M output,gpt-4o是它的十几倍。实测在这个任务上,只要Prompt设计得当,mini完全够用。

三、方案设计:四组Prompt对比

我设计了四组策略,控制变量,只改Prompt:

组别 策略 示例数 输出约束
A Zero-shot 0 无
B Few-shot 4 无
C Few-shot + CoT 4 无
D Few-shot + CoT + JSON Schema 4 json_object

A组就是基线,测模型裸能力。B组加4个代表性示例(每类至少1个,含1个易混淆的中评)。C组在B基础上要求模型先输出判断依据再给结论。D组在C基础上强制JSON输出。

示例的选择很关键。我一开始随机抽了4条,效果一般。后来改成"边界样本优先"——专门挑那种"质量还行但物流慢"的中评,模型对边界的判断明显变好。

四、核心实现

先看统一调用封装:

import json
import time
from openai import OpenAI

client = OpenAI(api_key="sk-xxx")

def call_model(prompt: str, use_json: bool = False) -> dict:
    kwargs = {
        "model": "gpt-4o-mini-2024-07-18",
        "messages": [{"role": "user", "content": prompt}],
        "temperature": 0,
        "max_tokens": 200,
    }
    if use_json:
        kwargs["response_format"] = {"type": "json_object"}

    start = time.time()
    resp = client.chat.completions.create(**kwargs)
    latency = time.time() - start

    return {
        "content": resp.choices[0].message.content,
        "prompt_tokens": resp.usage.prompt_tokens,
        "completion_tokens": resp.usage.completion_tokens,
        "latency": latency,
    }

D组的Prompt模板,这是最终胜出的版本:

SYSTEM_PROMPT = """你是电商评论情感分类专家。请严格按以下规则分类:

分类标准:
- 好评(pos):整体满意,无明显抱怨
- 中评(neu):褒贬参半,或问题轻微但被提及
- 差评(neg):核心体验受损,明确表达不满

判断步骤:
1. 提取评论中的正面点与负面点
2. 判断负面点是否影响核心体验(质量/功能/效果)
3. 若仅物流/包装问题且产品本身好评,归为neu
4. 输出JSON

输出格式(必须严格遵守):
{"label": "pos|neu|neg", "confidence": 0.0-1.0, "reason": "20字内理由"}
"""

def build_prompt(comment: str, examples: list) -> str:
    ex_text = "\n".join(
        f"评论:{e['text']}\n输出:{json.dumps(e['output'], ensure_ascii=False)}"
        for e in examples
    )
    return f"""{SYSTEM_PROMPT}

参考示例:
{ex_text}

待分类评论:{comment}
输出:"""

评测脚本:

def evaluate(dataset, prompt_builder, use_json=False):
    correct, total = 0, 0
    fmt_ok = 0
    total_tokens = 0

    for item in dataset:
        prompt = prompt_builder(item["text"])
        result = call_model(prompt, use_json=use_json)
        total_tokens += result["prompt_tokens"] + result["completion_tokens"]

        try:
            parsed = json.loads(result["content"])
            fmt_ok += 1
            pred = parsed["label"]
        except Exception:
            pred = result["content"].strip()[:3]

        if pred == item["label"]:
            correct += 1
        total += 1

    return {
        "accuracy": correct / total,
        "format_rate": fmt_ok / total,
        "avg_tokens": total_tokens / total,
    }

五、踩坑与优化

坑1:response_format=json_object 必须Prompt里出现"JSON"字样。 我一开始只在system里写了输出格式,没显式提"JSON"这个词,API直接报400。官方文档有写,但很容易忽略。

坑2:temperature=0 不等于完全确定性。 在1200条测试集上重跑两次,D组仍有7条结果不一致。所以生产环境我加了结果缓存,同一条评论用hash做key,避免重复调用。

坑3:CoT让output token涨了3倍。 C组平均completion token从18涨到61,虽然准确率上去了,但成本也上去了。D组通过"reason限制20字"把completion压回31,这是成本和效果的平衡点。

坑4:示例顺序有影响。 把差评示例放最后,模型对差评的召回率提升约2个百分点(可能是recency bias)。最终我把易混淆的中评放中间,差评放最后。

六、效果数据

1200条测试集,temperature=0,跑3次取平均:

组别 准确率 格式合规率 平均Token 月成本(5万/天)
A 78.3% 71.2% 42 $2.4
B 89.7% 88.5% 187 $10.1
C 94.2% 90.1% 243 $13.6
D 96.1% 99.8% 31 $4.2

几个值得说的点:

  1. D组比C组准确率高1.9个点,token反而少了87%。原因是JSON Schema约束让模型不再啰嗦,直接给结论。
  2. 格式合规率从71.2%到99.8%,这是工程上最有价值的提升——后端解析再也不用写一堆fallback。
  3. 按每天5万次调用算,D组月成本$4.2,比最初的B组方案省了77%。
  4. 延迟方面,D组平均1.2s,比B组的1.8s还快,因为output短了。

分类别看,差评的F1从0.81提升到0.94,中评从0.72提升到0.91。中评是最难的一类,CoT的推理步骤对它的帮助最大。

七、总结

这轮实验下来,我的几个结论:

第一,结构化输出约束的性价比极高。JSON Schema既提升准确率又降token,没有理由不用。

第二,Few-shot的示例质量比数量重要。4个精心挑选的边界样本,效果超过8个随机样本。别堆示例,要挑示例。

第三,CoT不是免费的。它提升准确率,但token涨得厉害。如果任务本身不难(比如二分类),CoT可能不划算。要算ROI。

第四,Prompt Engineering是可量化的工程。别凭感觉调,建测试集、跑指标、看token,跟调模型超参一样对待。

最后附上我现在的默认模板思路:System里写清分类标准和判断步骤,Few-shot给4个边界样本,强制JSON输出,reason限长。这套组合在电商情感分类上稳定在96%左右,成本可控,格式稳定。

如果你也在做类似任务,建议先跑一遍这四组对比,你会对"Prompt值多少钱"有非常直观的感受。