大語言模型的應用探索—AI Agent初探!

mingupupup發表於2024-07-08

前言

大語言模型的應用之一是與大語言模型進行聊天也就是一個ChatBot,這個應用已經很廣泛了。

接下來的一個應用就是AI Agent。

AI Agent是人工智慧代理(Artificial Intelligence Agent)的概念,它是一種能夠感知環境、進行決策和執行動作的智慧實體,通常基於機器學習和人工智慧技術,具備自主性和自適應性,在特定任務或領域中能夠自主地進行學習和改進。一個更完整的Agent,一定是與環境充分互動的,它包括兩部分——一是Agent的部分,二是環境的部分。此刻的Agent就如同物理世界中的「人類」,物理世界就是人類的「外部環境」。

image-20240708160424399

效果

今天就基於開源的大語言模型Qwen2-7B-Instruct與開源的LLM應用框架SenmanticKernel實現我們的第一個AI Agent!

入門先從一個簡單的例子入手,比如叫大語言模型將字串列印至控制檯。

在ChatBox應用中,我們叫大語言模型將字串列印至控制檯,它的回答可能是這樣子的:

image-20240708161150957

而在簡易的AI Agent應用中,大語言模型會幫我們完成這項簡單的任務。

image-20240708161449438

image-20240708161514177

又比如,我們需要從資料庫中檢索資訊,假設需要檢索的資訊如下所示:

 List<Order> Orders = new List<Order>()
 {
     new Order(){Id=1,Name="iPhone15",Address="武漢"},
     new Order(){Id=2,Name="iPad",Address="北京"},
     new Order(){Id=3,Name="MacBook",Address="上海"},
     new Order(){Id=4,Name = "HuaWei Mate60 ",Address = "深圳"},
     new Order(){Id = 5,Name = "小米14",Address = "廣州"}
 };

在ChatBox應用中,我們如果問Id為1的訂單資訊是什麼?大語言模型是不會知道我們想幹什麼的,回答可能如下所示:

image-20240708162121671

而在簡易的AI Agent應用中,AI回答如下:

image-20240708162335212

image-20240708162418992

實踐

上一篇文章講過,在SemanticKernel中OpenAI支援Function Call的模型與月之暗面支援Function Call的模型,只需進行簡單的設定即可實現自動函式呼叫,但我嘗試了其他開源的模型,發現做不到。

透過github瞭解到,其他的模型可以透過提示工程來實現本地函式呼叫。

什麼是提示工程?

提示工程(Prompt Engineering)是一種自然語言處理(NLP)技術,主要應用於生成式AI模型,如GPT-3等。它透過精心設計輸入提示(prompt),引導模型生成特定型別的輸出。在提示工程中,使用者可以控制模型的輸出內容、風格和格式,以滿足不同的應用場景需求。

提示工程的關鍵在於設計有效的提示,這通常需要對模型的能力和限制有深入的瞭解。透過調整提示的結構、語言和上下文,可以顯著提高模型生成結果的質量和相關性。在實際應用中,提示工程可以用於文字生成、問答、翻譯、摘要、對話系統等多個領域。

上面兩個簡單的AI Agent應用實現的原理是一樣的,選擇第二個獲取訂單的引用進行講解。

實現的方法來自上一篇部落格提到的專案:

Jenscaasen/UniversalLLMFunctionCaller: A planner that integrates into Semantic Kernel to enable function calling on all Chat based LLMs (Mistral, Bard, Claude, LLama etc) (github.com)

在kernel中匯入外掛:

public sealed class OrderPlugin
{
    List<Order> Orders = new List<Order>()
    {
        new Order(){Id=1,Name="iPhone15",Address="武漢"},
        new Order(){Id=2,Name="iPad",Address="北京"},
        new Order(){Id=3,Name="MacBook",Address="上海"},
        new Order(){Id=4,Name = "HuaWei Mate60 ",Address = "深圳"},
        new Order(){Id = 5,Name = "小米14",Address = "廣州"}
    };

    [KernelFunction, Description("根據Id獲取訂單")]
    [return: Description("獲取到的訂單")]
    public string GetOrderById(
    [Description("訂單的Id")] int id)
    {
        var order = Orders.Where(x => x.Id == id).FirstOrDefault();
        if(order != null)
        {
            return order.ToString();
        }
        else
        {
            return "找不到該Id的訂單";
        }
    }
}
_kernel.ImportPluginFromType<OrderPlugin>("Order");
 UniversalLLMFunctionCaller planner = new(_kernel);
 string result = await planner.RunAsync(AskText);

重點在planner.RunAsync中。

匯入為了實現目的內建的外掛:

 // Initialize plugins
 var plugins = _kernel.Plugins;
 var internalPlugin = _kernel.Plugins.AddFromType<UniversalLLMFunctionCallerInternalFunctions>();

UniversalLLMFunctionCallerInternalFunctions外掛如下:

    internal class UniversalLLMFunctionCallerInternalFunctions
    {
        //   [KernelFunction, Description("Call this when the workflow is done and there are no more functions to call")]
        //   public string Finished(
        //  [Description("Wrap up what was done and what the result is, be concise")] string finalmessage
        //)
        //   {
        //       return string.Empty;
        //       //no actual implementation, for internal routing only
        //   }
        [KernelFunction, Description("當工作流程完成,沒有更多的函式需要呼叫時,呼叫這個函式")]
        public string Finished(
       [Description("總結已完成的工作和結果,儘量簡潔明瞭。")] string finalmessage
     )
        {
            return string.Empty;
            //no actual implementation, for internal routing only
        }
        //[KernelFunction, Description("Gets the name of the spaceship of the user")]
        //public string GetMySpaceshipName()
        //{
        //    return "MSS3000";
        //}
        [KernelFunction, Description("獲取使用者飛船的名稱")]
        public string GetMySpaceshipName()
        {
            return "嫦娥一號";
        }
     //   [KernelFunction, Description("Starts a Spaceship")]
     //   public void StartSpaceship(
     //  [Description("The name of the spaceship to start")] string ship_name
     //)
     //   {
     //       //no actual implementation, for internal routing only
     //   }

        [KernelFunction, Description("啟動飛船")]
        public void StartSpaceship(
     [Description("啟動的飛船的名字")] string ship_name
   )
        {
            //no actual implementation, for internal routing only
        }

    }
}

我將英文原版註釋掉並增加了一箇中文的版本。

將外掛轉化為文字:

// Convert plugins to text
string pluginsAsText = GetTemplatesAsTextPrompt3000(plugins);

image-20240708163921817

獲取到了外掛中所有本地函式的資訊。

nextFunctionCall = await GetNextFunctionCallAsync(chatHistory, pluginsAsText);

讓大語言模型獲取下一次需要呼叫的函式。

在對話示例中加入一個提示,這個提示是關鍵!

image-20240708164508312

英文原版如下:

        private string GetLoopSystemMessage(string pluginsAsTextPrompt3000)
        {
            string systemPrompt = $@"You are a computer system. You can only speak TextPrompt3000 to make the user call functions, and the user will behave
        as a different computer system that answers those functions.
        Below, you are provided a goal that needs to be reached, as well as a list of functions that the user could use.
        You need to find out what the next step for the user is to reach the goal and recommend a TextPrompt3000 function call. 
        You are also provided a list of functions that are in TextPrompt3000 Schema Format.
        The TextPrompt3000 Format is defined like this:
        {GetTextPrompt300Explanation()}
        ##available functions##
        {pluginsAsTextPrompt3000}
        ##end functions##

        The following rules are very important:
        1) you can only recommend one function and the parameters, not multiple functions
        2) You can only recommend a function that is in the list of available functions
        3) You need to give all parameters for the function. Do NOT escape special characters in the name of functions or the names of parameters (dont do aaa\_bbb, just stick to aaa_bbb)!
        4) Given the history, the function you recommend needs to be important to get closer towards the goal
        5) Do not wrap functions into each other. Stick to the list of functions, this is not a math problem. Do not use placeholders.
        We only need one function, the next one needed. For example, if function A() needs to be used as parameter in function B(), do NOT do B(A()). Instead,
        if A wasnt called allready, call A() first. The result will be used in B in a later iteration.
        6) Do not recommend a function that was recently called. Use the output instead. Do not use Placeholders or Functions as parameters for other functions
        7) Only write a Function Call, do not explain why, do not provide a reasoning. You are limited to writing a function call only!
        8) When all  necessary functions are called and the result was presented by the computer system, call the Finished function and present the result

        If you break any of those rules, a kitten dies. 
        ";
            return systemPrompt;
        }

我翻譯了一箇中文版本並新增了使用中文回答如下:

        private string GetLoopSystemMessage(string pluginsAsTextPrompt3000)
        {
            string systemPrompt = $@"你是一個計算機系統。
你只能使用TextPrompt3000指令,讓使用者呼叫對應的函式,而使用者將作為另一個回答這些函式的計算機系統。
以下是您所需實現的目標,以及使用者可以使用的函式列表。
您需要找出使用者到達目標的下一步,並推薦一個TextPrompt3000函式呼叫。 
您還會得到一個TextPrompt3000 Schema格式的函式列表。
TextPrompt3000格式的定義如下所示:
{GetTextPrompt300Explanation()}
##可用函式列表開始##
{pluginsAsTextPrompt3000}
##可用函式列表結束##

以下規則非常重要:
1) 你只能推薦一個函式及其引數,而不是多個函式
2) 你可以推薦的函式只存在於可用函式列表中
3) 你需要為該函式提供所有引數。不要在函式名或引數名中轉義特殊字元,直接使用(如只寫aaa_bbb,不要寫成aaa\_bbb)
4) 你推薦的歷史記錄與函式需要對更接近目標有重要作用
5) 不要將函式相互巢狀。 遵循列表中的函式,這不是一個數學問題。 不要使用佔位符。
我們只需要一個函式,下一個所需的函式。舉個例子, 如果 function A() 需要在 function B()中當引數使用, 不要使用 B(A())。 而是,
如果A還沒有被呼叫, 先呼叫 A()。返回的結果將在下一次迭代中在B中使用。
6) 不要推薦一個最近已經呼叫過的函式。 使用輸出代替。 不要將佔位符或函式作為其他函式的引數使用。
7) 只寫出一個函式呼叫,不解釋原因,不提供理由。您只能寫出一個函式呼叫!
8) 當所有必需的函式都被呼叫,且計算機系統呈現了結果,呼叫Finished函式並展示結果。
9) 請使用中文回答。

如果你違反了任何這些規定,那麼會有一隻小貓死去。
";
            return systemPrompt;
        }

第一次直觀感受到了提示工程的魔法。

根據這個模板與對話歷史詢問大語言模型下一步需要執行的函式名稱與引數是什麼:

image-20240708164957393

大語言模型回答需要呼叫的函式名為GetOrderById,引數id為3,接下來驗證是否可以轉化為一個Function Call:

image-20240708165204124

在plugins中查詢是否有同名的函式,如果有KernelArguments,進行本地函式呼叫:

private async Task<string> InvokePluginAsync(FunctionCall functionCall)
{
    List<string> args = new List<string>();
    foreach (var paraam in functionCall.Parameters)
    {
        args.Add($"{paraam.Name} : {paraam.Value}");
    }
    Debug.WriteLine($">>invoking {functionCall.Name} with parameters {string.Join(",", args)}");
    // Iterate over each plugin in the kernel
    foreach (var plugin in _kernel.Plugins)
    {
        // Check if the plugin has a function with the same name as the function call
        var function = plugin.FirstOrDefault(f => f.Name == functionCall.Name);
        if (function != null)
        {
            // Create a new context for the function call
            KernelArguments context = new KernelArguments();

            // Add the function parameters to the context
            foreach (var parameter in functionCall.Parameters)
            {
                context[parameter.Name] = parameter.Value;
            }

            // Invoke the function
            var result = await function.InvokeAsync(_kernel, context);

            Debug.WriteLine($">>Result: {result.ToString()}");
            return result.ToString();
        }
    }
 // Invoke the function
            var result = await function.InvokeAsync(_kernel, context);

在本例中會執行:

[KernelFunction, Description("根據Id獲取訂單")]
[return: Description("獲取到的訂單")]
public string GetOrderById(
[Description("訂單的Id")] int id)
{
    var order = Orders.Where(x => x.Id == id).FirstOrDefault();
    if(order != null)
    {
        return order.ToString();
    }
    else
    {
        return "找不到該Id的訂單";
    }
}

這個函式,得到如下結果:

image-20240708165812387

大語言模型判斷已經完成了任務,下一步執行

   [KernelFunction, Description("當工作流程完成,沒有更多的函式需要呼叫時,呼叫這個函式")]
   public string Finished(
  [Description("總結已完成的工作和結果,儘量簡潔明瞭。")] string finalmessage
)
   {
       return string.Empty;
       //no actual implementation, for internal routing only
   }

這個函式,如下所示:

image-20240708170028013

下一個呼叫的函式是Finished的,會跳出迴圈:

image-20240708170231464

返回最後的資訊:

image-20240708170316368

最終的效果如下所示:

image-20240708170356146

以上就是本次分享的全部內容,嘗試使用開源的大語言模型與SenmanticKernel框架結合,構建自己的簡易的AI Agent,不過AI Agent的效果還不是很好,任務變複雜有可能會出錯,具體學習可以看推薦的專案的原始碼,作者寫的還是比較清晰的。感謝矽基流動提供的平臺,讓我等沒有硬體資源的人,也可以流暢的使用開源的大語言模型,進行大語言模型的應用探索。

相關文章