目录
使用Chat Completions API调用函数的步骤:
概述
继 OpenAI API - Function calling 的概念与使用(一)文章,这次,我以学习笔记的形式来介绍如何结合外部函数使用Chat Completions API来扩展GPT模型的功能,
学习笔记整理不易,如果对您有帮助还望大家多多点赞收藏+关注!谢谢大家!
tools
是Chat Completion API中的一个可选参数,可以用来提供函数规范。其目的是使模型生成符合提供的函数规范的函数参数。需要注意的是,API不会实际执行任何函数调用,函数调用需要开发人员根据模型输出自行执行。
在 tools
参数中,如果提供了 functions
参数,则默认情况下,模型会自行决定何时适当地使用这些函数。API可以通过设置 tool_choice
参数为 {"type": "function", "function": {"name": "my_function"}}
来强制使用特定函数。API也可以通过将 tool_choice
参数设置为 none
来强制不使用任何函数。如果使用了某个函数,输出中会包含 finish_reason
: "tool_calls"
以及一个 tool_calls
对象,其中包含函数的名称和生成的函数参数。
安装必要的依赖包
- 首先需要安装几个Python库,包括
scipy
、tenacity
、tiktoken
、termcolor
和openai
。
!pip install scipy --quiet
!pip install tenacity --quiet
!pip install tiktoken --quiet
!pip install termcolor --quiet
!pip install openai --quiet
导入必要的模块并初始化OpenAI客户端
- 导入
json
模块,OpenAI
模块,以及tenacity
模块中的retry
,wait_random_exponential
,stop_after_attempt
。另外,还需要导入termcolor
模块中的colored
。 - 初始化
OpenAI
客户端。
import json
from openai import OpenAI
from tenacity import retry, wait_random_exponential, stop_after_attempt
from termcolor import colored
GPT_MODEL = "gpt-4o"
client = OpenAI()
首先,我们定义一些用于调用Chat Completions API以及维护和跟踪对话状态的实用工具。
重试机制的装饰器 @retry
:
这个装饰器使用了 tenacity
库,用于在调用API时实现重试机制。wait_random_exponential
设置了指数退避策略,最大等待时间为40秒,stop_after_attempt(3)
设置了最大重试次数为3次。
chat_completion_request
函数:
该函数用于向Chat Completions API发送请求。函数接受消息、工具和工具选择作为参数,并返回API的响应。如果请求失败,捕获异常并打印错误信息。
pretty_print_conversation
函数:
这个函数用于美化打印对话内容。根据消息的角色(系统、用户、助手、函数),用不同的颜色打印消息内容。颜色由 termcolor
库的 colored
函数实现。
from tenacity import retry, wait_random_exponential, stop_after_attempt
from termcolor import colored
GPT_MODEL = "gpt-4o"
client = OpenAI()
# 定义重试机制的装饰器,设置重试策略
@retry(wait=wait_random_exponential(multiplier=1, max=40), stop=stop_after_attempt(3))
def chat_completion_request(messages, tools=None, tool_choice=None, model=GPT_MODEL):
try:
response = client.chat.completions.create(
model=model,
messages=messages,
tools=tools,
tool_choice=tool_choice,
)
return response
except Exception as e:
print("无法生成ChatCompletion响应")
print(f"异常: {e}")
return e
# 定义一个函数用于美化打印对话内容
def pretty_print_conversation(messages):
role_to_color = {
"system": "red",
"user": "green",
"assistant": "blue",
"function": "magenta",
}
for message in messages:
if message["role"] == "system":
print(colored(f"system: {message['content']}\n", role_to_color[message["role"]]))
elif message["role"] == "user":
print(colored(f"user: {message['content']}\n", role_to_color[message["role"]]))
elif message["role"] == "assistant" and message.get("function_call"):
print(colored(f"assistant: {message['function_call']}\n", role_to_color[message["role"]]))
elif message["role"] == "assistant" and not message.get("function_call"):
print(colored(f"assistant: {message['content']}\n", role_to_color[message["role"]]))
elif message["role"] == "function":
print(colored(f"function ({message['name']}): {message['content']}\n", role_to_color[message["role"]]))
我们再来创建一些函数规范,以便与假设的天气API进行接口。我们将这些函数规范传递给Chat Completions API,以生成符合规范的函数参数。
tools = [
{
"type": "function",
"function": {
"name": "get_current_weather",
"description": "获取当前天气",
"parameters": {
"type": "object",
"properties": {
"location": {
"type": "string",
"description": "城市和州,例如 San Francisco, CA",
},
"format": {
"type": "string",
"enum": ["celsius", "fahrenheit"],
"description": "使用的温度单位。从用户的位置推断。",
},
},
"required": ["location", "format"],
},
}
},
{
"type": "function",
"function": {
"name": "get_n_day_weather_forecast",
"description": "获取N天的天气预报",
"parameters": {
"type": "object",
"properties": {
"location": {
"type": "string",
"description": "城市和州,例如 San Francisco, CA",
},
"format": {
"type": "string",
"enum": ["celsius", "fahrenheit"],
"description": "使用的温度单位。从用户的位置推断。",
},
"num_days": {
"type": "integer",
"description": "预测的天数",
}
},
"required": ["location", "format", "num_days"]
},
}
},
]
如果我们提示模型提供当前天气,它将会提出一些澄清问题。
messages = []
messages.append({"role": "system", "content": "不要对函数的值做出假设。如果用户请求模糊,请求澄清。"})
messages.append({"role": "user", "content": "今天的天气怎么样?"})
chat_response = chat_completion_request(
messages, tools=tools
)
assistant_message = chat_response.choices[0].message
messages.append(assistant_message)
print(assistant_message)
# 输出
ChatCompletionMessage(content="I need to know your location to provide you with the current weather. Could you please specify the city and state (or country) you're in?", role='assistant', function_call=None, tool_calls=None)
当我们提供缺失的信息后,它将生成适当的函数参数。
messages.append({"role": "user", "content": "我在苏格兰格拉斯哥。"})
chat_response = chat_completion_request(
messages, tools=tools
)
assistant_message = chat_response.choices[0].message
messages.append(assistant_message)
print(assistant_message)
# 输出
ChatCompletionMessage(content=None, role='assistant', function_call=None, tool_calls=[ChatCompletionMessageToolCall(id='call_Dn2RJJSxzDm49vlVTehseJ0k', function=Function(arguments='{"location":"Glasgow, Scotland","format":"celsius"}', name='get_current_weather'), type='function')])
通过不同的提示,我们可以让它针对我们告诉它的另一个函数。
messages = []
messages.append({"role": "system", "content": "不要对函数的值做出假设。如果用户请求模糊,请求澄清。"})
messages.append({"role": "user", "content": "接下来几天苏格兰格拉斯哥的天气怎么样?"})
chat_response = chat_completion_request(
messages, tools=tools
)
assistant_message = chat_response.choices[0].message
messages.append(assistant_message)
print(assistant_message)
# 输出
ChatCompletionMessage(content='请指定你想要预测的天数(x天)的天气预报。', role='assistant', function_call=None, tool_calls=None)
在这种情况下,模型已经知道了位置,但需要知道预测的天数。
messages.append({"role": "user", "content": "5天"})
chat_response = chat_completion_request(
messages, tools=tools
)
print(chat_response.choices[0])
# 输出
Choice(finish_reason='tool_calls', index=0, logprobs=None, message=ChatCompletionMessage(content=None, role='assistant', function_call=None, tool_calls=[ChatCompletionMessageToolCall(id='call_Yg5ydH9lHhLjjYQyXbNvh004', function=Function(arguments='{"location":"Glasgow, Scotland","format":"celsius","num_days":5}', name='get_n_day_weather_forecast'), type='function')]))
强制使用特定函数或不使用函数
我们可以通过使用 function_call
参数强制模型使用特定函数,例如 get_n_day_weather_forecast
。通过这种方式,我们强制模型使用它。
# 在这个例子中我们强制模型使用 get_n_day_weather_forecast
messages = []
messages.append({"role": "system", "content": "不要对函数的值做出假设。如果用户请求模糊,请求澄清。"})
messages.append({"role": "user", "content": "给我一个多伦多,加拿大的天气报告。"})
chat_response = chat_completion_request(
messages, tools=tools, tool_choice={"type": "function", "function": {"name": "get_n_day_weather_forecast"}}
)
print(chat_response.choices[0].message)
# 输出
ChatCompletionMessage(content=None, role='assistant', function_call=None, tool_calls=[ChatCompletionMessageToolCall(id='call_aP8ZEtGcyseL0btTMYxTCKbk', function=Function(arguments='{"location":"Toronto, Canada","format":"celsius","num_days":1}', name='get_n_day_weather_forecast'), type='function')])
如果我们不强制模型使用 get_n_day_weather_forecast
,它可能不会使用。
messages = []
messages.append({"role": "system", "content": "不要对函数的值做出假设。如果用户请求模糊,请求澄清。"})
messages.append({"role": "user", "content": "给我一个多伦多,加拿大的天气报告。"})
chat_response = chat_completion_request(
messages, tools=tools
)
print(chat_response.choices[0].message)
# 输出
ChatCompletionMessage(content=None, role='assistant', function_call=None, tool_calls=[ChatCompletionMessageToolCall(id='call_5HqCVRaAoBuU0uTlO3MUwaWX', function=Function(arguments='{"location": "Toronto, Canada", "format": "celsius"}', name='get_current_weather'), type='function'), ChatCompletionMessageToolCall(id='call_C9kCha28xHEsxYl4PxZ1l5LI', function=Function(arguments='{"location": "Toronto, Canada", "format": "celsius", "num_days": 3}', name='get_n_day_weather_forecast'), type='function')])
我们也可以强制模型不使用任何函数。通过这样做,我们防止它生成一个正确的函数调用。
messages = []
messages.append({"role": "system", "content": "不要对函数的值做出假设。如果用户请求模糊,请求澄清。"})
messages.append({"role": "user", "content": "给我多伦多,加拿大的当前天气(使用摄氏温度)。"})
chat_response = chat_completion_request(
messages, tools=tools, tool_choice="none"
)
print(chat_response.choices[0].message)
# 输出
ChatCompletionMessage(content="我会获取多伦多,加拿大的当前天气,使用摄氏温度。", role='assistant', function_call=None, tool_calls=None)
并行函数调用
较新的模型如 gpt-4o 或 gpt-3.5-turbo 可以在一次请求中调用多个函数。
-
定义消息:
- 在消息列表中添加一条系统消息,告知模型不要对函数的值做出假设。如果用户的请求不明确,应该请求澄清。
- 添加用户消息,询问接下来4天旧金山和格拉斯哥的天气情况。
-
调用Chat Completions API:
- 使用
chat_completion_request
函数发送消息和工具列表,并指定模型。 - 接收模型响应并解析出
tool_calls
。
- 使用
-
解析并行函数调用:
- 输出中包含两个并行的函数调用,分别获取旧金山和格拉斯哥的4天天气预报。
messages = []
messages.append({"role": "system", "content": "不要对函数的值做出假设。如果用户请求模糊,请求澄清。"})
messages.append({"role": "user", "content": "接下来4天旧金山和格拉斯哥的天气如何?"})
chat_response = chat_completion_request(
messages, tools=tools, model=GPT_MODEL
)
assistant_message = chat_response.choices[0].message.tool_calls
print(assistant_message)
# 输出
[ChatCompletionMessageToolCall(id='call_pFdKcCu5taDTtOOfX14vEDRp', function=Function(arguments='{"location": "San Francisco, CA", "format": "fahrenheit", "num_days": 4}', name='get_n_day_weather_forecast'), type='function'),
ChatCompletionMessageToolCall(id='call_Veeyp2hYJOKp0wT7ODxmTjaS', function=Function(arguments='{"location": "Glasgow, Scotland", "format": "celsius", "num_days": 4}', name='get_n_day_weather_forecast'), type='function')]
如何使用模型生成的参数调用函数
在这个例子中,我们将展示如何执行输入由模型生成的函数,并用它来实现一个可以回答有关数据库问题的代理。为了简化,我们将使用 Chinook 示例数据库。
注意:在生产环境中,SQL 生成可能存在高风险,因为模型在生成正确的 SQL 方面并不完全可靠。
-
指定一个执行 SQL 查询的函数
首先,让我们定义一些用于从 SQLite 数据库中提取数据的实用函数。
import sqlite3
conn = sqlite3.connect("data/Chinook.db")
print("数据库打开成功")
def get_table_names(conn):
"""返回表名列表。"""
table_names = []
tables = conn.execute("SELECT name FROM sqlite_master WHERE type='table';")
for table in tables.fetchall():
table_names.append(table[0])
return table_names
def get_column_names(conn, table_name):
"""返回列名列表。"""
column_names = []
columns = conn.execute(f"PRAGMA table_info('{table_name}');").fetchall()
for col in columns:
column_names.append(col[1])
return column_names
def get_database_info(conn):
"""返回包含数据库中每个表的表名和列名的字典列表。"""
table_dicts = []
for table_name in get_table_names(conn):
columns_names = get_column_names(conn, table_name)
table_dicts.append({"table_name": table_name, "column_names": columns_names})
return table_dicts
-
提取数据库架构的表示
使用这些实用函数提取数据库架构的表示。
database_schema_dict = get_database_info(conn)
database_schema_string = "\n".join(
[
f"Table: {table['table_name']}\nColumns: {', '.join(table['column_names'])}"
for table in database_schema_dict
]
)
-
定义函数规范
和之前一样,我们将定义一个函数规范,供 API 生成参数。注意,我们将数据库架构插入到函数规范中。这对于让模型了解数据库架构非常重要。
tools = [
{
"type": "function",
"function": {
"name": "ask_database",
"description": "使用此函数回答用户有关音乐的问题。输入应该是一个完全成型的 SQL 查询。",
"parameters": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": f"""
SQL 查询用于提取信息以回答用户的问题。
SQL 应使用以下数据库架构编写:
{database_schema_string}
查询应以纯文本形式返回,而不是 JSON。
""",
}
},
"required": ["query"],
},
}
}
]
-
执行SQL查询
现在让我们实现一个实际对数据库执行查询的函数。
def ask_database(conn, query):
"""使用提供的SQL查询查询SQLite数据库的函数。"""
try:
results = str(conn.execute(query).fetchall())
except Exception as e:
results = f"查询失败,错误信息: {e}"
return results
---------------------------------------------------------------------------------------------------------------------------------
使用Chat Completions API调用函数的步骤:
步骤1:使用可能导致模型选择使用工具的内容提示模型。工具的描述(如函数名称和签名)在 tools
列表中定义,并在API调用中传递给模型。如果被选择,函数名称和参数会包含在响应中。
步骤2:以编程方式检查模型是否想要调用函数。如果是,继续执行步骤3。
步骤3:从响应中提取函数名称和参数,并使用这些参数调用函数。将结果附加到消息列表中。
步骤4:使用消息列表调用Chat Completions API以获取响应。
# 步骤1
messages = [{
"role": "user",
"content": "哪张专辑的曲目最多?"
}]
response = client.chat.completions.create(
model='gpt-4o',
messages=messages,
tools=tools,
tool_choice="auto"
)
response_message = response.choices[0].message
messages.append(response_message)
print(response_message)
# 步骤2:确定模型的响应是否包含工具调用。
tool_calls = response_message.tool_calls
if tool_calls:
# 步骤3:提取函数名称和参数,并调用函数
tool_call_id = tool_calls[0].id
tool_function_name = tool_calls[0].function.name
tool_query_string = eval(tool_calls[0].function.arguments)['query']
if tool_function_name == 'ask_database':
results = ask_database(conn, tool_query_string)
messages.append({
"role": "tool",
"tool_call_id": tool_call_id,
"name": tool_function_name,
"content": results
})
# 步骤4:使用消息列表调用Chat Completions API以获取最终响应
model_response_with_function_call = client.chat.completions.create(
model="gpt-4o",
messages=messages,
)
print(model_response_with_function_call.choices[0].message.content)
else:
print(f"错误:函数 {tool_function_name} 不存在")
else:
# 模型未识别出要调用的函数,可以将结果返回给用户
print(response_message.content)
总结
Function calling 部分内容有点多,大家可以多点耐心,在操作过程中难免会出现一些错误遇到一些问题,或者也有我的笔记不正确的地方,所以实践出真知,多思考多动手~ 互勉,感谢大家,我们下期再见!