基于OpenAI大模型开发——Function calling之自动生成tool
前言
前面章节我们介绍了如何使用Function calling功能使大模型调用外部工具(api),如果有很多工具需要大模型去调用,我们需要一个个去生成吗?事实上是不需要的,可以发现tools中的各个function格式是一样的,所以可以封装一个函数来帮我们生成tool里面function内容,减少工作量。
一、测试api
import json
import os
from dotenv import load_dotenv
from openai import OpenAI
# 秘钥
load_dotenv()
WILDCARD_API_KEY = os.getenv('WILDCARD_API_KEY')
WILDCARD_API = os.getenv('WILDCARD_API')
client = OpenAI(api_key=WILDCARD_API_KEY, base_url=WILDCARD_API)
response = client.chat.completions.create(
model="gpt-4o",
messages=[
{"role": "user", "content": "什么是JSON Schema?"}
]
)
response.choices[0].message.content
'JSON Schema是一种用于验证JSON数据格式的规范。它定义了一个JSON数据结构的模式,包括数据类型、格式、值的范围等信息,可以帮助开发人员和API设计者规范和验证数据的正确性。通过定义JSON Schema,可以确保JSON数据符合特定的规范,有助于减少数据错误和提高数据的准确性。JSON Schema通常以JSON格式定义,用于描述JSON数据的结构和属性。'
二、自动生成tool
1、参考示例
这里还是以之前获取天气的function为例,我们把原来写的代码拿过来
{
"type": "function",
"function": {
"name": "get_weather",
"description": "用于获取天气情况的函数,获取城市或者地区当前的天气情况",
"parameters": {
"type": "object",
"properties": {
"location": {
"type": "string",
"description": "城市或地区,例如北京、香港",
},
},
"required": ["location"],
},
}
},
这就是一个tool,所以我们只需要按照这个结构去构建其他的tool就好了。
2、定义function
定义function,一定要写好注释,如param、return,大模型会根据这个信息去识别tool
import requests
def get_weather(location):
"""
获取天气的函数,该函数实现了如何获取天气信息
:param location: 必要参数,表示地理位置,用字符串表
:return: 返回天气信息,用字符串表示
"""
url = f"https://apis.tianapi.com/tianqi/index?key=a69b86e670dc4e86001f&city={location}&type=1"
response = requests.get(url)
data = response.json()
# print(data.get('result').get('weather'))
return f"{location}的天气是{data['result']['weather']},温度{data['result']['real']}。Tips:{data['result']['tips']}"
3、自动生成tool
- 获取函数信息
这里使用inspect模块获取函数的注释信息
import inspect
print(inspect.getdoc(get_weather))
function_description = inspect.getdoc(get_weather)
获取天气的函数,该函数实现了如何获取天气信息
:param location: 必要参数,表示地理位置,用字符串表
:return: 返回天气信息,用字符串表示
- 使用大模型生成生成json格式
messages = [
{"role": "system", "content": f"以下是获取天气信息的函数说明:{function_description}"},
{"role": "user", "content": "请帮我编写一个JSON Schema对象,用于说明获取天气信息函数的参数输入规范。输出结果要求是JSON Schema格式的JONS类型对象,不需要任何前后修饰语句。"},
]
response = client.chat.completions.create(
model="gpt-4o",
messages=messages
)
print(response.choices[0].message.content)
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"location": {
"type": "string",
"description": "The name of the location for which the weather information is requested."
},
"date": {
"type": ["string", "null"],
"format": "date",
"description": "The date for which the weather information is requested. If null, current weather information is retrieved."
},
"units": {
"type": "string",
"enum": ["metric", "imperial"],
"default": "metric",
"description": "The unit system to use for the weather information. Metric units (Celsius, meters per second) or Imperial units (Fahrenheit, miles per hour)."
},
"lang": {
"type": "string",
"default": "en",
"description": "The language in which the weather information should be returned."
}
},
"required": ["location"],
"additionalProperties": false
}
这里可以看到,返回结果里信息很多,但是还是可以看到我们需要的部分
- 优化prompt
system_prompt = f'以下是某的函数说明:{function_description}'
user_prompt = f'根据这个函数的函数说明,请帮我创建一个JSON格式的字典,这个字典有如下5点要求:\
1.字典总共有三个键值对;\
2.第一个键值对的Key是字符串name,value是该函数的名字:{get_weather.__name__},也是字符串;\
3.第二个键值对的Key是字符串description,value是该函数的函数的功能说明,也是字符串;\
4.第三个键值对的Key是字符串parameters,value是一个JSON Schema对象,用于说明该函数的参数输入规范。\
5.输出结果必须是一个JSON格式的字典,不需要添加任何修饰语句,不需要解释'
response = client.chat.completions.create(
model="gpt-4o",
messages=[
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_prompt}
]
)
print(response.choices[0].message.content)
{
"name": "get_weather",
"description": "获取天气的函数,该函数实现了如何获取天气信息",
"parameters": {
"type": "object",
"properties": {
"location": {
"type": "string"
}
},
"required": [
"location"
]
}
}
这次可以看到,基本是我们需要的格式了,只需要添加上type字段,然后将获取到的信息作为function
的value即可
json_function_description = json.loads(response.choices[0].message.content)
fin_function_description = {"type": "function","function":json_function_description}
fin_function_description
{'type': 'function',
'function': {'name': 'get_weather',
'description': '获取天气的函数,该函数实现了如何获取天气信息',
'parameters': {'type': 'object',
'properties': {'location': {'type': 'string'}},
'required': ['location']}}}
到这里格式已经和我们手写的一致了,可以拿去测试验证下
- 验证
这里直接拿fin_function_description
复制到tool
列表去就行,可以看到上篇文章去测试,这里不再演示。下面内容也会封装到函数中进行验证。
三、函数封装
现在,将上面整个生成tool的过程封装到一个函数里
def auto_functions(functions_list):
"""
Chat模型的functions自动生成函数
:param functions_list: 包含一个或者多个函数对象的列表;
:return:满足Chat模型functions参数要求的functions对象
"""
def functions_generate(functions_list):
# 创建空列表,用于保存每个函数的描述字典
functions = []
# 对每个外部函数进行循环
for function in functions_list:
# 读取函数对象的函数说明
function_description = inspect.getdoc(function)
# 读取函数的函数名字符串
function_name = function.__name__
system_prompt = f'以下是某的函数说明:{function_description}'
user_prompt = f'根据这个函数的函数说明,请帮我创建一个JSON格式的字典,这个字典有如下5点要求:\
1.字典总共有三个键值对;\
2.第一个键值对的Key是字符串name,value是该函数的名字:{function_name},也是字符串;\
3.第二个键值对的Key是字符串description,value是该函数的函数的功能说明,也是字符串;\
4.第三个键值对的Key是字符串parameters,value是一个JSON Schema对象,用于说明该函数的参数输入规范。\
5.输出结果必须是一个JSON格式的字典,不需要添加任何修饰语句,不需要解释'
response = client.chat.completions.create(
model="gpt-4o",
messages=[
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_prompt}
]
)
json_function_description = json.loads(response.choices[0].message.content.replace("```","").replace("json",""))
json_str={"type": "function","function":json_function_description}
functions.append(json_str)
return functions
## 失败重试机制,最大可以尝试4次
max_attempts = 4
attempts = 0
while attempts < max_attempts:
try:
functions = functions_generate(functions_list)
break # 如果代码成功执行,跳出循环
except Exception as e:
attempts += 1 # 增加尝试次数
print("发生错误:", e)
if attempts == max_attempts:
print("已达到最大尝试次数,程序终止。")
raise # 重新引发最后一个异常
else:
print("正在重新运行...")
return functions
function_list = [get_weather]
tools = auto_functions(function_list)
tools
[{'type': 'function',
'function': {'name': 'get_weather',
'description': '获取天气的函数,该函数实现了如何获取天气信息',
'parameters': {'type': 'object',
'properties': {'location': {'type': 'string'}},
'required': ['location']}}}]
messages = [
{"role": "system", "content": "你是一名全能小助手,无所不能,可以执行各种函数功能,如加法计算、获取天气等。在需要时调用适当的函数来处理。对于回答不作任何解释"},
{"role": "user", "content": "今天济南天气怎么样?"}
]
response = client.chat.completions.create(
model="gpt-4o",
temperature=0,
messages=messages,
tools=tools,
tool_choice="auto"
)
print(response.choices[0].message)
ChatCompletionMessage(content=None, role='assistant', function_call=None, tool_calls=[ChatCompletionMessageToolCall(id='call_JjDFO4oZ5JKJlvAOs8WsYpDf', function=Function(arguments='{\n "location": "济南"\n}', name='get_weather'), type='function')])
到这里,可以看到测试成功了,大模型成功获取到了tool_calls
信息,而且arguments
也符合我们定义的函数。
四、将整个Function calling过程封装到函数
- 新增一个tool
def add_sum(*args):
"""
定义add_sum函数,实现对传入多个参数进行求和
:param args: 必要参数,传入的是一个元组,元组中的元素是数字
:return: 相加后的结果
"""
result = 0
for num in args[0]:
result += int(num)
return result
function_list = [get_weather, add_sum]
tools = auto_functions(function_list)
tools
[{'type': 'function',
'function': {'name': 'get_weather',
'description': '获取天气',
'parameters': {'type': 'object',
'properties': {'location': {'type': 'string', 'description': '地区'}},
'required': ['location']}}},
{'type': 'function',
'function': {'name': 'add_sum',
'description': '对传入多个参数进行求和',
'parameters': {'type': 'object',
'properties': {'args': {'type': 'array', 'items': {'type': 'number'}}},
'required': ['args']}}}]
- 测试tool
messages = [
{"role": "system", "content": "你是一名全能小助手,无所不能,可以执行各种函数功能,如加法计算、获取天气等。在需要时调用适当的函数来处理。对于回答不作任何解释"},
{"role": "user", "content": "从1累加到100的和是多少"}
]
response = client.chat.completions.create(
model="gpt-4o",
temperature=0,
messages=messages,
tools=tools,
tool_choice="auto"
)
print(response.choices[0].message)
ChatCompletionMessage(content=None, role='assistant', function_call=None, tool_calls=[ChatCompletionMessageToolCall(id='call_OEpDOf1oXwZV4SAX0RoMNEgG', function=Function(arguments='{"args":[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100]}', name='add_sum'), type='function')])
- 函数封装
def run_conversation(messages, model="gpt-4o", functions_list=None):
"""
能够自动执行外部函数调用的对话模型
:param messages: 必要参数,字典类型,输入到Chat模型的messages参数对象
:param functions_list: 可选参数,默认为None,可以设置为包含全部外部函数的列表对象
:param model: Chat模型,可选参数,默认模型为gpt-4o
:return:Chat模型输出结果
"""
# 如果没有外部函数库,则执行普通的对话任务
if functions_list == None:
response = client.chat.completions.create(
model=model,
messages=messages,
)
response_message = response.choices[0].message
final_response = response_message.content
# 若存在外部函数库,则需要灵活选取外部函数并进行回答
else:
# 创建functions对象
tools = auto_functions(functions_list)
# 创建外部函数库字典
available_functions = {func.__name__: func for func in functions_list}
# 第一次调用大模型
response = client.chat.completions.create(
model=model,
messages=messages,
tools=tools,
tool_choice="auto", )
response_message = response.choices[0].message
tool_calls = response_message.tool_calls
if tool_calls:
messages.append(response_message)
for tool_call in tool_calls:
function_name = tool_call.function.name
function_to_call = available_functions[function_name]
function_args = json.loads(tool_call.function.arguments)
## 真正执行外部函数的就是这儿的代码
function_response = function_to_call(**function_args)
messages.append(
{
"tool_call_id": tool_call.id,
"role": "tool",
"name": function_name,
"content": function_response,
}
)
## 第二次调用模型
second_response = client.chat.completions.create(
model=model,
messages=messages,
)
# 获取最终结果
final_response = second_response.choices[0].message.content
else:
final_response = response_message.content
return final_response
总结
最后强调一下,tools生成的质量和大模型的能力有关系,大模型能力越强生成的越稳定。按照文章中的案例,如果将自动生成tool放在业务代码中,会因为每次都要生成tool花费额外的token,还会增加响应时间,介于此,我们可以把生成tool的函数单独封装成工具,生成之后直接放在业务代码中,一般情况下tool格式是不需要变动的。