• 如何使用ChatGPT调用自定义函数,模拟实现预约挂号及查询功能
  • 发布于 2个月前
  • 215 热度
    0 评论
  • 耀国
  • 0 粉丝 31 篇博客
  •   

Function calling是ChatGPT的新功能,它允许我们在API调用中描述特定函数的特性,模型会根据我们的描述,智能地决定是否生成一个包含函数参数的JSON对象作为输出。这样我们就可以用ChatGPT和其他的工具或API进行交互,实现更多的功能。

例如:

1.将自然语言转换为数据库查询和API调用。我们可以使用这个特性来将普通语言转换为内部API调用,来回答问题或提供更好的选项。
2.用自然语言获取网络上的数据,或查询自己的数据库等。我们可以在函数中做很多事情,只要符合自定义的JSON格式。

本文将介绍如何使用ChatGPT调用自定义函数,模拟实现预约挂号及查询功能。

Function Call API介绍
要使用function_call功能,我们需要在/v1/chat/completions端点中添加两个新的API参数:functions和function_call。functions参数可以让我们用JSON Schema来描述想要调用的函数的名称、参数和返回值。function_call参数可以让我们指定想要模型调用的函数的名称。
//文档地址
https://platform.openai.com/docs/guides/gpt/function-calling
//官网示例
https://github.com/openai/openai-cookbook/blob/main/examples/How_to_call_functions_with_chat_models.ipynb
例如我们想要模型调用一个名为get_current_weather的函数,来获取某个地点的当前天气情况,可以这样设置functions参数:
{
  "get_current_weather": {
    "type": "object",
    "properties": {
      "location": {
        "type": "string",
        "description": "地点,比如:北京、上海"
      },
      "unit": {
        "type": "string",
        "enum": ["摄氏度", "华氏度"]
      }
    },
    "required": ["location", "unit"]
  }
}
这段JSON Schema描述的意思是:
get_current_weather函数有两个参数:location和unit;
location是一个字符串类型的属性,它表示要查询天气的地点,比如“北京”或“纽约”;
unit是一个字符串类型的属性,它表示要查询天气的单位,只能是“摄氏度”或“华氏度”;
location和unit都是必须的属性,不能缺少或为空。

然后,我们还可以设置function_call参数:
{
  "name": "get_current_weather"
}
这样,当向模型发送一个类似于“今天北京的天气如何?”的输入时,模型就会尝试生成一个符合get_current_weather函数签名的JSON对象作为输出,比如:
{
  "location": "北京",
  "unit": "摄氏度"
}
当然,如果我们不添加function_call参数也可以,模型会根据我们的输入和函数描述,自动判断是否需要调用函数,以及调用哪个函数。但是这样可能会降低准确性和可靠性,所以建议尽量明确地告诉模型你想要它做什么。

function_call参数支持三种取值:
function_call: {“name”: “<insert-function-name>”} 表示让模型调用我们指定的函数,将<insert-function-name>替换为想要调用的函数的名称,比如"get_current_weather";
function_call: “none” 表示不想要模型调用任何函数,只想要它生成一个普通的文本输出,比如一句话或一个段落;
function_call: “auto” 表示让模型根据我们的输入和函数描述自动判断是否需要调用函数,以及调用哪一个函数。

经过上面的介绍,你应该大体了解了如何利用ChatGPT进行聊天交互,让它帮助我们实现预约挂号和查询的功能了吧。

挂号及挂号查询功能
为了方便演示,我创建了一个新的项目ChatGPT.Demo5,它的代码和ChatGPT.Demo4一样。

一、本地函数定义
我们先定义两个对象,RegistrationStatus表示挂号状态,RegistrationInfo表示挂号信息。
public class RegistrationStatus
{
    /// <summary>
    /// 挂号状态
    /// </summary>
    public bool Status { get; set; }
    /// <summary>
    /// 状态说明
    /// </summary>
    public string Message { get; set; }
    /// <summary>
    /// 挂号信息
    /// </summary>
    public RegistrationInfo RegistrationInfo { get; set; }
}

public class RegistrationInfo
{
    /// <summary>
    /// 序号
    /// </summary>
    public int QueueNumber { get; set; }
    /// <summary>
    /// 手机号码
    /// </summary>
    public string PhoneNumber { get; set; }
    /// <summary>
    /// 挂号时间
    /// </summary>
    public DateOnly RegistrationTime { get; set; }
    /// <summary>
    /// 提交时间
    /// </summary>
    public DateTime SubmissionTime { get; set; }
}
1、Register(挂号)
Register方法接受用户的电话号码和预约日期两个参数,然后检测是否存在相同的挂号信息。如果有,则返回失败。如果没有,则保存挂号信息并返回。
// 堆代码 duidaima.com
//挂号
public static string Register(string phoneNumber, DateOnly registrationTime)
{
    if (_registrationInfos.Any(m => m.PhoneNumber == phoneNumber && m.RegistrationTime == registrationTime))
        return JsonSerializer.Serialize(new RegistrationStatus
        {
            Status = false,
            Message = "请勿重复挂号",
        });

    var registrationInfo = new RegistrationInfo
    {
        QueueNumber = ++_queueNumber,
        PhoneNumber = phoneNumber,
        RegistrationTime = registrationTime,
        SubmissionTime = DateTime.Now
    };

    _registrationInfos.Add(registrationInfo);
    // 获取给定位置的当前天气信息
    return JsonSerializer.Serialize(new RegistrationStatus
    {
        Status = true,
        Message = "挂号成功",
        RegistrationInfo = registrationInfo
    });
}

2、Query(挂号查询)
Query方法接受用户的电话号码和预约日期两个参数,在挂号记录中查找是否有匹配的信息。如果有,则返回挂号信息,如果没有,则返回失败的信息。
//查询
public static string Query(string phoneNumber, DateOnly registrationTime)
{
    var registrationInfo = _registrationInfos.FirstOrDefault(m => m.PhoneNumber == phoneNumber && m.RegistrationTime == registrationTime);
    var registrationStatus = new RegistrationStatus
    {
        Status = registrationInfo != null,
        Message = registrationInfo != null ? "查询到挂号信息" : "未查询到挂号信息",
        RegistrationInfo = registrationInfo
    };
    return JsonSerializer.Serialize(registrationStatus);
}
为了方便调用,我们定义一个 AvailableFunctions 变量来存储函数名和函数委托。

二、JSON Schema编写

我们使用 Betalgo.OpenAI 提供的语法创建 JSON Schema,FunctionDefinition 和 FunctionParameters 分别表示函数和参数的信息,FunctionParameterPropertyValue 用于描述参数详情。注意,这里需要将 Betalgo.OpenAI 库更新到最新预览版本。
Install-Package Betalgo.OpenAI -Version 7.1.2-beta

1、挂号的JSON Schema编写
new FunctionDefinition
{
    Name = "gpt_register",
    Description = "通过指定的手机号码和日期进行挂号",
    Parameters = new FunctionParameters
    {
        Type = "object",
        Properties = new Dictionary<string, FunctionParameterPropertyValue>
        {
            {
                "phoneNumber",new FunctionParameterPropertyValue
                {
                    Type = "string",
                    Description = "挂号使用的手机号码"
                }
            },
            {
                "registrationTime" ,new FunctionParameterPropertyValue
                {
                    Type = "string",
                    Description = "挂号的日期,比如:明天",
                    Enum = new[] { "今天","明天","后天" }
                }
            }
        },
        Required = new[] { "phoneNumber", "registrationTime" }
    },
}
我们定义了一个名为 gpt_register 的函数,它的功能是通过指定的手机号码和日期进行挂号。phoneNumber 参数的类型是字符串,它表示挂号使用的手机号码。registrationTime 参数的类型也是字符串,它表示挂号的日期,并且它只能取三个值:今天、明天或后天。这两个参数都是必须提供的,否则函数无法执行。

2、挂号查询的JSON Schema编写
new FunctionDefinition
{
    Name = "gpt_query",
    Description = "根据手机号码查询挂号信息",
    Parameters = new FunctionParameters
    {
        Type = "object",
        Properties = new Dictionary<string, FunctionParameterPropertyValue>
    {
        {
            "phoneNumber",new FunctionParameterPropertyValue
            {
                Type = "string",
                Description = "要查询的手机号码"
            }
        },
        {
            "registrationTime" ,new FunctionParameterPropertyValue
            {
                Type = "string",
                Description = "挂号的日期,比如:明天",
                Enum = new[] { "今天","明天","后天" }
            }
        }
    },
        Required = new[] { "phoneNumber", "registrationTime" }
    }
}
我们定义了一个名为 gpt_query 的函数,它的功能是根据手机号码及日期查询挂号信息。phoneNumber 是一个字符串类型,表示要查询的手机号码。registrationTime 也是一个字符串类型,表示挂号的日期。这个参数只能取三个值:今天,明天,或后天。这两个参数都是必须提供的,否则函数无法执行。

三、配置ChatGPT调用本地函数
打开Controllers/ChatController.cs文件,修改Input方法,在CreateCompletionAsStream方法中加入functions参数,同时将模型指定为Gpt_3_5_Turbo_0613,为了方便调用,我们将整个参数对象独立出来。

接着将ChatGPT的响应结果处理修改为下面的方式:
await foreach (var completion in completionResult)
{
    if (cancellationToken.IsCancellationRequested)
        break;
    // 堆代码 duidaima.com
    if (completion.Successful)
    {
        var responseMessage = completion.Choices.First().Message;
        if (responseMessage.FunctionCall != null)
        {
            var functionName = responseMessage.FunctionCall.Name;
            var functionArgs = JsonSerializer.Deserialize<Dictionary<string, string>>(responseMessage.FunctionCall.Arguments);
            var functionToCall = ChatGPTFunctionCalling.AvailableFunctions[functionName];

            var registrationTime = functionArgs.GetValueOrDefault("registrationTime") switch
{
                "后天" => DateOnly.FromDateTime(DateTime.Now.AddDays(2)),
                "明天" => DateOnly.FromDateTime(DateTime.Now.AddDays(1)),
                _ => DateOnly.FromDateTime(DateTime.Now.Date),
            };
            var functionResponse = functionToCall(functionArgs.GetValueOrDefault("phoneNumber"), registrationTime);

            chatCompletionCreateRequest.Messages.Add(ChatMessage.FromFunction(functionResponse, functionName));

            var completionTwo = await _openAiService.ChatCompletion.CreateCompletion(
                new ChatCompletionCreateRequest
                {
                    Messages = chatCompletionCreateRequest.Messages,
                    Model = OpenAI.ObjectModels.Models.Gpt_3_5_Turbo_0613
                }, cancellationToken: cancellationToken);


            if (completionTwo.Successful)
            {
                await Response.WriteAsync(completionTwo.Choices.First().Message.Content ?? "", cancellationToken);
                await Response.Body.FlushAsync(cancellationToken);
            }
            else
            {
                if (completionTwo.Error == null)
                    throw new Exception("Unknown Error");

                await Response.WriteAsync($"{completionTwo.Error.Code}: {completionTwo.Error.Message}");
                await Response.Body.FlushAsync();
            }

        }
        else
        {
            await Response.WriteAsync(responseMessage.Content ?? "");
            await Response.Body.FlushAsync();
        }
    }
    else
    {
        if (completion.Error == null)
            throw new Exception("Unknown Error");

        await Response.WriteAsync($"{completion.Error.Code}: {completion.Error.Message}");
        await Response.Body.FlushAsync();
    }
}
整个调用过程可以分为以下几个步骤:
首先,通过foreach遍历获取ChatGPT的回复信息ChatMessage,它包含了Content和FunctionCall两个属性。Content属性是一个字符串类型,用于表示消息的内容;FunctionCall属性是一个ChatFunctionCall类的实例,用于表示消息中的函数调用信息;

然后,判断消息中是否有函数调用的信息,如果 FunctionCall 为 null,则直接向客户端输出消息内容;否则就表示有函数调用的信息,继续往下执行;

然后,使用 FunctionCall 的 Name 和 Arguments 属性来找出函数名和参数。Name 是一个字符串,表示函数名;Arguments 是一个字符串,表示参数的 JSON 格式。我们用 JsonSerializer.Deserialize 方法把 Arguments 转换成 C# 的 Dictionary<string, string> 类型的对象,它存放了相关的参数名和参数值。

然后,根据 Name 值在 ChatGPTFunctionCalling.AvailableFunctions 中找到对应的函数,在 Arguments 字典中找到phoneNumber和registrationTime两个对应参数,根据 registrationTime 的值,确定挂号的日期;

然后,传入参数执行函数,并将结果加入到聊天消息列表中,提交给ChatGPT重新进行编排;

最后,将ChatGPT编排的结果输出到客户端,整个调用过程结束。

我们看一下效果:

用户评论