一、为什么我盯着“招标公告”做抽取
上周接到一个需求:从PDF招标公告里抽取出“项目编号”、“预算金额”、“投标截止时间”、“资质要求”四个字段,然后灌入内部CRM系统。看起来简单,但公告文本有大量噪声——比如“本项目预算为人民币壹佰贰拾万元整(¥1,200,000.00)”,既有中文大写又有阿拉伯数字;又比如“时间:2024年3月15日9:30(北京时间)”,但前面可能还有一句“开标时间另行通知”。
我第一个想法是写正则,结果被现实教育——同一家代理机构发的公告,格式能有三套变体。于是转向Prompt Engineering,目标是让gpt-3.5-turbo稳定输出JSON。环境配置如下:
- 模型:gpt-3.5-turbo-1106(temperature=0.2,response_format=json_object)
- 调用库:openai-python v1.14.2
- 测试数据:100条真实招标公告,人工标注字段
- 评估指标:字段级精确率/召回率/F1,以及单次调用Token数(通过usage.prompt_tokens + completion_tokens统计)
二、第一轮:零样本直抽——基线惨不忍睹
第一版Prompt简单粗暴:
def extract_v1(text):
prompt = f"""从以下招标公告中提取字段:项目编号、预算金额、投标截止时间、资质要求。
输出JSON格式,键名分别为:project_no, budget, deadline, qualification。
公告内容:{text}"""
resp = client.chat.completions.create(
model="gpt-3.5-turbo-1106",
response_format={"type": "json_object"},
temperature=0.2,
messages=[{"role": "user", "content": prompt}]
)
return resp.choices[0].message.content
100条跑完,结果让我皱眉:准确率52%,F1=0.58。问题集中在三类:
- 预算金额:模型经常把“约120万元”抽成“120”,丢了币种和单位;或者把“最高限价”和“预算”混淆。
- 投标截止时间:如果正文里同时出现“报名截止”和“开标时间”,模型会把“开标时间”当成deadline,但业务要求的是“递交投标文件的截止时间”。
- 资质要求:公告里写“需具备建筑装修装饰工程专业承包二级及以上”,模型只输出“二级”,漏掉“及以上”的范围限定。
Token消耗倒是中规中矩:平均prompt_tokens=1240,completion_tokens=607,总计1847。但输出质量不达标,根本无法上线。
三、第二轮:角色约束+格式强控——提升有限且Token暴涨
网上都说“加角色设定能提升效果”,我试了:
def extract_v2(text):
prompt = f"""你是一位从事政府采购10年的资深评标专家,熟悉《招标投标法》及各类公告范本。
现在请从给定公告中提取结构化信息。注意:
1. 预算金额必须包含货币符号和单位,如"人民币120万元"。
2. 投标截止时间指"递交投标文件的截止时间",若公告未明确,则输出"未明确"。
3. 资质要求保留完整原文,包括等级后缀(如"及以上"、"(含)")。
输出JSON:{{"project_no": "", "budget": "", "deadline": "", "qualification": ""}}
公告:{text}"""
# 调用逻辑同v1
效果:准确率提升到56%,F1=0.61。只涨了4个点,但prompt_tokens飙到1580,completion_tokens变成690,总Token达到2270。为什么Token涨?因为角色设定和“注意”部分占了额外空间,而模型输出时又试图“复述”一部分规则。
坑1:角色设定对这类规则明确的抽取任务帮助甚微。模型不是不懂业务,而是不知道怎么在长文本里定位关键边界。与其给身份,不如给“定位策略”。
四、第三轮:思维链(CoT)拆解步骤——转折点
我重新分析错误样本,发现模型的问题本质是“跳过定位直接抽取”。于是改成两步走:先让模型找出包含关键信息的句子(定位),再基于句子填字段(抽取)。
def extract_v3(text):
prompt = f"""请按以下步骤处理招标公告:
步骤1-边界定位:依次找出以下四个字段对应的原文句子,用【】标出:
- 项目编号:通常是“项目编号”或“采购编号”后紧跟的编号串
- 预算金额:包含“预算”、“最高限价”、“采购预算”的句子
- 投标截止时间:包含“递交投标文件截止”、“投标截止时间”的句子
- 资质要求:包含“资格要求”、“资质条件”的段落
步骤2-字段抽取:从步骤1找到的句子中提取对应值,按规则处理:
- 预算金额:保留“人民币”和单位,数字转阿拉伯
- 时间:格式化为YYYY-MM-DD HH:MM
- 资质:保留完整修饰语
步骤3-输出JSON:{{"project_no": "", "budget": "", "deadline": "", "qualification": ""}}
公告原文如下:
{text}"""
# 调用同v1,但增加max_tokens=1200
结果让人振奋:准确率78%,F1=0.78,其中预算金额的召回从62%提升到89%。为什么有效?因为“步骤1-边界定位”强制模型先做信息检索,再做信息变换。这符合大模型的处理逻辑——直接问答案容易跨步,分步走能降低推理负担。
但代价是Token消耗继续上涨:prompt_tokens=1420,completion_tokens=950,总计2370。因为模型真的在输出里写了“步骤1找到的句子是...”以及中间推理过程。效果虽好,成本太高,100条数据花了约6美元(按$0.0015/1K token计算)。
五、第四轮:Few-shot对齐格式 + 自校验——准确率91%且Token下降
第三轮效果已可接受,但Token浪费在“中间推理过程”上。我观察了几条成功输出,发现模型会在推理中重复公告原文,比如把完整句子复制一遍再抽取。这显然是冗余。
优化思路:把CoT步骤隐含在Few-shot示例中,而不是显式要求模型输出推理过程。同时增加一个“自校验”指令——让模型检查输出值是否与原文一致,不一致则修正。
def extract_v4(text):
prompt = f"""你是信息抽取模型。严格按照示例格式抽取字段。
【示例1】
原文:项目编号:HJZB-2024-0158。采购预算为人民币捌拾万元整(¥800,000.00)。投标截止时间:2024年5月10日9:00。资质要求:具备电子与智能化工程专业承包二级及以上资质。
输出:{{"project_no": "HJZB-2024-0158", "budget": "人民币80万元", "deadline": "2024-05-10 09:00", "qualification": "电子与智能化工程专业承包二级及以上"}}
【示例2】
原文:编号:XM-2024-102。最高限价120.5万元(含税)。递交投标文件截止时间为2024年6月1日14时30分。资格条件:投标人须具有建筑机电安装工程专业承包三级(含)以上资质。
输出:{{"project_no": "XM-2024-102", "budget": "人民币120.5万元", "deadline": "2024-06-01 14:30", "qualification": "建筑机电安装工程专业承包三级(含)以上"}}
现在抽取以下公告:
{text}
要求:
1. 严格输出JSON,不要额外文字。
2. 输出前自查:检查budget是否包含币种,deadline是否精确到分钟,qualification是否包含“及以上/(含)”等范围词。如有遗漏,立即修正。
"""
# 调用同v1,max_tokens=800
效果数据:
- 准确率:91.2%(比v3提升13.2%)
- F1:0.91
- Token消耗:prompt_tokens=980(示例占空间但可接受),completion_tokens=140(大幅下降,因为不再输出推理过程),总计1120 —— 反而比v1的1847还低39%。
坑2:温度参数的反直觉影响。我试过temperature=0.0,结果反而偶尔输出非JSON(比如漏掉左花括号)。后来在OpenAI官方文档看到,response_format=json_object时,模型会强制JSON但不会保证schema正确。把温度调到0.2,配合Few-shot示例中的格式锚定,反而更稳定。
另一个坑:自校验指令不能写成“如果错误则修正”,因为模型没有“错误”的参照系。要写具体校验点,比如“检查budget是否包含币种”,否则模型会跳过校验步骤。
六、数据对比与工程落地建议
| 版本 | 准确率 | F1 | 平均Token | 单条成本(USD) |
|---|---|---|---|---|
| v1 零样本 | 52% | 0.58 | 1847 | $0.0028 |
| v2 角色约束 | 56% | 0.61 | 2270 | $0.0034 |
| v3 CoT分步 | 78% | 0.78 | 2370 | $0.0036 |
| v4 Few-shot+自检 | 91.2% | 0.91 | 1120 | $0.0017 |
落地建议(针对生产环境):
- 不要迷信角色设定:抽取类任务中,角色带来的先验知识模型本来就有,真正缺的是任务分解与格式锚定。
- Few-shot示例要覆盖边界情况:预算字段至少给一个“大写+数字”混合的示例,否则模型会按示例格式硬套。
- 自校验指令必须具体:抽象校验(如“请确保准确”)无效,要列举字段级的检查点。
- Token优化优先级:减少completion_tokens比减少prompt_tokens更划算。prompt_tokens是输入($0.001/1K),completion_tokens是输出($0.002/1K),价格差一倍。用Few-shot压缩输出格式是关键。
- 温度设置在0.1-0.3之间:过高会引入随机格式错误,过低(0.0)在json_object模式下偶发解码异常。
最后说句实话:这份91%的准确率是在100条样本上测的。换一批公告,如果出现“联合体投标”或“进口产品”等特殊条款,可能还会掉点。但至少,从v1到v4的迭代路径是清晰的——如果你也在做信息抽取,建议直接跳过v2的角色设定,从v1基线直接跳到v3的CoT,然后花时间打磨v4的Few-shot示例。
代码已上传至GitHub(仓库链接见评论区),欢迎跑一下自己的数据。有更好的Prompt策略,评论区交流。