1. 问题背景:Prompt不是“玄学”,是工程

上周我在做一个金融舆情监控系统,需要从上市公司公告中抽取“担保人”、“被担保人”、“担保金额”和“担保期限”四个实体。最初我用了一个简易模板,结果模型把“担保金额”识别成“被担保金额”,甚至把“人民币”也当成实体值。这让我意识到:Prompt设计直接决定业务效果,而不仅靠“提示词越长越好”的直觉。

于是我做了一组对照实验,目标是回答三个问题:
- 哪种Prompt模板在实体抽取任务上F1最高?
- token消耗差异有多大?换算成成本差多少?
- 哪些“花哨”技巧(比如角色扮演)真的值得?

2. 环境与版本

  • 模型:gpt-4-turbo-1106(通过OpenAI Python SDK v1.10.0调用)
  • 温度:0.2(降低随机性,保证可复现)
  • max_tokens:1024
  • 数据集:自建中文金融公告200条,人工标注实体(BIO格式)
  • 评估指标:F1(严格匹配实体边界+类型)、单条平均token消耗(usage.prompt_tokens + usage.completion_tokens
  • 成本换算:按OpenAI官方价格(输入$0.01/1K tokens,输出$0.03/1K tokens)

所有实验均在同一个test set(50条)上进行,每类模板重复3次取均值。

3. 方案设计:5种Prompt模板

我设计了5种模板,从最朴素到最“花哨”:

编号 模板类型 核心指令
T1 零样本 直接问“提取实体”
T2 少样本 给2个完整的输入-输出示例
T3 思维链 要求“先思考再输出”
T4 结构化输出 指定JSON schema,并给约束条件
T5 角色扮演 “你是专业金融NLP标注员”

注意:T3和T4都要求输出JSON,但T3增加了一步“推理过程”。T5在T4基础上加了角色前缀。

4. 核心实现:代码与关键细节

4.1 基础调用函数(所有模板共用)

# prompt_template.py
import openai
import json

client = openai.OpenAI(api_key="sk-xxx", base_url="https://api.openai.com/v1")

def call_model(system_prompt, user_prompt):
    response = client.chat.completions.create(
        model="gpt-4-turbo-1106",
        temperature=0.2,
        max_tokens=1024,
        messages=[
            {"role": "system", "content": system_prompt},
            {"role": "user", "content": user_prompt}
        ]
    )
    usage = response.usage
    total_tokens = usage.prompt_tokens + usage.completion_tokens
    return response.choices[0].message.content, total_tokens

4.2 结构化输出模板(T4)的关键实现

T4的核心不是简单问“提取”,而是强制模型遵循一个schema,并对边界做约束。这是我在实验中效果最稳的模板。

# t4_structured.py
def build_t4_prompt(text):
    schema = {
        "type": "object",
        "properties": {
            "guarantee": {"type": "string", "description": "担保人全称,去除'公司'后缀以外的修饰词"},
            "guaranteed": {"type": "string", "description": "被担保人全称"},
            "amount": {"type": "number", "description": "担保金额,单位万元,纯数字"},
            "period": {"type": "string", "description": "担保期限,如'1年'或'自合同生效之日起至2025年12月31日'"}
        },
        "required": ["guarantee", "guaranteed", "amount", "period"]
    }
    system_prompt = "你是实体抽取引擎。仅输出JSON,不要附加任何解释。"
    user_prompt = f"""
    从以下公告中抽取实体,严格按schema输出:
    {json.dumps(schema, ensure_ascii=False)}

    文本:
    {text}

    注意:金额统一转成数字(万元),日期保留原文。
    """
    return system_prompt, user_prompt

5. 踩坑与优化:三个没想到的坑

坑1:T3思维链模板输出不可控

我让T3先输出“理由”再输出JSON,结果模型经常把理由写很长,导致completion_tokens从平均150暴涨到400+。更坑的是,偶尔它会输出然后答案是:这样的非JSON文本,解析时直接报错。解决方案: 在T3的prompt末尾强加只输出JSON,不要理由——但这等于把T3退化成了T4。

坑2:T2少样本模板的“示例污染”

我给了两个示例,其中一个是“担保金额为5000万元”,结果测试时模型把“5000”直接复制到输出里,即使文本中实际是“6000万元”。这是经典的“示例锚定”效应。优化: 将示例中的金额改成不常见的“12345万元”,并明确标注“示例仅展示格式,不代表真实数据”。

坑3:角色扮演模板的隐藏成本

T5加了“你是专业金融NLP标注员”之后,输出质量确实提升了,但token消耗也明显增加。原因是模型倾向于“入戏”,输出一些“根据我的专业判断”之类的废话。优化: 将角色描述从system prompt移到user prompt末尾,并且加“直接输出,不要解释”——这样既保留角色影响,又压缩冗余。

6. 效果数据:一张表看清所有差异

模板 F1(严格) 平均token/条 每千条成本(USD) 输出格式稳定性
T1零样本 0.67 268 ~$7.2 差,经常漏字段
T2少样本 0.75 356 ~$9.5 中,有锚定偏差
T3思维链 0.79 513 ~$14.3 差,JSON解析失败率12%
T4结构化 0.82 412 ~$11.1 高,解析失败率0%
T5角色扮演 0.85 687 ~$19.0 高,但费用贵70%

关键结论:
- 从T1到T4,F1提升15个百分点,token成本只增加54%。T4是性价比最优解。
- T5比T4只提升3个点F1,但token消耗暴涨67%。对于业务场景,这3个点不值得。
- 如果预算敏感,建议用T4;如果对F1有硬指标(如>0.85),可以考虑T5,但需要配合更严格的max_tokens限制。

7. 总结:Prompt工程的核心是“约束”而非“堆砌”

这次实验让我改变了一个认知:好的Prompt不是更长的prompt,而是更严格的约束。 T4之所以胜出,是因为它把输出schema、类型约束、清洗规则全部显式写清楚,让模型没有“自由发挥”的空间。

实用的建议:
- 优先使用结构化输出模板,并明确JSON schema。
- 少样本模板要小心“示例锚定”,用极端值做例子。
- 思维链(CoT)适合复杂推理,但不是抽取任务的首选。
- 角色扮演可以提质量,但必须搭配“直接输出”指令来控成本。

最后留个问题:我的实验用的是GPT-4-turbo,如果你换成Claude-3或者Llama-3-70B,结果可能完全不同。建议在业务落地前,至少测试3个不同厂商的模型,用同一套模板做个快速评测——这比盲目调prompt更高效。

代码和数据集已放GitHub(仓库名:prompt-entity-benchmark),欢迎star和提issue。