一、问题背景:为什么我又开始折腾Prompt了

先说场景。我们有个电商评论分析服务,需要对用户评论做结构化抽取,输出固定字段:

{
  "sentiment": "positive|negative|neutral",
  "category": "物流|质量|客服|价格|其他",
  "keywords": ["..."]
}

最初上线的版本用的是“教科书式”Prompt:角色设定 + 详细任务说明 + 3个示例 + 输出格式约束。跑了一个月,两个问题越来越明显:

  1. 成本高:每条评论平均消耗1600~1900 token,日处理20万条,光输入token一个月就是一笔不小的开销。
  2. 稳定性差:模型偶尔会在JSON外面加解释性文字,导致解析失败率约3.7%。

于是我决定系统性地做一轮Prompt对比实验,目标很明确:在保证甚至提升抽取质量的前提下,把token消耗压下来。

二、环境与版本

实验环境如下,都是实际跑实验时用的版本:

  • Python 3.11.6
  • openai==1.35.0(调GPT-4o-mini)
  • dashscope==1.20.1(调Qwen2.5-7B-Instruct)
  • pandas==2.2.2
  • 评测集:人工标注的500条电商评论,覆盖5个类目、3种情感极性
  • 评测指标:字段级F1(sentiment/category/keywords分别算再平均)、JSON解析成功率、平均token消耗
  • 温度:统一 temperature=0.2,top_p=0.9,max_tokens=256

选这两个模型是因为线上就是双模型灰度:GPT-4o-mini负责高价值订单,Qwen2.5-7B-Instruct负责长尾流量。

三、方案设计:5种Prompt变体

我把Prompt拆成几个可控变量:角色设定有无、任务说明详略、示例数量、输出格式约束方式。组合出5个版本:

版本 角色设定 任务说明 示例数 输出约束 特点
P1 有 详细 3 自然语言描述 初始线上版
P2 有 详细 0 自然语言描述 去示例
P3 无 精简 1 JSON Schema 精简+单示例
P4 无 极简 0 JSON Schema 极简
P5 无 极简 0 JSON Schema + 枚举约束 本文最优

核心思路:把“给模型讲道理”换成“给模型划边界”。自然语言描述格式容易让模型自由发挥,而JSON Schema + 枚举约束把输出空间直接压缩。

四、核心实现

先贴评测主流程代码:

import json
import time
import pandas as pd
from openai import OpenAI

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

def call_gpt(prompt: str) -> dict:
    start = time.time()
    resp = client.chat.completions.create(
        model="gpt-4o-mini",
        messages=[{"role": "user", "content": prompt}],
        temperature=0.2,
        top_p=0.9,
        max_tokens=256,
        response_format={"type": "json_object"},  # P3/P4/P5启用
    )
    latency = time.time() - start
    text = resp.choices[0].message.content
    return {
        "text": text,
        "prompt_tokens": resp.usage.prompt_tokens,
        "completion_tokens": resp.usage.completion_tokens,
        "latency": latency,
    }

def evaluate(prompt_template: str, dataset: pd.DataFrame):
    records = []
    for _, row in dataset.iterrows():
        prompt = prompt_template.format(review=row["review"])
        out = call_gpt(prompt)
        try:
            parsed = json.loads(out["text"])
            parse_ok = 1
        except json.JSONDecodeError:
            parsed, parse_ok = {}, 0
        records.append({
            "parse_ok": parse_ok,
            "prompt_tokens": out["prompt_tokens"],
            "completion_tokens": out["completion_tokens"],
            "latency": out["latency"],
            "pred": parsed,
            "gold": json.loads(row["label"]),
        })
    return pd.DataFrame(records)

P5的Prompt模板(最终胜出版本):

P5_TEMPLATE = """从评论中抽取结构化信息,只输出JSON。

字段定义:
- sentiment: 枚举["positive","negative","neutral"]
- category: 枚举["物流","质量","客服","价格","其他"]
- keywords: 字符串数组,最多3个,取自评论原文

评论:{review}
JSON:"""

对比P1的初始版本(节选):

P1_TEMPLATE = """你是一位资深的电商评论分析专家,拥有多年用户反馈处理经验。
请你仔细阅读下面的用户评论,从情感倾向、问题类目、关键词三个维度进行分析。

要求:
1. sentiment字段必须是positive、negative、neutral三者之一...
(此处省略约400字说明)
2. category字段需要判断评论主要涉及哪个方面...
3. keywords请提取最能代表评论内容的词汇...

示例1:
评论:物流太慢了,等了一周才到
输出:{{"sentiment":"negative","category":"物流","keywords":["物流慢","一周"]}}

(再省略2个示例)

现在请处理这条评论:{review}
请输出JSON格式结果:"""

五、踩坑与优化

坑1:response_format 不是万能的。 我一开始以为加了 response_format={"type": "json_object"} 就万事大吉,结果P3在Qwen上仍然偶发解析失败。原因是Qwen2.5-7B-Instruct对OpenAI兼容接口的这个参数支持不完整,需要在Prompt里再显式强调“只输出JSON”。所以P5的模板里保留了“只输出JSON”这句话。

坑2:示例不是越多越好。 P1用了3个示例,看似稳妥,实际上模型会过度模仿示例的措辞。比如示例里keywords都是2个词,模型就很少输出3个词,导致召回率下降。P3只留1个示例反而更均衡。

坑3:枚举约束的写法很关键。 我试过写“category应该是物流、质量、客服、价格或其他之一”,模型偶尔会输出“配送”这种近义词。改成 枚举["物流","质量","客服","价格","其他"] 这种带方括号的写法后,越界率从2.1%降到了0.3%。

坑4:极简Prompt对弱模型不友好。 P4在GPT-4o-mini上表现很好,但在Qwen2.5-7B-Instruct上F1掉了6个点。所以P5在P4基础上加回了字段定义,属于“极简但有边界”。

六、效果数据

500条评测集,两个模型各跑一遍,结果如下(token为单条平均):

GPT-4o-mini

版本 平均Prompt Token 平均Completion Token JSON解析成功率 字段级F1
P1 1782 68 96.3% 0.82
P2 412 71 97.1% 0.85
P3 486 64 99.4% 0.91
P4 268 62 99.6% 0.92
P5 420 61 99.8% 0.94

Qwen2.5-7B-Instruct

版本 平均Prompt Token JSON解析成功率 字段级F1
P1 1782 93.8% 0.78
P3 486 98.2% 0.87
P4 268 95.1% 0.86
P5 420 99.0% 0.90

几个关键结论:

  1. Token降幅:P5相比P1,Prompt token从1782降到420,降幅76.4%。按日20万条算,每天省下约2.7亿token。
  2. 质量不降反升:GPT-4o-mini上F1从0.82提升到0.94,解析成功率从96.3%到99.8%。
  3. P4是性价比拐点:如果只追token,P4最省;但P5多花152 token换回2个点F1和0.2%解析率,我认为更值。
  4. 弱模型更依赖结构约束:Qwen上P4明显不如P5,说明小模型需要更明确的字段定义。

延迟方面,P5平均响应时间从P1的1.42s降到0.91s,主要因为输入变短、解码更确定。

七、总结

这轮实验最大的收获不是“Prompt要短”,而是要把Prompt当成接口契约来写,而不是当成需求文档来写。模型不需要你告诉它“你是资深专家”,它需要你告诉它“输出空间长什么样”。

具体三条经验:

  • 能用枚举和Schema约束的,别用自然语言描述。
  • 示例数量控制在0~1个,多了会限制输出分布。
  • 极简Prompt在强模型上安全,在弱模型上要补字段定义。

最终线上切到P5,跑了三周,解析失败率稳定在0.2%以下,月度token成本下降约七成。Prompt Engineering不是玄学,它就是一个可以量化、可以A/B、可以持续优化的工程问题。