ChatGPT中的function calling入门
介绍
function calling到底是什么东西?下面是来自官方文档的介绍
In an API call, you can describe functions and have the model intelligently choose to output a JSON object containing arguments to call one or many functions. The Chat Completions API does not call the function; instead, the model generates JSON that you can use to call the function in your code.
翻译成中文大概是这么个意思:
你可以定义一组函数工具,并发送给AI。然后在对话过程中,AI会根据你的意思判断是否要调用某个函数工具。假如需要调用,那么AI会反馈给你一组调用信息,其中包含要调用的函数名称和函数参数的值。假如不需要调用,那么信息为空。
这里的重点是: AI并不会真正地执行代码,它只会返回一组json格式的信息,里面包含了函数名称和函数参数的值,用户用这些信息去调用真正的函数。
开始动手
首先重申一下几个基本概念和说明一下基本的步骤:
- 描述给AI的函数:这些函数并不是真正的代码函数,而是一推内容描述(在python中是一个列表,列表中的每一字典代表一组函数)。
- 代码函数:与上述函数对应的真正的代码,比如你为AI描述了一个“get_weather”函数,那么相对应的你也要新建一个“def get_weather()”函数。
- AI调用函数:返回函数函数信息的json信息。
- 调用:当你得到AI返回的函数信息后,你拿着这些信息去调用真正的代码。
- 你把调用的结果再反馈给AI,让他继续生成内容。
下面就拿官方文档中的例子来说明:
我们的目的是让AI调用你定义的天气查询函数,你会询问AI某地的天气情况,AI接受到你的询问,结合自己工具库里的函数,会想要去调用这个天气查询函数,但是它不会直接调用到我们定义的python函数,但它会返回这个函数名称以及相关参数的值。我们用这些“名称”、“参数”去调用python函数。
本文入门的例子基于官方文档做了一些改编,本文的函数是可以查询到真实天气的,而不像官方例子只是一个示范不惧实际功能。
我们会按照以下步骤来编写一个fucntion calling的完整示例:
- 描述一个供AI使用的函数工具
- 使用openweather的api实现天气查询(免费版本即可)
- 整合代码,实现function calling功能
第一步: 使用python的list和dict描述一个函数工具,后续会在使用openai的api中tools同名参数传递给AI,具体代码如下:
tools = [ { "type": "function", "function": { "name": "get_current_weather", "description": "获取所在位置的天气情况", "parameters": { "type": "object", # properties 即可以理解为函数的参数 "properties": { # 天气的位置 "location": { "type": "string", "description": "城市的名称, e.g. San Francisco, CA", }, # 天气的单位,符合openweather api的参数规范 "unit": { "type": "string", "enum": ["metric", "imperial"], }, }, # 只有位置是必须的参数。 "required": ["location"], }, }, } ]
第二步:写一个与第一步中所描述的函数对应的python函数,函数名可以不一样(此处用的是一样的),因为只需要在下面做函数名映射时,保持对应即可。
def get_current_weather(location, unit="metric"): # open weather的api key,需要去 https://openweathermap.org/ 注册并申请 OPEN_WEAHTER_API_KEY = "YOUR_OPENWEATHER_API_KEY" # 首先根据 location 参数获得对应城市的地理坐标值 resp_geo = requests.get( f"http://api.openweathermap.org/geo/1.0/direct?q={location}&limit=5&appid={OPEN_WEAHTER_API_KEY}" ) if resp_geo.status_code == 200: geo_data = resp_geo.json()[0] lat, lon = geo_data.get("lat"), geo_data.get("lon") print(lat, lon) if lat and lon: resp_weather = requests.get( f"https://api.openweathermap.org/data/2.5/weather?lat={lat}&lon={lon}&appid={OPEN_WEAHTER_API_KEY}&units={unit}" ) if resp_weather.status_code == 200: print(resp_weather.json()) # 返回的结果最好是json格式,便于ai理解 return json.dumps({ "天气": resp_weather.json().get("weather")[0].get("main"), "体感温度": resp_weather.json() .get("main") .get("feels_like"), }) else: print("failed to fetch the weather data") return "failed to fetch the weather data" else: print(f"illegal lat or lon:{lat}:{lon}") return f"illegal lat or lon:{lat}:{lon}" else: print("failed to fetch the geo data") return "failed to fetch the geo data"
第三步:整合
整个流程如下:新建一个对话,同时将tools传递给openai的api。向对话传入一条信息询问目前的天气。通过AI的反馈,得到它调用的函数的名称和参数值,用这些信息去调用我们真正的函数get_current_weather(location, unit="metric")
,再将调用函数的结果反馈给AI,AI根据反馈再次生成文本内容,告诉我们现在的天气。下面时整合后的代码:
from openai import OpenAI import json import requests from rich import print tools = [ { "type": "function", "function": { "name": "get_current_weather", "description": "获取所在位置的天气情况", "parameters": { "type": "object", # properties 即可以理解为函数的参数 "properties": { # 天气的位置 "location": { "type": "string", "description": "城市的名称, e.g. San Francisco, CA", }, # 天气的单位,符合openweather api的参数规范 "unit": { "type": "string", "enum": ["metric", "imperial"], }, }, # 只有位置是必须的参数。 "required": ["location"], }, }, } ] def get_current_weather(location, unit="metric"): # open weather的api key,需要去 https://openweathermap.org/ 注册并申请 OPEN_WEAHTER_API_KEY = "YOUR_OPENWEATHER_API_KEY" # 首先根据 location 参数获得对应城市的地理坐标值 resp_geo = requests.get( f"http://api.openweathermap.org/geo/1.0/direct?q={location}&limit=5&appid={OPEN_WEAHTER_API_KEY}" ) if resp_geo.status_code == 200: geo_data = resp_geo.json()[0] lat, lon = geo_data.get("lat"), geo_data.get("lon") print(lat, lon) if lat and lon: resp_weather = requests.get( f"https://api.openweathermap.org/data/2.5/weather?lat={lat}&lon={lon}&appid={OPEN_WEAHTER_API_KEY}&units={unit}" ) if resp_weather.status_code == 200: print(resp_weather.json()) # 返回的结果最好是json格式,便于ai理解 return json.dumps({ "天气": resp_weather.json().get("weather")[0].get("main"), "体感温度": resp_weather.json() .get("main") .get("feels_like"), }) else: print("failed to fetch the weather data") return "failed to fetch the weather data" else: print(f"illegal lat or lon:{lat}:{lon}") return f"illegal lat or lon:{lat}:{lon}" else: print("failed to fetch the geo data") return "failed to fetch the geo data" # 新建一个openai的client client = OpenAI() # 编写一个辅助函数,其实不一定要放在函数中 def run_conversation(): # Step 1: # 与ai对话,告诉它你的诉求,让它决定是否调用函数。 # 这里因为我们非常明确的说了想知道天气,这个内容与我们对函数工具的描述:“获取所在位置的天气情况”是匹配的 # 所以这里预期ai会调用这个函数,就是说它会把函数名和参数值返回给我们 messages = [ { "role": "user", "content": "告诉我上海的天气怎么样", } ] # 执行一次对话,把我们的诉求告诉ai response = client.chat.completions.create( model="gpt-3.5-turbo-0125", messages=messages, # 把上面定义好的函数告诉ai,让他知道哪些函数工具可以用。 tools=tools, tool_choice="auto", ) # 获取对话的结果 response_message = response.choices[0].message # 获取ai反馈给我的函数调用情况 tool_calls = response_message.tool_calls # 打印测试,可忽略 print(tool_calls) # Step 2: 检查 tool calls 中是否有调用函数的信息,即ai有没有使用某些函数工具,这里预期它使用了"get_current_weather"这个函数 if tool_calls: # Step 3: 准备利用ai返回的调用信息,来调用我们真正的函数 # Note: the JSON response may not always be valid; be sure to handle errors # 把我们定义的函数放入一个字典,一边他用过函数名称来调用 available_functions = { "get_current_weather": get_current_weather, } # 把第一次对话的返回结果添加到整个对话中 messages.append( response_message ) # Step 4: 把函数调用的情况都添加到真个对话中,这里一般来说我们只调用了一次"get_current_weather" 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( location=function_args.get("location"), unit=function_args.get("unit"), ) messages.append( { "tool_call_id": tool_call.id, "role": "tool", "name": function_name, "content": function_response, } ) # extend conversation with function response second_response = client.chat.completions.create( model="gpt-3.5-turbo-0125", messages=messages, ) # get a new response from the model where it can see the function response return second_response print(run_conversation())
将执行结果打印出来会得到以下结果:
首先我们将第一次对话,即询问ai天气后反馈中的response_message.tool_calls
打印出来,可以看到其中有我们要调用的函数名和参数值,这里ai应该是自动把“北京”转换成了“Beijing”。然后我们看到在get_current_weather函数中我们将位置对应的坐标打印出来了。之后我们将从openweather获取的未经处理的天气数据也打印出来。可以看到大多数数据我们没有用到。我们只用到了其中的主要天气情况和体感温度两项。将这两个数据编写为一个json字符串,即get_current_weather函数的返回值,再将此内容反馈给ai。
最后ai结合天气数据,返回了我们需要的答案。(在这个简单的例子中,给人感觉获取天气这个行为通不通过ai都可以,我们可以直接调用openweather的api获取天气,而不必把数据传给ai再让他返回给我们文本。确实在此只是为了演示function calling的功能,没有体现ai的文本分析整合能力。)
[ ChatCompletionMessageToolCall( id='call_Q5UJMWDJQ6JWNOu3T7fqHQVT', function=Function(arguments='{"location":"Beijing","unit":"metric"}', name='get_current_weather'), type='function' ) ] geo data of Beijing:39.906217, 116.3912757 { 'coord': {'lon': 116.3913, 'lat': 39.9062}, 'weather': [{'id': 804, 'main': 'Clouds', 'description': 'overcast clouds', 'icon': '04d'}], 'base': 'stations', 'main': { 'temp': 25.94, 'feels_like': 25.01, 'temp_min': 25.94, 'temp_max': 25.94, 'pressure': 1013, 'humidity': 16, 'sea_level': 1013, 'grnd_level': 1008 }, 'visibility': 10000, 'wind': {'speed': 4.63, 'deg': 195, 'gust': 6.53}, 'clouds': {'all': 100}, 'dt': 1714728321, 'sys': {'type': 1, 'id': 9609, 'country': 'CN', 'sunrise': 1714684331, 'sunset': 1714734618}, 'timezone': 28800, 'id': 1816670, 'name': 'Beijing', 'cod': 200 } ChatCompletion( id='chatcmpl-9KjTSNKiY5L4PucKzfAI5jZ8HOWXx', choices=[ Choice( finish_reason='stop', index=0, logprobs=None, message=ChatCompletionMessage( content='北京今天的天气是多云,体感温度为25.01摄氏度。请您注意适时携带雨具。', role='assistant', function_call=None, tool_calls=None ) ) ], created=1714728390, model='gpt-3.5-turbo-0125', object='chat.completion', system_fingerprint='fp_3b956da36b', usage=CompletionUsage(completion_tokens=42, prompt_tokens=83, total_tokens=125) )
以上就是介绍ChatGPT function calling的全部内容了。
延申阅读
本文的代码示例是根据官方介绍文档改编而来,不同的是,本文利用openweather的api实现了真正的天气查询,而官方介绍文档中每次只会返回预先编好的固定内容,而不是真实天气。
官方的Cookbook文档更深入的介绍了function calling的用法,并涉及到有关数据库相关的内容,有兴趣的可以进一步阅读。
杂谈:关于function calling和JSON mode
正如介绍文档描述的那样,function calling可以智能的输出json格式的内容,与此同时还存在另一种工具叫做”JSON mode‘,如文档描述JSON mode是用来返回json格式的内容(看它的名字也能猜到),除了function calling返回的json是被用作函数调用之外,两者在使用场景以及在返回json这件事情上有什么区别呢?从网络上找到的相关线索中,有一篇回复看似来自JSON mode的开发人员,阐述了两者的关系:
(I work on JSON mode at OpenAI) JSON mode is always enabled for the generation of function arguments, so those are guaranteed to parse. JSON mode is opt in for regular messages. Note that JSON mode sadly doesn’t guarantee that the output will match your schema (though the model tries to do this and is continually getting better at it), only that it is JSON that will parse. So TL;DR is that you can use either functions or messages, and in both cases know you will get parseable JSON back :+1:
可见JSON mode可以看作是某种更通用的功能。在function calling中,JSON mode永远是启用的,而在一般的消息对话中JSON mode是可选的。显然这是因为function calling对返回结果的格式要求更严格,它需要确定的格式和数据来给用户用于调用真实的代码函数。
可以参考这篇英文文章进一步了解JSON mode。
另外前段时间Claude ai推出了自己的function calling功能,名字叫做”use tool“,从文档标题中可以看到括号里面也直接写为function calling,虽然我没有用过,但感觉应该是类似的东西。参见:Tool use (function calling)