一、问题背景:当通用Prompt撞上金融黑话
上个月接了个需求:从券商研报PDF提取“目标价”、“评级”、“EPS预测”三个字段,喂给下游量化模型。原本以为调个GPT-4o-mini就完事,结果第一次跑完测试集,准确率42%——大部分错误集中在“目标价”字段,模型把“12个月目标价区间18.5-21.3元”提取成了“18.5”,丢了区间;还有把“维持买入评级”里的“买入”识别成目标价。
这活儿远看是NER,细看是语义解析。通用Prompt直接问“请提取以下文本中的目标价”,模型对金融文本的隐含格式(区间、条件句、比较级)完全无感。我决定系统性地做一轮Prompt工程实验,记录每个版本的改动和代价。
二、环境与版本:固定模型,控制变量
- 模型:
gpt-4o-mini-2024-07-18(主实验)、deepseek-chat(对比实验) - 推理框架:OpenAI Python SDK
v1.35.3,DeepSeek SDKv1.0.2 - 测试集:120条人工标注的研报句子(来自Wind终端公开摘要),每条平均长度85字
- 评估指标:字段级精确匹配(完全一致才算对)
- 温度:0(所有实验固定)
- Token计费:OpenAI $0.15/1M输入,$0.60/1M输出;DeepSeek 按官方价
我要强调的是:所有实验必须固定模型版本。有一次我在中途升级了gpt-4o-mini的deployment,导致结果不可比,重跑了一整天。
三、方案设计:从零样本到结构化输出的五步阶梯
我设计了一个递增复杂度的实验矩阵,每步只改一个变量:
- V1 零样本裸Prompt:直接问“提取XX”
- V2 加角色与输出格式:指定JSON结构
- V3 加入few-shot示例(2条高质量示例)
- V4 增加否定约束与边界条件(如“若为区间,保留完整区间”)
- V5 引入思维链引导(先判断文本类型,再抽取)
每一步都记录:准确率、平均输入token、平均输出token、单条成本。
四、核心实现:五个版本的Prompt演变与代码
下面是V1和V5的完整Prompt模板(实际发送的messages数组):
# V1 - 零样本裸Prompt(准确率42%)
def build_prompt_v1(text: str) -> list[dict]:
return [
{"role": "system", "content": "你是一个信息抽取助手。"},
{"role": "user", "content": f"从以下研报句子中提取目标价、评级、EPS预测。\n句子:{text}\n输出:"}
]
# V5 - 思维链 + 格式锚定 + 边界约束(准确率91%)
def build_prompt_v5(text: str) -> list[dict]:
system_prompt = """你是金融研报结构化抽取器。遵循以下规则:
1. 先判断句子类型:估值段、评级段、财务预测段。
2. 目标价规则:若是区间,输出完整区间如"18.5-21.3"; 若是单一值,输出单一值;若未提及,输出null。
3. 评级规则:只提取评级词(买入/增持/中性/减持)及其修饰词(维持/上调/下调),格式为"操作_评级"。
4. EPS预测:提取数字和单位(元/股),忽略“同比”等非数字信息。
5. 所有输出必须为合法JSON:{"目标价": "...", "评级": "...", "EPS": "..."}
不要输出任何额外解释。"""
few_shot_1 = "句子:考虑公司增长确定性,给予目标价28.5元,对应2024年35倍PE,维持增持评级,预计2024年EPS为0.81元。"
few_shot_1_out = '{"目标价": "28.5", "评级": "维持_增持", "EPS": "0.81"}'
few_shot_2 = "句子:我们预计2024-2025年EPS分别为1.12/1.35元,目标价区间上调至45-50元,评级由中性上调至买入。"
few_shot_2_out = '{"目标价": "45-50", "评级": "上调_买入", "EPS": "1.12/1.35"}'
user_content = f"""句子:{text}
请先内部推理(不输出),然后严格输出JSON。"""
return [
{"role": "system", "content": system_prompt},
{"role": "user", "content": few_shot_1},
{"role": "assistant", "content": few_shot_1_out},
{"role": "user", "content": few_shot_2},
{"role": "assistant", "content": few_shot_2_out},
{"role": "user", "content": user_content}
]
注意V5的system prompt里我用了“内部推理(不输出)”来暗扣思维链——这比让模型显式输出“思考过程”更省token,且避免被安全过滤器拦。实测显式思维链会让输出token暴涨到300+,而暗扣方式只增加约15个token。
五、踩坑与优化:三个隐藏的“准确率杀手”
坑1:few-shot示例顺序影响巨大。我把V5的两个示例调换顺序后,准确率从91%掉到87%。模型对最后的示例有“近因偏好”,跟人类做选择题似的。解决方案:把最典型的例子(含区间目标价)放最后。
坑2:输出格式漂移。V3版本我问“请提取”,模型偶尔输出自然语言“目标价是18.5元”,导致我json.loads直接崩。后来在system prompt里加了“必须为合法JSON”并给了两个严格示例,才把格式稳定性从76%拉到99.2%。
坑3:DeepSeek对“null”的处理。同一套V5模板,DeepSeek在未提及EPS时会输出"EPS": "无"而不是null。后来发现DeepSeek的中文对齐偏好导致它倾向于输出中文词。我在system prompt里加了一句“对于缺失字段,必须输出小写英文null”,问题解决。
成本方面,V5的输入token因为system prompt膨胀(从60到220),但输出token反而下降(从平均95降到62),因为模型不再废话。综合算下来:
| 版本 | 准确率 | 平均输入token | 平均输出token | 单条成本(¥) |
|---|---|---|---|---|
| V1 | 42% | 98 | 76 | 0.083 |
| V2 | 58% | 112 | 83 | 0.091 |
| V3 | 74% | 210 | 91 | 0.107 |
| V4 | 82% | 245 | 88 | 0.094 |
| V5 | 91% | 268 | 62 | 0.047 |
注意V5成本比V1还低——因为输出token几乎砍半。价格是按token算,不是按Prompt长度算的。
六、效果数据与最终对比
120条测试集上,V5的失败模式分布如下:
- 目标价提取错误:7条(多为“保守/乐观目标价”这类修饰词干扰)
- 评级提取错误:2条(罕见“谨慎增持”未覆盖)
- EPS错误:2条(区间EPS如“1.10-1.20元”只提取了起点)
用DeepSeek-V3跑V5模板,准确率85%,主要差在“区间目标价”识别率低(它倾向于取区间中值)。但DeepSeek单条成本仅0.021元,如果业务对92%与85%的差距不敏感,DeepSeek性价比更高。
总结
Prompt工程不是玄学,它本质是对模型先验分布与任务分布之间差异的补偿。这次实验最大收获不是那91%的准确率,而是确立了“每个Prompt改动必须配一次成本核算”的流程——毕竟线上跑一天几百块钱,省下来的token都是利润。下一步我打算试一下动态few-shot(按句子相似度检索示例),预计能把目标价错误再压一半。
如果你在生产环境调Prompt,我的建议是:永远先写一个能输出JSON的裸Prompt,再逐步加约束,每步都要跑同一份测试集。别信感觉,数据说话。