一、问题背景:为什么我又开始折腾Prompt了
先说场景。我们有个电商评论分析服务,需要对用户评论做结构化抽取,输出固定字段:
{
"sentiment": "positive|negative|neutral",
"category": "物流|质量|客服|价格|其他",
"keywords": ["..."]
}
最初上线的版本用的是“教科书式”Prompt:角色设定 + 详细任务说明 + 3个示例 + 输出格式约束。跑了一个月,两个问题越来越明显:
- 成本高:每条评论平均消耗1600~1900 token,日处理20万条,光输入token一个月就是一笔不小的开销。
- 稳定性差:模型偶尔会在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 |
几个关键结论:
- Token降幅:P5相比P1,Prompt token从1782降到420,降幅76.4%。按日20万条算,每天省下约2.7亿token。
- 质量不降反升:GPT-4o-mini上F1从0.82提升到0.94,解析成功率从96.3%到99.8%。
- P4是性价比拐点:如果只追token,P4最省;但P5多花152 token换回2个点F1和0.2%解析率,我认为更值。
- 弱模型更依赖结构约束:Qwen上P4明显不如P5,说明小模型需要更明确的字段定义。
延迟方面,P5平均响应时间从P1的1.42s降到0.91s,主要因为输入变短、解码更确定。
七、总结
这轮实验最大的收获不是“Prompt要短”,而是要把Prompt当成接口契约来写,而不是当成需求文档来写。模型不需要你告诉它“你是资深专家”,它需要你告诉它“输出空间长什么样”。
具体三条经验:
- 能用枚举和Schema约束的,别用自然语言描述。
- 示例数量控制在0~1个,多了会限制输出分布。
- 极简Prompt在强模型上安全,在弱模型上要补字段定义。
最终线上切到P5,跑了三周,解析失败率稳定在0.2%以下,月度token成本下降约七成。Prompt Engineering不是玄学,它就是一个可以量化、可以A/B、可以持续优化的工程问题。