在C#中基于Semantic?Kernel的檢索增強生成(RAG)實踐記錄
Semantic Kernel簡介
玩過大語言模型(LLM)的都知道OpenAI,然后微軟Azure也提供了OpenAI的服務(wù):Azure OpenAI,只需要申請到API Key,就可以使用這些AI服務(wù)。使用方式可以是通過在線Web頁面直接與AI聊天,也可以調(diào)用AI的API服務(wù),將AI的能力集成到自己的應(yīng)用程序中。不過這些服務(wù)都是在線提供的,都會需要根據(jù)token計費,所以不僅需要依賴互聯(lián)網(wǎng),而且在使用上會有一定成本。于是,就出現(xiàn)了像Ollama這樣的本地大語言模型服務(wù),只要你的電腦足夠強悍,應(yīng)用場景允許的情況下,使用本地大語言模型也是一個不錯的選擇。
既然有這么多AI服務(wù)可以選擇,那如果在我的應(yīng)用程序中需要能夠很方便地對接不同的AI服務(wù),應(yīng)該怎么做呢?這就是Semantic Kernel的基本功能,它是一個基于大語言模型開發(fā)應(yīng)用程序的框架,可以讓你的應(yīng)用程序更加方便地集成大語言模型。Semantic Kernel可用于輕松生成 AI 代理并將最新的 AI 模型集成到 C#、Python 或 Java 代碼庫中。因此,它雖然在.NET AI生態(tài)中扮演著非常重要的角色,但它是支持多編程語言跨平臺的應(yīng)用開發(fā)套件。
Semantic Kernel主要包含以下這些核心概念:
- 連接(Connection):與外部 AI 服務(wù)和數(shù)據(jù)源交互,比如在應(yīng)用程序中實現(xiàn)Open AI和Ollama的無縫整合
- 插件(Plugins):封裝應(yīng)用程序可以使用的功能,比如增強提示詞功能,為大語言模型提供更多的上下文信息
- 規(guī)劃器(Planner):根據(jù)用戶行為編排執(zhí)行計劃和策略
- 內(nèi)存(Memory):抽象并簡化 AI 應(yīng)用程序的上下文管理,比如文本向量(Text Embedding)的存儲等
有關(guān)Semantic Kernel的具體介紹可以參考微軟官方文檔。
演練:通過Semantic Kernel使用Microsoft Azure OpenAI Service
話不多說,直接實操。這個演練的目的,就是使用部署在Azure上的gpt-4o大語言模型來實現(xiàn)一個簡單的問答系統(tǒng)。
微軟于2024年10月21日終止面向個人用戶的Azure OpenAI服務(wù),企業(yè)用戶仍能繼續(xù)使用。參考:https://finance.sina.com.cn/roll/2024-10-18/doc-incsymyx4982064.shtml
在Azure中部署大語言模型
登錄Azure Portal,新建一個Azure AI service,然后點擊Go to Azure OpenAI Studio,進入OpenAI Studio:
進入后,在左側(cè)側(cè)邊欄的共享資源部分,選擇部署標(biāo)簽頁,然后在模型部署頁面,點擊部署模型按鈕,在下拉的菜單中,選擇部署基本模型:
在選擇模型對話框中,選擇gpt-4o,然后點擊確認(rèn)按鈕:
在彈出的對話框部署模型 gpt-4o中,給模型取個名字,然后直接點擊部署按鈕,如果希望對模型版本、安全性等做一些設(shè)置,也可以點擊自定義按鈕展開選項。
部署成功后,就可以在模型部署頁面的列表中看到已部署模型的版本以及狀態(tài):
點擊新部署的模型的名稱,進入模型詳細(xì)信息頁面,在頁面的終結(jié)點部分,把目標(biāo)URI和密鑰復(fù)制下來,待會要用。目標(biāo)URI只需要復(fù)制主機名部分即可,比如https://qingy-m2e0gbl3-eastus.openai.azure.com這樣:
在C#中使用Semantic Kernel實現(xiàn)問答應(yīng)用
首先創(chuàng)建一個控制臺應(yīng)用程序,然后添加Microsoft.SemanticKernel
NuGet包的引用:
$ dotnet new console --name ChatApp $ dotnet add package Microsoft.SemanticKernel
然后編輯Program.cs文件,加入下面的代碼:
using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.ChatCompletion; using System.Text; var apikey = Environment.GetEnvironmentVariable("azureopenaiapikey")!; // 初始化Semantic Kernel var kernel = Kernel.CreateBuilder() .AddAzureOpenAIChatCompletion( "gpt-4", "https://qingy-m2e0gbl3-eastus.openai.azure.com", apikey) .Build(); // 創(chuàng)建一個對話完成服務(wù)以及對話歷史對象,用來保存對話歷史,以便后續(xù)為大模型 // 提供對話上下文信息。 var chatCompletionService = kernel.GetRequiredService<IChatCompletionService>(); var chat = new ChatHistory("你是一個AI助手,幫助人們查找信息和回答問題"); StringBuilder chatResponse = new(); while (true) { Console.Write("請輸入問題>> "); // 將用戶輸入的問題添加到對話中 chat.AddUserMessage(Console.ReadLine()!); chatResponse.Clear(); // 獲取大語言模型的反饋,并將結(jié)果逐字輸出 await foreach (var message in chatCompletionService.GetStreamingChatMessageContentsAsync(chat)) { // 輸出當(dāng)前獲取的結(jié)果字符串 Console.Write(message); // 將輸出內(nèi)容添加到臨時變量中 chatResponse.Append(message.Content); } Console.WriteLine(); // 在進入下一次問答之前,將當(dāng)前回答結(jié)果添加到對話歷史中,為大語言模型提供問答上下文 chat.AddAssistantMessage(chatResponse.ToString()); Console.WriteLine(); }
在上面的代碼中,需要將你的API Key和終結(jié)點URI配置進去,為了安全性,這里我使用環(huán)境變量保存API Key,然后由程序讀入。為了讓大語言模型能夠了解在一次對話中,我和它之間都討論了什么內(nèi)容,在代碼中,使用一個StringBuilder
臨時保存了當(dāng)前對話的應(yīng)答結(jié)果,然后將這個結(jié)果又通過Semantic Kernel的AddAssistantMessage
方法加回到對話中,以便在下一次對話中,大語言模型能夠知道我們在聊什么話題。
比如下面的例子中,在第二次提問時我問到“有那幾次遷徙?”,AI能知道我是在說人類歷史上的大遷徙,然后將我想要的答案列舉出來:
到這里,一個簡單的基于gpt-4o的問答應(yīng)用就完成了,它的工作流程大致如下:
AI能回答所有的問題嗎?
由于這里使用的gpt-4o大語言模型是在今年5月份發(fā)布的,而大語言模型都是基于現(xiàn)有數(shù)據(jù)經(jīng)過訓(xùn)練得到的,所以,它應(yīng)該不會知道5月份以后的事情,遇到這樣的問題,AI只能回答不知道,或者給出一個比較離譜的答案:
你或許會想,那我將這些知識或者新聞文章下載下來,然后基于上面的代碼,將這些信息先添加到對話歷史中,讓大語言模型能夠了解上下文,這樣回答問題的時候準(zhǔn)確率不是提高了嗎?這個思路是對的,可以在進行問答之前,將新聞的文本信息添加到對話歷史中:
using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.ChatCompletion; using System.Text; using Microsoft.SemanticKernel.Connectors.AzureOpenAI; using Microsoft.SemanticKernel.Memory; using Microsoft.SemanticKernel.Text; #pragma warning disable SKEXP0010, SKEXP0001, SKEXP0050 const string CollectionName = "LatestNews"; var apikey = Environment.GetEnvironmentVariable("azureopenaiapikey")!; // 初始化Semantic Kernel var kernel = Kernel.CreateBuilder() .AddAzureOpenAIChatCompletion( "gpt-4", "https://qingy-m2e0gbl3-eastus.openai.azure.com", apikey) .Build(); // 創(chuàng)建文本向量生成服務(wù) var textEmbeddingGenerationService = new AzureOpenAITextEmbeddingGenerationService( "text-embedding-3-small", "https://qingy-m2e0gbl3-eastus.openai.azure.com", apikey); // 創(chuàng)建用于保存文本向量的內(nèi)存向量數(shù)據(jù)庫 var memory = new MemoryBuilder() .WithMemoryStore(new VolatileMemoryStore()) .WithTextEmbeddingGeneration(textEmbeddingGenerationService) .Build(); // 從外部文件以Markdown格式讀入內(nèi)容,然后根據(jù)語義產(chǎn)生多個段落 var markdownContent = await File.ReadAllTextAsync(@"input.md"); var paragraphs = TextChunker.SplitMarkdownParagraphs( TextChunker.SplitMarkDownLines(markdownContent.Replace("\r\n", " "), 128), 64); // 將各個段落進行量化并保存到向量數(shù)據(jù)庫 for (var i = 0; i < paragraphs.Count; i++) { await memory.SaveInformationAsync(CollectionName, paragraphs[i], $"paragraph{i}"); } // 創(chuàng)建一個對話完成服務(wù)以及對話歷史對象,用來保存對話歷史,以便后續(xù)為大模型 // 提供對話上下文信息。 var chatCompletionService = kernel.GetRequiredService<IChatCompletionService>(); var chat = new ChatHistory("你是一個AI助手,幫助人們查找信息和回答問題"); StringBuilder additionalInfo = new(); StringBuilder chatResponse = new(); while (true) { Console.Write("請輸入問題>> "); var question = Console.ReadLine()!; additionalInfo.Clear(); // 從向量數(shù)據(jù)庫中找到跟提問最為相近的3條信息,將其添加到對話歷史中 await foreach (var hit in memory.SearchAsync(CollectionName, question, limit: 3)) { additionalInfo.AppendLine(hit.Metadata.Text); } var contextLinesToRemove = -1; if (additionalInfo.Length != 0) { additionalInfo.Insert(0, "以下是一些附加信息:"); contextLinesToRemove = chat.Count; chat.AddUserMessage(additionalInfo.ToString()); } // 將用戶輸入的問題添加到對話中 chat.AddUserMessage(question); chatResponse.Clear(); // 獲取大語言模型的反饋,并將結(jié)果逐字輸出 await foreach (var message in chatCompletionService.GetStreamingChatMessageContentsAsync(chat)) { // 輸出當(dāng)前獲取的結(jié)果字符串 Console.Write(message); // 將輸出內(nèi)容添加到臨時變量中 chatResponse.Append(message.Content); } Console.WriteLine(); // 在進入下一次問答之前,將當(dāng)前回答結(jié)果添加到對話歷史中,為大語言模型提供問答上下文 chat.AddAssistantMessage(chatResponse.ToString()); // 將當(dāng)次問題相關(guān)的內(nèi)容從對話歷史中移除 if (contextLinesToRemove >= 0) chat.RemoveAt(contextLinesToRemove); Console.WriteLine(); }
但是這樣做,會造成下面的異常信息:
這個問題其實就跟大語言模型的Context Window有關(guān)。當(dāng)今所有的大語言模型在一次數(shù)據(jù)處理上都有一定的限制,這個限制就是Context Window,在這個例子中,我們的模型一次最多處理12萬8千個token(token是大語言模型的數(shù)據(jù)處理單元,它可以是一個詞組,一個單詞或者是一個字符),而我們卻輸入了147,845個token,于是就報錯了。很明顯,我們應(yīng)該減少傳入的數(shù)據(jù)量,但這樣又沒辦法把完整的新聞文章信息發(fā)送給大語言模型。此時就要用到“檢索增強生成(RAG)”。
Semantic Kernel的檢索增強生成(RAG)實踐
其實,并不一定非要把整篇新聞文章發(fā)給大語言模型,可以換個思路:只需要在新聞文章中摘出跟提問相關(guān)的內(nèi)容發(fā)送給大語言模型就可以了,這樣就可以大大減小需要發(fā)送到大語言模型的token數(shù)量。所以,這里就出現(xiàn)了額外的一些步驟:
- 對大量的文檔進行預(yù)處理,將文本信息量化并保存下來(Text Embedding)
- 在提出新問題時,根據(jù)問題語義,從保存的文本量化信息(Embeddings)中,找到與問題相關(guān)的信息
- 將這些信息發(fā)送給大語言模型,并從大語言模型獲得應(yīng)答
- 將結(jié)果反饋給調(diào)用方
流程大致如下:
虛線灰色框中就是檢索增強生成(RAG)相關(guān)流程,這里就不針對每個標(biāo)號一一說明了,能夠理解上面所述的4個大的步驟,就很好理解這張圖中的整體流程。下面我們直接使用Semantic Kernel,通過RAG來增強模型應(yīng)答。
首先,在Azure OpenAI Studio中,按照上文的步驟,部署一個text-embedding-3-small的模型,同樣將終結(jié)點URI和API Key記錄下來,然后,在項目中添加Microsoft.SemanticKernel.Plugins.Memory
NuGet包的引用,因為我們打算先使用基于內(nèi)存的文本向量數(shù)據(jù)庫來運行我們的代碼。Semantic Kernel支持多種向量數(shù)據(jù)庫,比如Sqlite,Azure AI Search,Chroma,Milvus,Pinecone,Qdrant,Weaviate等等。在添加引用的時候,需要使用--prerelease
參數(shù),因為Microsoft.SemanticKernel.Plugins.Memory
包目前還處于alpha階段。
將上面的代碼改成下面的形式:
using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.ChatCompletion; using System.Text; using Microsoft.SemanticKernel.Connectors.AzureOpenAI; using Microsoft.SemanticKernel.Memory; using Microsoft.SemanticKernel.Text; #pragma warning disable SKEXP0010, SKEXP0001, SKEXP0050 const string CollectionName = "LatestNews"; var apikey = Environment.GetEnvironmentVariable("azureopenaiapikey")!; // 初始化Semantic Kernel var kernel = Kernel.CreateBuilder() .AddAzureOpenAIChatCompletion( "gpt-4", "https://qingy-m2e0gbl3-eastus.openai.azure.com", apikey) .Build(); // 創(chuàng)建文本向量生成服務(wù) var textEmbeddingGenerationService = new AzureOpenAITextEmbeddingGenerationService( "text-embedding-3-small", "https://qingy-m2e0gbl3-eastus.openai.azure.com", apikey); // 創(chuàng)建用于保存文本向量的內(nèi)存向量數(shù)據(jù)庫 var memory = new MemoryBuilder() .WithMemoryStore(new VolatileMemoryStore()) .WithTextEmbeddingGeneration(textEmbeddingGenerationService) .Build(); // 從外部文件以Markdown格式讀入內(nèi)容,然后根據(jù)語義產(chǎn)生多個段落 var markdownContent = await File.ReadAllTextAsync(@"input.md"); var paragraphs = TextChunker.SplitMarkdownParagraphs( TextChunker.SplitMarkDownLines(markdownContent.Replace("\r\n", " "), 128), 64); // 將各個段落進行量化并保存到向量數(shù)據(jù)庫 for (var i = 0; i < paragraphs.Count; i++) { await memory.SaveInformationAsync(CollectionName, paragraphs[i], $"paragraph{i}"); } // 創(chuàng)建一個對話完成服務(wù)以及對話歷史對象,用來保存對話歷史,以便后續(xù)為大模型 // 提供對話上下文信息。 var chatCompletionService = kernel.GetRequiredService<IChatCompletionService>(); var chat = new ChatHistory("你是一個AI助手,幫助人們查找信息和回答問題"); StringBuilder additionalInfo = new(); StringBuilder chatResponse = new(); while (true) { Console.Write("請輸入問題>> "); var question = Console.ReadLine()!; additionalInfo.Clear(); // 從向量數(shù)據(jù)庫中找到跟提問最為相近的3條信息,將其添加到對話歷史中 await foreach (var hit in memory.SearchAsync(CollectionName, question, limit: 3)) { additionalInfo.AppendLine(hit.Metadata.Text); } var contextLinesToRemove = -1; if (additionalInfo.Length != 0) { additionalInfo.Insert(0, "以下是一些附加信息:"); contextLinesToRemove = chat.Count; chat.AddUserMessage(additionalInfo.ToString()); } // 將用戶輸入的問題添加到對話中 chat.AddUserMessage(question); chatResponse.Clear(); // 獲取大語言模型的反饋,并將結(jié)果逐字輸出 await foreach (var message in chatCompletionService.GetStreamingChatMessageContentsAsync(chat)) { // 輸出當(dāng)前獲取的結(jié)果字符串 Console.Write(message); // 將輸出內(nèi)容添加到臨時變量中 chatResponse.Append(message.Content); } Console.WriteLine(); // 在進入下一次問答之前,將當(dāng)前回答結(jié)果添加到對話歷史中,為大語言模型提供問答上下文 chat.AddAssistantMessage(chatResponse.ToString()); // 將當(dāng)次問題相關(guān)的內(nèi)容從對話歷史中移除 if (contextLinesToRemove >= 0) chat.RemoveAt(contextLinesToRemove); Console.WriteLine(); }
重新運行程序,然后提出同樣的問題,可以看到,現(xiàn)在的答案就正確了:
現(xiàn)在看看向量數(shù)據(jù)庫中到底有什么。新添加一個對Microsoft.SemanticKernel.Connectors.Sqlite
NuGet包的引用,然后,將上面代碼的:
.WithMemoryStore(new VolatileMemoryStore())
改為:
.WithMemoryStore(await SqliteMemoryStore.ConnectAsync("vectors.db"))
重新運行程序,執(zhí)行成功后,在bin\Debug\net8.0
目錄下,可以找到vectors.db
文件,用Sqlite查看工具(我用的是SQLiteStudio)打開數(shù)據(jù)庫文件,可以看到下面的表和數(shù)據(jù):
Metadata字段保存的就是每個段落的原始數(shù)據(jù)信息,而Embedding字段則是文本向量,其實它就是一系列的浮點值,代表著文本之間在語義上的距離。
使用基于Ollama的本地大語言模型
Semantic Kernel現(xiàn)在已經(jīng)可以支持Ollama本地大語言模型了,雖然它目前也還是預(yù)覽版。可以在項目中通過添加Microsoft.SemanticKernel.Connectors.Ollama
NuGet包來體驗。建議安裝最新版本的Ollama,然后,下載兩個大語言模型,一個是Chat Completion類型的,另一個是Text Embedding類型的。我選擇了llama3.2:3b
和mxbai-embed-large
這兩個模型:
代碼上只需要將Azure OpenAI替換為Ollama即可:
// 初始化Semantic Kernel var kernel = Kernel.CreateBuilder() .AddOllamaChatCompletion( "llama3.2:3b", new Uri("http://localhost:11434")) .Build(); // 創(chuàng)建文本向量生成服務(wù) var textEmbeddingGenerationService = new OllamaTextEmbeddingGenerationService( "mxbai-embed-large:latest", new Uri("http://localhost:11434"));
總結(jié)
通過本文的介紹,應(yīng)該可以對Semantic Kernel、RAG以及在C#中的應(yīng)用有一定的了解,雖然沒有涉及原理性的內(nèi)容,但基本已經(jīng)可以在應(yīng)用層面上提供一定的參考價值。Semantic Kernel雖然有些Plugins還處于預(yù)覽階段,但通過本文的介紹,我們已經(jīng)可以看到它的強大功能,比如,允許我們很方便地接入各種流行的向量數(shù)據(jù)庫,也允許我們很方便地切換到不同的AI大語言模型服務(wù),在AI的應(yīng)用集成上,Semantic Kernel發(fā)揮著重要的作用。
參考
本文部分內(nèi)容參考了微軟官方文檔《Demystifying Retrieval Augmented Generation with .NET》,代碼也部分參考了文中內(nèi)容。文章介紹得更為詳細(xì),建議有興趣的讀者移步閱讀。
到此這篇關(guān)于在C#中基于Semantic Kernel的檢索增強生成(RAG)實踐的文章就介紹到這了,更多相關(guān)C#檢索增強生成內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
C#程序員應(yīng)該養(yǎng)成的程序性能優(yōu)化寫法
工作和生活中經(jīng)??梢钥吹揭恍┏绦蛟?寫代碼的時候只關(guān)注代碼的邏輯性,而不考慮運行效率,其實這對大多數(shù)程序猿來說都是沒有問題的,不過作為一只有理想的CodeMonkey,我還是希望給大家分享一些性能優(yōu)化心得2017-08-08C#使用IronPython庫調(diào)用Python腳本
這篇文章介紹了C#使用IronPython庫調(diào)用Python腳本的方法,文中通過示例代碼介紹的非常詳細(xì)。對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2022-06-06C#用Topshelf創(chuàng)建Windows服務(wù)的步驟分享
這篇文章主要給大家介紹了關(guān)于C#如何利用Topshelf創(chuàng)建Windows服務(wù)的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對大家學(xué)習(xí)或者使用C#具有一定的參考學(xué)習(xí)價值,需要的朋友們下面來一起學(xué)習(xí)學(xué)習(xí)吧2020-05-05