如何结合聊天模型调用函数

本笔记本介绍了如何结合使用聊天补全 API 和外部函数来扩展 GPT 模型的功能。

tools 是聊天补全 API 中的一个可选参数,可用于提供函数规范。目的是使模型能够生成符合所提供规范的函数参数。请注意,API 不会实际执行任何函数调用。由开发者使用模型输出来执行函数调用。

tools 参数中,如果提供了 functions 参数,则默认情况下模型将决定何时适合使用其中一个函数。可以通过将 tool_choice 参数设置为 {"type": "function", "function": {"name": "my_function"}} 来强制 API 使用特定函数。还可以通过将 tool_choice 参数设置为 "none" 来强制 API 不使用任何函数。如果使用了函数,响应将包含 "finish_reason": "tool_calls" 以及一个包含函数名称和生成的函数参数的 tool_calls 对象。

概述

本笔记本包含以下 2 个部分:

  • 如何生成函数参数: 指定一组函数并使用 API 生成函数参数。
  • 如何使用模型生成的参数调用函数: 通过实际使用模型生成的参数执行函数来完成闭环。

如何生成函数参数

!pip install scipy --quiet
!pip install tenacity --quiet
!pip install tiktoken --quiet
!pip install termcolor --quiet
!pip install openai --quiet
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()

实用工具

首先,我们定义一些用于调用聊天补全 API 和维护和跟踪对话状态的实用工具。

@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("无法生成聊天补全响应")
        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 进行交互。我们将这些函数规范传递给聊天补全 API,以生成符合规范的函数参数。

tools = [
    {
        "type": "function",
        "function": {
            "name": "get_current_weather",
            "description": "获取当前天气",
            "parameters": {
                "type": "object",
                "properties": {
                    "location": {
                        "type": "string",
                        "description": "城市和州,例如旧金山, 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": "城市和州,例如旧金山, 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)
assistant_message
ChatCompletionMessage(content='好的,请告诉我您的城市和州名。', 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)
assistant_message
ChatCompletionMessage(content=None, role='assistant', function_call=None, tool_calls=[ChatCompletionMessageToolCall(id='call_xb7QwwNnx90LkmhtlW0YrgP2', 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)
assistant_message
ChatCompletionMessage(content='为了向您提供苏格兰格拉斯哥的天气预报,请问您需要预报多少天?', role='assistant', function_call=None, tool_calls=None)

模型再次要求我们澄清,因为它还没有足够的信息。在这种情况下,它已经知道了预报的地点,但需要知道预报需要多少天。

messages.append({"role": "user", "content": "5天"})
chat_response = chat_completion_request(
    messages, tools=tools
)
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_34PBraFdNN6KR95uD5rHF8Aw', function=Function(arguments='{"location":"Glasgow, Scotland","format":"celsius","num_days":5}', name='get_n_day_weather_forecast'), type='function')]))

强制使用特定函数或不使用函数

我们可以强制模型使用特定函数,例如 get_n_day_weather_forecast,方法是使用 function_call 参数。通过这样做,我们强制模型对其如何使用该函数做出假设。

# 在此单元格中,我们强制模型使用 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"}}
)
chat_response.choices[0].message
ChatCompletionMessage(content=None, role='assistant', function_call=None, tool_calls=[ChatCompletionMessageToolCall(id='call_FImGxrLowOAOszCaaQqQWmEN', function=Function(arguments='{"location":"Toronto, Canada","format":"celsius","num_days":7}', 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
)
chat_response.choices[0].message
ChatCompletionMessage(content=None, role='assistant', function_call=None, tool_calls=[ChatCompletionMessageToolCall(id='call_n84kYFqjNFDPNGDEnjnrd2KC', function=Function(arguments='{"location": "Toronto, Canada", "format": "celsius"}', name='get_current_weather'), type='function'), ChatCompletionMessageToolCall(id='call_AEs3AFhJc9pn42hWSbHTaIDh', 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"
)
chat_response.choices[0].message
ChatCompletionMessage(content="好的,我将获取加拿大多伦多当前的天气(摄氏度)。", role='assistant', function_call=None, tool_calls=None)

并行函数调用

较新的模型,如 gpt-4o 或 gpt-3.5-turbo,可以在一个回合中调用多个函数。

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
assistant_message
[ChatCompletionMessageToolCall(id='call_ObhLiJwaHwc3U1KyB4Pdpx8y', function=Function(arguments='{"location": "San Francisco, CA", "format": "fahrenheit", "num_days": 4}', name='get_n_day_weather_forecast'), type='function'),
 ChatCompletionMessageToolCall(id='call_5YRgeZ0MGBMFKE3hZiLouwg7', function=Function(arguments='{"location": "Glasgow, SCT", "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_name']}\n列: {', '.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
使用聊天补全 API 调用函数的步骤:

步骤 1:使用可能导致模型选择工具的内容提示模型。工具的描述,例如函数名称和签名,在“工具”列表中定义,并在 API 调用中传递给模型。如果选择,函数名称和参数将包含在响应中。

步骤 2:以编程方式检查模型是否要调用函数。如果为 true,请继续执行步骤 3。

步骤 3:从响应中提取函数名称和参数,并使用参数调用函数。将结果附加到消息列表中。

步骤 4:使用包含消息列表的聊天补全 API 调用,以获取响应。

# 步骤 #1:使用可能导致函数调用的内容进行提示。在这种情况下,模型可以识别用户请求的信息可能存在于传递给模型的工具描述中的数据库架构中。 
messages = [{
    "role":"user", 
    "content": "哪个专辑的曲目最多?"
}]

response = client.chat.completions.create(
    model='gpt-4o', 
    messages=messages, 
    tools= tools, 
    tool_choice="auto"
)

# 将消息附加到 messages 列表
response_message = response.choices[0].message 
messages.append(response_message)

print(response_message)
ChatCompletionMessage(content=None, role='assistant', function_call=None, tool_calls=[ChatCompletionMessageToolCall(id='call_wDN8uLjq2ofuU6rVx1k8Gw0e', function=Function(arguments='{"query":"SELECT Album.Title, COUNT(Track.TrackId) AS TrackCount FROM Album INNER JOIN Track ON Album.AlbumId = Track.AlbumId GROUP BY Album.Title ORDER BY TrackCount DESC LIMIT 1;"}', name='ask_database'), type='function')])
# 步骤 2:确定模型响应是否包含工具调用。   
tool_calls = response_message.tool_calls
if tool_calls:
    # 如果为 true,模型将返回要调用的工具/函数的名称和参数  
    tool_call_id = tool_calls[0].id
    tool_function_name = tool_calls[0].function.name
    tool_query_string = json.loads(tool_calls[0].function.arguments)['query']

    # 步骤 3:调用函数并检索结果。将结果附加到消息列表中。      
    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:使用附加了函数响应的消息列表调用聊天补全 API
        # 注意:角色为“tool”的消息必须是对前面带有“tool_calls”的消息的响应
        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) 
曲目最多的专辑是“Greatest Hits”,包含 57 首曲目。

后续步骤

请参阅我们的另一个 笔记本,其中演示了如何使用聊天补全 API 和函数进行知识检索,以与知识库进行对话式交互。