一、前言
有时候,会遇到一些单纯使用 Python 很难解决的问题。例如 从一批给定的企业名单(表格)中,根据企业的经营范围,找出涉农企业并标记“农业及相关产业标签”,例如农林牧渔业、食用农林牧渔产品加工与制造…… 这就是一个偏主观的任务,过去我们尝尝使用正则表达式匹配关键词的方式去处理,但是这种方法算不上完美。比方说,农产品的细分种类那么多,用关键词进行匹配,难免出现遗漏。况且经营范围还会出现一些逆向的表示(例如:不含苹果汁生产),使得处理结果与实际结果偏差更大。
但是大模型的出现和成熟让这种需求的处理变得更加轻松和可靠了。大模型就像是一个学识丰富且具备独立思考能力,又可以连续工作的机器人,如果使用 Python 调用大模型来解决这类问题,必然节省不少人力。本期笔者就向大家分享一个调用大模型解决上述问题的实操案例。
二、创建 DeepSeek API
以当下最火爆的 DeepSeek 为例,要调用大模型,需要先注册账号并创建 API key。
(1)注册 DeepSeek 账号
(2)实名认证
(3)充值
(4)创建 API key
创建好的 key 需要立即复制 key,后续无法重新复制。如果忘记复制,删除创建的 key,然后重新创建即可。
三、编写提示词
使用 Python 调用大模型批量处理数据,本质上就是就是连续不断地问答(注意不是连续对话,连续对话每一次对话都需要加载上文,耗费的 Toke 数量较大),我们抛出问题,大模型给出答案。所以需要提前准备好能让 DeepSeek 给出正确答案的提示词。
提示词的编写还需要考虑大模型的特性,例如在 DeepSeek 官方网站和手机 APP 中使用 DeepSeek 大模型时,可以上传图片进行文字识别,也可以上传表格进行简单的数据处理,但是调用 API 时这些操作都不能实现。即提示词必须是纯文本,那么我们应该如何让大模型处理表格数据呢?我们可以把表格内容转换为 Python 中的字典格式即可,就像下面这样。
这一步可以通过 Python 实现。同时,我们也可以限制 DeepSeek 回答内容的格式,使其返回可以被简单解析成表格的 JSON 格式(与 python 字典非常相似),通过阅读 DeepSeek API 接口文档,可以知道应该怎么做。详细做法见下图。
综合以上信息,笔者的提示词如下。
四、Token 计算与费用问题
DeepSeek 每轮对话能接受的 Token 有限,也就是说我们不能一次性让它帮我们处理所有数据。从官方接口文档来看,推理模型R1 与通用模型V3 的上下文长度,都是 64K(实际为65792),最大输出长度也完全一样,都是 8K(实际为 8192)。我们每次传给 API 的提示词和数据的内容总和,加起来不能超过上述限制,否则 API 会立即报错,同时还需要考虑输出内容的长度限制,如果是一些少输入多输出的数据处理场景,还需要根据这一限制进一步减少输入的 Token 数量,也就是交由 API 处理的数据量。那么如何估算 Token 大小呢,DeepSeek 官方给出了估算的公式,如下图所示。
至于费用问题,DeepSeek 的收费标准并不高,以推理模型 R1 来看,每一百万 Tokens 输入,(缓存命中情况下)仅收费 1 元,如果是优惠时段,还能享受 2.5 折价格,不过需要注意,输出内容部分的收费标准是输入内容的 4 倍,估算成本时需要考虑这一点。
五、案例代码(全流程)
1.拆分数据
笔者要处理的数据有近十万条,也就是从约十万家企业中找到涉农企业并标记对应的农业产业标签。由于不能一次性将所有数据传入 API,所以需要分批传入,也就是一次性传入不超过 Token 数量限制的数据条数,这样的话就需要提前估算每一行数据的 Token,确保每次传入 Token 不超标,同时也要考虑输出内容的 Token 限制。
💡
每次输入的 Token 并不是说越接近限额,性价比就越高。这其中有很多因素,首先,API 是以 Token 数额计费的,而不是调用次数;其次,一次性输入过多,会让 AI 的注意力无法集中,即便提示词中已经说明不允许遗漏数据,但实际处理时遗漏现象还是难免出现;再者,每次输入的数据量越多,AI 思考的时间也会越久;最后,由于 Token 的估算有误差问题,必须要保守一些。所以建议在输出 Token 不超标的情况下,每次输入的 Token 没必要直逼 64K 的限额,大约 20K - 30K 即可。
所以这里笔者先根据字符数量估算了每一行数据的 Token,相关代码如下。
import pandas as pd
# 读取数据
DATA= pd.read_csv('./原始数据/企业数据.csv', dtype=str).fillna('')
# 清洗数据,剔除或替换特殊字符,避免影响后续步骤
DATA= DATA.applymap(lambda x: x.replace('`', '‘').replace("'", '‘').replace('"', '“')\
.replace('【', '(').replace('】', ')').replace('\n', '').replace('\r', '')\
.replace('\t', ''))
# 估算 Tokens 并形成一个字段
DATA['Tokens'] = DATA.apply(lambda r: int(len(''.join(list(r.values)))*0.6), axis=1)
DATA.head(5)
随后根据估算的 Tokens,将全部数据拆分为若干个子数据,每个子数据都只包含可以被 API 一次性处理的数据量。笔者为了避免 AI 处理数据时出现遗漏,每次传入的数据量经过严格限制,不仅 Token 不能超过 20000,一次输入的数据量也被限制在了 50 以内。拆分数据的代码和拆分后的数据如下图所示。
def Get_index_Limit_Tokens(df, max_tokens=20000, max_qynum=50):
'''
输入一个数据 df,根据前面生成的 Tokens 字段,获取合适数量的
数据行数,需要满足两个条件,一是这些数据行对应的 Tokens
不超过参数 max_tokens 的值,二是数据行数不超过设定的最大值
max_qynum,两个条件同时满足。返回值则是选取的数据行的索引值列表
'''
# 确保传入的数据 df 的行索引不存在重复
index_list = list(df.index)
add_tokens = 0
target_index = []
qynum = 0
for ind in index_list:
qynum += 1
add_tokens += df['Tokens'][ind]
if add_tokens > max_tokens or qynum > max_qynum:
break
else:
target_index.append(ind)
if not target_index:
# 说明这一行的文本 Token 已经超过设置的值,那么只返回一行数据的索引即可
# 后续如果 Token 超标,也方便及时处理
Special_index = [index_list][0]
return Special_index
return target_index
# 初始化拆分后子数据的编号
file_id = 0
while not DATA.empty:
file_id += 1
# 获取子数据的索引
target_index = Get_index_Limit_Tokens(DATA, max_tokens=20000, max_qynum=50)
# 根据索引获取数据
df_one = DATA.loc[target_index, :]
# 保存获取的数据,存入一个提前创建的文件夹中
df_one.to_csv(f'./提前拆分的数据/{file_id}.csv', index=False)
# 将获取的数据从全部数据集中删除
DATA = DATA.drop(target_index)
2.循环调用 DeepSeek API
数据提前拆分后就可以开始循环调用 API 来处理数据了。关于 API 的选用问题,是选择推理模型 R1 还是通用模型 V3,可以参考这篇文章:《AI 视界 | 推理模型和通用模型的区别究竟是什么?》 ,对于本文这种简单的判断问题,通用模型 V3 模型足以胜任,且思考时间更短,标准时段下的价格更低。如果希望只在优惠时段时才调用 API 那么可以编写 Python 函数,让循环只在规定时段内才会运行。
至于如何调用 API,DeepSeek 官方给出了调用示范,我们只需按照步骤进行测试即可。
接下来根据官方的演示代码稍加修改后进行循环调用即可,注意调用前需要先安装第三方库 openai。
import pandas as pd
import os, time, glob
from openai import OpenAI
client = OpenAI(api_key='sk-bf9c867698e048a5844e3403acecf810', base_url="https://api.deepseek.com")
# 获取拆分后的全部文件的路径
files_paths = glob.glob('./提前拆分的数据/*.csv')
# 根据文件序号排序,防止混乱
files_paths.sort(key=lambda x: int(os.path.basename(x).split('.')[0]))
DATA_paths = [path.replace('\\', '/') for path in files_paths]
def is_time_in_range():
"""
判断当前时间是否在 00:31 - 8:29 之间。
:return: 如果当前时间在范围内,返回 True;否则返回 False。
"""
# 获取当前时间的小时和分钟
current_time = time.localtime()
current_hour = current_time.tm_hour
current_minute = current_time.tm_min
# 将时间转换为分钟数进行比较
start_time_minutes = 0 * 60 + 30 # 00:31 转换为分钟数
end_time_minutes = 8 * 60 + 29 # 08:29 转换为分钟数
current_time_minutes = current_hour * 60 + current_minute # 当前时间转换为分钟数
# 判断当前时间是否在范围内
if start_time_minutes <= current_time_minutes < end_time_minutes:
return True
else:
return False
## 将多行数据合成一个字典,如果字典的值为空,则去除
def Row_2_Dict(df:pd.DataFrame):
'''
将表格 df 转为字典格式的纯文本内容
'''
Target_dict = {}
# 刷新索引
df = df.reset_index(drop=True)
index_list = list(df.index)
for ind in index_list:
row_dict = df.loc[ind, :]
entname = row_dict.pop('企业名称')
del row_dict['Tokens']
row_dict = {k:v for k,v in row_dict.items() if str(v).strip() and k not in ('Tokens')}
# 将清洗后的字典存入 Target_dict
Target_dict[entname] = row_dict
return str(Target_dict)
# 提示词.txt 文件内存储的是提前编写好的命令提示词,里面不包含任何企业相关信息
with open('./提示词.txt', 'r', encoding='utf-8') as TSC:
提示词 = TSC.read().strip()
# 用于保存调用报错的文件
Error_files = []
for datapath in DATA_paths:
start = time.time()
# 等待优惠时段,如果可以接受开销增加,那么可以不用等待
while not is_time_in_range():
# 如果不在规定时段内,等待一分钟后重新进入循环
print(f'\r正在等待优惠时段', end='')
time.sleep(60)
continue
print(f'正在处理文件:{os.path.basename(datapath)}')
df = pd.read_csv(datapath, dtype=str).fillna('')
## 先获取企业信息
Company_Info = Row_2_Dict(df)
try:
# 使用 tyr-except 增加一定的容错,如果一次调用报错,不至于让循环彻底停止
messages = [{"role": "system", "content": 提示词},
{"role": "user", "content": Company_Info}]
response = client.chat.completions.create(
# model="deepseek-reasoner",
model="deepseek-chat",
max_tokens=8100,
# response_format={'type': 'json_object'},
messages=messages
)
# 思考链文本,使用 R1 模型时才会有
# reasoning_content = response.choices[0].message.reasoning_content
# 获取返回的文本
content = response.choices[0].message.content
# 计算本次调用花费的秒数
# 保存刚刚返回的文本结果,保存在存放拆分后 csv 的文件夹中
with open(datapath.replace('.csv', '.txt'), 'w', encoding='utf-8') as f:
f.write(content)
except Exception as e:
# 如果调用时报错,记录报错的文件
Error_files.append(datapath)
print('返回报错了,稍微等一会儿……')
time.sleep(60)
usetimes = time.time() - start
print(f'本次调用完成, 用时: {usetimes} 秒')
保存后的返回结果如下图所示。
3.解析返回结果,重新形成表格
# 获取报错的 txt 文件的路径
TXT_files = glob.glob('./提前拆分的数据/*.txt')
TXT_files.sort(key=lambda x: int(os.path.basename(x).split('.')[0]))
# 逐一解析为表格
def json_to_table(D, source='-'):
'''
将 API 返回的 json 文本解析为表格
'''
if len(D) == 0:
return pd.DataFrame(columns=['企业ID', '企业名称', '产业标签', '判断依据', '来源'])
table_values_list = []
for d in D:
table_values_list.append([d['企业ID'], d['企业名称'],d['产业标签'], d['判断依据'], source])
df = pd.DataFrame(table_values_list, columns=['企业ID', '企业名称', '产业标签', '判断依据', '来源'])
return df
# 存放所有解析结果的列表
dflist = []
for txt in TXT_files:
with open(txt, 'r', encoding='utf-8') as f:
TEXT_JS = f.read().strip()
TEXT_JS = re.sub('`|json|\s+', '', TEXT_JS)
TEXT_JS = re.sub('^.{,10}(\[)', r'\1', TEXT_JS)
try:
JS = eval(TEXT_JS)
df = json_to_table(JS, txt)
dflist.append(df)
except:
print(f'文件 {txt} 无法被解析为表格')
# 合并所有解析结果并排序
DATA_RESULT = pd.concat(dflist)
DATA_RESULT['企业ID'] = DATA_RESULT['企业ID'].apply(int)
DATA_RESULT = DATA_RESULT.sort_values(['企业ID']).reset_index(drop=True)
# 保存为 excel 文件,如果结果太大,建议保存为 csv 文件
DATA_RESULT.to_excel('./处理结果.xlsx', index=False)
六、关于调用限速问题
这是一个很现实的问题,如果这个流程用来处理大量的数据,肯定希望调用速度越快越好。DeepSeek 官方文档明确说明:
但是经过笔者实测发现,这里所说的不限速,并非完全不限速,同一个账号,无论创建了多少 key,同一时间都只能有一段进行中的对话。也就是说只有等一次调用完成了,才能进行下一次调用。
七、总结
本文向大家分享了一个如何使用 Python 调用 DeepSeek API 解决实际问题的案例,并向大家提供了详细的代码。值得一说的是,让大家更方便地理解代码,笔者简化了实际流程,去除了不少容错机制,大家在使用时,还请根据实际情况再做调整。最后希望这次分享对大家有所帮助。