十、基于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

  1. 获取函数信息
    这里使用inspect模块获取函数的注释信息
import inspect

print(inspect.getdoc(get_weather))
function_description = inspect.getdoc(get_weather)
获取天气的函数,该函数实现了如何获取天气信息
:param location: 必要参数,表示地理位置,用字符串表
:return: 返回天气信息,用字符串表示
  1. 使用大模型生成生成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
}

这里可以看到,返回结果里信息很多,但是还是可以看到我们需要的部分

  1. 优化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']}}}

到这里格式已经和我们手写的一致了,可以拿去测试验证下

  1. 验证
    这里直接拿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过程封装到函数

  1. 新增一个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']}}}]
  1. 测试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')])
  1. 函数封装
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格式是不需要变动的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

偷学技术的梁胖胖yo

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值