一、问题背景:为什么我要重新设计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字段"的经典悲剧。