第一时间捕获有价值的信号
本文深入解析 OpenAI Function Calling 特性,揭示 GPT-3.5 和 GPT-4 模型如何智能调用外部工具和 API,实现 AI 应用与真实世界的交互。通过代码示例,助你掌握 Function Call 的核心用法。
核心内容
OpenAI最近发布了一次更新,3.5可以支持16k的token,更新了gpt-3.5-turbo-0613 和 gpt-4-0613两个模型,同时这两个模型在chat completion的api中增加了一个叫 Function Calling 的新功能,本篇文章对其功能进行探究,并分析其作用。
Function Calling 接口说明
chat接口的messages数组除了原有的system、user、assisant,新增了role参数选项functionchat接口新增functions参数,格式为数组,意味着可以传入一组函数定义,模型将智能选择用哪个- 每个
function支持三个参数:name(函数名)、description(函数功能说明)和parameters(模型输出的数据格式说明) chat接口新增了function_call参数,默认值为none,可设置为autochat接口的function_call用于决定是否启用函数式回答。如果值为none,则不需要传入functions参数,如果值为auto,你需要提供一些函数供模型选择。只有在function_call为auto,且functions包含函数数组,且模型根据你的函数功能说明匹配到函数时,返回的message中才会包含function_call- 通过 LLM 调用函数并获取输出后,需要将结果传回给模型以生成自然语言回复(非必须)。此时,应在请求的
messages中添加一个role为assisant的message,包含function_call信息(即模型返回的数据),同时需要添加一个role为function的数据,包含本地函数执行的结果
Show Me Code
下面通过代码实践的方式进行探索
下面以费用统计应用为例,告诉AI:“今天买了一斤肉,花了多少钱”,正常思路来说,交互流程应该是这样的:
- 用户向LLM输入prompt
- AI进行智能分析
- 返回结构化的数据(每个子项是什么,数量是多少)
- 通过现实生活中的实时物价计算
- 传入LLM获得自然语言解答
import openai
import json
from enum import Enum
class BaseTool(Enum):
Bookkeeping = "record_price"
RecordingTask = "record_task"
# record_price是用来给Function Calling调用的函数,
# 这个函数接收两个必填的参数,category类目(string类型),count 数量(int类型)
def record_price(category, count):
print(category, count)
print("调用获取实时物价的 API")
return count*12
def funtion_call_conversation(message):
messages = [
{"role": "user", "content": "今天买了一斤肉,花了多少钱"}
]
response = openai.ChatCompletion.create(
model="gpt-3.5-turbo-0613",
messages = message,
temperature=0,
functions=[
{
"name": BaseTool.Bookkeeping.value,
"description": "返回物品的数量",
"parameters": {
"type": "object",
"properties": {
"category": {"type": "string","description": "类目",},
"count": {"type": "integer", "description": "数量"},
},
"required": ["category","count"],
},
}
],
function_call="auto",
)
message = response["choices"][0]["message"]
print(message)
if(message.get("function_call")):
function_name = message["function_call"]["name"]
if function_name == BaseTool.Bookkeeping.value:
arguments = json.loads(message["function_call"]["arguments"])
price = record_price(arguments.get('category'), arguments.get('count'))
messages.append(message)
messages.append({"role": "function", "name": BaseTool.Bookkeeping.value, "content": price})
print(messages)
role_function_conversation(messages)
def role_function_conversation(message):
response = openai.ChatCompletion.create(
model="gpt-3.5-turbo-0613",
messages = message,
temperature=0,
# name字段表示要调用的函数名,description表示函数描述(让 LLM 读得懂的函数说明)
# paramters是一个符合JSON Schema格式的对象,用来描述这个函数的入参信息
functions=[
{
"name": BaseTool.Bookkeeping.value,
"description": "返回物品的数量",
"parameters": {
"type": "object",
"properties": {
"category": {"type": "string","description": "类目",},
"count": {"type": "integer", "description": "数量"},
},
"required": ["category","count"],
},
}
],
function_call="auto",
)
message = response["choices"][0]["message"].get("content")
print(message)
if __name__ == "__main__":
funtion_call_conversation()
执行结果解读
- LLM 分析后命中了函数签名描述,就会返回给我们 function_call 这个字段以及函数签名中我们预定义的字段信息:
- category返回给了类目是猪肉
- count返回数量是 1 (斤)
{ "role": "assistant", "content": null, "function_call": { "name": "record_price", "arguments": "{\n \"category\": \"\u8089\",\n \"count\": 1\n}" } }
- 拿到参数,调用 record_price 进行费用统计的操作即可;如果没有命中函数签名描述,就不会返回function_call字段,也就不需要进行任何操作:
if(message.get("function_call")):
function_name = message["function_call"]["name"]
if function_name == BaseTool.Bookkeeping.value:
arguments = json.loads(message["function_call"]["arguments"])
price = record_price(arguments.get('category'), arguments.get('price'))
- 通过外部实时价格 API 获取到实时物价信息,将其作为
role为function的数据,组合成最终的 messages列表,再次传入模型接口:
[{'role': 'user', 'content': '今天买了一斤肉,花了多少钱'}, <OpenAIObject at 0x103365540> JSON: {
"role": "assistant",
"content": null,
"function_call": {
"name": "record_price",
"arguments": "{\n \"category\": \"\u8089\",\n \"count\": 1\n}"
}
}, {'role': 'function', 'name': 'record_price', 'content': '12'}]
- 最后返回预期可控的结果
今天买了一斤肉,花了12元。
存在问题
- 函数描述是会被计入 token 的
- 潜在的风险:分析不准确,出现无法命中函数的情况