在C#中基于Semantic?Kernel的檢索增強(qiáng)生成(RAG)實(shí)踐記錄
Semantic Kernel簡介
玩過大語言模型(LLM)的都知道OpenAI,然后微軟Azure也提供了OpenAI的服務(wù):Azure OpenAI,只需要申請到API Key,就可以使用這些AI服務(wù)。使用方式可以是通過在線Web頁面直接與AI聊天,也可以調(diào)用AI的API服務(wù),將AI的能力集成到自己的應(yīng)用程序中。不過這些服務(wù)都是在線提供的,都會(huì)需要根據(jù)token計(jì)費(fèi),所以不僅需要依賴互聯(lián)網(wǎng),而且在使用上會(huì)有一定成本。于是,就出現(xiàn)了像Ollama這樣的本地大語言模型服務(wù),只要你的電腦足夠強(qiáng)悍,應(yīng)用場景允許的情況下,使用本地大語言模型也是一個(gè)不錯(cuò)的選擇。
既然有這么多AI服務(wù)可以選擇,那如果在我的應(yīng)用程序中需要能夠很方便地對(duì)接不同的AI服務(wù),應(yīng)該怎么做呢?這就是Semantic Kernel的基本功能,它是一個(gè)基于大語言模型開發(fā)應(yīng)用程序的框架,可以讓你的應(yīng)用程序更加方便地集成大語言模型。Semantic Kernel可用于輕松生成 AI 代理并將最新的 AI 模型集成到 C#、Python 或 Java 代碼庫中。因此,它雖然在.NET AI生態(tài)中扮演著非常重要的角色,但它是支持多編程語言跨平臺(tái)的應(yīng)用開發(fā)套件。
Semantic Kernel主要包含以下這些核心概念:
- 連接(Connection):與外部 AI 服務(wù)和數(shù)據(jù)源交互,比如在應(yīng)用程序中實(shí)現(xiàn)Open AI和Ollama的無縫整合
- 插件(Plugins):封裝應(yīng)用程序可以使用的功能,比如增強(qiáng)提示詞功能,為大語言模型提供更多的上下文信息
- 規(guī)劃器(Planner):根據(jù)用戶行為編排執(zhí)行計(jì)劃和策略
- 內(nèi)存(Memory):抽象并簡化 AI 應(yīng)用程序的上下文管理,比如文本向量(Text Embedding)的存儲(chǔ)等
有關(guān)Semantic Kernel的具體介紹可以參考微軟官方文檔。
演練:通過Semantic Kernel使用Microsoft Azure OpenAI Service
話不多說,直接實(shí)操。這個(gè)演練的目的,就是使用部署在Azure上的gpt-4o大語言模型來實(shí)現(xiàn)一個(gè)簡單的問答系統(tǒng)。
微軟于2024年10月21日終止面向個(gè)人用戶的Azure OpenAI服務(wù),企業(yè)用戶仍能繼續(xù)使用。參考:https://finance.sina.com.cn/roll/2024-10-18/doc-incsymyx4982064.shtml
在Azure中部署大語言模型
登錄Azure Portal,新建一個(gè)Azure AI service,然后點(diǎn)擊Go to Azure OpenAI Studio,進(jìn)入OpenAI Studio:

進(jìn)入后,在左側(cè)側(cè)邊欄的共享資源部分,選擇部署標(biāo)簽頁,然后在模型部署頁面,點(diǎn)擊部署模型按鈕,在下拉的菜單中,選擇部署基本模型:

在選擇模型對(duì)話框中,選擇gpt-4o,然后點(diǎn)擊確認(rèn)按鈕:

在彈出的對(duì)話框部署模型 gpt-4o中,給模型取個(gè)名字,然后直接點(diǎn)擊部署按鈕,如果希望對(duì)模型版本、安全性等做一些設(shè)置,也可以點(diǎn)擊自定義按鈕展開選項(xiàng)。

部署成功后,就可以在模型部署頁面的列表中看到已部署模型的版本以及狀態(tài):

點(diǎn)擊新部署的模型的名稱,進(jìn)入模型詳細(xì)信息頁面,在頁面的終結(jié)點(diǎn)部分,把目標(biāo)URI和密鑰復(fù)制下來,待會(huì)要用。目標(biāo)URI只需要復(fù)制主機(jī)名部分即可,比如https://qingy-m2e0gbl3-eastus.openai.azure.com這樣:

在C#中使用Semantic Kernel實(shí)現(xiàn)問答應(yīng)用
首先創(chuàng)建一個(gè)控制臺(tái)應(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)建一個(gè)對(duì)話完成服務(wù)以及對(duì)話歷史對(duì)象,用來保存對(duì)話歷史,以便后續(xù)為大模型
// 提供對(duì)話上下文信息。
var chatCompletionService = kernel.GetRequiredService<IChatCompletionService>();
var chat = new ChatHistory("你是一個(gè)AI助手,幫助人們查找信息和回答問題");
StringBuilder chatResponse = new();
while (true)
{
Console.Write("請輸入問題>> ");
// 將用戶輸入的問題添加到對(duì)話中
chat.AddUserMessage(Console.ReadLine()!);
chatResponse.Clear();
// 獲取大語言模型的反饋,并將結(jié)果逐字輸出
await foreach (var message in
chatCompletionService.GetStreamingChatMessageContentsAsync(chat))
{
// 輸出當(dāng)前獲取的結(jié)果字符串
Console.Write(message);
// 將輸出內(nèi)容添加到臨時(shí)變量中
chatResponse.Append(message.Content);
}
Console.WriteLine();
// 在進(jìn)入下一次問答之前,將當(dāng)前回答結(jié)果添加到對(duì)話歷史中,為大語言模型提供問答上下文
chat.AddAssistantMessage(chatResponse.ToString());
Console.WriteLine();
}在上面的代碼中,需要將你的API Key和終結(jié)點(diǎn)URI配置進(jìn)去,為了安全性,這里我使用環(huán)境變量保存API Key,然后由程序讀入。為了讓大語言模型能夠了解在一次對(duì)話中,我和它之間都討論了什么內(nèi)容,在代碼中,使用一個(gè)StringBuilder臨時(shí)保存了當(dāng)前對(duì)話的應(yīng)答結(jié)果,然后將這個(gè)結(jié)果又通過Semantic Kernel的AddAssistantMessage方法加回到對(duì)話中,以便在下一次對(duì)話中,大語言模型能夠知道我們在聊什么話題。
比如下面的例子中,在第二次提問時(shí)我問到“有那幾次遷徙?”,AI能知道我是在說人類歷史上的大遷徙,然后將我想要的答案列舉出來:

到這里,一個(gè)簡單的基于gpt-4o的問答應(yīng)用就完成了,它的工作流程大致如下:

AI能回答所有的問題嗎?
由于這里使用的gpt-4o大語言模型是在今年5月份發(fā)布的,而大語言模型都是基于現(xiàn)有數(shù)據(jù)經(jīng)過訓(xùn)練得到的,所以,它應(yīng)該不會(huì)知道5月份以后的事情,遇到這樣的問題,AI只能回答不知道,或者給出一個(gè)比較離譜的答案:

你或許會(huì)想,那我將這些知識(shí)或者新聞文章下載下來,然后基于上面的代碼,將這些信息先添加到對(duì)話歷史中,讓大語言模型能夠了解上下文,這樣回答問題的時(shí)候準(zhǔn)確率不是提高了嗎?這個(gè)思路是對(duì)的,可以在進(jìn)行問答之前,將新聞的文本信息添加到對(duì)話歷史中:
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)生多個(gè)段落
var markdownContent = await File.ReadAllTextAsync(@"input.md");
var paragraphs =
TextChunker.SplitMarkdownParagraphs(
TextChunker.SplitMarkDownLines(markdownContent.Replace("\r\n", " "), 128),
64);
// 將各個(gè)段落進(jìn)行量化并保存到向量數(shù)據(jù)庫
for (var i = 0; i < paragraphs.Count; i++)
{
await memory.SaveInformationAsync(CollectionName, paragraphs[i], $"paragraph{i}");
}
// 創(chuàng)建一個(gè)對(duì)話完成服務(wù)以及對(duì)話歷史對(duì)象,用來保存對(duì)話歷史,以便后續(xù)為大模型
// 提供對(duì)話上下文信息。
var chatCompletionService = kernel.GetRequiredService<IChatCompletionService>();
var chat = new ChatHistory("你是一個(gè)AI助手,幫助人們查找信息和回答問題");
StringBuilder additionalInfo = new();
StringBuilder chatResponse = new();
while (true)
{
Console.Write("請輸入問題>> ");
var question = Console.ReadLine()!;
additionalInfo.Clear();
// 從向量數(shù)據(jù)庫中找到跟提問最為相近的3條信息,將其添加到對(duì)話歷史中
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());
}
// 將用戶輸入的問題添加到對(duì)話中
chat.AddUserMessage(question);
chatResponse.Clear();
// 獲取大語言模型的反饋,并將結(jié)果逐字輸出
await foreach (var message in
chatCompletionService.GetStreamingChatMessageContentsAsync(chat))
{
// 輸出當(dāng)前獲取的結(jié)果字符串
Console.Write(message);
// 將輸出內(nèi)容添加到臨時(shí)變量中
chatResponse.Append(message.Content);
}
Console.WriteLine();
// 在進(jìn)入下一次問答之前,將當(dāng)前回答結(jié)果添加到對(duì)話歷史中,為大語言模型提供問答上下文
chat.AddAssistantMessage(chatResponse.ToString());
// 將當(dāng)次問題相關(guān)的內(nèi)容從對(duì)話歷史中移除
if (contextLinesToRemove >= 0) chat.RemoveAt(contextLinesToRemove);
Console.WriteLine();
}但是這樣做,會(huì)造成下面的異常信息:

這個(gè)問題其實(shí)就跟大語言模型的Context Window有關(guān)。當(dāng)今所有的大語言模型在一次數(shù)據(jù)處理上都有一定的限制,這個(gè)限制就是Context Window,在這個(gè)例子中,我們的模型一次最多處理12萬8千個(gè)token(token是大語言模型的數(shù)據(jù)處理單元,它可以是一個(gè)詞組,一個(gè)單詞或者是一個(gè)字符),而我們卻輸入了147,845個(gè)token,于是就報(bào)錯(cuò)了。很明顯,我們應(yīng)該減少傳入的數(shù)據(jù)量,但這樣又沒辦法把完整的新聞文章信息發(fā)送給大語言模型。此時(shí)就要用到“檢索增強(qiáng)生成(RAG)”。
Semantic Kernel的檢索增強(qiáng)生成(RAG)實(shí)踐
其實(shí),并不一定非要把整篇新聞文章發(fā)給大語言模型,可以換個(gè)思路:只需要在新聞文章中摘出跟提問相關(guān)的內(nèi)容發(fā)送給大語言模型就可以了,這樣就可以大大減小需要發(fā)送到大語言模型的token數(shù)量。所以,這里就出現(xiàn)了額外的一些步驟:
- 對(duì)大量的文檔進(jìn)行預(yù)處理,將文本信息量化并保存下來(Text Embedding)
- 在提出新問題時(shí),根據(jù)問題語義,從保存的文本量化信息(Embeddings)中,找到與問題相關(guān)的信息
- 將這些信息發(fā)送給大語言模型,并從大語言模型獲得應(yīng)答
- 將結(jié)果反饋給調(diào)用方
流程大致如下:

虛線灰色框中就是檢索增強(qiáng)生成(RAG)相關(guān)流程,這里就不針對(duì)每個(gè)標(biāo)號(hào)一一說明了,能夠理解上面所述的4個(gè)大的步驟,就很好理解這張圖中的整體流程。下面我們直接使用Semantic Kernel,通過RAG來增強(qiáng)模型應(yīng)答。
首先,在Azure OpenAI Studio中,按照上文的步驟,部署一個(gè)text-embedding-3-small的模型,同樣將終結(jié)點(diǎn)URI和API Key記錄下來,然后,在項(xiàng)目中添加Microsoft.SemanticKernel.Plugins.Memory NuGet包的引用,因?yàn)槲覀兇蛩阆仁褂没趦?nèi)存的文本向量數(shù)據(jù)庫來運(yùn)行我們的代碼。Semantic Kernel支持多種向量數(shù)據(jù)庫,比如Sqlite,Azure AI Search,Chroma,Milvus,Pinecone,Qdrant,Weaviate等等。在添加引用的時(shí)候,需要使用--prerelease參數(shù),因?yàn)?code>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)生多個(gè)段落
var markdownContent = await File.ReadAllTextAsync(@"input.md");
var paragraphs =
TextChunker.SplitMarkdownParagraphs(
TextChunker.SplitMarkDownLines(markdownContent.Replace("\r\n", " "), 128),
64);
// 將各個(gè)段落進(jìn)行量化并保存到向量數(shù)據(jù)庫
for (var i = 0; i < paragraphs.Count; i++)
{
await memory.SaveInformationAsync(CollectionName, paragraphs[i], $"paragraph{i}");
}
// 創(chuàng)建一個(gè)對(duì)話完成服務(wù)以及對(duì)話歷史對(duì)象,用來保存對(duì)話歷史,以便后續(xù)為大模型
// 提供對(duì)話上下文信息。
var chatCompletionService = kernel.GetRequiredService<IChatCompletionService>();
var chat = new ChatHistory("你是一個(gè)AI助手,幫助人們查找信息和回答問題");
StringBuilder additionalInfo = new();
StringBuilder chatResponse = new();
while (true)
{
Console.Write("請輸入問題>> ");
var question = Console.ReadLine()!;
additionalInfo.Clear();
// 從向量數(shù)據(jù)庫中找到跟提問最為相近的3條信息,將其添加到對(duì)話歷史中
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());
}
// 將用戶輸入的問題添加到對(duì)話中
chat.AddUserMessage(question);
chatResponse.Clear();
// 獲取大語言模型的反饋,并將結(jié)果逐字輸出
await foreach (var message in
chatCompletionService.GetStreamingChatMessageContentsAsync(chat))
{
// 輸出當(dāng)前獲取的結(jié)果字符串
Console.Write(message);
// 將輸出內(nèi)容添加到臨時(shí)變量中
chatResponse.Append(message.Content);
}
Console.WriteLine();
// 在進(jìn)入下一次問答之前,將當(dāng)前回答結(jié)果添加到對(duì)話歷史中,為大語言模型提供問答上下文
chat.AddAssistantMessage(chatResponse.ToString());
// 將當(dāng)次問題相關(guān)的內(nèi)容從對(duì)話歷史中移除
if (contextLinesToRemove >= 0) chat.RemoveAt(contextLinesToRemove);
Console.WriteLine();
}重新運(yùn)行程序,然后提出同樣的問題,可以看到,現(xiàn)在的答案就正確了:

現(xiàn)在看看向量數(shù)據(jù)庫中到底有什么。新添加一個(gè)對(duì)Microsoft.SemanticKernel.Connectors.Sqlite NuGet包的引用,然后,將上面代碼的:
.WithMemoryStore(new VolatileMemoryStore())
改為:
.WithMemoryStore(await SqliteMemoryStore.ConnectAsync("vectors.db"))重新運(yùn)行程序,執(zhí)行成功后,在bin\Debug\net8.0目錄下,可以找到vectors.db文件,用Sqlite查看工具(我用的是SQLiteStudio)打開數(shù)據(jù)庫文件,可以看到下面的表和數(shù)據(jù):

Metadata字段保存的就是每個(gè)段落的原始數(shù)據(jù)信息,而Embedding字段則是文本向量,其實(shí)它就是一系列的浮點(diǎn)值,代表著文本之間在語義上的距離。
使用基于Ollama的本地大語言模型
Semantic Kernel現(xiàn)在已經(jīng)可以支持Ollama本地大語言模型了,雖然它目前也還是預(yù)覽版??梢栽陧?xiàng)目中通過添加Microsoft.SemanticKernel.Connectors.Ollama NuGet包來體驗(yàn)。建議安裝最新版本的Ollama,然后,下載兩個(gè)大語言模型,一個(gè)是Chat Completion類型的,另一個(gè)是Text Embedding類型的。我選擇了llama3.2:3b和mxbai-embed-large這兩個(gè)模型:

代碼上只需要將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)該可以對(duì)Semantic Kernel、RAG以及在C#中的應(yīng)用有一定的了解,雖然沒有涉及原理性的內(nèi)容,但基本已經(jīng)可以在應(yīng)用層面上提供一定的參考價(jià)值。Semantic Kernel雖然有些Plugins還處于預(yù)覽階段,但通過本文的介紹,我們已經(jīng)可以看到它的強(qiá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的檢索增強(qiáng)生成(RAG)實(shí)踐的文章就介紹到這了,更多相關(guān)C#檢索增強(qiáng)生成內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
C#程序員應(yīng)該養(yǎng)成的程序性能優(yōu)化寫法
工作和生活中經(jīng)??梢钥吹揭恍┏绦蛟?寫代碼的時(shí)候只關(guān)注代碼的邏輯性,而不考慮運(yùn)行效率,其實(shí)這對(duì)大多數(shù)程序猿來說都是沒有問題的,不過作為一只有理想的CodeMonkey,我還是希望給大家分享一些性能優(yōu)化心得2017-08-08
C#運(yùn)算符之與,或,異或及移位運(yùn)算小結(jié)
本文是對(duì)C#中的與,或,異或及移位運(yùn)算進(jìn)行了詳細(xì)的介紹,需要的朋友可以過來參考下,希望對(duì)大家有所幫助2013-10-10
C#使用IronPython庫調(diào)用Python腳本
這篇文章介紹了C#使用IronPython庫調(diào)用Python腳本的方法,文中通過示例代碼介紹的非常詳細(xì)。對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2022-06-06
C#用Topshelf創(chuàng)建Windows服務(wù)的步驟分享
這篇文章主要給大家介紹了關(guān)于C#如何利用Topshelf創(chuàng)建Windows服務(wù)的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家學(xué)習(xí)或者使用C#具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面來一起學(xué)習(xí)學(xué)習(xí)吧2020-05-05

