前言

好家伙,OpenAI 这帮人真的是不让人睡觉。6月13号大半夜的,我又收到了那封熟悉的邮件——「OpenAI API Updates」。点开一看,好嘛,GPT-4 和 GPT-3.5 Turbo 都更新了,还降了价,但这都不是重点。重点是一个叫 Function Calling 的新功能。

之前写了那篇 ChatGPT API 初体验,当时就吐槽过 ChatGPT 不能直接调外部接口,输出的格式也不好控制。现在 OpenAI 直接给你整了个大活——让模型学会「打电话」叫函数了。

二话不说,赶紧来试一波。

Function Calling 到底是啥?

说人话就是:你给 ChatGPT 一堆函数的「说明书」,它看完之后,当你问它问题的时候,它会告诉你:「兄弟,你应该调这个函数,参数给你准备好了」。

举个栗子🌰:

  • 你问:「今天北京天气咋样?」
  • ChatGPT 回你:「你去调 get_current_weather 这个函数吧,location 填 北京
  • 你拿到参数,调你的天气 API,把结果扔回给 ChatGPT
  • ChatGPT 再用自然语言把天气告诉你

是不是有种「我全都要」的感觉?模型负责理解意图和生成参数,你负责执行实际操作。这个设计真的妙,模型不用真的去调 API,它只需要「指挥」你去调就行。

就像 JoJo 里的替身使者一样——模型是本体,你写的应用就是它的替身,它指哪你打哪(笑)。

支持的模型

Function Calling 目前支持以下模型(2023年6月):

  • gpt-4-0613
  • gpt-3.5-turbo-0613

注意:旧版模型(gpt-3.5-turbo-0301、gpt-4-0314 等)不支持 Function Calling。你需要明确指定 0613 版本的模型,或者使用会自动升级到新版的别名 gpt-3.5-turbo / gpt-4

实战:用天气查询来跑一遍

说了这么多,直接上代码。我们来实现一个最经典的例子:让 ChatGPT 帮我们查天气。

第一步:定义函数

先写一个假的天气查询函数(实际项目里你换成真实 API 就行):

1
2
3
4
5
6
7
8
9
10
11
12
import json

def get_current_weather(location: str, unit: str = "celsius"):
"""获取指定城市的当前天气(模拟数据)"""
# 这里只是演示,实际项目中请调用真实天气 API
weather_data = {
"location": location,
"temperature": "28",
"unit": unit,
"forecast": ["晴天", "微风"],
}
return json.dumps(weather_data, ensure_ascii=False)

第二步:告诉模型有哪些函数可用

这里就是 Function Calling 的核心了。你需要用 JSON Schema 的格式把函数的「说明书」传给 OpenAI:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import openai

openai.api_key = "sk-你的key"

functions = [
{
"name": "get_current_weather",
"description": "获取指定城市的当前天气",
"parameters": {
"type": "object",
"properties": {
"location": {
"type": "string",
"description": "城市名,例如:北京、上海",
},
"unit": {
"type": "string",
"enum": ["celsius", "fahrenheit"],
},
},
"required": ["location"],
},
}
]

第三步:发送请求,处理 Function Call

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
messages = [
{"role": "user", "content": "今天北京天气咋样?"}
]

response = openai.ChatCompletion.create(
model="gpt-3.5-turbo-0613",
messages=messages,
functions=functions,
function_call="auto", # 让模型自己决定要不要调函数
)

response_message = response["choices"][0]["message"]

# 判断模型是否想调用函数
if response_message.get("function_call"):
function_name = response_message["function_call"]["name"]
function_args = json.loads(response_message["function_call"]["arguments"])

print(f"模型想调用函数: {function_name}")
print(f"参数: {function_args}")

# 调用对应的函数
if function_name == "get_current_weather":
function_response = get_current_weather(
location=function_args.get("location"),
unit=function_args.get("unit", "celsius"),
)

这时候模型的返回大概长这样:

1
2
3
4
5
6
7
8
{
"role": "assistant",
"content": null,
"function_call": {
"name": "get_current_weather",
"arguments": "{\"location\": \"北京\", \"unit\": \"celsius\"}"
}
}

注意 {% emp content 是 null %},说明模型没有直接回答用户,而是选择「让函数来回答」。

第四步:把函数结果扔回给模型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 把函数结果加到对话历史里
messages.append(response_message) # 模型的 function_call 消息
messages.append(
{
"role": "function",
"name": function_name,
"content": function_response,
}
)

# 再次请求,让模型根据函数结果生成自然语言回答
second_response = openai.ChatCompletion.create(
model="gpt-3.5-turbo-0613",
messages=messages,
)

print(second_response["choices"][0]["message"]["content"])

最终输出类似:

北京今天天气晴朗,温度 28°C,伴有微风,非常适合外出活动哦!

整个流程就是:用户提问 → 模型说「调这个函数」→ 你调函数拿到结果 → 把结果交给模型 → 模型生成最终回答

完整代码

把上面的代码整合一下,完整可运行版本:

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
import json
import openai

openai.api_key = "sk-你的key"

def get_current_weather(location, unit="celsius"):
weather_data = {
"location": location,
"temperature": "28",
"unit": unit,
"forecast": ["晴天", "微风"],
}
return json.dumps(weather_data, ensure_ascii=False)

functions = [
{
"name": "get_current_weather",
"description": "获取指定城市的当前天气",
"parameters": {
"type": "object",
"properties": {
"location": {"type": "string", "description": "城市名"},
"unit": {"type": "string", "enum": ["celsius", "fahrenheit"]},
},
"required": ["location"],
},
}
]

messages = [{"role": "user", "content": "今天北京天气咋样?"}]

response = openai.ChatCompletion.create(
model="gpt-3.5-turbo-0613",
messages=messages,
functions=functions,
function_call="auto",
)

response_message = response["choices"][0]["message"]

if response_message.get("function_call"):
function_name = response_message["function_call"]["name"]
function_args = json.loads(response_message["function_call"]["arguments"])

if function_name == "get_current_weather":
function_response = get_current_weather(
location=function_args.get("location"),
unit=function_args.get("unit", "celsius"),
)

messages.append(response_message)
messages.append({
"role": "function",
"name": function_name,
"content": function_response,
})

second_response = openai.ChatCompletion.create(
model="gpt-3.5-turbo-0613",
messages=messages,
)
print(second_response["choices"][0]["message"]["content"])

踩坑注意事项

用了之后发现几个坑,给大家提个醒:

1. function name 的限制

函数名只能包含 a-zA-Z0-9、下划线 _ 和连字符 -,最长 64 个字符。千万别用中文、空格或者特殊符号,不然直接报错给你看。

2. 参数必须是合法的 JSON Schema

你的 parameters 字段必须是标准的 JSON Schema 格式。别想着随便写个对象糊弄过去,OpenAI 会校验的。properties 里的每个字段都需要有 typerequired 数组也要配好。

3. 模型不一定会调函数

function_call 参数有三个选项:

  • "auto":模型自己决定(默认值)
  • "none":强制不调函数
  • {"name": "xxx"}:强制调指定的函数

如果你设了 "auto",模型可能会觉得不需要调函数就直接回答了。这不一定是 bug,可能是你的 prompt 没引导好,或者模型觉得能直接回答。

如果你想让模型「必须」调函数,可以用 function_call: {"name": "get_current_weather"} 来强制指定。但要注意,这样即使用户的问题跟天气无关,模型也会硬着头皮调这个函数,参数可能是瞎编的。

4. arguments 是字符串不是对象

这是一个很坑的地方!模型返回的 `function_call.arguments` 是一个 JSON 字符串,不是直接的对象。你需要自己 json.loads() 解析一下。别问我怎么知道的,我 debug 了半小时才发现这个坑。

5. content 为 null 是正常的

当模型决定调函数时,message.content 会是 null。别手贱去 .get("content") 就直接用了,记得先检查 function_call 字段。

6. 费用问题

Function Calling 整个流程至少要发两次请求(一次让模型决定调函数,一次把结果扔回去生成回答),所以 token 消耗会翻倍。如果模型判断失误多调了一次,那就是三倍。穷哥们悠着点用 💸

其他好玩的玩法

除了查天气,Function Calling 还能做很多事:

  • 查数据库:把 SQL 查询封装成函数,让模型帮你写 SQL 并执行
  • 发邮件:告诉模型收件人和内容,它帮你调发送函数
  • 控制智能家居:「帮我把客厅灯关了」→ 调用智能家居 API
  • 结构化数据提取:从一段文字里提取 JSON 格式的信息

说实话这个功能相当于给 ChatGPT 装上了手脚。之前它只会说「我无法访问互联网」,现在它能通过你的代码间接操控一切了。这不就是 EVA 的初号机配上阳电子炮的感觉吗?(二次元浓度拉满了属于是)

总结

Function Calling 是 OpenAI 在 2023 年 6 月放出的一个非常实用的功能更新。它解决了之前 LLM 最大的痛点之一——模型只能聊天,不能干活。虽然整个流程还是需要你写代码来中转,但这已经让 ChatGPT 从一个「嘴强王者」变成了能真正执行任务的「实干家」。

目前这个功能还比较新,后面肯定还会有更多玩法被社区挖掘出来。建议大家先跑通这个 demo,然后根据自己项目的需求去扩展。有啥问题评论区见~

参考资料