一、问题背景:为什么我又开始折腾 Prompt
事情起因很简单。我们有个内部工单系统,每天大概 3000 条用户反馈进来,需要自动打上分类标签(退款、物流、账号、产品咨询、投诉、其他)。之前用的是微调后的 BERT-base,准确率 89%,但有两个问题:
- 新类别出现时要重新标注 + 重训,周期至少一周;
- 模型部署要占一张 T4,推理延迟 80ms,但维护成本高。
于是我想试试用 LLM + Prompt Engineering 能不能直接替代。目标很明确:准确率不低于 89%,单条成本低于 0.0005 美元,延迟低于 1.5s。
但 Prompt 这东西,写法和写法之间差距巨大。我不想凭感觉说“这个 Prompt 更好”,所以干脆做了一次系统性对比。
二、环境与版本
- 模型:
gpt-4o-mini,快照gpt-4o-mini-2024-07-18 - SDK:
openai==1.40.0 - Python:3.11.6
- 测试集:200 条人工标注工单,6 类分布均衡(每类 33-34 条)
- 温度:
temperature=0,top_p=1,保证可复现 - 计费口径:按 OpenAI 官方 2024-07 价格,输入 $0.15/1M tokens,输出 $0.60/1M tokens
- 评测脚本:准确率 + 混淆矩阵 + Token 统计
三、方案设计:6 个版本的 Prompt
我把 Prompt 拆成几个可控变量:是否给类别定义、是否给 Few-shot、是否要求思维链、是否强制 JSON 输出。组合出 6 版:
| 版本 | 特征 | 大致思路 |
|---|---|---|
| V1 | 零样本裸提示 | “把下面文本分类” |
| V2 | 加类别定义 | 每类给一句解释 |
| V3 | 加 Few-shot(每类 1 条) | 6 条示例 |
| V4 | Few-shot + JSON 输出 | 强制结构化 |
| V5 | V4 + 思维链 | 先解释再给标签 |
| V6 | V5 + Few-shot 每类 3 条 | 18 条示例 |
四、核心实现
先看评测主循环,这块所有版本共用:
import json
import time
from openai import OpenAI
client = OpenAI(api_key="sk-xxx")
def run_eval(prompt_builder, testset, model="gpt-4o-mini-2024-07-18"):
results = []
total_in, total_out = 0, 0
for item in testset:
messages = prompt_builder(item["text"])
t0 = time.time()
resp = client.chat.completions.create(
model=model,
messages=messages,
temperature=0,
top_p=1,
max_tokens=200,
)
latency = time.time() - t0
content = resp.choices[0].message.content.strip()
total_in += resp.usage.prompt_tokens
total_out += resp.usage.completion_tokens
results.append({
"text": item["text"],
"gold": item["label"],
"pred": parse_label(content),
"raw": content,
"latency": latency,
})
acc = sum(r["pred"] == r["gold"] for r in results) / len(results)
cost = total_in / 1e6 * 0.15 + total_out / 1e6 * 0.60
return {
"acc": acc,
"avg_in": total_in / len(results),
"avg_out": total_out / len(results),
"cost_per_item": cost / len(results),
"avg_latency": sum(r["latency"] for r in results) / len(results),
"details": results,
}
再看 V1 和 V4 的 Prompt 构造,对比最明显:
# V1:零样本裸提示
def v1_builder(text):
return [
{"role": "system", "content": "你是一个文本分类助手。"},
{"role": "user", "content": f"请把下面的文本分类:\n{text}"},
]
# V4:Few-shot + JSON 输出
CATEGORIES = ["退款", "物流", "账号", "产品咨询", "投诉", "其他"]
FEW_SHOT = [
("我要退货,订单号 12345", "退款"),
("快递三天没动了", "物流"),
("登录一直提示密码错误", "账号"),
("你们这个功能怎么用", "产品咨询"),
("客服态度太差了,我要投诉", "投诉"),
("你们公司地址在哪", "其他"),
]
def v4_builder(text):
examples = "\n".join(
f'文本:{t}\n输出:{{"label": "{l}"}}' for t, l in FEW_SHOT
)
system = (
"你是一个工单分类器。只能从以下类别中选择一个:"
+ "、".join(CATEGORIES)
+ "。必须只输出 JSON,格式为 {\"label\": \"类别\"},不要输出任何其他内容。"
)
user = f"示例:\n{examples}\n\n现在分类:\n文本:{text}\n输出:"
return [
{"role": "system", "content": system},
{"role": "user", "content": user},
]
V5 在 V4 基础上加了一句“先在心里判断关键词,再输出 JSON”,但要求最终只输出 JSON;V6 则把 Few-shot 从 6 条扩到 18 条。
五、踩坑与优化
坑 1:V1 输出根本没法解析。 模型会返回“这段文本属于退款类,因为提到了退货”。parse_label 只能靠关键词匹配,准确率自然崩。
坑 2:V3 的 Few-shot 顺序有影响。 我一开始把“其他”放在第一条,结果模型偏向输出“其他”,F1 掉了 4 个点。后来把类别按业务频率排序,才稳定下来。
坑 3:V5 的思维链在 temperature=0 下反而拖后腿。 模型有时会在解释里自我否定,最后给的标签和解释矛盾。加了一句“解释只用于内部推理,最终标签以 JSON 为准”后才好转。
坑 4:JSON 输出偶尔带 markdown 代码块。 即使 system 里写了“不要输出其他内容”,还是有 3% 概率包 json。解决办法是用 response_format={"type": "json_object"},但注意这个参数要求 prompt 里必须出现 "JSON" 字样,否则会报 400。
优化后 V4 的解析成功率从 97% 提到 100%。
六、效果数据
200 条测试集上的结果:
| 版本 | 准确率 | 平均输入 Token | 平均输出 Token | 单条成本(USD) | 平均延迟(s) |
|---|---|---|---|---|---|
| V1 | 71.5% | 42 | 16 | 0.0000159 | 0.62 |
| V2 | 82.0% | 118 | 14 | 0.0000261 | 0.71 |
| V3 | 90.5% | 246 | 12 | 0.0000441 | 0.83 |
| V4 | 93.0% | 268 | 11 | 0.0000468 | 0.86 |
| V5 | 94.0% | 312 | 48 | 0.0000756 | 1.21 |
| V6 | 94.5% | 412 | 11 | 0.0000684 | 1.05 |
几个关键结论:
- Few-shot 是准确率跃迁的关键:V2→V3 涨了 8.5 个点,成本只多了 0.000018 美元。
- JSON 强制输出性价比极高:V4 比 V3 只多 22 个输入 Token,准确率 +2.5%,还省了后处理。
- 思维链不是万能药:V5 准确率只比 V4 高 1 个点,但输出 Token 翻了 4 倍多,延迟 +40%。
- Few-shot 加到 18 条边际收益递减:V6 比 V5 只高 0.5 个点,但输入 Token 多了 100。
最终我选了 V4:准确率 93%,已经超过 BERT 的 89%,单条成本 0.0000468 美元,按每天 3000 条算,一个月成本约 4.2 美元,比养一张 T4 便宜太多。延迟 0.86s 也在可接受范围。
如果对准确率要求更高,可以上 V6,但我会建议先把 V4 的 Few-shot 示例质量再打磨一遍,而不是盲目加数量。
七、总结
这次实验最大的感受是:Prompt Engineering 不是玄学,是可以量化的工程问题。同样一个分类任务,Prompt 从 42 个输入 Token 写到 412 个,准确率差 23 个点,成本差 4.3 倍。关键不是“写得越长越好”,而是找到那个性价比拐点。
我的经验是:
- 先写 V1 摸清模型 baseline;
- 加类别定义,通常能涨 5-10 个点;
- 加 Few-shot,每类 1-2 条足够,顺序按业务频率排;
- 强制 JSON 输出,省后处理还提准确率;
- 思维链只在推理类任务上用,分类任务慎用;
- 每次改动都跑固定测试集,记录 Token 和准确率,别凭感觉。
代码和测试集脱敏后我放在了内部仓库,核心逻辑就是上面那两段。如果你也在做类似的事,欢迎交流。