跳转到正文
莫尔索随笔
返回

OpenAI Function Calling 详解:GPT 模型如何调用外部工具

预计 3 分钟

第一时间捕获有价值的信号

本文深入解析 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 接口说明

  1. chat接口的messages数组除了原有的systemuserassisant,新增了role参数选项function
  2. chat接口新增functions参数,格式为数组,意味着可以传入一组函数定义,模型将智能选择用哪个
  3. 每个function支持三个参数:name(函数名)、description(函数功能说明)和parameters(模型输出的数据格式说明)
  4. chat接口新增了function_call参数,默认值为none,可设置为auto
  5. chat接口的function_call用于决定是否启用函数式回答。如果值为none,则不需要传入functions参数,如果值为auto,你需要提供一些函数供模型选择。只有在function_callauto,且functions包含函数数组,且模型根据你的函数功能说明匹配到函数时,返回的message中才会包含function_call
  6. 通过 LLM 调用函数并获取输出后,需要将结果传回给模型以生成自然语言回复(非必须)。此时,应在请求的messages中添加一个roleassisantmessage,包含function_call信息(即模型返回的数据),同时需要添加一个rolefunction的数据,包含本地函数执行的结果

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()

执行结果解读

  1. LLM 分析后命中了函数签名描述,就会返回给我们 function_call 这个字段以及函数签名中我们预定义的字段信息:
  • category返回给了类目是猪肉
  • count返回数量是 1 (斤)
    {
      "role": "assistant",
      "content": null,
      "function_call": {
        "name": "record_price",
        "arguments": "{\n  \"category\": \"\u8089\",\n  \"count\": 1\n}"
      }
    }
  1. 拿到参数,调用 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'))
  1. 通过外部实时价格 API 获取到实时物价信息,将其作为rolefunction的数据,组合成最终的 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'}]
  1. 最后返回预期可控的结果 今天买了一斤肉,花了12元。

存在问题

  1. 函数描述是会被计入 token 的
  2. 潜在的风险:分析不准确,出现无法命中函数的情况

参考