一、问题背景:为什么我要重新设计Prompt

事情起因很简单:我们团队要做一个合同管理系统,需要从上传的PDF合同文本里自动抽取"甲方名称、乙方名称、合同金额、签署日期、有效期限"这5个字段,写入数据库。

最初我随手写了个Prompt丢给模型,觉得这活儿简单。结果上线一周,数据清洗的同学天天来找我——字段缺失、金额格式五花八门(有的带"人民币"前缀,有的写成"壹拾万元整")、日期有的返回"2024年3月1日"有的返回"March 1, 2024",甚至还有一个case模型把"甲方"和"乙方"搞反了。

这让我意识到:信息抽取任务里,Prompt不是"随便写一段话",而是直接影响下游数据质量的关键工程。于是我决定系统性地做一轮Prompt对比实验,用数据说话。

二、环境与版本

先交代实验环境,避免大家复现时踩版本坑:

  • Python 3.11.6
  • openai SDK 1.35.0(注意:1.x版本API和0.x差异很大,网上很多老教程不能直接用)
  • 模型:gpt-4o-mini-2024-07-18(主力测试)、gpt-4o-2024-08-06(对照)
  • 测试集:50份真实合同文本(脱敏后),每份约800-2500字
  • 评测脚本:自写字段级准确率统计

关键参数统一设置:temperature=0(保证可复现)、max_tokens=512、top_p=1。

三、方案设计:4种Prompt写法

我设计了4个版本的Prompt,针对同一任务:

V1 零样本(Zero-shot):用自然语言描述任务,让模型自由输出。

V2 Few-shot:给2个输入输出示例。

V3 结构化JSON约束:强制模型输出JSON,字段名固定,并规定格式规范。

V4 思维链+结构化混合:先让模型在``标签里分析,再输出JSON。

评测维度:字段完整率(5个字段都抽到的比例)、字段准确率(与人工标注一致)、平均输出Token、平均延迟。

四、核心实现(含代码)

先看调用封装和V3结构化Prompt的实现:

from openai import OpenAI
import json, time

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

SYSTEM_PROMPT_V3 = """你是一个合同信息抽取引擎。请从用户提供的合同文本中抽取以下5个字段,严格按JSON格式输出,不要输出任何JSON以外的内容:

{
  "party_a": "甲方全称,字符串",
  "party_b": "乙方全称,字符串",
  "amount": "合同金额,仅保留阿拉伯数字和货币符号,如 100000.00 或 100000.00 CNY,无法确定填 null",
  "sign_date": "签署日期,格式 YYYY-MM-DD,无法确定填 null",
  "valid_period": "有效期限,格式如 '2024-01-01 至 2025-12-31',无法确定填 null"
}

规则:
1. 金额若为中文大写,请转换为阿拉伯数字。
2. 日期若为中文格式,请转换为 YYYY-MM-DD。
3. 字段无法从文本中确定时,必须填 null,禁止编造。
"""

def extract_v3(text: str) -> dict:
    t0 = time.time()
    resp = client.chat.completions.create(
        model="gpt-4o-mini-2024-07-18",
        messages=[
            {"role": "system", "content": SYSTEM_PROMPT_V3},
            {"role": "user", "content": text},
        ],
        temperature=0,
        max_tokens=512,
        response_format={"type": "json_object"},  # 关键:强制JSON
    )
    latency = time.time() - t0
    content = resp.choices[0].message.content
    usage = resp.usage
    return {
        "data": json.loads(content),
        "completion_tokens": usage.completion_tokens,
        "prompt_tokens": usage.prompt_tokens,
        "latency": latency,
    }

再看V4思维链版本,重点在于用分隔标签引导推理:

SYSTEM_PROMPT_V4 = """你是一个合同信息抽取引擎。请分两步处理:

第一步:在  标签内,逐字段分析文本中是否出现该信息、出现在哪句话、是否需要格式转换。若不存在请说明。
第二步:在  标签内输出JSON,格式如下:
{"party_a": "", "party_b": "", "amount": "", "sign_date": "", "valid_period": ""}
规则同前:金额转阿拉伯数字,日期转YYYY-MM-DD,无法确定填null,禁止编造。

输出示例:
party_a: 文本第2行"甲方:北京XX科技有限公司"...
{"party_a": "北京XX科技有限公司", ...}
"""

def extract_v4(text: str) -> dict:
    resp = client.chat.completions.create(
        model="gpt-4o-mini-2024-07-18",
        messages=[
            {"role": "system", "content": SYSTEM_PROMPT_V4},
            {"role": "user", "content": text},
        ],
        temperature=0,
        max_tokens=800,
    )
    content = resp.choices[0].message.content
    # 用正则截取  段
    import re
    m = re.search(r"(.*?)", content, re.S)
    data = json.loads(m.group(1)) if m else {}
    return {
        "data": data,
        "completion_tokens": resp.usage.completion_tokens,
        "prompt_tokens": resp.usage.prompt_tokens,
    }

评测脚本核心逻辑(字段级比对):

def evaluate(pred: dict, gold: dict) -> dict:
    fields = ["party_a", "party_b", "amount", "sign_date", "valid_period"]
    complete = all(pred.get(f) not in (None, "") for f in fields)
    correct = sum(1 for f in fields if str(pred.get(f)).strip() == str(gold[f]).strip())
    return {"complete": complete, "correct_fields": correct}

五、踩坑与优化记录

坑1:不加response_format时,模型爱加解释。 V1和V2版本里,模型经常输出"根据合同内容,甲方是……",导致json.loads直接报错。加response_format={"type": "json_object"}后解决,但注意:必须在Prompt里出现"JSON"字样,否则OpenAI会报400错误,这个细节文档里写得不显眼。

坑2:金额中文大写转换不稳定。 V2的Few-shot示例里我只放了一个"壹拾万元整"的例子,结果模型对"叁佰贰拾万零伍仟元"这种带"零"的转换频繁出错。后来在V3规则里明确写"中文大写转阿拉伯数字",并在Few-shot里补了3种边界示例,准确率才上来。

坑3:思维链版本的解析成本。 V4输出里有`,Token消耗明显更高,而且偶尔标签不闭合。我用正则兜底并设置max_tokens=800`,但仍有约4%的样本解析失败,需要重试。这提醒我:思维链不是免费的,工程上要权衡。

坑4:gpt-4o-mini对长文本尾部信息注意力弱。 有2份合同关键信息在文末,mini模型漏抽。我把文本做了简单截断策略(保留首尾各1200字),漏抽问题基本消失。

六、效果数据

50份样本、temperature=0下的平均结果(单位:Token为completion_tokens均值):

版本 字段完整率 字段准确率 平均输出Token 平均延迟
V1 零样本 62% 71.2% 312 1.8s
V2 Few-shot 78% 83.5% 268 2.1s
V3 结构化JSON 96% 94.0% 187 1.6s
V4 思维链+结构化 98% 96.4% 431 3.4s

补充对照:用gpt-4o跑V3,准确率95.2%,但输出Token均值201、延迟2.9s,成本约是mini的15倍。对我们这个日均5万次调用的场景,mini+V3是性价比最优解。

一个反直觉的发现:V3不仅质量最高之一,Token反而最少——因为结构化约束让模型"少说废话",输出长度直接砍掉40%。这说明约束输出格式本身就是一种成本优化。

七、总结

这轮实验给我的最大启发是三点:

第一,信息抽取类任务,结构化输出约束的收益远大于堆Few-shot示例。V3相比V2,准确率+10.5个百分点,Token还降了30%。

第二,思维链要按需使用。V4准确率只比V3高2.4个百分点,但Token翻倍、延迟翻倍,对于字段明确的结构化抽取,性价比不高;它更适合需要推理判断的场景(比如条款是否冲突)。

第三,Prompt工程要用数据闭环。我最初凭感觉写的Prompt自我感觉良好,一上评测集就原形毕露。建议每个Prompt改动都跑一遍固定测试集,记录完整率、准确率、Token三项指标,形成回归基线。

后续我打算把这套评测脚本做成CI的一部分,任何Prompt变更都自动跑50样本回归,避免"改一个Prompt修好A字段、弄坏B字段"的经典悲剧。