一、问题背景:为什么一个“简单”分类任务翻车了

上个月接手一个客服工单自动打标项目,需求是把用户反馈分成“硬件故障/软件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调优已经够用——至少省下了几千条标注数据的钱。