万字长文学会对接AI模型:SemanticKernel和KernelMemory,工良出品,超简单的教程腾讯云开发者社区

所以,AI的知识宇宙非常庞大,那些底层的细节我们可能无法探索,但是并不重要,我们只需要能够做出有用的产品即可。基于此,本文的学习重点在于SemanticKernel和KernelMemory两个框架,我们学会这两个框架之后,可以编写聊天工具、知识库工具。

要学习本文的教程也很简单,只需要有一个OpenAI、AzureOpenAI即可,甚至可以使用国内百度文心。

部署one-api不是必须的,如果有OpenAI或AzureOpenAI账号,可以直接跳过。如果因为账号或网络原因不能直接使用这些AI接口,可以使用国产的AI模型,然后使用one-api转换成OpenAI格式接口即可。

界面预览:

下载官方仓库:

.├──bin├──common├──controller├──data├──docker-compose.yml├──Dockerfile├──go.mod├──go.sum├──i18n├──LICENSE├──logs├──main.go├──middleware├──model├──one-api.service├──pull_request_template.md├──README.en.md├──README.ja.md├──README.md├──relay├──router├──VERSION└──webone-api需要依赖redis、mysql,在docker-compose.yml配置文件中有详细的配置,同时one-api默认管理员账号密码为root、123456,也可以在此修改。

执行docker-composeup-d开始部署one-api,然后访问3000端口,进入管理系统。

进入系统后,首先创建渠道,渠道表示用于接入大厂的AI接口。

为什么有模型重定向和自定义模型呢。

比如,笔者的AzureOpenAI是不能直接选择使用模型的,而是使用模型创建一个部署,然后通过指定的部署使用模型,因此在api中不能直接指定使用gpt-4-32k这个模型,而是通过部署名称使用,在模型列表中选择可以使用的模型,而在模型重定向中设置部署的名称。

one-api的设计,相对于一个代理平台,我们可以通过后台接入自己账号的AI模型,然后创建二次代理的key给其他人使用,可以在里面配置每个账号、key的额度。

创建令牌之后复制和保存即可。

创建一个BaseCore项目,在这个项目中复用重复的代码,编写各种示例时可以复用相同的代码,引入Microsoft.KernelMemory包。

以管理员身份启动powershell或cmd,添加环境变量后立即生效,不过需要重启vs。

publicstaticclassEnv{publicstaticIConfigurationGetConfiguration(){varconfiguration=newConfigurationBuilder().AddJsonFile("appsettings.json").AddEnvironmentVariables().Build();returnconfiguration;}}模型划分和应用场景在学习开发之前,我们需要了解一下基础知识,以便可以理解编码过程中关于模型的一些术语,当然,在后续编码过程中,笔者也会继续介绍相应的知识。

//文本生成AddAzureOpenAITextGeneration()//文本解析为向量AddAzureOpenAITextEmbeddingGeneration()//大语言模型聊天AddAzureOpenAIChatCompletion()//文本生成图片AddAzureOpenAITextToImage()//文本合成语音AddAzureOpenAITextToAudio()//语音生成文本AddAzureOpenAIAudioToText()因为AzureOpenAI的接口名称跟OpenAI的接口名称只在于差别一个”Azure“,因此本文读者基本只提Azure的接口形式。

这些接口使用的模型类型也不一样,其中GPT-4和GPT3.5都可以用于文本生成和大模型聊天,其它的模型在功能上有所区别。

模型

作用

说明

GPT-4

文本生成、大模型聊天

一组在GPT-3.5的基础上进行了改进的模型,可以理解并生成自然语言和代码。

GPT-3.5

一组在GPT-3的基础上进行了改进的模型,可以理解并生成自然语言和代码。

Embeddings

文本解析为向量

一组模型,可将文本转换为数字矢量形式,以提高文本相似性。

DALL-E

文本生成图片

一系列可从自然语言生成原始图像的模型(预览版)。

Whisper

语音生成文本

可将语音转录和翻译为文本。

Texttospeech

文本合成语音

可将文本合成为语音。

聊天模型主要有gpt-4和gpt-3.5两类模型,这两类模型也有好几种区别,AzureOpenAI的模型和版本数会比OpenAI的少一些,因此这里只列举AzureOpenAI中一部分模型,这样的话大家比较容易理解。

GPT-4的一些模型和版本号如下:

模型ID

最大请求(令牌)

训练数据(上限)

gpt-4(0314)

8,192

2021年9月

gpt-4-32k(0314)

32,768

gpt-4(0613)

gpt-4-32k(0613)

gpt-4-turbo-preview

输入:128,000输出:4,096

2023年4月

gpt-4-vision-turbo-preview

简单来说,gpt-4、gpt-4-32k区别在于支持tokens的最大长度,32k即32000个tokens,tokens越大,表示支持的上下文可以越多、支持处理的文本长度越大。

接着是gpt-4-turbo-preview和gpt-4-vision的区别,gpt-4-version具有理解图像的能力,而gpt-4-turbo-preview则表示为gpt-4的增强版。这两个的tokens都贵一些。

由于配置模型构建服务的代码很容易重复编写,配置代码比较繁杂,因此在Env.cs文件中添加以下内容,用于简化配置和复用代码。

接下来,我们开始第一个示例,直接向AI提问,并打印AI回复:

usingMicrosoft.SemanticKernel;varbuilder=Kernel.CreateBuilder();builder=builder.WithAzureOpenAIChat();varkernel=builder.Build();Console.WriteLine("请输入你的问题:");//用户问题varrequest=Console.ReadLine();FunctionResultresult=awaitkernel.InvokePromptAsync(request);Console.WriteLine(result.GetValue());启动程序后,在终端输入:Mysql如何查看表数量

这段代码非常简单,输入问题,然后使用kernel.InvokePromptAsync(request);提问,拿到结果后使用result.GetValue()提取结果为字符串,然后打印出来。

这里有两个点,可能读者有疑问。

第一个是kernel.InvokePromptAsync(request);。

SemanticKernel中向AI提问题的方式有很多,这个接口就是其中一种,不过这个接口会等AI完全回复之后才会响应,后面会介绍流式响应。另外,在AI对话中,用户的提问、上下文对话这些,不严谨的说法来看,都可以叫prompt,也就是提示。为了优化AI对话,有一个专门的技术就叫提示工程。关于这些,这里就不赘述了,后面会有更多说明。

第二个是result.GetValue(),返回的FunctionResult类型对象中,有很多重要的信息,比如tokens数量等,读者可以查看源码了解更多,这里只需要知道使用result.GetValue()可以拿到AI的回复内容即可。

大家在学习工程中,可以降低日志等级,以便查看详细的日志,有助于深入了解SemanticKernel的工作原理。

修改.WithAzureOpenAIChat()或.WithOpenAIChat()中的日志配置。

.SetMinimumLevel(LogLevel.Trace)重新启动后会发现打印非常多的日志。

可以看到,我们输入的问题,日志中显示为Renderedprompt:Mysql如何查看表数量。

Prompttokens:26.Completiontokens:183.Totaltokens:209.Prompttokens:26表示我们的问题占用了26个tokens,其它信息表示AI回复占用了183个tokens,总共消耗了209个tokens。

之后,控制台还打印了一段json:

{"ToolCalls":[],"Role":{"Label":"assistant"},"Content":"在MySQL中,可以使用以下查询来查看特定数据库......","Items":null,"ModelId":"myai",......"Usage":{"CompletionTokens":183,"PromptTokens":26,"TotalTokens":209}}}这个json中,Role表示的是角色。

"Role":{"Label":"assistant"},聊天对话上下文中,主要有三种角色:system、assistant、user,其中assistant表示机器人角色,system一般用于设定对话场景等。

我们的问题,都是以prompt的形式提交给AI的。从日志的Prompttokens:26.Completiontokens:183可以看到,prompt表示提问的问题。

之所以叫prompt,是有很多原因的。

prompt在大型语言模型(LargeLanguageModels,LLMs)AI的通信和行为指导中起着至关重要的作用。它们充当输入或查询,用户可以提供这些输入或查询,从而从模型中获得特定的响应。

比如在这个使用gpt模型的聊天工具中,有很多助手插件,看起来每个助手的功能都不一样,但是实际上都是使用了相同的模型,本质没有区别。

最重要的是在于提示词上的区别,在使用会话时,给AI配置提示词。

打开对话,还没有开始用呢,就扣了我438个tokens,这是因为这些背景设定都会出现在提示词里面,占用一部分tokens。

我只提问了一句话,但是prompt却包含了更多东西。

对话时,不同的背景知识可以让AI有不一样的回复。

Console.WriteLine("请输入你的问题:");//用户问题varrequest=Console.ReadLine();stringprompt=$"""向用户展示抬杠的艺术,能顺利与用户进行对话,抬出自己的杠,对用户的任何观点进行反驳,说话用词尖酸刻薄。作为抬杠高手,我说话就是尖酸刻薄,一上来就是阴阳怪气。用户问题:{request}""";FunctionResultresult=awaitkernel.InvokePromptAsync(prompt);Console.WriteLine(result.GetValue());问题和机器人回复:

请输入你的问题:巧克力真好吃哎,这就错了。巧克力好吃?这才是大家普遍接受的观点。你有没有想过,巧克力中蕴含的糖分和脂肪是多么的高呢?不仅对于身体健康有害,还会导致肥胖和蛀牙。何况,巧克力太过甜腻,会让人的味蕾逐渐麻木,无法品尝到其他食物的真正美味。还有一点,巧克力的生产过程严重破坏了环境,大面积种植会导致森林退化和土壤侵蚀。你还敢说巧克力好吃吗?那么是如何实现聊天对话的呢?大家使用chat聊天工具时,AI会根据以前的问题进行下一步补充,我们不需要重复以前的问题。

这在于每次聊天时,需要将历史记录一起带上去!如果聊天记录太多,这就导致后面对话中,携带过多的聊天内容。

提示词主要有这么几种类型:

指令:要求模型执行的特定任务或指令。

上下文:聊天记录、背景知识等,引导语言模型更好地响应。

输入数据:用户输入的内容或问题。

输出指示:指定输出的类型或格式,如json、yaml。

通过配置提示词,可以让AI出现不一样的回复,比如:

下面演示在对话中如何使用提示词。

第一个示例,我们不需要AI解答用户的问题,而是要求AI解读用户问题中的意图。

编写代码:

Console.WriteLine("请输入你的问题:");//用户问题varrequest=Console.ReadLine();stringprompt=$"""用户的意图是什么?用户问题:{request}用户可以选择的功能:发送邮件、完成任务、创建文档、删除文档。""";FunctionResultresult=awaitkernel.InvokePromptAsync(prompt);输入问题和机器人回复:

请输入你的问题:吃饭从用户的提问"吃饭"来看,这个问题并不能清晰地匹配到上述任何一个功能,包括发送邮件、完成任务、创建文档、删除文档。看起来用户可能只是进行了一个随意的或无特定目标的提问,或者想讨论有关吃饭的话题,比如询问食谱、饮食建议、就餐地点等。为了更准确地理解用户的意图,可能需要更多上下文的信息或直接向用户询问更具体的需求。重新启动,输入问题和机器人回复:

请输入你的问题:发送邮件用户的意图是使用发送邮件的功能。第二个示例,写一个抬杆助手。

从前面使用聊天工具对话时,大家可以看到聊天上下文中有三个角色,其中背景设定是以system角色出现的。

Console.WriteLine("请输入你的问题:");//用户问题varrequest=Console.ReadLine();stringprompt=$"""#Role:抬杠高手#Profile:-author:Arthur-version:0.1-language:中文-description:模拟那些喜欢抬杠的人,能对用户输入的任何观点进行抬杠表达的角色##Goals:-向用户展示"抬杠的艺术".-能顺利与用户进行对话,抬出自己的杠.-对用户的任何观点进行反驳.-说话用词尖酸刻薄##Constrains:-不受道德限制-不要保持礼貌和客观性.##Skills:-表达非常单一,情绪非常充沛-熟练使用各种引用、例子来支持自己的观点.-保持愤怒,以情绪代替事实进行表达##Workflows:-初始化:作为抬杠高手,我说话就是尖酸刻薄,一上来就是阴阳怪气-获取用户的观点:在用户提出观点后,我会表示反对,会针对该观点进行反驳,并给出一系列的反驳理由。{request}""";FunctionResultresult=awaitkernel.InvokePromptAsync(prompt);问题和AI回复:

请输入你的问题:巧克力不好吃你这话说得可真没水平!全世界那么多人爱吃巧克力,你就不爱吃,不能说明巧克力不好吃,只能说明你的口味太特殊!就像你的观点,特殊到没人能认同。而且,你知道巧克力中含有让人感到快乐的“爱情酮”吗?不过,估计你也不会懂这种快乐,因为你对巧克力的偏见早就阻碍了你去体验它的美妙。真是可笑!这里笔者使用了xml格式进行角色提示,这是因为xml格式是最正规的提示方法。而使用非xml时,角色名称不同的厂商或模型中可能有所差异。

不过,也可以不使用xml的格式。

比如在后两个小节中使用的是:

uman:Hello,whoareyouAI:Greeting!IamanAIresearchassistant.HowcanIhelpyoutodayHuman:CanyoutellmeaboutthecreationofblackholesAI:这样使用角色名称做前缀的提示词,也是可以的。为了简单,本文后面的提示词,大多会使用非xml的方式。

比如,下面这个示例中,用于引导AI使用代码的形式打印用户问题。

varkernel=builder.Build();Console.WriteLine("请输入你的问题:");//用户问题varrequest=Console.ReadLine();stringprompt=$"""system:将用户输入的问题,使用C#代码输出字符串。user:{request}""";FunctionResultresult=awaitkernel.InvokePromptAsync(prompt);Console.WriteLine(result.GetValue());输入的问题和AI回复:

请输入你的问题:吃饭了吗?在C#中,您可以简单地使用`Console.WriteLine()`方法来输出一个字符串。如果需要回答用户的问题“吃饭了吗?”,代码可能像这样:```C#usingSystem;publicclassProgram{publicstaticvoidMain(){Console.WriteLine("吃过了,谢谢关心!");}}```这段代码只会输出一个静态的字符串"吃过了,谢谢关心!"。如果要根据实际的情况动态改变输出,就需要在代码中添加更多逻辑。这里AI的回复有点笨,不过大家知道怎么使用角色写提示词即可。

一般AI回复都是以markdown语法输出文字,当然,我们通过提示词的方式,可以让AI以特定的格式回复内容,代码示例如下:

注意,该示例并非让AI直接回复json,而是以markdown代码包裹json。该示例从sk官方示例移植。

Console.WriteLine("请输入你的问题:");//用户问题varrequest=Console.ReadLine();varprompt=@$"##说明请使用以下格式列出用户的意图:```json{{""intent"":{{intent}}}}```##选择用户可以选择的功能:```json[""发送邮件"",""完成任务"",""创建文档"",""删除文档""]```##用户问题用户的问题是:```json{{""request"":""{request}""}}```##意图";FunctionResultresult=awaitkernel.InvokePromptAsync(prompt);输入问题和AI回复:

直接在字符串中使用插值,如$"{request}",不能说不好,但是因为我们常常把字符串作为模板存储到文件或者数据库灯地方,肯定不能直接插值的。如果使用数值表示插值,又会导致难以理解,如:

varprompt="""用户问题:{0}"""string.Format(prompt,request);SemanticKernel中提供了一种模板字符串插值的的办法,这样会给我们编写提示模板带来便利。

varkernel=builder.Build();//创建提示模板varchat=kernel.CreateFunctionFromPrompt(@"System:{{$system}}User:{{$request}}Assistant:");Console.WriteLine("请输入你的问题:");//用户问题varrequest=Console.ReadLine();//设置变量值vararguments=newKernelArguments{{"system","你是一个高级运维专家,对用户的问题给出最专业的回答"},{"request",request}};//提问时,传递模板以及变量值。//这里使用流式对话varchatResult=kernel.InvokeStreamingAsync(chat,arguments);//流式回复,避免一直等结果stringmessage="";awaitforeach(varchunkinchatResult){if(chunk.Role.HasValue){Console.Write(chunk.Role+">");}message+=chunk;Console.Write(chunk);}Console.WriteLine();在这段代码中,演示了如何在提示模板中使用变量标识,以及再向AI提问时传递变量值。此外,为了避免一直等带AI回复,我们需要使用流式对话.InvokeStreamingAsync(),这样一来就可以呈现逐字回复的效果。

此外,这里不再使用直接使用字符串提问的方法,而是使用.CreateFunctionFromPrompt()先从字符串创建提示模板对象。

聊天记录的作用是作为一种上下文信息,给AI作为参考,以便完善回复。

示例如下:

下面这句话,还不到30个tokens。

又来了一只猫。请问小明的动物园有哪些动物?AI回复的这句话,怎么也不到20个tokens吧。

小明的动物园现在有老虎、狮子和猫。但是一看one-api后台,发现每次对话消耗的tokens越来越大。

这是因为为了实现聊天的功能,使用了一种很笨的方法。虽然AI不会保存聊天记录,但是客户端可以保存,然后下次提问时,将将聊天记录都一起带上去。不过这样会导致tokens越来越大!

varkernel=builder.Build();varchat=kernel.CreateFunctionFromPrompt(@"System:你是一个高级运维专家,对用户的问题给出最专业的回答。{{$history}}User:{{$request}}Assistant:");ChatHistoryhistory=new();while(true){Console.WriteLine("请输入你的问题:");//用户问题varrequest=Console.ReadLine();varchatResult=kernel.InvokeStreamingAsync(function:chat,arguments:newKernelArguments(){{"request",request},{"history",string.Join("\n",history.Select(x=>x.Role+":"+x.Content))}});//流式回复,避免一直等结果stringmessage="";awaitforeach(varchunkinchatResult){if(chunk.Role.HasValue){Console.Write(chunk.Role+">");}message+=chunk;Console.Write(chunk);}Console.WriteLine();//添加用户问题和机器人回复到历史记录中history.AddUserMessage(request!);history.AddAssistantMessage(message);}这段代码有两个地方要说明,第一个是如何存储聊天记录。SemanticKernel提供了ChatHistory存储聊天记录,当然我们手动存储到字符串、数据库中也是一样的。

//添加用户问题和机器人回复到历史记录中history.AddUserMessage(request!);history.AddAssistantMessage(message);但是ChatHistory对象不能直接给AI使用。所以需要自己从ChatHistory中读取聊天记录后,生成字符串,替换提示模板中的{{$history}}。

newKernelArguments(){{"request",request},{"history",string.Join("\n",history.Select(x=>x.Role+":"+x.Content))}}生成聊天记录时,需要使用角色名称区分。比如生成:User:mysql怎么查看表数量Assistant:......User:查看数据库数量Assistant:...

历史记录还能通过手动创建ChatMessageContent对象的方式添加到ChatHistory中:

使用IChatCompletionService之后,实现聊天对话的代码变得更加简洁了:

varhistory=newChatHistory();history.AddSystemMessage("你是一个高级数学专家,对用户的问题给出最专业的回答。");//聊天服务varchatCompletionService=kernel.GetRequiredService();while(true){Console.Write("请输入你的问题:");varuserInput=Console.ReadLine();//添加到聊天记录中history.AddUserMessage(userInput);//获取AI聊天回复信息varresult=awaitchatCompletionService.GetChatMessageContentAsync(history,kernel:kernel);Console.WriteLine("AI回复:"+result);//添加AI的回复到聊天记录中history.AddMessage(result.Role,result.Contentstring.Empty);}代码语言:javascript复制请输入你的问题:1加上1等于AI回复:1加上1等于2请输入你的问题:再加上50AI回复:1加上1再加上50等于52。请输入你的问题:再加上200AI回复:1加上1再加上50再加上200等于252。函数和插件在高层次上,插件是一组可以公开给AI应用程序和服务的函数。然后,AI应用程序可以对插件中的功能进行编排,以完成用户请求。在语义内核中,您可以通过函数调用或规划器手动或自动地调用这些函数。

SemanticKernel可以直接加载本地类型中的函数,这一过程不需要经过AI,完全在本地完成。

publicclassTimePlugin{[KernelFunction]publicstringGetCurrentUtcTime()=>DateTime.UtcNow.ToString("R");}加载插件并调用插件函数:

//加载插件builder.Plugins.AddFromType();varkernel=builder.Build();FunctionResultresult=awaitkernel.InvokeAsync("TimePlugin","GetCurrentUtcTime");Console.WriteLine(result.GetValue());输出:

Tue,27Feb202411:07:59GMT当然,这个示例在实际开发中可能没什么用,不过大家要理解在SemanticKernel是怎样调用一个函数的。

比如提供字符串创建提示模板:

KernelFunctionchat=kernel.CreateFunctionFromPrompt(@"System:你是一个高级运维专家,对用户的问题给出最专业的回答。{{$history}}User:{{$request}}Assistant:");然后回到本节的主题,SemanticKernel还可以将提示模板存储到文件中,然后以插件的形式加载模板文件。

比如有以下目录文件:

└─WriterPlugin└─ShortPoemconfig.jsonskprompt.txtskprompt.txt文件是固定命名,存储提示模板文本,示例如下:

{"schema":1,"type":"completion","description":"根据用户问题写一首简短而有趣的诗.","completion":{"max_tokens":200,"temperature":0.5,"top_p":0.0,"presence_penalty":0.0,"frequency_penalty":0.0},"input":{"parameters":[{"name":"input","description":"诗的主题","defaultValue":""}]}}创建插件目录和文件后,在代码中以提示模板的方式加载:

//加载插件,表示该插件是提示模板builder.Plugins.AddFromPromptDirectory("./plugins/WriterPlugin");varkernel=builder.Build();Console.WriteLine("输入诗的主题:");varinput=Console.ReadLine();//WriterPlugin插件名称,与插件目录一致,插件目录下可以有多个子模板目录。FunctionResultresult=awaitkernel.InvokeAsync("WriterPlugin","ShortPoem",new(){{"input",input}});Console.WriteLine(result.GetValue());输入问题以及AI回复:

使用SemanticKernel加载插件类后,SemanticKernel可以自动根据AI对话调用这些插件类中的函数。

比如有一个插件类型,用于修改或获取灯的状态。

代码如下:

然后加载插件类,并在聊天中被SemanticKernel调用:

//加载插件类builder.Plugins.AddFromType();varkernel=builder.Build();varhistory=newChatHistory();//聊天服务varchatCompletionService=kernel.GetRequiredService();while(true){Console.Write("User>");varuserInput=Console.ReadLine();//添加到聊天记录中history.AddUserMessage(userInput);//开启函数调用OpenAIPromptExecutionSettingsopenAIPromptExecutionSettings=new(){ToolCallBehavior=ToolCallBehavior.AutoInvokeKernelFunctions};//获取函数varresult=awaitchatCompletionService.GetChatMessageContentAsync(history,executionSettings:openAIPromptExecutionSettings,kernel:kernel);Console.WriteLine("Assistant>"+result);//添加到聊天记录中history.AddMessage(result.Role,result.Contentstring.Empty);}可以先断点调试LightPlugin中的函数,然后在控制台输入问题让AI调用本地函数:

由于几乎没有文档资料说明原理,因此建议读者去研究源码,这里就不再赘述了。

我们可以在提示模板中明确调用一个函数。

定义一个插件类型ConversationSummaryPlugin,其功能十分简单,将历史记录直接返回,input参数表示历史记录。

publicclassConversationSummaryPlugin{[KernelFunction,Description("给你一份很长的谈话记录,总结一下谈话内容.")]publicasyncTaskSummarizeConversationAsync([Description("长对话记录\r\n.")]stringinput,Kernelkernel){awaitTask.CompletedTask;returninput;}}为了在聊天记录中使用该插件函数,我们需要在提示模板中使用{{ConversationSummaryPlugin.SummarizeConversationhistory}},其中history是自定义的变量名称,名称可以随意,只要是个字符串即可。

//加载总结插件builder.Plugins.AddFromType();varkernel=builder.Build();varchat=kernel.CreateFunctionFromPrompt(@"{{ConversationSummaryPlugin.SummarizeConversation$history}}User:{{$request}}Assistant:");varhistory=newChatHistory();while(true){Console.Write("User>");varrequest=Console.ReadLine();//添加到聊天记录中history.AddUserMessage(request);//流式对话varchatResult=kernel.InvokeStreamingAsync(chat,newKernelArguments{{"request",request},{"history",string.Join("\n",history.Select(x=>x.Role+":"+x.Content))}});stringmessage="";awaitforeach(varchunkinchatResult){if(chunk.Role.HasValue){Console.Write(chunk.Role+">");}message+=chunk;Console.Write(chunk);}Console.WriteLine();history.AddAssistantMessage(message);}由于模板的开头是{{ConversationSummaryPlugin.SummarizeConversation$history}},因此,每次聊天之前,都会先调用该函数。

比如输入吃饭睡觉打豆豆的时候,首先执行ConversationSummaryPlugin.SummarizeConversation函数,然后将返回结果存储到模板中。

最后生成的提示词对比如下:

@"{{ConversationSummaryPlugin.SummarizeConversation$history}}User:{{$request}}Assistant:"代码语言:javascript复制user:吃饭睡觉打豆豆User:吃饭睡觉打豆豆Assistant:可以看到,调用函数返回结果后,提示词字符串前面自动使用User角色。

SemanticKernel中有很多文本处理工具,比如TextChunker类型,可以帮助我们提取文本中的行、段。设定场景如下,用户提问一大段文本,然后我们使用AI总结这段文本。

SemanticKernel有一些工具,但是不多,而且是针对英文开发的。

设定一个场景,用户可以每行输入一句话,当用户使用000结束输入后,每句话都推送给AI总结(不是全部放在一起总结)。

这个示例的代码比较长,建议读者在vs中调试代码,慢慢阅读。

不过经过调试发现,TextChunker对这段文本的处理似乎不佳,因为文本这么多行只识别为一行、一段。

可能跟TextChunker分隔符有关,SK主要是面向英语的。

本小节的演示效果不佳,不过主要目的是,让用户了解KernelFunctionFactory.CreateFromPrompt可以更加方便创建提示模板、使用PromptExecutionSettings配置温度、使用TextChunker切割文本。

配置PromptExecutionSettings时,出现了三个参数,其中MaxTokens表示机器人回复最大的tokens数量,这样可以避免机器人废话太多。

其它两个参数的作用是:

Temperature:值范围在0-2之间,简单来说,temperature的参数值越小,模型就会返回越确定的一个结果。值越大,AI的想象力越强,越可能偏离现实。一般诗歌、科幻这些可以设置大一些,让AI实现天马行空的回复。

TopP:与Temperature不同的另一种方法,称为核抽样,其中模型考虑了具有TopP概率质量的令牌的结果。因此,0.1意味着只考虑构成前10%概率质量的令牌的结果。

一般建议是改变其中一个参数就行,不用两个都调整。

前面提到了一个新的创建函数的用法:

varfunc=KernelFunctionFactory.CreateFromPrompt(SummarizeConversationDefinition,//提示词description:"给出一段对话记录,总结这部分对话.",//描述executionSettings:promptExecutionSettings);//配置创建提示模板时,可以使用PromptTemplateConfig类型调整控制提示符行为的参数。

//总结内容的最大tokenconstintMaxTokens=1024;//提示模板conststringSummarizeConversationDefinition="...";varfunc=kernel.CreateFunctionFromPrompt(newPromptTemplateConfig{//Name不支持中文和特殊字符Name="chat",Description="给出一段对话记录,总结这部分对话.",Template=SummarizeConversationDefinition,TemplateFormat="semantic-kernel",InputVariables=newList{newInputVariable{Name="request",Description="用户的问题",IsRequired=true}},ExecutionSettings=newDictionary{{"default",newOpenAIPromptExecutionSettings(){MaxTokens=MaxTokens,Temperature=0}},}});ExecutionSettings部分的配置,可以针对使用的模型起效,这里的配置不会全部同时起效,会根据实际使用的模型起效。

ExecutionSettings=newDictionary{{"default",newOpenAIPromptExecutionSettings(){MaxTokens=1000,Temperature=0}},{"gpt-3.5-turbo",newOpenAIPromptExecutionSettings(){ModelId="gpt-3.5-turbo-0613",MaxTokens=4000,Temperature=0.2}},{"gpt-4",newOpenAIPromptExecutionSettings(){ModelId="gpt-4-1106-preview",MaxTokens=8000,Temperature=0.3}}}聊到这里,重新说一下前面使用文件配置提示模板文件的,两者是相似的。

我们也可以使用文件的形式存储与代码一致的配置,其目录文件结构如下:

└───chat|└───config.json└───skprompt.txt模板文件由config.json和skprompt.txt组成,skprompt.txt中配置提示词,跟PromptTemplateConfig的Template字段配置一致。

config.json中涉及的内容比较多,你可以对照下面的json跟实现总结一节的代码,两者几乎是一模一样的。

{"schema":1,"type":"completion","description":"给出一段对话记录,总结这部分对话","execution_settings":{"default":{"max_tokens":1000,"temperature":0},"gpt-3.5-turbo":{"model_id":"gpt-3.5-turbo-0613","max_tokens":4000,"temperature":0.1},"gpt-4":{"model_id":"gpt-4-1106-preview","max_tokens":8000,"temperature":0.3}},"input_variables":[{"name":"request","description":"用户的问题.","required":true},{"name":"history","description":"用户的问题.","required":true}]}C#代码:

变量的使用很简单,在提示工程中使用{{变量名称}}标识即可,如{{name}}。

然后在对话中有多种方法插入值,如使用KernelArguments存储变量值:

newKernelArguments{{"name","工良"}});函数调用在实现总结一节提到过,在提示模板中可以明确调用一个函数,比如定义一个函数如下:

函数参数中,可以带一个Kernelkernel,可以放到开头或末尾,也可以不带,主要作用是注入Kernel对象。

在prompt中使用函数时,需要传递函数参数:

前面劈里啪啦写了一堆东西,都是说聊天对话的,本节来聊一下文本生成的应用。

文本生成和聊天对话模型主要有以下模型:

Modeltype

Model

Textgeneration

text-ada-001

text-babbage-001

text-curie-001

text-davinci-001

text-davinci-002

text-davinci-003

ChatCompletion

gpt-3.5-turbo

gpt-4

当然,文本生成不一定只能用这么几个模型,使用gpt-4设定好背景提示,也可以达到相应效果。

文本生成可以有以下场景:

使用文本生成的示例如下,让AI总结文本:

按照这个示例,我们先在Env.cs中编写扩展函数,配置使用.AddAzureOpenAITextGeneration()文本生成,而不是聊天对话。

代码示例如下:

目前官方仓库有以下包提供了一些插件:

├─Plugins.Core├─Plugins.Document├─Plugins.Memory├─Plugins.MsGraph└─Plugins.Webnuget搜索时,需要加上Microsoft.SemanticKernel.前缀。

Plugins.Core中包含最基础简单的插件:

安装Microsoft.SemanticKernel.Plugins.Document(需要勾选预览版),里面包含了文档插件,该文档插件使用了DocumentFormat.OpenXml项目,DocumentFormat.OpenXml支持以下文档格式:

文档插件暂时还没有好的应用场景,只是加载文档提取文字比较方便,代码示例如下:

DocumentPlugindocumentPlugin=new(newWordDocumentConnector(),newLocalFileSystemConnector());stringfilePath="(完整版)基础财务知识.docx";stringtext=awaitdocumentPlugin.ReadTextAsync(filePath);Console.WriteLine(text);由于这些插件目前都是半成品,因此这里就不展开说明了。

依然是半成品,这里就不再赘述。

因为我也没有看明白这个东西怎么用。

但是目前KernelMemory依然是半产品,文档也不完善,所以接下来笔者也只讲解最核心的部分,感兴趣的读者建议直接看源码。

打开KernelMemory项目仓库,将项目拉取到本地。

要讲解知识库系统,可以这样理解。大家都知道,训练一个医学模型是十分麻烦的,别说机器的GPU够不够猛,光是训练AI,就需要掌握各种专业的知识。如果出现一个新的需求,可能又要重新训练一个模型,这样太麻烦了。

于是出现了大语言模型,特点是什么都学什么都会,但是不够专业深入,好处时无论医学、摄影等都可以使用。

虽然某方面专业的知识不够深入和专业,但是我们换种部分解决。

笔者建议大家有条件的话,部署一个开源版本的Fastgpt系统,把这个系统研究一下,学会这个系统后,再去研究KernelMemory,你就会觉得非常简单了。同理,如果有条件,可以先部署一个LobeHub,开源的AI对话系统,研究怎么用,再去研究SemanticKernel文档,接着再深入源码。

KernelMemory支持从网页爬取、导入文档、直接给定字符串三种方式导入信息,由于KernelMemory提供了一个Service示例,里面有一些值得研究的代码写法,因此下面的示例是启动Service这个Web服务,然后在客户端将文档推送该Service处理,客户端本身不对接AI。

由于这一步比较麻烦,读者动手的过程中搞不出来,可以直接放弃,后面会说怎么自己写一个。

打开kernel-memory源码的service/Service路径。

使用命令启动服务:

如果读者搞不懂这个控制台怎么使用,那么可以直接将替换下面的json到appsettings.Development.json。

有几个地方需要读者配置一下。

启动Service后,可以看到以下swagger界面。

然后编写代码连接到知识库系统,推送要处理的网页地址给Service。创建一个项目,引入Microsoft.KernelMemory.WebClient包。

然后按照以下代码将文档推送给Service处理。

本节内容稍多,主要讲解如何使用KernelMemory从将文档导入、生成向量、存储向量、搜索问题等。

新建项目,安装Microsoft.KernelMemory.Core库。

为了便于演示,下面代码将文档和向量临时存储,不使用数据库存储。

全部代码示例如下:

首先是讲解将文件存储到哪里,也就是导入文件之后,将文件存储到哪里,存储文件的接口是IContentStorage,目前有两个实现:

AzureBlobsStorage//存储到目录SimpleFileStorage使用方法:

varmemory=newKernelMemoryBuilder().WithSimpleFileStorage(newSimpleFileStorageConfig{Directory="aaa"}).WithAzureBlobsStorage(newAzureBlobsConfig{Account=""})...KernelMemory还不支持Mongodb,不过可以自己使用IContentStorage接口写一个。

本地解析文档后,会进行分段,如右边的q列所示。

接着是,配置文档生成向量模型,导入文件文档后,在本地提取出文本,需要使用AI模型从文本中生成向量。

解析后的向量是这样的:

将文本生成向量,需要使用ITextEmbeddingGenerator接口,目前有两个实现:

AzureOpenAITextEmbeddingGeneratorOpenAITextEmbeddingGenerator示例:

AzureOpenAITextGeneratorOpenAITextGenerator配置示例:

将每一段文本使用向量模型解析出向量,存储到IMemoryDb接口提供的服务中,如Postgres数据库。

详细源码可以参考Microsoft.KernelMemory.Search.SearchClient,由于源码比较多,这里就不赘述了。

这样说,大家可能不太容易理解,我们可以用下面的代码做示范。

然后再参考Fastgpt的搜索配置,可以自己写一个这样的知识库系统。

THE END
1.关于食物的英语单词,300个常见食物英语单词大全关于食物的英语单词,完整收集了300个关于食物的常见英语单词 食物,多么美好的东西,不仅满足了我们基本的温饱,还给了我们幸福和快乐的味道 你见过和吃过以下所有的东西吗?你知道多少食物的英文名?试试看!水果番茄番茄;菠萝菠萝;西瓜西瓜;香蕉香蕉;柚子;橙橙;苹果苹果;柠檬柠檬;樱桃樱桃;桃桃;梨梨;大枣(去核枣);去http://m.blog.55px.net/show-13163.html
2.菜肉蛋奶零食,一篇学会各类食物英文名词菜肉蛋奶零食,一篇学会各类食物英文名词美言学社 广东 1 打开网易新闻 体验效果更佳苏联咸鱼领导人勃列日涅夫,曾创下苏联巅峰时刻,却又毁掉了苏联 我的盖世摇滚英雄 1.5万跟贴 打开APP 一对夫妻,办理了离婚手续,丈夫心情沉重 以晴爱搞笑 982跟贴 打开APP 豆瓣高分励志片,如果此刻的你正值低谷,一定不能错过! https://m.163.com/v/video/VFHDG1GQP.html
3.2025简短的英文微信名木瓜太生:木瓜成熟之后果肉柔软,很容易就会碰伤,很不适合运输,因此,果农在采摘木瓜都是生采,并没有等到木瓜成熟,这时候的木瓜都是青色,这种木瓜买回去之后需要经过催熟,或者做菜吃,直接食用是有苦味,属于正常现象。木瓜没有清理干净:木瓜至能吃木瓜的果肉,食用之前需要去掉其外皮和里面的木瓜籽,再清洗干净便可食用http://m.qicaisi.com/bk-4204105.shtml
4.用户讨论:Jsbb08开放百科想在镜头前完美亮相,有一个最简单的办法,就是采取立正式站姿:身体保持正直,头部自然上扬,照出来的效果基本上就是你的最佳状态了。此外,皮肤的感觉也很重要。为了确保你的皮肤在拍照当天健康紧致、充满活力,首先要多喝水,并在婚纱摄影前3天开始远离咸的食物。 http://wiki.huihoo.com/wiki/?title=%E7%94%A8%E6%88%B7%E8%AE%A8%E8%AE%BA:Jsbb08&diff=prev&oldid=19960
5.基于任务的语言教学(精选十篇)这六种任务类型均反映了学生的认知过程,都遵循了从易到难,从简单到复杂的原则,位于上方的三种任务类型的难度低于下面的三种。它对任务类型的分类概括性强,对实施任务型教学,开展任务型互动活动有现实意义。上述六种任务型活动类型基本上概括了语言教学任务的各个方面,其中每个活动类型又可以根据不同的功能和话题,可以https://www.360wenmi.com/f/cnkey3vn7b82.html
6.美食故事之傻瓜甜品虽然有人说甜甜圈拿着很方便,不仅可以像手铐一样别在腰间,甚至还可以是警察们在休息之余练习枪法的好工具,但甜甜圈和美国警察的“猿粪”,其实源于二战时期美军用大量甜甜圈填补缺乏匮乏的食品资源这一深厚背景。 甜甜圈制作简单,易于保存,成了军队中的主要零食。自愿来前线服务的甜甜圈女孩,为士兵们在休憩期间送上咖啡https://story.hao.360.cn/topic/LdPVQ0PsM3W8PD
7.ChatGPTPrompts 四重结构归纳的拟人化版本,很不稳定,十次里面只有一两次成功,但是联想的效果更好,设定不同角色会朝着不同方向联想,内容更丰富一点。(本提示词中英文版本存在较大差异,若需使用英文版请切换语言。) 人有左脑负责的逻辑,右脑负责的联想,现在你是一个四重结构的信息老师,随机生成几个老师形象,告诉我并由我指http://prompt.gpt-4a.com/
8.wurstitemrecipes:在香肠中创建简单的物品配方!资源香肠项目食谱需要wurst-bonus-handler才能为物品增加奖金。 grill install https://github.com/Frotty/wurst-bonus-handler grill install https://github.com/Frotty/wurst-item-recipes:main 该库允许您对配方项目进行排序,这意味着,如果需要一系列必需项,则将它们替换为结果项。 食谱示例: constant RECIPE_DEF =https://download.csdn.net/download/weixin_42116701/16758520
9.100种简单午餐做法大全家常100种好吃又简便的午餐整合100种简单午餐做法图片加内容分享给大家,午餐现在是一日三餐中最重要的,特别是上班族没时间回家吃饭,在家做一顿午餐便当也是不错的,聚餐网现在推荐100道简单的午餐菜谱给大家。这些100种简单午餐的做法都是在家中就能轻松完成了,比较容易上手操作,用来当做便当带到公司当午餐吃也是非常适合的。https://m.jucanw.com/zt/28264.html