一、问题背景:为什么一个“简单”分类任务翻车了
上个月接手一个客服工单自动打标项目,需求是把用户反馈分成“硬件故障/软件Bug/操作咨询/售后投诉”四类。我最初以为这是LLM的基操,直接甩了个Prompt让GPT-4分类,结果验证集准确率只有42%——比随机猜(25%)好不了多少。最离谱的是,模型把“屏幕闪烁”归类为“操作咨询”,把“退款流程”归类为“硬件故障”。
深挖原因有三:
1. 标签边界模糊(“软件Bug”和“操作咨询”在用户表述中高度重叠)
2. 用户口语噪音多(错别字、中英混杂、emoji)
3. 原始Prompt没有给模型任何推理锚点,它只是在“猜”我的意图
这个项目让我意识到:Prompt不是写句话就行,它是个需要量化调优的工程模块。
二、环境与版本基线
- 模型:OpenAI gpt-4-0613(temperature=0.2, top_p=0.9)
- 调用库:openai-python v1.14.2
- 数据集:自建200条中文工单(来自历史客服记录),训练集/验证集=160/40,另备50条作为最终测试
- 评估指标:macro-F1(四类均衡计算)
- 成本核算:OpenAI账单API拉取,美元计价
所有实验在单台MacBook Pro M2上跑通,代码见下。我强烈建议你锁定模型版本——同一个Prompt在gpt-4-0125-preview和gpt-4-0613上的表现能差出15个百分点。
三、方案设计:四个递进式Prompt模板
我按“信息密度”从低到高设计了四个版本:
- V1 - 裸Prompt:直接问“这段文字属于哪类?”
- V2 - 结构化指令:加上“四类定义+输出格式要求”
- V3 - Few-shot示例:每类给2个典型样本(共8个示例)
- V4 - 思维链+格式锁:要求先输出推理过程,再给标签,且标签必须用“【】”包裹
关键设计决策:V4的推理部分不参与最终标签提取,只作为“中间计算过程”让模型注意力更聚焦。实践证明这一步极其关键。
四、核心实现:代码与token统计
调用函数统一封装(含重试机制),核心代码如下:
# prompt_engine.py
import openai
import json
client = openai.OpenAI(api_key="sk-...")
def call_gpt(system_prompt, user_text, model="gpt-4-0613", temp=0.2):
resp = client.chat.completions.create(
model=model,
temperature=temp,
messages=[
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_text}
],
max_tokens=300
)
# 返回内容与token消耗
return (
resp.choices[0].message.content,
resp.usage.prompt_tokens,
resp.usage.completion_tokens
)
V4 Prompt模板(核心改动):
V4_SYSTEM = """你是工单分类引擎。任务:将用户描述归类为以下四类之一:
- 硬件故障: 物理损坏/部件不工作/高温/异响
- 软件Bug: 报错/闪退/界面错乱/功能无响应
- 操作咨询: 询问使用步骤/功能存在性/配置方法
- 售后投诉: 退款/换货/服务态度/时效不满
规则:
1. 先输出“推理:”然后简述你的归类依据(不超过50字)
2. 再输出“标签:”并紧跟【类别名】
3. 若信息不足,输出【操作咨询】但推理中必须写明“缺关键信息”
示例:
用户:电脑开机蓝屏,代码0x000000f
推理:蓝屏有错误代码,指向系统底层故障,属软件Bug。
标签:【软件Bug】
"""
每个请求的token消耗我用一个装饰器记录下来,200轮测试后汇总:
- V1平均prompt_tokens=1203,completion_tokens=42
- V4平均prompt_tokens=488,completion_tokens=124
注意:V4的prompt更短是因为few-shot示例精简了(每类1个示例而非2个),但效果反超V3——说明示例质量>数量。
五、踩坑与优化:三个血泪教训
教训1:few-shot示例不能“雨露均沾”
V3我用每类2个示例,结果“操作咨询”被过度泛化,模型把所有不确定的都归进去了。优化:只保留边界模糊的示例(比如“手机掉水里还能用吗”归为硬件故障而不是操作咨询),准确率直接+6%。
教训2:中文标点会破坏格式锁
V4要求标签用【】包裹,但用户原文中的全角括号会让模型混淆。解决:在prompt里加一句“用户文本中的括号视为普通字符,不用于标签判断”。
教训3:思维链长度限制
V4如果推理部分超过50字,模型会开始“编造”依据。我把max_tokens从300降到200,强制它精简推理——反而让F1从0.87升到0.91。短推理迫使模型抓主因,而非罗列可能性。
六、效果数据:完整对比
| 版本 | Macro-F1 | 准确率 | 平均请求耗时 | 平均总tokens | 成本/千次请求 |
|---|---|---|---|---|---|
| V1 | 0.421 | 42% | 1.2s | 1245 | $0.037 |
| V2 | 0.638 | 61% | 1.1s | 876 | $0.026 |
| V3 | 0.784 | 78% | 1.5s | 1587 | $0.048 |
| V4 | 0.913 | 91% | 1.4s | 612 | $0.018 |
V4相对V1,成本降低51%,F1提升117%。另外我单测了20条错别字严重的工单(如“wifi连不上,重启猫也没用”),V4的鲁棒性明显优于V3——它能在推理中主动补全“猫=光猫”,然后正确归到硬件故障。
最终部署版本我选择了V4的变体,在system prompt中加了一句“若用户描述与硬件相关但无物理损坏证据,优先归为软件Bug”,把最后5%的错误压到了2%。目前线上运行两周,日调用量约3000次,月成本稳定在$1.8左右。
七、总结与建议
Prompt工程不是玄学,是可复现的迭代过程。我的经验:
1. 先跑裸Prompt拿到下限,别自嗨
2. 分类任务必须给边界定义+格式锁,否则模型自由发挥
3. few-shot示例宁缺毋滥,挑“易混淆”样本
4. 思维链在分类任务中有效,但必须限长
5. 每次修改都记录token和F1,用数据说话
如果你也在做类似的文本分类,建议直接复刻V4模板,把示例换成你的业务数据。有个坑提前预警:OpenAI的token计费对中文不友好,一个汉字约等于2-3个token,所以prompt里别写废话。后续我打算试试fine-tuning,但就目前效果看,Prompt调优已经够用——至少省下了几千条标注数据的钱。