前言 這算是一篇學習記錄博客了,主要是學習語義內核(Semantic Kernel)的實踐,以及Aspire進行全棧開發的上手體驗,我是採用Aspire同時啟動API服務,Blazor前端服務以及WinUI的桌面端項目,同時進行三個項目的代碼修改,整體感覺很方便,如果代碼都修改了只需要啟動Aspir ...
前言
這算是一篇學習記錄博客了,主要是學習語義內核(Semantic Kernel)的實踐,以及Aspire進行全棧開發的上手體驗,我是採用Aspire同時啟動API服務,Blazor前端服務以及WinUI的桌面端項目,同時進行三個項目的代碼修改,整體感覺很方便,如果代碼都修改了只需要啟動Aspire項目,不用每個項目單獨起一遍了,而且速度很快,即使是有用容器服務的情況下。
技術方案
1. 框架選型
- WebApi使用Asp.Net Core WebApi實現。
- Bing搜索結果獲取,以及網頁解析內容提取使用的是PlayWright庫。
- 網頁內容總結使用的是WinUI編寫的客戶端,結合語義內核(Semantic Kernel)調用國產智普清言LLM。
- 後臺管理頁面使用的Blazor,不過只是一個demo頁面。
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結果展示如下:
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;
}
效果如下:
到此總結就已經完成了,大家可以去看看代碼,看看有沒有幫助。
個人心得體會
在進行一段時間的學習之後,對大語言模型有了一些全面的認識,意識到大語言模型並不是萬能的,但是它能夠很輕鬆的做到我們之前要很複雜才能做到的事情。輕鬆做到的前提就是要給出很好的提示詞。
如果把大語言模型比作戰鬥機,那提示詞就可以比作是駕駛員了,提示詞的好壞直接決定大語言模型輸出的準確度。
作為軟體開發人員,對於提示詞的編寫一定要多學習,多總結才行了。