这是「提示词工程」这门课的综合落地章,用同一个金融行业场景把 Zero-Shot / Few-Shot / In-context Learning 串成三条完整流水线:金融文本分类、金融信息抽取、金融文本匹配。它的价值不在算法,而在一个可复制的项目模板:角色设定 → 类别/字段 Schema → Few-Shot 示例 → 输出格式约束 → 逐条推理 → 后处理。
一、这份材料解决什么问题
前面几章讲的是”提示词怎么写才规范”(5 大原则、6 类进阶技术)。但知道怎么写和能交付一个功能之间隔着一整条工程流水线。这一章补的就是这一段:
给定一个真实业务场景(金融),如何从零设计出一套能跑的 Prompt 方案?
它的三个业务落点不是随便挑的,而是 LLM 在文本处理上的三种最基础的任务范式:
| 任务 | 本质 | 输出形态 |
|---|---|---|
| 金融文本分类 | N 选 1 的判别 | 一个类别名 |
| 金融文本信息抽取 | 从自由文本里抠结构化字段 | 一个 JSON 对象 |
| 金融文本匹配 | 两段文本的关系判定 | 一个是/否布尔值 |
这三种输出形态(枚举值 / 结构化 JSON / 布尔)恰好覆盖了 LLM 应用里 90% 的输出解析场景。学会用提示词把模型”夹”到这三种格式上,后面不管做 RAG 的答案抽取、Agent 的工具参数生成,还是新闻分类项目的新闻标题分类,都是同一套手法。
它在新闻分类流水线里的位置:新闻分类项目的 LLM 环节(DeepSeek API 做零样本/少样本分类)和这一章是同一件事,只换了业务和模型。这一章可以当作新闻分类项目 LLM 环节的”预演版”,只是类别数是 4 而不是 10、模型是 Qwen 而不是 DeepSeek。所以第七节我会把它逐环节映射过去。
课件的学习目标原文(也是本报告的权重来源):了解项目背景、了解整体项目任务、掌握 Few-Shot / Zero-Shot 的思想、完成课程代码开发。也就是说,背景是【了解】,提示词范式是【掌握】,代码是要跑通的实操。
二、核心结论
-
一个万能四步流水线:
加载模型 → 构建提示词(讲清任务 + 钉死输出格式)→ 模型预测 → 后处理。三条任务线的”项目思路”完全是这四步的复制粘贴,说明这是一套模板化方法而不是三次独立踩坑。 -
Prompt 设计永远只抓两件事:课件在三个任务里反复写了同一句话,”需要向模型解释什么叫作「XX 任务」” + “需要让模型按照我们指定的格式输出”。第一句决定模型懂不懂,第二句决定程序能不能解析。这是全章最该背下来的一句话。
-
Few-Shot 的载体有两种,适用范围不同:分类任务把示例拼进一条 user 消息(
pre_prompt + 问句);抽取和匹配任务用多轮对话历史([system, user, assistant, user, assistant, ...],调用时messages = history + messages)。前者省事,后者能借role把”标准答案”和”提问”分得更干净。 -
示例(Bot/assistant 部分)是人工写死的:课件两处都特意强调”Bot 的部分也是由人工输入的”。模型的 In-context Learning 学的不是你说了什么道理,而是你给的那对「输入→输出」的形状。
-
一套方案能不能上线,卡在后处理而不是模型:三个任务的输出都是自然语言(
新闻报道。、{'日期': ['2023-01-10'], ...}、是/不是),必须strip()、剥 markdown 代码块、json.loads()、与class_list精确比对。这中间任何一步失败,模型答对了也没用。
三、知识地图
3.1 基于Prompt工程的金融行业项目
├── 学习目标【了解+掌握】
│ ├── 了解项目背景
│ ├── 了解整体项目任务
│ ├── 掌握 Few-Shot / Zero-Shot 的思想 ← 【掌握】本章核心
│ └── 完成课程代码开发
│
├── 一、项目介绍【了解】
│ ├── 1 项目背景:金融数据激增 → 需要自动化分析
│ │ └── AI 在金融的三个应用方向
│ │ ├── 风险评估(风险事件识别 / 反欺诈 / 信用评分)
│ │ ├── 投资决策(历史数据 / 财务报表分析)
│ │ └── 客户服务(智能客服)
│ └── 2 项目任务与方法
│ ├── 三大业务场景:文本分类 / 信息抽取 / 文本匹配
│ ├── 大模型选择:Qwen (实际调用 `qwen-plus`)
│ └── 方法:Few-Shot + Zero-Shot 设计 prompt,激发"涌现能力"
│
├── 二、LLM 实现金融文本分类【掌握】
│ ├── 1 需求:4 段金融文本 → 判定所属报告类型
│ ├── 2 项目思路:加载模型 → 构建提示词 → 模型预测 → 后处理
│ ├── 3 Prompt 设计:解释「文本分类」 + 指定输出格式 + Few-Shot 示例
│ └── 4 代码实现:class_examples → init_prompts() → model_chat() → 循环推理
│
├── 三、LLM 实现金融文本信息抽取【掌握】
│ ├── 1 需求:股票信息文本 → SPO / 实体字段,Schema = {'金融': [日期, 股票名称, 开盘价, 收盘价, 成交量]}
│ ├── 3 Prompt 设计:解释「信息抽取」 + 要求 JSON 输出 + 缺失值约定
│ └── 4 代码实现:ie_examples → IE_PATTERN → build_prompt()(多轮 history)→ model_chat()
│
├── 四、LLM 实现金融文本匹配【掌握】
│ ├── 1 需求:3 个句子对 → 判定语义是否相似
│ ├── 3 Prompt 设计:解释「文本匹配」 + "只回答是否相似,不要多余回答"
│ └── 4 代码实现:examples{'是':[...], '不是':[...]} → init_prompts() → model_chat()
│
└── 五、本章小结:Zero-Shot / Few-Shot 完成三任务闭环
四、原理详解
4.1 为什么金融行业是 Prompt 工程的天然靶场
课件的背景论证很直白:金融数据量激增 → 投资者和研究者想靠深度分析减少决策失误 → 所以”针对金融数据的分析方法研究是目前十分有益且热门的话题”。
它列出的三个 AI 应用方向,其实对应三种对准确率极度敏感的场景:
| 方向 | 课件原文描述 | 对 Prompt 的要求 |
|---|---|---|
| 风险评估 | 识别不同金融风险事件类型、反欺诈行为、信用评分低等风险,全面评估贷款人信用状况 | 类别边界必须稳,宁可说”不确定”也不能错判 |
| 投资决策 | 分析历史数据、财务报表,提供精准投资决策支持 | 抽取的数值必须一字不差,单位不能丢 |
| 客户服务 | 智能客服,提供便捷而准确的答案 | 输出格式稳定,方便程序直接消费 |
这就解释了为什么课件选的三个任务全部是判别/抽取类而不是生成类:金融场景不要”文采”,要可解析、可复核、可审计的结构化输出。这也是所有 to-B LLM 项目的共同特征。
注意课件的一处表述:它说方法目的是”激发大模型的涌现能力”。严格讲,Prompt 里的 Few-Shot 激活的是上下文学习(In-context Learning)能力,模型在预训练中已经学会了”看几个例子然后照做”这个模式,示例只是把这个能力引导出来,并没有让模型产生新能力。被追问时用”In-context Learning”这个词比”涌现”更准确。
4.2 通用四步流水线(三条任务线共用)
课件在分类、抽取、匹配三节的”项目思路”里,写了完全相同的四步:
加载模型 → 构建提示词(描述清楚任务及输出格式) → 模型预测 → 后处理
拆开看每一步的工程含义:
| 步骤 | 课件里的实际代码 | 工程含义 |
|---|---|---|
| 加载模型 | openai.OpenAI(api_key=os.environ['DASHSCOPE_API_KEY'], base_url='https://dashscope.aliyuncs.com/compatible-mode/v1') |
不自己部署模型,走 DashScope 的 OpenAI 兼容模式。换厂商只改 base_url + model,业务代码不动 |
| 构建提示词 | init_prompts() / build_prompt() |
把”任务说明 + 类别/字段定义 + 示例 + 格式要求”一次性组装好,与推理逻辑解耦(这俩函数是纯函数,不碰网络) |
| 模型预测 | client.chat.completions.create(model='qwen-plus', messages=..., stream=False) |
单次同步调用,stream=False(分类场景不需要流式) |
| 后处理 | print("class result: ", resp) / 人眼核对 / json.dumps(..., ensure_ascii=False) |
课件在这里最弱,这是它最大的工程缺口,见 4.7 |
这个四步结构本身就是最该带走的东西:它把”提示词”从一个字符串变成了一个有输入、有组装函数、有调用接口、有解析出口的模块。
4.3 任务一:金融文本分类(Few-Shot 拼进单条 prompt)
需求:4 段金融文本,判定每段属于 ['新闻报道', '公司公告', '财务公告', '分析师报告'] 中的哪一类。
业务价值(课件列出):金融新闻自动分类、公司文档智能归档、投资研究材料整理、金融数据预处理。
Prompt 设计的两条考虑(课件原文):
1. 需要向模型解释什么叫作「文本分类任务」;
2. 需要让模型按照我们指定的格式输出。
具体做法:先做一次 Zero-Shot 定位,用一句 '你是一个金融专家,需要对输入的金融领域文本进行分析...' 把角色和任务说清楚;再用 Few-Shot 铸造形状,给模型展示几组”User 问 → Bot 答”的正确示范。课件明确指出:“上述例子中 Bot 的部分也是由人工输入的,其目的是希望看到在看到类似 User 中的句子时,模型应当做出类似 Bot 的回答。”
代码结构:
# 1. 四类,每类一个人工示例(这是 Few-Shot 的"示例池")
class_examples = {
'新闻报道': '今日,股市经历了一轮震荡,受到宏观经济数据和全球贸易紧张局势的影响。...',
'财务报告': '本公司年度财务报告显示,去年公司实现了稳步增长的盈利,...',
'公司公告': '本公司高兴地宣布成功完成最新一轮并购交易,收购了一家在人工智能领域领先的公司。...',
'分析师报告': '最新的行业分析报告指出,科技公司的创新将成为未来增长的主要推动力。...'
}
# 2. 组装前置 prompt(把示例"拼"进一条字符串)
def init_prompts(class_examples):
class_list = list(class_examples.keys())
pre_prompt = f'你是一个金融专家,需要对输入的金融领域文本进行分析,将类型归类到{class_list},对于不在这四类中的数据,输出‘不清楚类型’。以下是几个示例分类:'
for class_type, example in class_examples.items():
pre_prompt = pre_prompt + f'"""{example}是{class_list}里的什么类别?"""' + f'"""{class_type}。"""'
return {'class_list': class_list, 'pre_prompt': pre_prompt}
组装出来的 pre_prompt 长这样:
你是一个金融专家,需要对输入的金融领域文本进行分析,将类型归类到['新闻报道', '财务报告', '公司公告', '分析师报告'],对于不在这四类中的数据,输出'不清楚类型'。以下是几个示例分类:
"""今日,股市经历了一轮震荡...是['新闻报道', '财务报告', '公司公告', '分析师报告']里的什么类别?"""
"""新闻报道。"""
"""本公司年度财务报告显示...是[...]里的什么类别?"""
"""财务报告。"""
(其余 2 组同理)
这段 prompt 里有 4 个设计决策,每一个都值得单独记住:
| 设计决策 | 原文片段 | 为什么这么做 |
|---|---|---|
| 角色设定 | 你是一个金融专家 |
用一个 persona 把模型的输出分布收窄到金融语域,减少”通用助手”式的解释性废话 |
| 类别枚举 | 将类型归类到['新闻报道', ...] |
把闭集写进 prompt。不写清楚类别全集,模型会自由发明标签 |
| 兜底类别 | 对于不在这四类中的数据,输出'不清楚类型' |
给模型一个合法的”我不知道”出口,防止它硬凑一个类别。这是分类任务的经典防幻觉手段 |
| 示例锚定格式 | """...是[...]里的什么类别?""" / """新闻报道。""" |
用三引号把”问句”和”答案”隔开,让模型看到答案就是类别名本身,不带解释 |
推理时:
for sentence in sentences:
content = f"{prompts_info['pre_prompt']}" + f'"""{sentence}是 {prompts_info["class_list"]} 里的什么类别?"""'
resp = model_chat(content)
注意这里:示例部分 + 当前问题被拼成了一条 user 消息(messages = [{"role": "user", "content": content}])。这是 Few-Shot 最省事的实现方式。
4.4 任务二:金融文本信息抽取(多轮 history + JSON 约束)
需求:从股票信息文本中抽取实体。课件给的 Schema 是一个 dict:
# 不同实体下的具备属性
schema = {
'金融': ['日期', '股票名称', '开盘价', '收盘价', '成交量'],
}
期望输出(严格 JSON):
{"日期": ["2023-02-15"], "股票名称": ["佰笃[BD]美股"], "开盘价": ["10美元"], "收盘价": ["13美元"], "成交量": ["460,000"]}
{"日期": ["2023-04-05"], "股票名称": ["盘古(0021)"], "开盘价": ["23元"], "收盘价": ["26美元"], "成交量": ["310,000"]}
刻意注意输出结构的两个细节:所有值都是列表(即使只有一个值),这样才能表达”一句话里出现多个日期/多只股票”。这是 Schema 设计的通用习惯:单值字段也用 list 包,避免后续改结构。
Prompt 设计的两条考虑(与分类同构):
1. 需要向模型解释什么叫作「信息抽取任务」;
2. 需要让模型按照我们指定的格式(json)输出。
三条输出纪律(写进示例的 user 消息里):
– 按 JSON 格式输出;
– 句子中没有的信息用 ['原文中未提及'] 表示;
– 多个值之间用 ',' 分隔。
代码结构,这次改用多轮对话历史:
ie_examples = [{
'content': '2023-01-10,股市震荡。股票古哥-D[EOOE]美股今日开盘价100美元,一度飙升至105美元,随后回落至98美元,最终以102美元收盘,成交量达到520000。',
'answers': {'日期': ['2023-01-10'], '股票名称': ['古哥-D[EOOE]美股'], '开盘价': ['100美元'],
'收盘价': ['102美元'], '成交量': ['520000']}
}]
IE_PATTERN = "{}\n\n提取上述句子中的实体,并输出,上述句子中不存在的信息用['原文中未提及']来表示。"
def build_prompt(ie_examples):
history_list = [{'role': 'system',
'content': "你是信息提取专家,需要需要完成信息抽取任务。"
"我会给你一个句子,你需要提取句子中的实体,并输出,如果句子中有不存在的信息用['原文中未提及']来表示。"}]
for example in ie_examples:
history_list.append({'role': 'user', 'content': IE_PATTERN.format(example['content'])})
history_list.append({'role': 'assistant', 'content': json.dumps(example['answers'], ensure_ascii=False)})
return {'history_list': history_list}
调用时把历史拼在当前问题前面:
messages = [{"role": "user", "content": content}]
response = client.chat.completions.create(model='qwen-plus', messages=history + messages, stream=False)
两个容易被忽略的工程点:
json.dumps(example['answers'], ensure_ascii=False),ensure_ascii=False让中文原样输出而不是\u65e5\u671f转义。人类看得懂、token 也更省。写 Few-Shot 的 assistant 示例时几乎总要加这个参数。role分工的语义:system讲规矩(你是抽取专家 + 缺失值约定),user给任务(这句文本要抽什么),assistant示范答案(标准 JSON)。比分类任务”全部塞进一条 user 字符串”更接近真实对话格式,模型的遵守度通常更高。
4.5 任务三:金融文本匹配(布尔输出 + 强抑制”多余回答”)
需求:3 个句子对,判断语义是否相似。期望输出 ['相似', '不相似', '相似']。
Prompt 设计的两条考虑:
1. 需要向模型解释什么叫作「文本匹配任务」;
2. 需要让模型按照我们指定的格式输出。
示例池按”标签分桶”组织,这是本节最值得抄的一个结构:
examples = {
'是': [
('公司ABC发布了季度财报,显示盈利增长。', '财报披露,公司ABC利润上升。'),
],
'不是': [
('黄金价格下跌,投资者抛售。', '外汇市场交易额创下新高。'),
('央行降息,刺激经济增长。', '新能源技术的创新。')
]
}
负例给了 2 组、正例只给 1 组,而且两个负例是两种不同的”不相似”:
– 黄金价格下跌 / 外汇市场交易额创下新高,同一个大金融领域,但讲的是两个不同事实(易混淆的”领域相同 ≠ 语义相同”);
– 央行降息 / 新能源技术创新,跨领域,一眼不同。
这就是 Few-Shot 里”示例要覆盖边界情况”的具体做法:不要只给显然的例子,要专门给容易被模型判错的那一类。
system 里的”话痨抑制”:
'现在你需要帮助我完成文本匹配任务,当我给你两个句子时,你需要回答我这两句话语义是否相似。'
'只需要回答是否相似,不要做多余的回答。'
不要做多余的回答这七个字是判断题场景的标配。不加它,模型很爱输出”这两句话都涉及金融市场,但一句讲……另一句讲……因此不相似。”,人看着挺好,程序解析直接崩。
统一提问模板(保证示例和真实请求格式完全一致):
sentence_with_prompt = f'句子一: {sentence1}\n句子二: {sentence2}\n上面两句话是相似的语义吗?'
这是 Few-Shot 的隐含铁律:示例里的问题模板和推理时的问题模板必须逐字符一致,否则模型学到的是”某种格式”而不是”这个任务”。
4.6 三种任务的 Prompt 组装方式对比(本章的技术主线)
| 维度 | 分类 | 信息抽取 | 文本匹配 |
|---|---|---|---|
| 输出形态 | 枚举标签 | 结构化 JSON | 布尔(是/不是) |
| 示例载体 | 拼进单条 user 字符串 | 多轮 messages(system+user+assistant) | 多轮 messages(system+user+assistant) |
| 角色设定 | 你是一个金融专家 |
你是信息提取专家 |
现在你需要帮助我完成文本匹配任务 |
| Schema 显式给出 | 是(4 个类别) | 是(5 个字段)但只在需求描述里,没进代码(见 5.6) | 否(不需要) |
| 兜底机制 | 不清楚类型 |
['原文中未提及'] |
只允许”是/不是”两个词 |
| 格式约束强度 | 中(示例锚定) | 强(JSON + 缺失值约定 + 分隔符约定) | 强(禁用多余回答) |
| 示例数量 | 4 组(每类 1 组) | 1 组(完整覆盖 5 字段) | 3 组(1 正 2 负) |
| 业务难点 | 类别边界(新闻报道 vs 分析师报告) | 字段完整性与单位保真 | 领域相同但语义不同的干扰 |
为什么分类用拼接、后两个用 history?课件没有解释,但从结构看是自然演化的:分类任务的”问答”是同一句话(输入即问题),拼在一起语义通顺;抽取和匹配的”问题”是一个固定模板(IE_PATTERN / 句子一...句子二...),把它和标准答案拆成 user/assistant 两轮更贴合聊天模型的训练格式。实践建议:能用多轮 messages 就用多轮,因为 role 提供了额外的结构信号,而且示例池可以动态增删而不必改字符串模板。
4.7 后处理:课件里最薄、工程上最厚的一环
课件在三处都写了”后处理”是流水线的第四步,但代码里只写到 print(resp) 就结束了,分类任务打印 class result: ...,抽取任务直接 print(result),匹配任务打印 result-->...。
也就是说课件的后处理是”人眼核对”。真实上线必须自己补上:
import json, re
def parse_label(raw: str, class_list: list) -> str:
"""分类后处理:先清洗,再对齐枚举,最后兜底"""
text = raw.strip().strip('。.’').strip()
# 1) 完全命中枚举 → 直接用
if text in class_list:
return text
# 2) 模型多说了几句 → 找出唯一命中的类别名
hits = [c for c in class_list if c in text]
if len(hits) == 1:
return hits[0]
# 3) 命中兜底词 / 命中多个 / 没命中 → 标记待人工或降级
return '__UNKNOWN__'
def parse_json(raw: str) -> dict:
"""抽取后处理:剥 markdown 代码块 + 容错解析"""
text = raw.strip()
text = re.sub(r'^
(?:json)?|
try:
return json.loads(text)
except json.JSONDecodeError:
return {} # 解析失败 → 走重试或降级
这三个函数的每一行都对应一个真实会发生的失败模式(详见第五节)。
4.8 关于 Zero-Shot 与 Few-Shot 的准确理解
课件把方法表述为”基于 Few-Shot + Zero-Shot 的思想,设计 prompt”。严格说,本章三条任务线实际用的都是 Few-Shot(都给了示例)。Zero-Shot 在这里的角色是基线:
- Zero-Shot:不提供示例,直接靠模型预训练知识完成任务(”像问专家问题,他凭经验直接回答”)。分类任务里那句
你是一个金融专家,需要对输入的金融领域文本进行分析...就是 Zero-Shot 的第一层;但一旦后面附上 4 组示例,整体就升级为 Few-Shot 了。 - Few-Shot:通过 1~3 个参考示例引导模型理解模式。范例的作用是把模糊的任务定义”实例化”——与其解释一百字”什么叫分类”,不如给一组例子。
- In-context Learning:这才是底层机制的名字。示例不更新任何权重,它们只是待在上下文里,让模型在前向推理时就”照葫芦画瓢”。所以示例越多、越贴近真实分布,效果越好;但它们不改变模型的长期知识,换个会话就没了。
五、易错点与常见误解
5.1 ❌ 示例里的答案(Bot 部分)是模型生成的 → ✅ 是人工写死的
课件在两处(分类、抽取)都特意写明”Bot 的部分也是由人工输入的”。Few-Shot 的质量完全取决于你写的这几个标准答案,写错了模型就学错。
为什么:In-context Learning 学的就是”输入→输出”的映射形状。
5.2 ❌ 类别标签可以在不同地方换着写(财务报告 / 财务公告)→ ✅ 标签字符串必须全局唯一、逐字符一致
这是课件里真实存在的标签漂移:需求里期望输出写的是 ['新闻报道', '公司公告', '财务公告 '分析师报告'](还漏了一个逗号),而代码里的 class_examples 用的是 '财务报告';Few-Shot 示例的 Bot 答案也是 财务报告。。
为什么:后处理是拿模型输出跟 class_list 做字符串比对的。只要模型学会了写成 财务公告(因为它在你给的期望列表里见过这个词),你的 if text in class_list 就永远命中不了,模型答得”对”也全被判为错。
5.3 ❌ 期望输出格式可以每次写不一样 → ✅ 期望输出必须和 prompt 里的格式约定完全对齐
匹配任务的需求里写”期望模型输出的结果为 ['相似', '不相似', '相似']“,但 prompt 里的 system 说”回答是否相似”、示例的 assistant 答案写的是 '是' / '不是'。
为什么:模型只会遵守 prompt 里那套约定,不会遵守你脑子里那套。评测脚本按 ['相似','不相似'] 判定的话,准确率会直接归零。这类不一致是”模型没做对”和”评测写错了”最难分辨的一类 bug。
5.4 ❌ Few-Shot 示例给一份就够了、当然是越多越好 → ✅ 示例要覆盖边界,不是堆数量
匹配任务的示例池给的是 1 正 2 负,两个负例分别代表”同领域不同事实“和”跨领域“。
为什么:示例的作用是划定决策边界。只给显然的例子(”股票涨/股票大涨 = 是”),模型学不到”同属金融但不相似该怎么判”——而这恰恰是唯一的难点。示例该从混淆矩阵里挑,不该从数据里随手抽。
5.5 ❌ 分类任务不需要兜底,模型总能选一个 → ✅ 必须有 不清楚类型 这样的出口
课件在 pre_prompt 里显式写了”对于不在这四类中的数据,输出’不清楚类型'”。
为什么:闭集分类的 prompt 不给出口时,模型面对域外文本会硬塞一个类别(这是幻觉在分类任务上的表现形态)。给了出口之后,”不清楚类型”本身就是一个可被监控的信号(兜底率突然上升 = 输入分布漂移),工程价值比硬猜高得多。
5.6 ❌ prompt 设计稿和代码里的 prompt 是一回事 → ✅ 代码里 inline 的版本常常比设计稿”缩水”
抽取任务的设计稿里,user 消息包含”提取上述句子中’金融'('日期', '股票名称', '开盘价', '收盘价', '成交量')类型的实体,并按照 JSON 格式输出”;但代码里的 IE_PATTERN 只剩 "{}\n\n提取上述句子中的实体,并输出,上述句子中不存在的信息用['原文中未提及']来表示。"——字段枚举和”JSON 格式”这两个关键约束在代码里消失了,靠 assistant 示例里 json.dumps() 的答案间接暗示”要输出 JSON”。
为什么:这类”文档-代码漂移”是项目里最常见的隐性 bug。示例确实能隐含格式,但缺了字段枚举,模型就不知道 schema 里的五个字段才是抽取目标,很容易多抽(比如把”一度飙升至105美元”也抽出来)。约束要写进 prompt,不要只靠示例暗示。
5.7 ❌ 抽取出来的数字就是数字 → ✅ 抽出来的是”字符串字面量”,单位必须原样保留
课件的数据本身就是脏的:'2023-04-05,...股票盘古(0021)开盘价23元,...最终以26美元收盘' —— 同一句话里开盘价用”元”、收盘价用”美元”;'成交量微幅增加至460,000' 里带千分位逗号。
期望输出里保留了原样:["23元"]、["26美元"]、["460,000"]。
为什么:LLM 做的是文本抽取,不是单位换算。让它顺手做归一化,等于把”抽取”和”计算”两个错误来源混在一起了。正确做法:抽原样 → 后处理阶段再用代码/规则统一单位和量纲。这也是所有”LLM 输出 → 下游系统消费”场景的通用原则。
5.8 ❌ messages 只要把历史拼在前面就行,顺序无所谓 → ✅ 必须 history + messages,当前问题永远在最后
课件三处调用的都是 messages=history + messages(messages 是当前这一轮的 user 消息)。Python 的 list 相加顺序就是消息顺序。
为什么:聊天模型的输入是有序的消息序列,最后一条 user 消息才是”要回答的问题”。顺序反了,模型会把示例当成待处理的输入,或者直接困惑。
5.9 ❌ 数据版本、日期这类小细节不重要 → ✅ 需求稿和代码里的样本要同步
抽取任务的需求里两条样本日期是 2023-02-15 / 2023-04-05,代码里是 2025-02-15 / 2025-04-05,其余文字完全相同。
为什么:这类不一致说明”文档更新了代码没更新”(或者反过来)。在 LLM 项目里它会直接污染评估——你按文档写的期望输出,跑代码时可能不一样,最后算出来的准确率是假的。
六、横向对比与研究脉络
6.1 三条任务线的技术方案对比
| 维度 | 金融文本分类 | 金融信息抽取 | 金融文本匹配 |
|---|---|---|---|
| Prompt 范式 | Few-Shot(4 类各 1 示例) | Few-Shot(1 个完整示例) | Few-Shot(1 正 2 负示例) |
| 消息结构 | 单条 user(示例拼接) | [system, user, assistant] ×1 |
[system, (user, assistant) ×3] |
| 输出约束手段 | 类别枚举 + 示例锚定 | JSON + 缺失值约定 + 分隔符约定 | 自然语言强抑制(”不要多余回答”) |
| 参数敏感度 | 高(标签必须精确匹配) | 高(JSON 必须可解析) | 中(两个词二选一) |
| 主要失败模式 | 标签漂移、域外硬猜 | 漏抽/多抽、JSON 包 markdown、字段名错 | 输出解释性长句 |
| 后处理关键动作 | strip → 枚举对齐 → 兜底 |
剥 `
|
|
| 规则 / 词典匹配 | |||
| └─ 问题:写法千变万化,规则写不完 | |||
| ↓ | |||
| TF-IDF + 传统机器学习(随机森林 / SVM / 朴素贝叶斯) | |||
| └─ 解决:从”写规则”变成”从数据里学”,但只看得见词、看不见语义 | |||
| ↓ | |||
| 预训练 + 微调(BERT / FastText / 各种 encoder) | |||
| └─ 解决:词向量 + 上下文语义,判别任务精度大幅提升 | |||
| └─ 新增问题:需要标注数据与训练流程,任务一变就得重来 | |||
| ↓ | |||
| Prompt + 大模型(Zero-Shot / Few-Shot / CoT / Self-Consistency) | |||
| └─ 解决:不训练就能做任务,需求变更只改提示词 | |||
| └─ 新增问题:输出格式不受控、幻觉、token 成本、上下文长度限制 | |||
| ↓ | |||
| 工程化收口:格式约束(JSON Schema / 结构化输出)+ 后处理校验 + 重试降级 + 多路投票 | |||
| └─ 也就是本章”后处理”这一步必须补齐的东西 | |||
python |
|||
| LABELS = […] # 10 类,从训练集标签列生成 | |||
| FALLBACK = [‘其他’, ‘不清楚类型’, ‘未知’] |
def postprocess(raw: str):
text = raw.strip().strip(‘。.!!’).strip()
text = text.replace(‘类别:’, ”).replace(‘类型:’, ”).strip()
if text in LABELS:
return text, ‘ok’
hits = [l for l in LABELS if l in text]
if len(hits) == 1:
return hits[0], ‘fuzzy’
return ‘其他’, ‘fallback’
三级降级链(每一级都要记日志,方便事后统计各档占比):
1. 重试:解析失败 → 重新调用一次(把失败原因追加到 prompt,比如"上次输出无法解析,请只输出类别名");
2. 模型降级:LLM 连续失败或超时 → 回退到已训好的 BERT 模型分类;
3. 兜底:BERT 也没把握 → 归入 `其他` 并打标"待人工复核",绝不让异常值污染准确率统计。
### 7.4 评估方式(课件没写,但必被问)
| 评估项 | 金融案例现状 | 新闻分类项目做法 |
|---|---|---|
| 测试集 | 4 条,人眼看 | 从 1w 测试集分层抽样 500~1000 条(每类等量),控制 API 成本 |
| 指标 | 无 | accuracy + macro-F1 + 各类 P/R(10 类不均衡时 accuracy 会骗人,macro-F1 更诚实) |
| 对照 | 无 | 与 TF-IDF+RF / FastText / BERT 用同一份抽样集对比,形成四模型对照表 |
| 稳定性 | 无 | 同一条标题跑 3 次看结果是否一致(检验 temperature 与采样抖动) |
| 成本 | 无 | 记录 平均 prompt token / completion token / 单条成本,再乘 1w 推算全量成本 |
| 兜底率 | 无 | 统计 `其他` 占比;占比异常升高说明类别体系或数据分布有问题 |
一个必须提的取舍:LLM 逐条调用在 1w 测试集上是明显偏贵且慢的。工程上要么抽样评估(推荐)、要么批处理(一次请求塞 20 条标题让模型返回 JSON 数组,但要承担"少返/漏返/顺序错乱"的解析风险)。被追问时主动说出这个取舍,比只报一个准确率分数加分得多。
### 7.5 上线部署(Flask + Streamlit 场景)
| 环节 | 做法 |
|---|---|
| 服务化 | 把本章的 `model_chat()` 封装成 Flask 路由 `POST /classify`,入参 `{"title": "..."}`,出参 `{"label": "...", "raw": "...", "status": "ok/fuzzy/fallback", "latency_ms": ...}` |
| 客户端 | Streamlit 页面提供输入框,人输一条标题看分类结果 + 命中状态(这个"命中状态"字段就是本章后处理思想的直接体现) |
| 提示词外置 | `pre_prompt` 不要硬编码在 py 里,放到 `prompts.yaml` / 环境变量,改提示词不用重新发版,这是避免 5.6"文档-代码漂移"的结构性解法 |
| 密钥 | 只从环境变量读(课件用 `os.environ['DASHSCOPE_API_KEY']`,新闻分类项目同理用 `os.environ['DEEPSEEK_API_KEY']`),绝不写进代码或提交到仓库 |
| 健壮性 | 请求超时(如 30s)+ 指数退避重试(2~3 次)+ 并发上限;相同标题走本地缓存直接返回 |
| 可观测 | 落库 `(标题, prompt, raw_output, 解析结果, status, tokens, latency)`。这份日志既是 bug 复现的唯一依据,也是后续挖难例、补 Few-Shot 示例的数据源 |
| 批量离线 | 与线上服务分离:跑批脚本(带并发 + 断点续跑 + 结果落 csv),用于生成四模型对照表 |
### 7.6 与新闻分类项目其它方案的配合关系
TF-IDF + 随机森林 → 基线。全量 18w 训练秒级完成,给 LLM 一个”及格线”参考
↓ 对照
FastText → 轻量中线。训练快、推理快,提供”便宜但不弱”的选项
↓ 对照
BERT 微调 → 精度主力 + LLM 的降级后备(7.3 的第 2 级)
↓ 对照
LLM(DeepSeek) → 零样本/少样本对照项。核心价值是证明”不训练也能到多少分”
以及”改类别体系时的迁移成本极低”
↓
模型压缩(量化/蒸馏/剪枝)→ 针对 BERT/FastText 这条线做部署优化。
注意:LLM 走 API 时不涉及本地压缩,压缩是”若要把小模型塞进
低成本服务器/端侧”时才用得上
“`
一句话表述:LLM 在新闻分类项目里不是要取代 BERT,而是作为一个对照组回答”加提示词能提升多少、不训练能到什么水平”,这正好是评审者最想听的工程判断力。
八、重点问答
Q1:这个金融项目的整体技术方案是什么?
A:总的是”一个场景、三种任务、一套提示词方法”。我用金融行业的公开文本做了三类基础任务:文本分类(判断文本属于新闻报道/财务报告/公司公告/分析师报告哪一类)、信息抽取(从股票信息里抽日期、股票名称、开盘价、收盘价、成交量五个字段)、文本匹配(判断两个句子语义是否相似)。三条线走的是同一个四步流水线,加载模型、构建提示词、模型预测、后处理,方法上统一用 Few-Shot 加 Zero-Shot,模型用的是 Qwen 的 qwen-plus,通过 DashScope 的 OpenAI 兼容接口调用。也就是说,我交付的不是三个脚本,而是一套可以复制的 Prompt 工程模板。
Q2:你的 Prompt 是怎么设计的?设计时只考虑哪两件事?
A:我每个任务都只抓两件事。第一是让模型明白”这是什么任务”,第二是让模型按我指定的格式输出。第一件事靠角色设定加任务描述,比如”你是一个金融专家,需要对输入的金融领域文本进行分析”;第二件事靠三样东西,类别或字段的显式枚举、输出格式的明确要求、以及 Few-Shot 示例把格式”演示”一遍。补充一点,我做分类的时候还专门加了一个兜底类别”不清楚类型”,就是为了让模型面对域外文本时有一个合法的”不知道”,不要硬凑一个答案。
Q3:Few-Shot 里的示例是怎么用的?为什么说它有效?
A:Few-Shot 就是在 prompt 里塞几组”输入→输出”的示范,让模型照着做。关键是这些示范里的答案部分是我人工写死的,不是模型生成的,模型的机制是 In-context Learning,它不更新任何权重,只是在看到几组形状一致的样例后,推断出”哦,这个任务要的输出长这样”,然后对新的输入产生同形状的输出。所以示例的作用其实是两重的:既教会模型任务边界(比如”同属金融领域但讲的是两件事,也应该判不相似”),又把输出格式钉死成可直接解析的样子。示例的质量直接决定效果上限。
Q4:示例放在消息里,你有几种放法?怎么选?
A:两种。第一种是全部拼进一条 user 消息,把角色、类别枚举、几组示例、当前问题用一个字符串串起来,实现最简单,我在分类任务上用的就是这种。第二种是用多轮 messages,把 system 用来讲规矩、user 用来给任务、assistant 用来给标准答案,一组示例占两条消息,调用的时候把历史列表和当前这条 user 消息拼起来,注意顺序一定是 history + messages,当前问题在最后。信息抽取和文本匹配我用的是第二种。选多轮的好处是角色分工清晰、示例池可以动态增删不用改字符串模板;代价是消息条数变多,token 稍贵。
Q5:信息抽取任务为什么要求输出 JSON?直接让模型说人话不行吗?
A:不行,因为下游是程序。我用一个 schema 字典定义要抽的字段,金融类型下有日期、股票名称、开盘价、收盘价、成交量五个;然后要求模型按 JSON 输出,并且约定两件事:原文里没有的信息用 ['原文中未提及'] 表示,多个值之间用逗号分隔。所有字段我都用列表来装,哪怕只有一个值,因为一句话里可能提到多个日期或多只股票。这里有个很容易踩的坑:模型经常把 JSON 包在 markdown 代码块里输出,直接 json.loads 会报错,所以必须先剥掉反引号再解析,解析失败就走重试或降级。另外我要求模型抽原样,数据里同一句话可能”开盘价 23 元、收盘价 26 美元”,我要求原样保留单位字符串,单位归一化放到后处理用规则做,把”抽取”和”计算”这两类错误来源分开。
Q6:文本匹配任务你做了什么特别的设计?
A:两个地方。第一个是示例的组织方式,我按标签分桶:正例”是”给一组,负例”不是”给两组,而且这两组负例是故意不一样的,一组是”同属金融领域但讲的是两件不同的事”,另一组是”跨领域完全无关”。这么设计是因为模型最容易错的不是明显不相似的,而是那种领域相同、看起来都在讲金融、其实说的不是一回事的句子,负例必须专门覆盖这种边界。第二个是抑制话痨,我在 system 里明确写”只需要回答是否相似,不要做多余的回答”。不加这句,模型很爱输出一段分析过程,人看着挺合理,但程序没法解析。
Q7:这套方案上线,你觉得最薄弱的环节在哪?
A:后处理。课件里三个任务的第四步都叫”后处理”,但代码只打印了模型返回结果,靠人眼核对。真正上线必须补三件事:一是解析,分类要 strip、去标点、跟类别枚举做精确匹配,抽取消掉 markdown 代码块再 json.loads;二是校验,字段缺失要补默认值,类别命中不了要标成异常;三是降级,我设计了三级链,解析失败先重试一次并把失败原因追加进提示词、还失败就回退到已经训好的 BERT 模型、再兜不住就归入”其他”并打标待人工复核。还有两个细节:一是判别任务要显式把 temperature 设成 0,不然同一条输入多次调用结果可能不一样;二是所有 prompt 和原始输出都要落日志,这是复现 bug 和挖难例补示例的唯一数据源。
Q8:你会在生产里用 LLM 做分类,还是用 BERT 微调?
A:看阶段和约束。LLM 加提示词的优势是零训练、改需求只改提示词,四类改十类,我只改示例字典的 key,几分钟就能出新版本,这在需求快速变化的早期阶段价值极高。BERT 微调的优势是精度上限高、推理便宜、输出天然受控,适合类别固定、调用量巨大的稳定阶段。所以我的实际做法是两条腿走:用 LLM 做快速验证和难例挖掘,用 BERT 做生产主力,同时把 BERT 当成 LLM 失败时的降级后备。至于成本,LLM 逐条调 API 在万级数据上很贵,评估阶段我会分层抽样几百到一千条来算准确率和 macro-F1,而不是全量跑。
九、延伸阅读
官方文档 / 工具
– 通义千问(Qwen)与 DashScope 模型服务文档:https://dashscope.aliyuncs.com/compatible-mode/v1 是本章使用的 OpenAI 兼容模式端点,改 base_url + model 即可切换厂商
– OpenAI Python SDK(openai):本章所有调用的客户端,client.chat.completions.create() 的 messages 结构是通用协议
– 阿里云百炼(Model Studio)/ DashScope API-KEY 管理:本章通过 DASHSCOPE_API_KEY 环境变量注入
论文(奠定 Few-Shot / Zero-Shot 的理论基础)
– Brown et al., Language Models are Few-Shot Learners, 2020,GPT-3 论文,In-context Learning / Few-Shot 的奠基之作
– Wei et al., Chain-of-Thought Prompting Elicits Reasoning in Large Language Models, 2022,思维链
– Wang et al., Self-Consistency Improves Chain of Thought Reasoning in Language Models, 2022,自一致性投票,对应本章方案的稳定性增强方向
本课件内的关联章节(同一目录树)
– index.html(课程总览):核心能力五条主线与本章的定位
– ② 大模型提示词工程指南/01-大模型Prompt工程指南:编写规范与 5 大原则
– ② 大模型提示词工程指南/02-大模型提示词工程进阶:Zero-Shot(1.1)、Few-Shot(1.2)、CoT(2.1)、Self-Consistency 等进阶技术的原理与示例
– 本目录 ③ 大模型提示词项目案例 后续章节:同一套方法在其它行业场景的延续