使用Aspire優雅的進行全棧開發——WinUI使用Semantic Kernel呼叫智普清言LLM總結Asp.Net Core透過Playwright解析的網頁內容

绿荫阿广發表於2024-08-04

前言

這算是一篇學習記錄部落格了,主要是學習語義核心(Semantic Kernel)的實踐,以及Aspire進行全棧開發的上手體驗,我是採用Aspire同時啟動API服務,Blazor前端服務以及WinUI的桌面端專案,同時進行三個專案的程式碼修改,整體感覺很方便,如果程式碼都修改了只需要啟動Aspire專案,不用每個專案單獨起一遍了,而且速度很快,即使是有用容器服務的情況下。

技術方案

1. 框架選型

  • WebApi使用Asp.Net Core WebApi實現。
  • Bing搜尋結果獲取,以及網頁解析內容提取使用的是PlayWright庫。
  • 網頁內容總結使用的是WinUI編寫的客戶端,結合語義核心(Semantic Kernel)呼叫國產智普清言LLM。
  • 後臺管理頁面使用的Blazor,不過只是一個demo頁面。

img

2. 為什麼這樣選

作為一個.Net開發,肯定優先使用.Net相關的技術了,也為了能實踐最新的技術,就進行了一些新技術的選擇。

主要說明一下選擇這幾個技術框架的原因:

  • Playwright 原因是透過測試發現它的表現最好,其他型別的庫也有測試,比如Selenium,HtmlAgilityPack,HtmlAgilityPack對靜態網頁解析比較好,但是如果遇到js渲染的資料很多的頁面就不好了,Selenium比Playwright提取的內容差了一些,Playwright是透過模擬使用者操作啟動瀏覽器,然後獲取內容,感覺如果一次性處理很多的頁面應該也會負載很大。

  • Aspire 這個是因為這是微軟最新的專門給開發人員開發的工具,那既然是給開發人員做的,那肯定要體驗一把了,體驗完感覺是真的不錯,能夠節省很多的步驟。

  • 語義核心(Semantic Kernel)選擇它是因為這算是.Net社群對接大語言模型最流行的框架了,提供了很多的開箱即用的功能,對於開發智慧APP幫助很大,而且社群熱度也很高。

  • 智普清言LLM 選擇它是多方面考慮的結果,第一是它相容OpenAI的介面,這樣語義核心就可以透過配置就能使用它,第二是它是支援Function Call的,也就是說它可以作為OpenAI的國內平替,用它開發一些智慧APP是很好的。

  • WinUI 選擇它是個人對客戶端開發主要使用的是WinUI,而且用它對接大語言模型不把對接放到後端也是為了後面對接離線大語言模型做基礎,比如微軟的Phi3之類的。

程式碼講解

本部落格涉及的程式碼連結如下:

https://github.com/GreenShadeZhang/BingSearchSummary

1. 搜尋結果獲取

示例程式碼如下:

先建立Playwright例項,然後進行使用者操作模擬。

   var playwright = await Playwright.CreateAsync();
   var browser = await playwright.Chromium.LaunchAsync(new BrowserTypeLaunchOptions { Headless = true });
   var page = await browser.NewPageAsync();

   // 設定 User-Agent 和視口大小
   var js = @"Object.defineProperties(navigator, {webdriver:{get:()=>false}});";
   await page.AddInitScriptAsync(js);

   await page.GotoAsync("https://www.bing.com");

   // 模擬使用者輸入搜尋關鍵詞
   await page.FillAsync("input[name=q]", keyword);
   await page.Keyboard.PressAsync("Enter");

   // 等待搜尋結果載入
   await page.WaitForLoadStateAsync(LoadState.NetworkIdle);

   // 獲取搜尋結果內容
   var content = await page.ContentAsync();
   var dataList = BingSearchHelper.ParseHtmlToJson(content);
   var result = new List<BingSearchItem>();
   

將搜尋結果解析成json資料如下:

這一步是因為我沒有bing搜尋的訂閱,所以只能解析頁面,如果有bing搜尋的訂閱這一步可以省略。

using BingSearchSummary.ApiService.Models;
using HtmlAgilityPack;

namespace BingSearchSummary.ApiService;

public class BingSearchHelper
{
    public static List<BingSearchItem> ParseHtmlToJson(string htmlContent)
    {
        var htmlDocument = new HtmlDocument();
        htmlDocument.LoadHtml(htmlContent);

        var results = new List<BingSearchItem>();

        foreach (var node in htmlDocument.DocumentNode.SelectNodes("//li[@class='b_algo']"))
        {
            var titleNode = node.SelectSingleNode(".//h2/a");
            var snippetNode = node.SelectSingleNode(".//p");
            var urlNode = node.SelectSingleNode(".//cite");

            var title = titleNode?.InnerText.Trim();
            var snippet = snippetNode?.InnerText.Trim();
            var url = urlNode?.InnerText.Trim();

            if (string.IsNullOrEmpty(title))
            {
                continue;
            }

            var searchItem = new BingSearchItem
            {
                Title = title,
                Snippet = snippet ?? "",
                Url = url ?? ""
            };

            results.Add(searchItem);
        }

        return results;
    }
}

透過上面的程式碼操作,關鍵詞搜尋的網頁URL就已經拿到了,然後就可以繼續進行頁面內容的解析了。

2. 網頁內容解析

客戶端透過呼叫介面,然後獲取關鍵詞的前三條的搜尋結果和網頁內容。

    // 獲取搜尋結果內容
    var content = await page.ContentAsync();
    var dataList = BingSearchHelper.ParseHtmlToJson(content);
    var result = new List<BingSearchItem>();

    foreach (var data in dataList)
    {
        if (result.Count >= 3)
        {
            break;
        }//只處理三條資料
        await page.GotoAsync(data.Url);

        var divContent = await page.QuerySelectorAsync(".content");

        divContent ??= await page.QuerySelectorAsync("body");

        if (divContent != null)
        {
            var pageContent = await divContent.InnerTextAsync();

            result.Add(new BingSearchItem
            {
                Title = data.Title,
                Url = data.Url,
                Snippet = data.Snippet,
                PageContent = pageContent
            });
        }

swagger結果展示如下:

img

3. 網頁結果總結

這部分程式碼在WinUI專案中實現,WinUI呼叫介面獲取到結果,並透過Microsoft.SemanticKernel.PromptTemplates.Liquid庫進行訊息模板動態生成訊息,呼叫語義核心(Semantic Kernel)進行內容總結。

語義核心(Semantic Kernel)注入程式碼如下:

            //測試token被刪除 已經無效 請換成自己的智普token
            builder.AddOpenAIChatCompletion(modelId: "GLM-4-Air", apiKey: "4827638425a6b9d48bea3b0599246ff2.pFjhEKShPOZE8OFd", httpClient: GetProxyClient("https://open.bigmodel.cn/api/paas/v4/chat/completions"));

            builder.Plugins.AddFromType<TimeInformationPlugin>();

            services.AddSingleton(builder.Build());

#pragma warning disable SKEXP0040 // 型別僅用於評估,在將來的更新中可能會被更改或刪除。取消此診斷以繼續。
            services.AddSingleton<IPromptTemplateFactory, LiquidPromptTemplateFactory>();
#pragma warning restore SKEXP0040 // 型別僅用於評估,在將來的更新中可能會被更改或刪除。取消此診斷以繼續。

內容總結程式碼如下:

  [RelayCommand]
  private async Task SummaryAndUploadAsync(BingSearchItem item)
  {
      _chatHistory.Clear();

      SummaryProcessRingStatus = true;
      try
      {
          var arguments = new KernelArguments
          {
              ["startTime"] = DateTimeOffset.Now.ToString("hh:mm:ss tt zz", CultureInfo.CurrentCulture),

              ["userMessage"] = item.PageContent
          };

          var systemMessage = await _promptTemplateFactory.Create(new PromptTemplateConfig(_systemPromptTemplate)
          {
              TemplateFormat = "liquid",
          }).RenderAsync(_kernel, arguments);

          var userMessage = await _promptTemplateFactory.Create(new PromptTemplateConfig(_userPromptTemplate)
          {
              TemplateFormat = "liquid",
          }).RenderAsync(_kernel, arguments);

          _chatHistory.AddSystemMessage(systemMessage);

          _chatHistory.AddUserMessage(userMessage);


          var chatResult = await _chatCompletionService.GetChatMessageContentAsync(_chatHistory, _openAIPromptExecutionSettings, _kernel);

          SummaryResult = chatResult.ToString();

          await _apiClient.PostContentsAsync(new BingSearchSummaryItem
          {
              Title = item.Title,
              Summary = chatResult.ToString(),
              Url = item.Url
          });

      }
      catch (Exception ex)
      {
          System.Diagnostics.Debug.WriteLine(ex.Message);
          SummaryProcessRingStatus = false;
      }

      SummaryProcessRingStatus = false;
  }

效果如下:

img

到此總結就已經完成了,大家可以去看看程式碼,看看有沒有幫助。

個人心得體會

在進行一段時間的學習之後,對大語言模型有了一些全面的認識,意識到大語言模型並不是萬能的,但是它能夠很輕鬆的做到我們之前要很複雜才能做到的事情。輕鬆做到的前提就是要給出很好的提示詞。

如果把大語言模型比作戰鬥機,那提示詞就可以比作是駕駛員了,提示詞的好壞直接決定大語言模型輸出的準確度。

作為軟體開發人員,對於提示詞的編寫一定要多學習,多總結才行了。

參考推薦文件專案如下:

  • 部落格程式碼

  • BotSharp

  • Rodel Agent

相關文章