一、背景
本文基于 Ollama + Qwen2:0.5b 本地模型,完成了三个经典的 NLP 任务:
(另附了基于langchain_openai的ChatOpenAI配合deepseek线上大模型接口处理Prompt提示词工程)
| 任务 | 目标 | 核心技术 |
|---|---|---|
| 文本分类 | 判断句子属于哪个类别 | Few-shot + System Prompt |
| 信息抽取 | 从文本中提取结构化实体 | Few-shot + Schema 约束 + JSON 解析 |
| 语义匹配 | 判断两个句子语义是否相似 | Few-shot + 二元分类 Prompt |
二、任务一:文本分类
2.1 Prompt 设计
python
system_prompt = "现在你是一个文本分类器,你需要按照要求将我给你的句子分类到:{class_list}类别中。"
user_prompt = f""{sentence}"是 {class_list} 里的什么类别?直接输出类别结果,不要给出解释等其他内容。"
2.2 Few-shot 示例
python
class_examples = {
"新闻报道": "今日,股市经历了一轮震荡...",
"财务报告": "本公司年度财务报告显示...",
"公司公告": "本公司高兴地宣布成功完成...",
"分析师报告": "最新的行业分析报告指出...",
}
2.3 关键技巧
| 技巧 | 作用 |
|---|---|
| System Prompt 明确角色 | 让模型知道自己是"分类器" |
| 限制输出格式 | 直接输出类别结果,不要给出解释 |
| Few-shot 覆盖所有类别 | 每个类别给一个示例,避免类别偏见 |
2.4 效果示例
输入:
央行发布公告宣布降低利率,以刺激经济增长。
输出:
新闻报道
代码
python
"""
利用 LLM 进行文本分类任务。
"""
# rich第三方库,用来美化终端输出结果
# from pprint import pprint
from rich import print # 格式化输出, 需要安装 pip install rich
from rich.console import Console
import ollama
from langchain_openai import ChatOpenAI
from config import Config
import re
conf = Config()
llm = ChatOpenAI(
model=conf.model_name,
api_key=conf.api_key,
base_url=conf.base_url,
temperature=1.0, # 温度参数
max_tokens=1024,
max_retries=3,
timeout=60,
)
# 提供所有类别以及每个类别下的样例 few_shot example
class_examples = {
"新闻报道": "今日,股市经历了一轮震荡,受到宏观经济数据和全球贸易紧张局势的影响。投资者密切关注美联储可能的政策调整,以适应市场的不确定性。",
"财务报告": "本公司年度财务报告显示,去年公司实现了稳步增长的盈利,同时资产负债表呈现强劲的状况。经济环境的稳定和管理层的有效战略执行为公司的健康发展奠定了基础。",
"公司公告": "本公司高兴地宣布成功完成最新一轮并购交易,收购了一家在人工智能领域领先的公司。这一战略举措将有助于扩大我们的业务领域,提高市场竞争力",
"分析师报告": "最新的行业分析报告指出,科技公司的创新将成为未来增长的主要推动力。云计算、人工智能和数字化转型被认为是引领行业发展的关键因素,投资者应关注这些趋势",
}
# 初始化prompt, 构造ollama需要的messages信息
def init_prompts():
"""
初始化前置prompt,便于模型做 incontext learning。
"""
# 获取class_examples字典中的所有key值保存到列表中 所有类别
class_list = list(class_examples.keys())
# print(f'class_list--->{class_list}')
# system:系统角色 messages列表 [{}, {}, ...]
pre_history = [
{
"role": "system",
"content": f"现在你是一个文本分类器,你需要按照要求将我给你的句子分类到:{class_list}类别中。",
}
]
# 构造历史记录,包含所有类别的样例
# print(f'class_examples.items()--->{class_examples.items()}')
for _type, exmpale in class_examples.items(): # [(文本分类, 文本内容), (文本分类, 文本内容), ...]
# print(f'"{exmpale}"是 {class_list} 里的什么类别?')
# print("content--->", _type)
# 在list中追加字典, user:用户角色
pre_history.append(
{"role": "user", "content": f""{exmpale}"是 {class_list} 里的什么类别?"}
)
# assistant:模型角色
pre_history.append({"role": "assistant", "content": _type})
return {"class_list": class_list, "pre_history": pre_history}
# 模型推理
def inference(sentences: list, custom_settings: dict):
"""
推理函数。
Args:
sentences (List[str]): 待推理的句子。
custom_settings (dict): 初始设定,包含人为给定的 few-shot example。
"""
# print(f'sentences--->{sentences}')
for sentence in sentences:
# console.status(): 终端显示时创建一个高亮绿色的状态栏,用于显示模型推理的进度。
with console.status("[bold bright_green] Model Inference..."):
# sentence_with_prompt = f""{sentence}"是 {custom_settings['class_list']} 里的什么类别?"
sentence_with_prompt = f""{sentence}"是 {custom_settings['class_list']} 里的什么类别?直接输出类别结果, 不要给出解释等其他内容。"
# print(f'sentence_with_prompt--》{sentence_with_prompt}')
# 方式一: ollama api调用
# *custom_settings['pre_history']: 列表拆包 eg:*[{},{},{},...]->{},{},{}...
response = ollama.chat(
model="qwen2:0.5b",
messages=[
*custom_settings["pre_history"],
{"role": "user", "content": sentence_with_prompt},
],
)
# print(f'response--->{response}')
# response = response["message"]["content"]
response = response.message.content
print(f'response--->{response}')
# # 方式二: api调用
# response = llm.invoke(input=[
# *custom_settings["pre_history"],
# {"role": "user", "content": sentence_with_prompt},
# ])
# response = response.content
# # print(f'response--->{response}')
# response = re.sub(r"<think>.*?</think>", "", response, flags=re.DOTALL).strip()
# # print(f'response--->{response}')
#
# print(f">>> [bold bright_red]sentence: {sentence}")
# print(f">>> [bold bright_green]inference answer: {response}")
# print("*" * 70)
if __name__ == "__main__":
# 实例化终端对象, 美化终端输出
console = Console()
# 待推理的句子
sentences = [
"今日,央行发布公告宣布降低利率,以刺激经济增长。这一降息举措将影响贷款利率,并在未来几个季度内对金融市场产生影响。",
"本公司宣布成功收购一家在创新科技领域领先的公司,这一战略性收购将有助于公司拓展技术能力和加速产品研发。",
"公司资产负债表显示,公司偿债能力强劲,现金流充足,为未来投资和扩张提供了坚实的财务基础。",
"最新的分析报告指出,可再生能源行业预计将在未来几年经历持续增长,投资者应该关注这一领域的投资机会",
"PDD公司2025年财报显示, 公司预计亏损7000w到1个亿之间"
]
# 初始化prompt配置, 调用init_prompts函数, 返回prompt配置内容
custom_settings = init_prompts()
print('custom_settings--->', custom_settings)
# 模型推理
inference(sentences, custom_settings)
三、任务二:信息抽取
3.1 Schema 定义
python
schema = {
"金融": ["日期", "股票名称", "开盘价", "收盘价", "成交量"],
"新闻": ["日期", "新闻标题", "新闻内容"],
}
3.2 Prompt 模板
python
IE_PATTERN = "{}\n\n提取上述句子中{}的实体,并按照JSON格式输出,上述句子中不存在的信息用['原文中未提及']来表示,多个值之间用','分隔。"
3.3 Few-shot 示例
python
ie_examples = {
"金融": [{
"content": "2023-01-10,股票古哥-D美股开盘价100美元...",
"answers": {
"日期": ["2023-01-10"],
"股票名称": ["古哥-D[EOOE]美股"],
"开盘价": ["100美元"],
"收盘价": ["102美元"],
"成交量": ["520000"],
},
}],
}
3.4 后处理容错
python
def clean_response(response: str):
# 1. 提取 Markdown 代码块中的 JSON
if "```json" in response:
res = re.findall(r"```json(.*?)```", response, re.DOTALL)
if len(res) and res[0]:
response = res[0]
# 2. 中文顿号 → 英文逗号
response = response.replace("、", ",")
# 3. JSON 解析兜底
try:
return json.loads(response)
except:
return response
3.5 关键技巧
| 技巧 | 作用 |
|---|---|
| Schema 约束 | 明确要抽取哪些字段 |
| 兜底处理 | ['原文中未提及'] 处理缺失信息 |
| 正则提取 | 处理模型输出的 Markdown 包裹 |
| 标点替换 | 、 → , 避免 JSON 解析失败 |
3.6 效果示例
输入:
2023-02-15,股票佰笃[BD]美股开盘价10美元,收盘价13美元,成交量460,000
输出:
python
{"日期": ["2023-02-15"], "股票名称": ["佰笃[BD]美股"], "开盘价": ["10美元"], "收盘价": ["13美元"], "成交量": ["460,000"]}
代码
python
import json
import ollama
import re # 使用正则表达式来匹配字符串
from rich import print
# 定义不同类型下的实体类型
schema = {
"金融": ["日期", "股票名称", "开盘价", "收盘价", "成交量"],
"新闻": ["日期", "新闻标题", "新闻内容"],
}
# 定义信息抽取的prompt模板, 其中{}为占位符,会被替换为句子和实体类型
# 字符串格式化输出
IE_PATTERN = "{}\n\n提取上述句子中{}的实体,并按照JSON格式输出,上述句子中不存在的信息用['原文中未提及']来表示,多个值之间用','分隔。"
# print(type(IE_PATTERN))
# print('IE_PATTERN--->', IE_PATTERN)
# 提供一些例子供模型参考 one-shot example
ie_examples = {
"金融": [
{
"content": "2023-01-10,股市震荡。股票古哥-D[EOOE]美股今日开盘价100美元,一度飙升至105美元,随后回落至98美元,最终以102美元收盘,成交量达到520000。",
"answers": {
"日期": ["2023-01-10"],
"股票名称": ["古哥-D[EOOE]美股"],
"开盘价": ["100美元"],
"收盘价": ["102美元"],
"成交量": ["520000"],
},
}
],
"新闻": [
{
"content": "2025-01-10,央行发布公告宣布降低利率,以刺激经济增长。这一降息举措将影响贷款利率,并在未来几个季度内对金融市场产生影响。",
"answers": {
"日期": ["2025-01-10"],
"新闻标题": ["央行发布公告宣布降低利率"],
"新闻内容": [
"这一降息举措将影响贷款利率,并在未来几个季度内对金融市场产生影响。"
],
},
}
],
}
def init_prompts():
"""
初始化前置prompt,便于模型做 incontext learning。
"""
# 系统角色描述
ie_pre_history = [
{"role": "system", "content": "你是一个信息抽取助手。"},
]
# 循环遍历不同类型例子 对应不同类型
for _type, example_list in ie_examples.items(): # [(key, value), ()...]
# print(f'_type-->{_type}')
# print(f'example_list-->{example_list}')
# 循环遍历不同类型例子中的每个例子
for example in example_list:
# print(f'example-->{example}')
# 根据content key值获取字典中的value值 文本信息
sentence = example["content"]
# print(f'sentence--》{sentence}')
# schema[_type]->根据字典中的key值获取value值
# 根据_type值获取该类型对应的实体类型, 用', '拼接成字符串
properties_str = ", ".join(schema[_type])
# print(f'properties_str--》{properties_str}')
# 格式化_type和_type对应的实体类型,用于后续的prompt构造
schema_str_list = f""{_type}"({properties_str})"
# print(f'schema_str_list-->{schema_str_list}')
# 根据sentence和schema_str_list构造prompt
sentence_with_prompt = IE_PATTERN.format(sentence, schema_str_list)
# print(f'sentence_with_prompt-->{sentence_with_prompt}')
# 用户和模型对话,其中role为user,content为sentence_with_prompt
ie_pre_history.append(
{"role": "user", "content": f"{sentence_with_prompt}"}
)
# 模型输出,其中role为assistant,content为example['answers']
# json.dumps(): 将字典转换为json字符串,ensure_ascii=False表示输出的json字符串中,中文字符不会被转码为unicode编码,而是直接输出为中文字符。
ie_pre_history.append(
{
"role": "assistant",
"content": f"{json.dumps(example['answers'], ensure_ascii=False)}",
}
)
# ie_pre_history: ollama.chat(message=) message的参数值
return {"ie_pre_history": ie_pre_history}
def clean_response(response: str):
"""
后处理模型输出
Args:
response (str): _description_
"""
# response1='```json["name":lucy]```abc```json["name":lucy]```'
# print(f'response--->{response}')
if "```json" in response:
# 正则表达式匹配所有分组,并返回一个列表
# re.DOTALL: 匹配包括换行在内的任何字符,包括 '\n','\\n'
res = re.findall(r"```json(.*?)```", response, re.DOTALL) # 返回列表
# print(f'res--》{res}')
# res是正则返回的匹配列表,如果len(res)不为空代表列表内有东西且res[0]不为空说明不是空字符串,则返回res[0]
if len(res) and res[0]:
response = res[0]
# 替换中文、为英文,处理中文顿号导致 JSON 解析失败的完美兜底方案
response = response.replace("、", ",")
# print(f'response--》{response}')
try:
# 将response转换为json格式
return json.loads(response)
except:
# 如果转换失败,则返回response
return response
def inference(sentences: list[dict[str, str]],
custom_settings: dict):
"""
推理函数
Args:
sentences (List[str]): 待抽取的句子。
custom_settings (dict): 初始设定,包含人为给定的 one-shot/few-shot example。
"""
for item in sentences: # [{}, {}, ...]
# item -> {}
# 获取文本内容
sentence = item["text"]
# 获取文本类型
cls_res = item["cls"]
# 判断文本类型是否在schema字典中
if cls_res not in schema:
print(
f"The type model inferenced {cls_res} which is not in schema dict, exited."
)
continue
# 根据cls_res获取该类型对应的实体类型, 用', '拼接成字符串
properties_str = ", ".join(schema[cls_res])
# print(f'properties_str-->{properties_str}')
# 格式化cls_res和cls_res对应的实体类型, 用于后续的prompt构造
schema_str_list = f""{cls_res}"({properties_str})"
# print(f'schema_str_list--->{schema_str_list}')
# 根据sentence和schema_str_list构造prompt
sentence_with_ie_prompt = IE_PATTERN.format(sentence, schema_str_list)
# print(f'sentence_with_ie_prompt-->{sentence_with_ie_prompt}')
# 使用 Ollama 调用 Qwen2.5:7b 模型
# 构造模型需要的messages信息
# *custom_settings['ie_pre_history']: 列表拆包 eg:*[{},{},{},...]->{},{},{}...
# messages = custom_settings["ie_pre_history"] + [{"role": "user", "content": sentence_with_ie_prompt}]
# print(messages)
messages = [
*custom_settings["ie_pre_history"],
{"role": "user", "content": sentence_with_ie_prompt},
]
# print(f'messages--->{messages}')
# 调用模型, 获取模型输出
response = ollama.chat(model="qwen2:0.5b", messages=messages)
res_content = response["message"]["content"]
# print(type(res_content)) # json格式的字符串类型
print(f'res_content-->{res_content}')
# 通过自定义函数clean_response清洗模型输出
# ie_res = clean_response(res_content)
# print(f"sentence: {sentence}")
# print(f"inference answer: {ie_res}")
if __name__ == "__main__":
# 初始化句子和自定义设置
sentences = [
{
"text": "2023-02-15,寓意吉祥的节日,股票佰笃[BD]美股开盘价10美元,虽然经历了波动,但最终以13美元收盘,成交量微幅增加至460,000,投资者情绪较为平稳。",
"cls": "金融",
},
{
"text": "2023-04-05,市场迎来轻松氛围,股票盘古(0021)开盘价23元,尽管经历了波动,但最终以26美元收盘,成交量缩小至310,000,投资者保持观望态度。",
"cls": "金融",
},
{
"text": "2025-01-10,央行发布公告宣布降低利率,以刺激经济增长。这一降息举措将影响贷款利率,并在未来几个季度内对金融市场产生影响。",
"cls": "新闻",
},
]
# 初始化自定义设置
custom_settings = init_prompts()
# print("custom_settings--->", custom_settings)
# 开始推理
inference(sentences, custom_settings)
四、任务三:语义匹配
4.1 Prompt 设计
python
system_prompt = "现在你需要帮助我完成文本匹配任务,当我给你两个句子时,你需要回答我这两句话语义是否相似。只需要回答是否相似,不要做多余的回答。"
user_prompt = f'句子一: {sentence1}\n句子二: {sentence2}\n上面两句话是相似的语义吗?'
4.2 Few-shot 示例
python
examples = {
"是": [
('公司ABC发布了季度财报,显示盈利增长。', '财报披露,公司ABC利润上升。'),
],
"不是": [
('黄金价格下跌,投资者抛售。', '外汇市场交易额创下新高。'),
('央行降息,刺激经济增长。', '新能源技术的创新。')
]
}
4.3 关键技巧
| 技巧 | 作用 |
|---|---|
| 正负样本均衡 | 同时提供"是"和"不是"的示例 |
| 限制输出 | 只回答是否相似,不要做多余的回答 |
| 结构化输入 | 句子一: ...\n句子二: ... 清晰分隔 |
4.4 效果示例
输入:
句子一: 股票市场今日大涨,投资者乐观。
句子二: 持续上涨的市场让投资者感到满意。
输出:
是
代码
python
"""
利用 LLM 进行文本匹配任务。
"""
from rich import print
from rich.console import Console
import ollama
# 提供相似,不相似的语义匹配例子 few-shot example
examples = {
'是': [
('公司ABC发布了季度财报,显示盈利增长。', '财报披露,公司ABC利润上升。'),
],
'不是': [
('黄金价格下跌,投资者抛售。', '外汇市场交易额创下新高。'),
('央行降息,刺激经济增长。', '新能源技术的创新。')
]
}
def init_prompts():
"""
初始化前置prompt,便于模型做 incontext learning。
"""
# 系统角色描述,构造prompt
pre_history = [{"role": "system",
"content": "现在你需要帮助我完成文本匹配任务,当我给你两个句子时,你需要回答我这两句话语义是否相似。只需要回答是否相似,不要做多余的回答。"}, ]
# 循序遍历所有例子,构造前置prompt
for key, sentence_pairs in examples.items():
# print(f'key-->{key}')
# print(f'sentence_pairs-->{sentence_pairs}')
# 训练遍历每个例子的句子对
for sentence_pair in sentence_pairs:
# print(f'sentence_pair-->{sentence_pair}')
# 获取每个例子中的句子对 元组拆包
sentence1, sentence2 = sentence_pair
# 用户角色描述,构造prompt
pre_history.append(
{"role": "user", "content": f'句子一: {sentence1}\n句子二: {sentence2}\n上面两句话是相似的语义吗?'})
# 模型角色描述,构造prompt
pre_history.append({"role": "assistant", "content": key})
return {'pre_history': pre_history}
def inference(
sentence_pairs: list,
custom_settings: dict
):
"""
推理函数
Args:
sentence_pairs (List[str]): 待推理的句子对。
custom_settings (dict): 初始设定,包含人为给定的 few-shot example。
"""
# print('*' * 60)
for sentence_pair in sentence_pairs:
# print(f'sentence_pair-->{sentence_pair}')
with console.status("[bold bright_green] Model Inference..."):
sentence1, sentence2 = sentence_pair
sentence_with_prompt = f'句子一: {sentence1}\n句子二: {sentence2}\n上面两句话是相似的语义吗?'
# 调用模型
response = ollama.chat(model="qwen2:0.5b",
messages=[*custom_settings["pre_history"],
{"role": 'user', "content": sentence_with_prompt}])
# 获取模型输出
response = response["message"]["content"]
print(f'>>> [bold bright_red]sentence_pair:{sentence_pair}')
print(f'>>> [bold bright_green]inference answer:{response}')
if __name__ == '__main__':
console = Console()
# 推理数据
sentence_pairs = [
('股票市场今日大涨,投资者乐观。', '持续上涨的市场让投资者感到满意。'),
('油价大幅下跌,能源公司面临挑战。', '未来智能城市的建设趋势愈发明显。'),
('利率上升,影响房地产市场。', '高利率对房地产有一定冲击。'),
]
# 初始化prompt
custom_settings = init_prompts()
print(f'custom_settings-->{custom_settings}')
# 模型推理
inference(
sentence_pairs,
custom_settings
)
五、三个任务的通用经验
| 经验 | 说明 |
|---|---|
| System Prompt 定角色 | 让模型明确自己的任务身份 |
| Few-shot 是核心 | 1-3 个示例比长篇指令更有效 |
| 限制输出格式 | 减少模型"废话",便于解析 |
| 后处理容错 | 正则提取 + 标点替换 + try-catch |
| 小模型能力有限 | 0.5B 能跑,但 1.5B/7B 更稳定 |
六、一句话心得
Prompt 不是写作文,而是写"约束"。Few-shot 是给模型看标准答案,后处理是给模型兜底。三者配合,小模型也能做出稳定的效果。
希望这篇总结对你有帮助!你的代码结构清晰、容错完善,已经是一个很成熟的 Prompt 工程实践了。