1. 问题背景:当GPT-4o把“甲方”识别成“甲方公司”

上个月接了个法律文书结构化项目,要求从判决书中抽取原被告、案由、赔偿金额。最初用最朴素的Prompt:

请从以下文本中抽取实体:原告、被告、金额。

结果GPT-4o(版本:gpt-4o-2024-05-13)返回的JSON里,把“甲方(某建设集团)”拆成两个实体,金额“人民币10万元”识别成“10”。最离谱的是,某条文书里“原告某某公司”被识别为“被告”。这让我意识到——不是模型不行,是Prompt没给够约束

2. 环境与版本:一套可复现的基准配置

  • 模型:gpt-4o-2024-05-13(temperature=0,top_p=0.1)
  • 测试集:200条人工标注的民事判决书片段,平均长度800字
  • 评估指标:实体级F1(严格匹配)+ token消耗(含输入输出)
  • 框架:openai Python包 v1.30.1,langchain 0.2.1(仅用于解析输出)

所有实验采用同一批数据,只改动Prompt模板,控制变量。

3. 方案设计:四代Prompt的演进路线

我按复杂度递增设计了四个版本:

  • V1零样本:一句话指令,无示例
  • V2少样本:3条完整示例+输出格式定义
  • V3思维链:先让模型“找线索”,再抽取
  • V4结构化约束:XML标签+两步推理+禁止猜测规则

关键设计原则:把模型当实习生带,而不是当搜索引擎用

4. 核心实现:从翻车到稳定的代码演化

4.1 V1到V2:少样本的威力与代价

第一版代码(零样本)长这样:

from openai import OpenAI
client = OpenAI(api_key="sk-xxx", base_url="...")

def extract_v1(text):
    prompt = f"从文本中抽取原告、被告、金额。文本:{text}"
    resp = client.chat.completions.create(
        model="gpt-4o-2024-05-13",
        messages=[{"role": "user", "content": prompt}],
        temperature=0
    )
    return resp.choices[0].message.content

跑完200条,F1只有0.45。分析错误案例发现:模型不知道“原告”的定义边界(是否包含“原审原告”),且输出格式混乱(有的返回JSON,有的返回纯文本)。

V2加入3条人工标注示例,并强制response_format={"type": "json_object"}

def extract_v2(text):
    examples = """
    示例1:
    文本:原告张三(男,1990年生)诉被告李四建设工程合同纠纷一案。
    输出:{"原告": "张三", "被告": "李四", "金额": null}
    """
    prompt = f"{examples}\n请按相同格式抽取。文本:{text}"
    # ... 同上,加response_format

F1提升到0.71,但token消耗暴涨——每条示例约800 token,200条测试多烧了16万token(约0.3美元)。少样本不是免费的

4.2 V3思维链:让模型先思考再回答

V3改用两步提示:先让模型列出所有与“原告”“被告”相关的短语,再基于这些短语生成JSON。这利用了模型的推理缓存在短文本上的优势:

def extract_v3(text):
    prompt = f"""
    第一步:找出文本中所有指代原告、被告的短语(包括全称、简称、代词)。
    第二步:基于第一步结果,输出JSON,键为"原告"/"被告"/"金额",值为完整字符串。
    文本:{text}
    """
    # 输出解析:先取第一步的列表,再取第二步JSON

F1升至0.85,但出现了新问题:模型偶尔会在第二步“修改”第一步的结果(比如补全简称)。而且有些案例中,第一步输出了20多个短语,导致输出token达到4000+。

5. 踩坑与优化:那些让我抓狂的细节

坑1:金额“壹拾万元” vs “10万元”
V3模型经常把汉字大写金额原样输出,而标注里要求统一为阿拉伯数字。解决方案:在Prompt里加一条“所有金额转换为阿拉伯数字,保留两位小数”,F1直接+0.03。

坑2:被告“某某公司”指代不清
判决书常出现“被告一”“被告二”,模型会合并。我把输出格式改为数组:

{"被告": ["公司A", "公司B"]}

并在Prompt里明确“每个被告独立元素”,解决了90%的合并错误。

坑3:XML标签比Markdown更稳
V4实验发现,用``包裹输出,比用json围栏更少出现格式断裂。原因是GPT-4o对XML的约束力感知更强。最终模板:

请按以下结构输出:

  ...
  ...
  ...

禁止输出其他内容,缺少的字段填null。

6. 效果数据:最终成绩与成本核算

版本 F1(严格匹配) 平均token/条 200条总成本
V1零样本 0.45 1200 0.15美元
V2少样本 0.71 3200 0.42美元
V3思维链 0.85 2500 0.33美元
V4结构化 0.92 1800 0.24美元

V4最终Prompt核心片段(已脱敏):

你是一名法律信息抽取专家。任务:从判决书中抽取原被告及赔偿金额。
规则:
1. 原告、被告各为数组,包含所有指代(全称+简称)。
2. 金额统一为阿拉伯数字,如“壹拾万”转为“100000.00”。
3. 若文本无金额,输出null,禁止猜测。
4. 先判断“谁起诉谁”,再找金额。
输出格式(XML):
[...][...]...

7. 总结:Prompt工程的三个反直觉结论

  1. 更长的Prompt不一定更贵——V4比V2短,但准确率高21%,因为去了冗余示例,加了精准规则。
  2. “禁止猜测”比“请准确”有效得多——负面约束能显著减少幻觉。
  3. 输出格式用XML,别用JSON——在GPT-4o上,XML的括号结构更不容易被截断。

最后,别迷信“万能Prompt模板”。我试过网上流传的“角色+任务+格式”三段式,对法律文本效果一般。真正的prompt优化,是拿你的数据去试,记录每一次失败的原因。现在这套V4模板,我已在另一个医疗文本抽取任务上复用,F1从0.38直接到0.89——虽然仍需微调金额规则,但框架是通用的。

如果你也在做类似的信息抽取,建议直接抄V4的XML结构,把plaintiff换成你的实体名,跑一遍自己的数据,再根据错误案例加规则。有疑问欢迎评论区交流。