6 月 13 日 OpenAI 官網突然發佈了重磅的 ChatGPT 更新,我相信大家都看到了 ,除了調用降本和增加更長的上下文版本外,開發者們最關心的應該還是新的函數調用能力。通過這項能力模型在需要的時候可以調用函數並生成對應的 JSON 對象作爲輸出。這使開發人員能更準確地從模型獲取結構化數據,實現從自然語言到 API 調用或數據庫查詢的轉換,也可以用於從文本中提取結構化數據。如果說之前的ChatGPT只能基於提示詞結合類似的工具來實現調用鏈提示(比如大火的python LLM自動化庫LangChain或者微軟的Semantic Kernel),那麼現在官方下場直接提供函數調用接口,無疑在穩定性(基於三方庫的函數調用主要是依賴提示詞實現,其穩定性和提示詞質量高度相關)和易用性上都上了一大臺階。
今天.NET社區相關的SDK終於更新到了新的版本可以支持函數調用。今天我們就以一個具體的案例來講一下什麼是函數調用,基於函數調用我們可以實現哪些能力,從而將一個只能聊天的大語言模型落地到更加真實的業務場景中。相關代碼demo已經更新到了github:https://github.com/sd797994/ChatgptFunctionCallDemo
現在我們假設一個業務場景,假設用戶需要詢問今天或者明天某個城市的天氣情況,並且將相關的查詢發送一封郵件到某個目標地址。在傳統的開發中,我們一般會定義一個表單,讓用戶選擇城市和日期,然後點擊發送。系統會調用天氣接口獲取到天氣,然後通過一段模板文本將佔位符中的城市+日期+天氣狀況替換成查詢的實際內容,然後發送給目標郵箱。整個流程大體如下:
在沒有chatgpt之前,以上這個簡單的操作是需要用戶通過相對規範的表單操作來實現的,就算是基於傳統的自然語言模型去處理這個任務,也需要大量的語意識別訓練來識別用戶的語意,然後根據語意去硬編碼一些過程調用才能實現以上邏輯。無論從開發的難度和用戶體驗上來講,都達不到商業化的預期的。但是現在基於大語言模型和函數調用,以上這些功能只需要單個開發者用極短的時間即可實現。因爲基於大語言模型本身的邏輯思維,它可以選擇調用哪些函數來實現功能,而我們要做的僅僅是告訴它有哪些功能而已。
接下來我們就基於實際的操作看看AI是如何實現的,首先我們更新到最新官方推薦的社區SDK版本
<PackageReference Include="Betalgo.OpenAI" Version="7.1.0-beta" />
接下來我們需要定義一個函數調用庫,這個調用庫主要的作用就是將我們的函數以表達式編譯的方式生成匿名委託緩存,同時使用反射生成ChatGpt可識別的函數命名規範,具體的調用庫實現這裏不再贅述,有興趣的可以具體看看項目下的ChatGptFunctionCallProcessor相關實現,重點是講講如何調用openai的接口實現業務功能的:
首先定義一個日期函數,用於將用戶口語化的日期轉化成真實的日期,比如“今天”,“明天”轉化成實際的日期來供天氣函數查詢。接着我們定義一個天氣查詢函數,用於查詢對應城市的某日的天氣情況,最後我們定義一個發郵件的函數,讓gpt可以通過它來發送郵件,完整的類函數定義如下:
public class FunctionCallCentner { [Description("查詢用戶希望的日期對應的真實日期")] public async Task<CommonOutput> GetDate(GetDayInput input) { await Task.CompletedTask; Console.WriteLine($"system:GetDate函數調用觸發,參數:city={input.DateType}"); return new CommonOutput() { data = new GetDayOutput { Date = DateTime.Now.AddDays(input.DateType == DateType.Yesterday ? -1 : input.DateType == DateType.Tomorrow ? 1 : input.DateType == DateType.DayAfterTomorrow ? 2 : 0).ToShortDateString(), }, Success = true }; } [Description("根據城市和真實日期獲取天氣信息")] public async Task<CommonOutput> GetWeather(GetWeatherInput input) { if (!DateTime.TryParse(input.Date, out _)) return new CommonOutput() { Success = false, message = "日期格式錯誤" }; await Task.CompletedTask; Console.WriteLine($"system:GetWeather函數調用觸發,參數:city={input.City},date={input.Date}"); return new CommonOutput() { data = new GetWeatherOutput { City = input.City, Date = input.Date, Weather = "overcast to cloudy", TemperatureRange = "22˚C-28˚C" }, Success = true }; } [Description("向目標郵箱發送電子郵件")] public async Task<CommonOutput> SendEmail(SendEmailInput input) { await Task.CompletedTask; Console.WriteLine($"system:SendEmail函數調用觸發,參數:targetemail={input.TargetEmail},content={input.Content}"); return new CommonOutput() { Success = true }; } }
這裏面的我就不做具體的實現了,只是打印了log而已。接着我們需要對這些入參和出參進行定義,如下:
public class GetDayInput { [Description("日期枚舉")] public DateType DateType { get; set; } } [JsonConverter(typeof(JsonStringEnumConverter))] public enum DateType { Yesterday, Today, Tomorrow, DayAfterTomorrow } public class GetDayOutput { public string Date { get; set; } } public class GetWeatherInput { [Description("城市名稱")] public string City { get; set; } [Description("真實日期,格式:yyyy/mm/dd")] public string Date { get; set; } } public class GetWeatherOutput: GetWeatherInput { public string Weather { get; set; } public string TemperatureRange { get; set; } } public class SendEmailInput { [Description("目標郵件地址")] public string TargetEmail { get; set; } [Description("郵件完整內容")] public string Content { get; set; } } public class CommonOutput { public string message { get; set; } public object data { get; set; } public bool Success { get; set; } }
可以看到無論是函數還是入參都需要編寫Description特性,這是gpt理解這個函數的方法用途以及入參定義的關鍵,一定不能缺少。另外官方的demo中並沒有涉及出參的描述,所以這裏我也沒有添加。猜測可能gpt會自動基於出參的內容自動化的提取結果。
接着我們編寫具體的業務代碼,這裏的關鍵是當gpt返回結果時,我們需要根據gpt返回的操作(直接輸出內容/函數調用)來判斷,如果gpt要求函數調用,則我們需要調用本地函數後再組裝成新的chatmessage[]再次調用gpt,也就是說其實本質上是多輪遞歸式的調用來實現的邏輯鏈,比如當我問“天氣+郵件”時,gpt首先會告訴我調用天氣,並給我對應的參數。我返回天氣,gpt在組裝郵件的內容並告訴我調用郵件,給我參數。我再調用發送郵件並返回操作成功。gpt最後判斷任務結束,輸出內容。核心業務如下:
var key = "sk-Ab...jW"; var openAiService = new OpenAIService(new OpenAiOptions() { ApiKey = key }); var email = "[email protected]"; var userprompt = $"我想分別獲取成都市今天和西安市明天的天氣情況,併發送到{email}這個郵箱"; Console.WriteLine($"user:{userprompt}"); var center = new FunctionCallCentner(); var messages = new List<ChatMessage> { ChatMessage.FromSystem("You are a helpful assistant."), ChatMessage.FromUser(userprompt) }; await SessionExecute(messages); async Task SessionExecute(List<ChatMessage> messages) { var completionResult = await openAiService.ChatCompletion.CreateCompletion(new ChatCompletionCreateRequest { Messages = messages, Model = Models.Gpt_3_5_Turbo_0613, Functions = center.GetDefinition().ToList() }); if (completionResult.Successful) { if (completionResult.Choices.First().Message.FunctionCall != null) { completionResult.Choices.First().Message.Content = ""; messages.Add(completionResult.Choices.First().Message); messages.Add(await center.CallFunction(completionResult.Choices.First().Message.FunctionCall.Name, completionResult.Choices.First().Message.FunctionCall.ParseArguments())); await SessionExecute(messages); } else { Console.WriteLine("assistant:" + completionResult.Choices.First().Message.Content); } } }
接下來我們看看gpt實際的運行情況:
可以看到gpt很聰明的將我們的任務進行了拆解,並且正確的調用了對應的函數(比如很聰明的基於用戶模糊的問題“今天”“明天”去調用日期函數並且傳遞正確的枚舉值),獲取到每一輪函數返回的內容後,執行了正確的發郵件這個動作。並且最後貼心的告訴用戶它已經執行完畢任務,讓用戶及時檢查自己的郵箱。
如果說半年前chatgpt的橫空出世還僅僅是讓人覺得它僅僅是一個大號的聊天plus的話,那麼現在基於函數調用讓我們見識到了其恐怖的任務拆解,調度執行能力。通過對零散的API進行組裝來實現用戶複雜需求的實現,這在以往的開發中是根本無法想象的存在,說實話這東西將會顛覆現有的IT軟件開發/交互,甚至很多IT崗位將面臨被GPT平替(比如基於函數調用+低代碼)。。。