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