上篇文章我們以 .NET AIChatWeb 專案為基礎,將其轉為可上傳圖檔詢問任意問題的互動式 AI 聊天網站。

如要進一步擴充能力,我們可加入自訂的 Tool、Agent,甚至 Skill,賦與 LLM 模式自訂的特殊技能。事實上,AIChatWeb 原本具備的 RAG 文件檢索查詢功能,就僅僅是在 chatOptions.Tools 宣告載入文件及搜尋文侔兩項工具,就實現了簡單的 RAG 查詢:(見中文註解)

    protected override void OnInitialized()
    {
        statefulMessageCount = 0;
        messages.Add(new(ChatRole.System, SystemPrompt));
        chatOptions.Tools = [
            // 使用片語或關鍵字搜尋文件
            AIFunctionFactory.Create(SearchAsync),        
            // 載入所需文件(第一次搜尋前需執行完畢)
            AIFunctionFactory.Create(LoadDocumentsAsync)
        ];
    }

    private async Task AddUserMessageAsync(ChatMessage userMessage)
    {
        CancelAnyCurrentResponse();

        // Add the user message to the conversation
        messages.Add(userMessage);
        chatSuggestions?.Clear();
        await chatInput!.FocusAsync();

        // Stream and display a new response from the IChatClient
        var responseText = new TextContent("");
        currentResponseMessage = new ChatMessage(ChatRole.Assistant, [responseText]);
        currentResponseCancellation = new();
        // 呼叫 LLM 模型時傳入的 chatOptions 包含 Tools 資訊,LLM 會依需求調用這些工具
        await foreach (var update in ChatClient.GetStreamingResponseAsync(messages.Skip(statefulMessageCount), chatOptions, currentResponseCancellation.Token))
        {
            messages.AddMessages(update, filter: c => c is not TextContent);
            responseText.Text += update.Text;
            chatOptions.ConversationId = update.ConversationId;
            ChatMessageItem.NotifyChanged(currentResponseMessage);
        }

        // Store the final response in the conversation, and begin getting suggestions
        messages.Add(currentResponseMessage!);
        statefulMessageCount = chatOptions.ConversationId is not null ? messages.Count : 0;
        currentResponseMessage = null;
        chatSuggestions?.Update(messages);
    }

要增加其他 Tool 的做法很簡單,在 chatOptions.Tools = [ ... ]AIFunctionFactory.Create(方法) 增加我們寫好的自訂函式就可以了~ 這個簡便做法來自前陣子介紹過的 Microsoft Agent Framework,是微軟針對 AI 應用開發提出的最新程式框架與 SDK (取代 Semantic Kernel 及 AutoGen),想用 .NET 開發 AI 應用程式必學,而 Agent Framework SDK 的設計十分簡潔,用類似 AIFunctionFactory.Create()、AsAIAgent() 就能建構需要的型別,寫起來蠻方便的。

為了示範,我打算寫一個簡單的樂透選號 Tool 及一個簡單的農民曆擇日 Agent。

先看樂透選號,這個需求超簡單,知道選號規則(例:42 選 6、49 選 6 加特別號...)用隨機數產生即可,為了怕模型自己太聰明知道規則不需要工具也能自己生成,我設計了一個虛構的「黑大樂透」,數字由 0 ~ 31 (0x1f),32 取 8 (2 的三次方),如此便能 100% 確認模型是呼叫我們的工具函式選號。

public class PickNumberFunctions
{
    static string[] KnownLotteries = new[] { "樂透", "大樂透", "威力彩", "黑大樂透" };

    [Description("為樂透、大樂透、威力彩、黑大樂透等彩券選擇號碼")]
    public Task<string> PickLotteryNumbersAsync(
        [Description("彩券名稱,包含 '樂透'、'大樂透'、'威力彩'、'黑大樂透'。")] string lotteryName)
    {
        if (!KnownLotteries.Contains(lotteryName))
        {
            throw new ArgumentException($"不支援 {lotteryName}. 支援的彩券有: {string.Join(", ", KnownLotteries)}");
        }
        int count, min, max;
        bool reqExtraNum = true;
        switch (lotteryName)
        {
            case "樂透":
                count = 6;
                min = 1;
                max = 49;
                reqExtraNum = false;
                break;
            case "大樂透":
                count = 6;
                min = 1;
                max = 49;
                break;
            case "威力彩":
                count = 6;
                min = 1;
                max = 38;
                break;
            case "黑大樂透":
                count = 8;
                min = 0;
                max = 31;
                reqExtraNum = false;
                break;
            default:
                throw new NotImplementedException();
        }
        var random = new Random();
        var numbers = Enumerable.Range(min, max - min + 1)
            .OrderBy(_ => random.Next()).Take(count)
            .OrderBy(o => o).ToArray();
        if (reqExtraNum)
        {
            var extraNum = random.Next(1, 9); // 額外號碼在 1-8 之間
            return Task.FromResult($"號碼為 {string.Join(", ", numbers)},特別號為 {extraNum},祝您中獎!");
        }
        return Task.FromResult($"號碼為 {string.Join(", ", numbers)},祝您中獎!");
    }
}

至於 Agent,我的點子是以前幾天提到的農民曆程式庫為核心,讓 AI 模型具備選日子的能力,算算哪天好動工,哪天宜嫁娶。這要寫成 Agent 不難,變兩個工具函式,一個取得現在時間,一個取得指定期間每天的宜、忌活動,再寫一段 Prompt 請模型依使用者需求找出合適的日子,剩下的交給冰雪聰明的 AI 模型,一切就搞定惹。(註:目前的 lunar-csharp NuGet 程式庫只支援簡體中文,我已經 Fork 專案增加繁體中文打算發 PR 看有沒有機會被合併,現階段簡繁轉換先交給模型處理)

public class PickDayAgent
{
    // 註:NuGet 現有 lunar-sharp 版本僅支持簡體中文,此處借用 LLM 模型的多國語言能力內部翻譯
    public ChatClient _chatClient { get; }
    string[] KnownActivities =  {"祭祀", "祈福", "求嗣", "开光", "塑绘", "齐醮", "斋醮", "沐浴", "酬神", "造庙", "祀灶", "焚香", "谢土", "出火", "雕刻", "嫁娶", "订婚", "纳采", "问名", "纳婿", "归宁", "安床", "合帐", "冠笄", "订盟", "进人口", "裁衣", "挽面", "开容", "修坟", "启钻", "破土", "安葬", "立碑", "成服", "除服", "开生坟", "合寿木", "入殓", "移柩", "普渡", "入宅", "安香", "安门", "修造", "起基", "动土", "上梁", "竖柱", "开井开池", "作陂放水", "拆卸", "破屋", "坏垣", "补垣", "伐木做梁", "作灶", "解除", "开柱眼", "穿屏扇架", "盖屋合脊", "开厕", "造仓", "塞穴", "平治道涂", "造桥", "作厕", "筑堤", "开池", "伐木", "开渠", "掘井", "扫舍", "放水", "造屋", "合脊", "造畜稠", "修门", "定磉", "作梁", "修饰垣墙", "架马", "开市", "挂匾", "纳财", "求财", "开仓", "买车", "置产", "雇佣", "出货财", "安机械", "造车器", "经络", "酝酿", "作染", "鼓铸", "造船", "割蜜", "栽种", "取渔", "结网", "牧养", "安碓磑", "习艺", "入学", "理发", "探病", "见贵", "乘船", "渡水", "针灸", "出行", "移徙", "分居", "剃头", "整手足甲", "纳畜", "捕捉", "畋猎", "教牛马", "会亲友", "赴任", "求医", "治病", "词讼", "起基动土", "破屋坏垣", "盖屋", "造仓库", "立券交易", "交易", "立券", "安机", "会友", "求医疗病", "诸事不宜", "馀事勿取", "行丧", "断蚁", "归岫", "无"};

    ChatClientAgent agent;
    public PickDayAgent(ChatClient chatClient)
    {
        _chatClient = chatClient;
        agent = _chatClient.AsAIAgent(
            instructions: $"""
            你是一個擇日小助手,依據使用者提供的日期範圍及活動類型,使用 ShowRecommendedActivitiesAsync 工具挑選適合的日子。
            - 若使用者指定相對日期區間(例如「未來一週」),使用 GetNowTimeAsync 工具取得現在的日期時間計算實際日期範圍。
            - 使用 ShowRecommendedActivitiesAsync 工具來獲取每一天的宜、忌活動。
            - 嘗試將活動歸類到以下項目之一: {string.Join(", ", KnownActivities)}等。
            - 根據使用者提供的活動類型,找出宜、忌該活動的日子,並顯示當日宜忌項目的完整原文。
            - 工具回傳結果可能包含簡體中文,請一律轉為繁體中文。
            """,
            tools: [ 
                AIFunctionFactory.Create(GetNowTimeAsync), // 取得現在時間
                // 列舉指定期間每天的宜忌項目
                AIFunctionFactory.Create(ShowRecommendedActivitiesAsync) 
            ]
        );
    }

    [Description("根據使用者提供的日期範圍及活動類型,挑選適合的日子。")]
    public async Task<string> PickDayAsync(string question)
    {
        var response = await agent.RunAsync(question);
        return response.ToString() ?? string.Empty;
    }

    [Description("取得現在日期時間")]
    public async Task<DateTime> GetNowTimeAsync() => DateTime.Now;

    [Description("顯示指定日期範圍,每天的宜、忌活動")]
    public async Task<string> ShowRecommendedActivitiesAsync(
        [Description("開始日期")] DateTime startDate,
        [Description("結束日期")] DateTime endDate
    )        
    {
        if (startDate.CompareTo(endDate) > 0)
        {
            (startDate, endDate) = (endDate, startDate);
        }
        var date = startDate;
        var sb = new StringBuilder();
        while (date.CompareTo(endDate) <= 0)
        {
            var lunarDate =Lunar.Lunar.FromDate(date);
            var okActivities = string.Join(", ", lunarDate.GetDayYi());
            var avoidActivities = string.Join(", ", lunarDate.GetDayJi());
            var result = $"""
            【{date:yyyy-MM-dd}】
                - 宜 {okActivities}
                - 忌 {avoidActivities}
            """;
            Console.WriteLine(result);
            sb.AppendLine(result);   
            date = date.AddDays(1);
        }
        return sb.ToString();
    }        
}

工具跟 Agent 寫好,Tools 清單加兩行 AIFunctionFactory.Create() 把方法掛上去,相關說明用 Description Attribute 就近寫在函數及參數名稱,.Create() 會自動抓取。如此,AI 模型自會在需要時使用它們,MS Agent Framework 的這種簡潔設計,深得我心。

果不其然,現在 GPT 模型已知道怎麼為黑大樂透選號,也會查農民曆看日子囉~ 依此要領,要為 AI 應用加上什麼新能力,全憑你的想像力。

本文程式範例可參考 add-tool-n-agent 分支

Demonstrates extending a .NET AIChatWeb app by adding custom Tools and an Agent using Microsoft Agent Framework. Shows how to implement a lottery number picker and a lunar calendar day-selection agent, highlighting how easily AI capabilities can be expanded.


Comments

Be the first to post a comment

Post a comment