在C#中基于Semantic Kernel的检索增强生成(RAG)实践

news/2024/10/19 17:26:41

Semantic Kernel简介

玩过大语言模型(LLM)的都知道OpenAI,然后微软Azure也提供了OpenAI的服务:Azure OpenAI,只需要申请到API Key,就可以使用这些AI服务。使用方式可以是通过在线Web页面直接与AI聊天,也可以调用AI的API服务,将AI的能力集成到自己的应用程序中。不过这些服务都是在线提供的,都会需要根据token计费,所以不仅需要依赖互联网,而且在使用上会有一定成本。于是,就出现了像Ollama这样的本地大语言模型服务,只要你的电脑足够强悍,应用场景允许的情况下,使用本地大语言模型也是一个不错的选择。

既然有这么多AI服务可以选择,那如果在我的应用程序中需要能够很方便地对接不同的AI服务,应该怎么做呢?这就是Semantic Kernel的基本功能,它是一个基于大语言模型开发应用程序的框架,可以让你的应用程序更加方便地集成大语言模型。Semantic Kernel可用于轻松生成 AI 代理并将最新的 AI 模型集成到 C#、Python 或 Java 代码库中。因此,它虽然在.NET AI生态中扮演着非常重要的角色,但它是支持多编程语言跨平台的应用开发套件。

Semantic Kernel主要包含以下这些核心概念:

  1. 连接(Connection):与外部 AI 服务和数据源交互,比如在应用程序中实现Open AI和Ollama的无缝整合
  2. 插件(Plugins):封装应用程序可以使用的功能,比如增强提示词功能,为大语言模型提供更多的上下文信息
  3. 规划器(Planner):根据用户行为编排执行计划和策略
  4. 内存(Memory):抽象并简化 AI 应用程序的上下文管理,比如文本向量(Text Embedding)的存储等

有关Semantic Kernel的具体介绍可以参考微软官方文档。

演练:通过Semantic Kernel使用Microsoft Azure OpenAI Service

话不多说,直接实操。这个演练的目的,就是使用部署在Azure上的gpt-4o大语言模型来实现一个简单的问答系统。

微软于2024年10月21日终止面向个人用户的Azure OpenAI服务,企业用户仍能继续使用。参考: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:

进入后,在左侧侧边栏的共享资源部分,选择部署标签页,然后在模型部署页面,点击部署模型按钮,在下拉的菜单中,选择部署基本模型

选择模型对话框中,选择gpt-4o,然后点击确认按钮:

在弹出的对话框部署模型 gpt-4o中,给模型取个名字,然后直接点击部署按钮,如果希望对模型版本、安全性等做一些设置,也可以点击自定义按钮展开选项。

部署成功后,就可以在模型部署页面的列表中看到已部署模型的版本以及状态:

点击新部署的模型的名称,进入模型详细信息页面,在页面的终结点部分,把目标URI密钥复制下来,待会要用。目标URI只需要复制主机名部分即可,比如https://qingy-m2e0gbl3-eastus.openai.azure.com这样:

在C#中使用Semantic Kernel实现问答应用

首先创建一个控制台应用程序,然后添加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();// 创建一个对话完成服务以及对话历史对象,用来保存对话历史,以便后续为大模型
// 提供对话上下文信息。
var chatCompletionService = kernel.GetRequiredService<IChatCompletionService>();
var chat = new ChatHistory("你是一个AI助手,帮助人们查找信息和回答问题");
StringBuilder chatResponse = new();while (true)
{Console.Write("请输入问题>> ");// 将用户输入的问题添加到对话中chat.AddUserMessage(Console.ReadLine()!);chatResponse.Clear();// 获取大语言模型的反馈,并将结果逐字输出await foreach (var message inchatCompletionService.GetStreamingChatMessageContentsAsync(chat)){// 输出当前获取的结果字符串Console.Write(message);// 将输出内容添加到临时变量中chatResponse.Append(message.Content);}Console.WriteLine();// 在进入下一次问答之前,将当前回答结果添加到对话历史中,为大语言模型提供问答上下文chat.AddAssistantMessage(chatResponse.ToString());Console.WriteLine();
}

在上面的代码中,需要将你的API Key和终结点URI配置进去,为了安全性,这里我使用环境变量保存API Key,然后由程序读入。为了让大语言模型能够了解在一次对话中,我和它之间都讨论了什么内容,在代码中,使用一个StringBuilder临时保存了当前对话的应答结果,然后将这个结果又通过Semantic Kernel的AddAssistantMessage方法加回到对话中,以便在下一次对话中,大语言模型能够知道我们在聊什么话题。

比如下面的例子中,在第二次提问时我问到“有那几次迁徙?”,AI能知道我是在说人类历史上的大迁徙,然后将我想要的答案列举出来:

到这里,一个简单的基于gpt-4o的问答应用就完成了,它的工作流程大致如下:

AI能回答所有的问题吗?

由于这里使用的gpt-4o大语言模型是在今年5月份发布的,而大语言模型都是基于现有数据经过训练得到的,所以,它应该不会知道5月份以后的事情,遇到这样的问题,AI只能回答不知道,或者给出一个比较离谱的答案:

你或许会想,那我将这些知识或者新闻文章下载下来,然后基于上面的代码,将这些信息先添加到对话历史中,让大语言模型能够了解上下文,这样回答问题的时候准确率不是提高了吗?这个思路是对的,可以在进行问答之前,将新闻的文本信息添加到对话历史中:

chat.AddUserMessage("这是一些额外的信息:" + await File.ReadAllTextAsync("input.txt"));

但是这样做,会造成下面的异常信息:

这个问题其实就跟大语言模型的Context Window有关。当今所有的大语言模型在一次数据处理上都有一定的限制,这个限制就是Context Window,在这个例子中,我们的模型一次最多处理12万8千个token(token是大语言模型的数据处理单元,它可以是一个词组,一个单词或者是一个字符),而我们却输入了147,845个token,于是就报错了。很明显,我们应该减少传入的数据量,但这样又没办法把完整的新闻文章信息发送给大语言模型。此时就要用到“检索增强生成(RAG)”。

Semantic Kernel的检索增强生成(RAG)实践

 其实,并不一定非要把整篇新闻文章发给大语言模型,可以换个思路:只需要在新闻文章中摘出跟提问相关的内容发送给大语言模型就可以了,这样就可以大大减小需要发送到大语言模型的token数量。所以,这里就出现了额外的一些步骤:

  1. 对大量的文档进行预处理,将文本信息量化并保存下来(Text Embedding)
  2. 在提出新问题时,根据问题语义,从保存的文本量化信息(Embeddings)中,找到与问题相关的信息
  3. 将这些信息发送给大语言模型,并从大语言模型获得应答
  4. 将结果反馈给调用方

流程大致如下:

虚线灰色框中就是检索增强生成(RAG)相关流程,这里就不针对每个标号一一说明了,能够理解上面所述的4个大的步骤,就很好理解这张图中的整体流程。下面我们直接使用Semantic Kernel,通过RAG来增强模型应答。

首先,在Azure OpenAI Studio中,按照上文的步骤,部署一个text-embedding-3-small的模型,同样将终结点URI和API Key记录下来,然后,在项目中添加Microsoft.SemanticKernel.Plugins.Memory NuGet包的引用,因为我们打算先使用基于内存的文本向量数据库来运行我们的代码。Semantic Kernel支持多种向量数据库,比如Sqlite,Azure AI Search,Chroma,Milvus,Pinecone,Qdrant,Weaviate等等。在添加引用的时候,需要使用--prerelease参数,因为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, SKEXP0050const 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();// 创建文本向量生成服务
var textEmbeddingGenerationService = new AzureOpenAITextEmbeddingGenerationService("text-embedding-3-small","https://qingy-m2e0gbl3-eastus.openai.azure.com",apikey);// 创建用于保存文本向量的内存向量数据库
var memory = new MemoryBuilder().WithMemoryStore(new VolatileMemoryStore()).WithTextEmbeddingGeneration(textEmbeddingGenerationService).Build();// 从外部文件以Markdown格式读入内容,然后根据语义产生多个段落
var markdownContent = await File.ReadAllTextAsync(@"input.md");
var paragraphs =TextChunker.SplitMarkdownParagraphs(TextChunker.SplitMarkDownLines(markdownContent.Replace("\r\n", " "), 128),64);// 将各个段落进行量化并保存到向量数据库
for (var i = 0; i < paragraphs.Count; i++)
{await memory.SaveInformationAsync(CollectionName, paragraphs[i], $"paragraph{i}");
}// 创建一个对话完成服务以及对话历史对象,用来保存对话历史,以便后续为大模型
// 提供对话上下文信息。
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();// 从向量数据库中找到跟提问最为相近的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();// 获取大语言模型的反馈,并将结果逐字输出await foreach (var message inchatCompletionService.GetStreamingChatMessageContentsAsync(chat)){// 输出当前获取的结果字符串Console.Write(message);// 将输出内容添加到临时变量中chatResponse.Append(message.Content);}Console.WriteLine();// 在进入下一次问答之前,将当前回答结果添加到对话历史中,为大语言模型提供问答上下文chat.AddAssistantMessage(chatResponse.ToString());// 将当次问题相关的内容从对话历史中移除if (contextLinesToRemove >= 0) chat.RemoveAt(contextLinesToRemove);Console.WriteLine();
}

重新运行程序,然后提出同样的问题,可以看到,现在的答案就正确了:

 现在看看向量数据库中到底有什么。新添加一个对Microsoft.SemanticKernel.Connectors.Sqlite NuGet包的引用,然后,将上面代码的:

.WithMemoryStore(new VolatileMemoryStore())

改为:

.WithMemoryStore(await SqliteMemoryStore.ConnectAsync("vectors.db"))

重新运行程序,执行成功后,在bin\Debug\net8.0目录下,可以找到vectors.db文件,用Sqlite查看工具(我用的是SQLiteStudio)打开数据库文件,可以看到下面的表和数据:

Metadata字段保存的就是每个段落的原始数据信息,而Embedding字段则是文本向量,其实它就是一系列的浮点值,代表着文本之间在语义上的距离

使用基于Ollama的本地大语言模型

Semantic Kernel现在已经可以支持Ollama本地大语言模型了,虽然它目前也还是预览版。可以在项目中通过添加Microsoft.SemanticKernel.Connectors.Ollama NuGet包来体验。建议安装最新版本的Ollama,然后,下载两个大语言模型,一个是Chat Completion类型的,另一个是Text Embedding类型的。我选择了llama3.2:3bmxbai-embed-large这两个模型:

代码上只需要将Azure OpenAI替换为Ollama即可:

// 初始化Semantic Kernel
var kernel = Kernel.CreateBuilder().AddOllamaChatCompletion("llama3.2:3b", new Uri("http://localhost:11434")).Build();// 创建文本向量生成服务
var textEmbeddingGenerationService = new OllamaTextEmbeddingGenerationService("mxbai-embed-large:latest", new Uri("http://localhost:11434"));

总结

通过本文的介绍,应该可以对Semantic Kernel、RAG以及在C#中的应用有一定的了解,虽然没有涉及原理性的内容,但基本已经可以在应用层面上提供一定的参考价值。Semantic Kernel虽然有些Plugins还处于预览阶段,但通过本文的介绍,我们已经可以看到它的强大功能,比如,允许我们很方便地接入各种流行的向量数据库,也允许我们很方便地切换到不同的AI大语言模型服务,在AI的应用集成上,Semantic Kernel发挥着重要的作用。

参考

本文部分内容参考了微软官方文档《Demystifying Retrieval Augmented Generation with .NET》,代码也部分参考了文中内容。文章介绍得更为详细,建议有兴趣的读者移步阅读。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.ryyt.cn/news/73508.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈,一经查实,立即删除!

相关文章

南沙C++信奥赛陈老师解一本通题 1286:怪盗基德的滑翔翼

​ 【题目描述】怪盗基德是一个充满传奇色彩的怪盗,专门以珠宝为目标的超级盗窃犯。而他最为突出的地方,就是他每次都能逃脱中村警部的重重围堵,而这也很大程度上是多亏了他随身携带的便于操作的滑翔翼。 有一天,怪盗基德像往常一样偷走了一颗珍贵的钻石,不料却被柯南小朋…

web端ant-design-vue-Anchor锚点组件使用小节(2)

项目开发中有幸遇到了更细化的页面滚动问题,详情中我有多个履约节点子模块,除了正常的锚点和页面联动之外,客户希望我从列表中点击某个履约模块子节点,跳转到选中的履约模块子节点下面;如果没有子节点模块,则跳转到父级履约节点模块。实现这个功能大概这么两步,1、在子节…

netcore grpc

netcore grpc 一、solution创建空解决方案 > dotnet new sln -n Apricot.Grpc二、Grpc.Server创建Apricot.Grpc类库项目 > dotnet new classlib -n Apricot.Grpc# 解决方案添加类库项目> dotnet sln add Apricot.Grpc/Apricot.Grpc.csproj安装依赖 > dotnet add pa…

NOIP 计划 R15

NOIP 计划 R15 \(\def\EZ{\textcolor{#51af44}{\text{EZ}}}\EZ\) 表示简单,10分钟内就能想到。 \(\def\HD{\textcolor{#3173b3}{\text{HD}}}\HD\) 表示中等,能独立想出 \(\def\IN{\textcolor{#be2d23}{\text{IN}}}\IN\) 表示困难,独立思考能想到 \(50\%\) 以上 \(\def\AT{\t…

星际战甲 - 武器配卡

题记部分 一、主武器二、副武器三、近战武器 3.1、弧光蓄电重锤 — 业精于勤荒于嬉,行成于思毁于随 —

web端ant-design-vue-Anchor锚点组件使用小节(1)

web端ant-design-vue-Anchor锚点组件使用小节。项目开发中如果要实现前端页面平滑滚动到指定的位置,Anchor组件是一个好的选择,灵活且平滑,能满足常见的项目需求。最近开发中幸运的用到这个组件,从此对她爱不释手。下面就把开发中遇到的一些问题及源码整理出来,供以后查看…

高等数学 6.2 定积分在几何学上的应用

目录一、平面图形的面积1.直角坐标情形2.极坐标情形二、体积1.旋转体体积2.平行截面面积为已知的立体的体积三、平面曲线的弧长 一、平面图形的面积 1.直角坐标情形 我们已经知道,由曲线 \(y = f(x) (f(x) \geqslant 0)\) 及直线 \(x = a, x = b (a < b)\) 与 \(x\) 轴所围…

Ubuntu 16.04 编译安装Python 2.7.18

安装python 2.7.18(注)使用apt install python安装的版本是2.7.10,该版本对部分项目存在兼容性问题,因此需要手动编译安装 安装python编译环境sudo apt install python-dev pkg-config libreadline-dev libc6-dev libncursesw5-dev build-essential gdb pkg-config libbz2-…