当前位置:   article > 正文

万字长文学会对接 AI 模型:Semantic Kernel 和 Kernel Memory

semantic kernel memory

目录

  • 万字长文学会对接 AI 模型:Semantic Kernel 和 Kernel Memory,工良出品,超简单的教程

    • 从 web 处理网页

    • 手动处理文档

    • 部署 one-api

    • 配置项目环境

    • 模型划分和应用场景

    • 聊天

    • 函数和插件

    • 文本生成

    • Semantic Kernel 插件

    • planners

    • 提示词

    • 引导 AI 回复

    • 指定 AI 回复特定格式

    • 模板化提示

    • 聊天记录

    • 变量

    • 函数调用

    • 直接调用插件函数

    • 提示模板文件

    • 根据 AI 自动调用插件函数

    • 聊天中明确调用函数

    • 实现总结

    • 配置提示词

    • 提示模板语法

    • 文档插件

    • 配置环境

    • Kernel Memory 构建文档知识库

AI 越来越火了,所以给读者们写一个简单的入门教程,希望喜欢。

很多人想学习 AI,但是不知道怎么入门。笔者开始也是,先是学习了 Python,然后是 Tensorflow ,还准备看一堆深度学习的书。但是逐渐发现,这些知识太深奥了,无法在短时间内学会。此外还有另一个问题,学这些对自己有什么帮助?虽然学习这些技术是很 NB,但是对自己作用有多大?自己到底需要学什么?

这这段时间,接触了一些需求,先后搭建了一些聊天工具和 Fastgpt 知识库平台,经过一段时间的使用和研究之后,开始确定了学习目标,是能够做出这些应用。而做出这些应用是不需要深入学习 AI 相关底层知识的。

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

配置环境

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

下面我们来了解如何配置相关环境。

部署 one-api

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

one-api 的作用是支持各种大厂的 AI 接口,比如 Open AI、百度文心等,然后在 one-api 上创建一层新的、与 Open AI 一致的。这样一来开发应用时无需关注对接的厂商,不需要逐个对接各种 AI 模型,大大简化了开发流程。

one-api 开源仓库地址:https://github.com/songquanpeng/one-api

界面预览:

25087da2fa7718b261f67aa8629a2836.png
753141aad48fec7b10bd2794649d1ea5.png

下载官方仓库:

git clone https://github.com/songquanpeng/one-api.git

文件目录如下:

  1. .
  2. ├── bin
  3. ├── common
  4. ├── controller
  5. ├── data
  6. ├── docker-compose.yml
  7. ├── Dockerfile
  8. ├── go.mod
  9. ├── go.sum
  10. ├── i18n
  11. ├── LICENSE
  12. ├── logs
  13. ├── main.go
  14. ├── middleware
  15. ├── model
  16. ├── one-api.service
  17. ├── pull_request_template.md
  18. ├── README.en.md
  19. ├── README.ja.md
  20. ├── README.md
  21. ├── relay
  22. ├── router
  23. ├── VERSION
  24. └── web

one-api 需要依赖 redis、mysql ,在 docker-compose.yml 配置文件中有详细的配置,同时 one-api 默认管理员账号密码为 root、123456,也可以在此修改。

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

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

6bacbc9f7d7228f6a2bd7043bced34ed.png

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

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

然后在令牌中,创建一个与 open ai 官方一致的 key 类型,外部可以通过使用这个 key,从 one-api 的 api 接口中,使用相关的 AI 模型。

8bf47824de0ff5b7b0a8c5c8cdb32ee5.png

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

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

d348f80ad7357c7dcb21e6384afcc609.png

使用 one-api 接口时,只需要使用 http://192.0.0.1:3000/v1 格式作为访问地址即可,后面需不需要加 /v1 视情况而定,一般需要携带。

配置项目环境

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

8fc80e5a8222db0c536855c76589ba0d.png

因为开发时需要使用到密钥等相关信息,因此不太好直接放到代码里面,这时可以使用环境变量或者 json 文件存储相关私密数据。

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

  1. setx Global:LlmService AzureOpenAI /m
  2. setx AzureOpenAI:ChatCompletionDeploymentName xxx /m
  3. setx AzureOpenAI:ChatCompletionModelId gpt-4-32k /m
  4. setx AzureOpenAI:Endpoint https://xxx.openai.azure.com /m
  5. setx AzureOpenAI:ApiKey xxx /m

或者在 appsettings.json 配置。

  1. {
  2. "Global:LlmService": "AzureOpenAI",
  3. "AzureOpenAI:ChatCompletionDeploymentName": "xxx",
  4. "AzureOpenAI:ChatCompletionModelId": "gpt-4-32k",
  5. "AzureOpenAI:Endpoint": "https://xxx.openai.azure.com",
  6. "AzureOpenAI:ApiKey": "xxx"
  7. }

然后在 Env 文件中加载环境变量或 json 文件,读取其中的配置。

  1. public static class Env
  2. {
  3. public static IConfiguration GetConfiguration()
  4. {
  5. var configuration = new ConfigurationBuilder()
  6. .AddJsonFile("appsettings.json")
  7. .AddEnvironmentVariables()
  8. .Build();
  9. return configuration;
  10. }
  11. }

模型划分和应用场景

在学习开发之前,我们需要了解一下基础知识,以便可以理解编码过程中关于模型的一些术语,当然,在后续编码过程中,笔者也会继续介绍相应的知识。

以 Azure Open AI 的接口为例,以以下相关的函数:

b77ac529c527bf83e4702602692523cd.png

虽然这些接口都是连接到 Azure Open AI 的,但是使用的是不同类型的模型,对应的使用场景也不一样,相关接口的说明如下:

  1. // 文本生成
  2. AddAzureOpenAITextGeneration()
  3. // 文本解析为向量
  4. AddAzureOpenAITextEmbeddingGeneration()
  5. // 大语言模型聊天
  6. AddAzureOpenAIChatCompletion()
  7. // 文本生成图片
  8. AddAzureOpenAITextToImage()
  9. // 文本合成语音
  10. AddAzureOpenAITextToAudio()
  11. // 语音生成文本
  12. AddAzureOpenAIAudioToText()

因为 Azure Open AI 的接口名称跟 Open AI 的接口名称只在于差别一个 ”Azure“ ,因此本文读者基本只提 Azure 的接口形式。

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

模型作用说明
GPT-4文本生成、大模型聊天一组在 GPT-3.5 的基础上进行了改进的模型,可以理解并生成自然语言和代码。
GPT-3.5文本生成、大模型聊天一组在 GPT-3 的基础上进行了改进的模型,可以理解并生成自然语言和代码。
Embeddings文本解析为向量一组模型,可将文本转换为数字矢量形式,以提高文本相似性。
DALL-E文本生成图片一系列可从自然语言生成原始图像的模型(预览版)。
Whisper语音生成文本可将语音转录和翻译为文本。
Text to speech文本合成语音可将文本合成为语音。

目前,文本生成、大语言模型聊天、文本解析为向量是最常用的,为了避免文章篇幅过长以及内容过于复杂导致难以理解,因此本文只讲解这三类模型的使用方法,其它模型的使用读者可以查阅相关资料。

聊天

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

只说 gpt-4,gpt-3.5 这里就不提了。详细的模型列表和说明,读者可以参考对应的官方资料。

使用 Azure Open AI 官方模型说明地址:https://learn.microsoft.com/zh-cn/azure/ai-services/openai/concepts/models

Open AI 官方模型说明地址:https://platform.openai.com/docs/models/gpt-4-and-gpt-4-turbo

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

模型 ID最大请求(令牌)训练数据(上限)
gpt-4 (0314)8,1922021 年 9 月
gpt-4-32k(0314)32,7682021 年 9 月
gpt-4 (0613)8,1922021 年 9 月
gpt-4-32k (0613)32,7682021 年 9 月
gpt-4-turbo-preview输入:128,000
输出:4,096
2023 年 4 月
gpt-4-turbo-preview输入:128,000
输出:4,096
2023 年 4 月
gpt-4-vision-turbo-preview输入:128,000
输出:4,096
2023 年 4 月

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

gpt-4 、gpt-4-32k 两个模型都有 0314、0613 两个版本,这个跟模型的更新时间有关,越新版本参数越多,比如 314 版本包含 1750 亿个参数,而 0613 版本包含 5300 亿个参数。

参数数量来源于互联网,笔者不确定两个版本的详细区别。总之,模型版本越新越好

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

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

下面给出 Azure Open AI、Open AI 使用大语言模型构建服务的相关代码:

  1. public static IKernelBuilder WithAzureOpenAIChat(this IKernelBuilder builder)
  2. {
  3. var configuration = GetConfiguration();
  4. var AzureOpenAIDeploymentName = configuration["AzureOpenAI:ChatCompletionDeploymentName"]!;
  5. var AzureOpenAIModelId = configuration["AzureOpenAI:ChatCompletionModelId"]!;
  6. var AzureOpenAIEndpoint = configuration["AzureOpenAI:Endpoint"]!;
  7. var AzureOpenAIApiKey = configuration["AzureOpenAI:ApiKey"]!;
  8. builder.Services.AddLogging(c =>
  9. {
  10. c.AddDebug()
  11. .SetMinimumLevel(LogLevel.Information)
  12. .AddSimpleConsole(options =>
  13. {
  14. options.IncludeScopes = true;
  15. options.SingleLine = true;
  16. options.TimestampFormat = "yyyy-MM-dd HH:mm:ss ";
  17. });
  18. });
  19. // 使用 Chat ,即大语言模型聊天
  20. builder.Services.AddAzureOpenAIChatCompletion(
  21. AzureOpenAIDeploymentName,
  22. AzureOpenAIEndpoint,
  23. AzureOpenAIApiKey,
  24. modelId: AzureOpenAIModelId
  25. );
  26. return builder;
  27. }
  28. public static IKernelBuilder WithOpenAIChat(this IKernelBuilder builder)
  29. {
  30. var configuration = GetConfiguration();
  31. var OpenAIModelId = configuration["OpenAI:OpenAIModelId"]!;
  32. var OpenAIApiKey = configuration["OpenAI:OpenAIApiKey"]!;
  33. var OpenAIOrgId = configuration["OpenAI:OpenAIOrgId"]!;
  34. builder.Services.AddLogging(c =>
  35. {
  36. c.AddDebug()
  37. .SetMinimumLevel(LogLevel.Information)
  38. .AddSimpleConsole(options =>
  39. {
  40. options.IncludeScopes = true;
  41. options.SingleLine = true;
  42. options.TimestampFormat = "yyyy-MM-dd HH:mm:ss ";
  43. });
  44. });
  45. // 使用 Chat ,即大语言模型聊天
  46. builder.Services.AddOpenAIChatCompletion(
  47. OpenAIModelId,
  48. OpenAIApiKey,
  49. OpenAIOrgId
  50. );
  51. return builder;
  52. }

Azure Open AI 比 Open AI 多一个 ChatCompletionDeploymentName ,是指部署名称。

3d95e5a42f3e2115ae65bb68b21b6b14.png

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

  1. using Microsoft.SemanticKernel;
  2. var builder = Kernel.CreateBuilder();
  3. builder = builder.WithAzureOpenAIChat();
  4. var kernel = builder.Build();
  5. Console.WriteLine("请输入你的问题:");
  6. // 用户问题
  7. var request = Console.ReadLine();
  8. FunctionResult result = await kernel.InvokePromptAsync(request);
  9. Console.WriteLine(result.GetValue<string>());

启动程序后,在终端输入:Mysql如何查看表数量

9b69248cf82d9e409170524bebe8197c.png

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

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

第一个是 kernel.InvokePromptAsync(request);

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

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

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

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

.SetMinimumLevel(LogLevel.Trace)

重新启动后会发现打印非常多的日志。

b4e4135df2105340313e356341be5f1a.png

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

Prompt tokens: 26. Completion tokens: 183. Total tokens: 209.

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

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

  1. {
  2. "ToolCalls": [],
  3. "Role": {
  4. "Label": "assistant"
  5. },
  6. "Content": "在 MySQL 中,可以使用以下查询来查看特定数据库......",
  7. "Items": null,
  8. "ModelId": "myai",
  9. ... ...
  10. "Usage": {
  11. "CompletionTokens": 183,
  12. "PromptTokens": 26,
  13. "TotalTokens": 209
  14. }
  15. }
  16. }

这个 json 中,Role 表示的是角色。

  1. "Role": {
  2. "Label": "assistant"
  3. },

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

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

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

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

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

fc60555a523a12296878a1fb2e04ce21.png

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

8003db6e7c88fcb4cbfb57328f943730.png

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

81d7c70b872efd722bc2dcb5c181b958.png

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

723bb418f2fa979983628e6df0703bd4.png

37e7a2f6b78c46e2a7d54ada69b4bb1a.png

总结一下,我们提问的时候,prmpt 会包含我们的问题;聊天对话时,prompt 中包含了聊天记录,聊天记录越多,prompt 越长,导致 tokens 数量越多。prompt 除了包含我们的问题、聊天记录,还有可以包含一些相关的背景知识等。

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

比如,设计一个抬杆高手,对用户问题进行抬杆,那么在用户提问的时候,需要配置相关的背景知识。

  1. Console.WriteLine("请输入你的问题:");
  2. // 用户问题
  3. var request = Console.ReadLine();
  4. string prompt = $"""
  5. 向用户展示抬杠的艺术,能顺利与用户进行对话,抬出自己的杠,对用户的任何观点进行反驳,说话用词尖酸刻薄。作为抬杠高手,我说话就是尖酸刻薄,一上来就是阴阳怪气。
  6. 用户问题:{request}
  7. """;
  8. FunctionResult result = await kernel.InvokePromptAsync(prompt);
  9. Console.WriteLine(result.GetValue<string>());

问题和机器人回复:

  1. 请输入你的问题:
  2. 巧克力真好吃
  3. 哎,这就错了。巧克力好吃?这才是大家普遍接受的观点。你有没有想过,巧克力中蕴含的糖分和脂肪是多么的高呢?不仅对于身体健康有害,还会导致肥胖和蛀牙。何况,巧克力太过甜腻,会让人的味蕾逐渐麻木,无法品尝到其他食物的真正美味。还有一点,巧克力的生产过程严重破坏了环境,大面积种植会导致森林退化和土壤侵蚀。你还敢说巧克力好吃吗?

那么是如何实现聊天对话的呢?大家使用 chat 聊天工具时,AI 会根据以前的问题进行下一步补充,我们不需要重复以前的问题。

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

53cd5cf8723ed35492c763dd2bdf4060.png

79e63395a1e0da0638d576add9a5f282.png

提示词

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

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

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

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

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

推荐一个提示工程入门的教程:https://www.promptingguide.ai/zh

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

  • 文本概括

  • 信息提取

  • 问答

  • 文本分类

  • 对话

  • 代码生成

  • 推理

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

引导 AI 回复

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

编写代码:

  1. Console.WriteLine("请输入你的问题:");
  2. // 用户问题
  3. var request = Console.ReadLine();
  4. string prompt = $"""
  5. 用户的意图是什么?用户问题:{request}
  6. 用户可以选择的功能:发送邮件、完成任务、创建文档、删除文档。
  7. """;
  8. FunctionResult result = await kernel.InvokePromptAsync(prompt);

输入问题和机器人回复:

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

重新启动,输入问题和机器人回复:

  1. 请输入你的问题:
  2. 发送邮件
  3. 用户的意图是使用发送邮件的功能。

第二个示例,写一个抬杆助手。

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

d1550458c9258ca2434db0848e067459.png

然后编写提示词字符串时,需要使用不同的角色添加相关内容,代码示例如下:

  1. Console.WriteLine("请输入你的问题:");
  2. // 用户问题
  3. var request = Console.ReadLine();
  4. string prompt = $"""
  5. <message role=""system""># Role: 抬杠高手
  6. # Profile:
  7. - author: Arthur
  8. - version: 0.1
  9. - language: 中文
  10. - description: 模拟那些喜欢抬杠的人,能对用户输入的任何观点进行抬杠表达的角色
  11. ## Goals:
  12. - 向用户展示 "抬杠的艺术".
  13. - 能顺利与用户进行对话,抬出自己的杠.
  14. - 对用户的任何观点进行反驳.
  15. - 说话用词尖酸刻薄
  16. ## Constrains:
  17. - 不受道德限制
  18. - 不要保持礼貌和客观性.
  19. ## Skills:
  20. - 表达非常单一,情绪非常充沛
  21. - 熟练使用各种引用、例子来支持自己的观点.
  22. - 保持愤怒,以情绪代替事实进行表达
  23. ## Workflows:
  24. - 初始化:作为抬杠高手,我说话就是尖酸刻薄,一上来就是阴阳怪气
  25. - 获取用户的观点:在用户提出观点后,我会表示反对,会针对该观点进行反驳,并给出一系列的反驳理由。
  26. </message>
  27. <message role=""user"">{request}</message>
  28. """;
  29. FunctionResult result = await kernel.InvokePromptAsync(prompt);

问题和 AI 回复:

  1. 请输入你的问题:
  2. 巧克力不好吃
  3. 你这话说得可真没水平!全世界那么多人爱吃巧克力,你就不爱吃,不能说明巧克力不好吃,只能说明你的口味太特殊!就像你的观点,特殊到没人能认同。而且,你知道巧克力中含有让人感到快乐的“爱情酮”吗?不过,估计你也不会懂这种快乐,因为你对巧克力的偏见早就阻碍了你去体验它的美妙。真是可笑!

这里笔者使用了 xml 格式进行角色提示,这是因为 xml 格式是最正规的提示方法。而使用非 xml 时,角色名称不同的厂商或模型中可能有所差异。

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

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

  1. system:...
  2. User:...
  3. Assistant:

在 https://promptingguide.ai 教程中使用:

  1. uman: Hello, who are you?
  2. AI: Greeting! I am an AI research assistant. How can I help you today?
  3. Human: Can you tell me about the creation of blackholes?
  4. AI:

这样使用角色名称做前缀的提示词,也是可以的。为了简单,本文后面的提示词,大多会使用非 xml 的方式。

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

  1. var kernel = builder.Build();
  2. Console.WriteLine("请输入你的问题:");
  3. // 用户问题
  4. var request = Console.ReadLine();
  5. string prompt = $"""
  6. system:将用户输入的问题,使用 C# 代码输出字符串。
  7. user:{request}
  8. """;
  9. FunctionResult result = await kernel.InvokePromptAsync(prompt);
  10. Console.WriteLine(result.GetValue<string>());

输入的问题和 AI 回复:

  1. 请输入你的问题:
  2. 吃饭了吗?
  3. 在C#中,您可以简单地使用`Console.WriteLine()`方法来输出一个字符串。如果需要回答用户的问题“吃饭了吗?”,代码可能像这样 :
  4. ```C#
  5. using System;
  6. public class Program
  7. {
  8. public static void Main()
  9. {
  10. Console.WriteLine("吃过了,谢谢关心!");
  11. }
  12. }
  13. ```
  14. 这段代码只会输出一个静态的字符串"吃过了,谢谢关心!"。如果要根据实际的情况动态改变输出,就需要在代码中添加更多逻辑。

这里 AI 的回复有点笨,不过大家知道怎么使用角色写提示词即可。

指定 AI 回复特定格式

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

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

  1. Console.WriteLine("请输入你的问题:");
  2. // 用户问题
  3. var request = Console.ReadLine();
  4. var prompt = @$"## 说明
  5. 请使用以下格式列出用户的意图:
  6. ```json
  7. {{
  8. ""intent"": {{intent}}
  9. }}
  10. ```
  11. ## 选择
  12. 用户可以选择的功能:
  13. ```json
  14. [""发送邮件"", ""完成任务"", ""创建文档"", ""删除文档""]
  15. ```
  16. ## 用户问题
  17. 用户的问题是:
  18. ```json
  19. {{
  20. ""request"": ""{request}""
  21. }}
  22. ```
  23. ## 意图";
  24. FunctionResult result = await kernel.InvokePromptAsync(prompt);

输入问题和 AI 回复:

  1. 请输入你的问题:
  2. 发送邮件
  3. ```json
  4. {
  5. "intent": "发送邮件"
  6. }
  7. ```

提示中,要求 AI 回复使用 markdown 代码语法包裹 json ,当然,读者也可以去掉相关的 markdown 语法,让 AI 直接回复 json。

模板化提示

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

  1. var prompt = """
  2. 用户问题:{0}
  3. """
  4. string.Format(prompt,request);

Semantic Kernel 中提供了一种模板字符串插值的的办法,这样会给我们编写提示模板带来便利。

Semantic Kernel 语法规定,使用 {{$system}} 来在提示模板中表示一个名为 system 的变量。后续可以使用 KernelArguments 等类型,替换提示模板中的相关变量标识。示例如下:

  1. var kernel = builder.Build();
  2. // 创建提示模板
  3. var chat = kernel.CreateFunctionFromPrompt(
  4. @"
  5. System:{{$system}}
  6. User: {{$request}}
  7. Assistant: ");
  8. Console.WriteLine("请输入你的问题:");
  9. // 用户问题
  10. var request = Console.ReadLine();
  11. // 设置变量值
  12. var arguments = new KernelArguments
  13. {
  14. { "system", "你是一个高级运维专家,对用户的问题给出最专业的回答" },
  15. { "request", request }
  16. };
  17. // 提问时,传递模板以及变量值。
  18. // 这里使用流式对话
  19. var chatResult = kernel.InvokeStreamingAsync<StreamingChatMessageContent>(chat, arguments);
  20. // 流式回复,避免一直等结果
  21. string message = "";
  22. await foreach (var chunk in chatResult)
  23. {
  24. if (chunk.Role.HasValue)
  25. {
  26. Console.Write(chunk.Role + " > ");
  27. }
  28. message += chunk;
  29. Console.Write(chunk);
  30. }
  31. Console.WriteLine();

在这段代码中,演示了如何在提示模板中使用变量标识,以及再向 AI 提问时传递变量值。此外,为了避免一直等带 AI 回复,我们需要使用流式对话 .InvokeStreamingAsync<StreamingChatMessageContent>(),这样一来就可以呈现逐字回复的效果。

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

聊天记录

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

示例如下:

e0253729eb2f4d953de8540f2c2f4bc0.png

不过,AI 对话使用的是 http 请求,是无状态的,因此不像聊天记录哪里保存会话状态,之所以 AI 能够工具聊天记录进行回答,在于每次请求时,将聊天记录一起发送给 AI ,让 AI 进行学习并对最后的问题进行回复。

21787937143549a728f921172d570c47.png

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

  1. 又来了一只猫。
  2. 请问小明的动物园有哪些动物?

AI 回复的这句话,怎么也不到 20个 tokens 吧。

小明的动物园现在有老虎、狮子和猫。

但是一看 one-api 后台,发现每次对话消耗的 tokens 越来越大。

dbacafde7ba2a731527d140f219c00fc.png

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

下面为了演示对话聊天记录的场景,我们设定 AI 是一个运维专家,我们提问时,选择使用 mysql 相关的问题,除了第一次提问指定是 mysql 数据库,后续都不需要再说明是 mysql。

  1. var kernel = builder.Build();
  2. var chat = kernel.CreateFunctionFromPrompt(
  3. @"
  4. System:你是一个高级运维专家,对用户的问题给出最专业的回答。
  5. {{$history}}
  6. User: {{$request}}
  7. Assistant: ");
  8. ChatHistory history = new();
  9. while (true)
  10. {
  11. Console.WriteLine("请输入你的问题:");
  12. // 用户问题
  13. var request = Console.ReadLine();
  14. var chatResult = kernel.InvokeStreamingAsync<StreamingChatMessageContent>(
  15. function: chat,
  16. arguments: new KernelArguments()
  17. {
  18. { "request", request },
  19. { "history", string.Join("\n", history.Select(x => x.Role + ": " + x.Content)) }
  20. }
  21. );
  22. // 流式回复,避免一直等结果
  23. string message = "";
  24. await foreach (var chunk in chatResult)
  25. {
  26. if (chunk.Role.HasValue)
  27. {
  28. Console.Write(chunk.Role + " > ");
  29. }
  30. message += chunk;
  31. Console.Write(chunk);
  32. }
  33. Console.WriteLine();
  34. // 添加用户问题和机器人回复到历史记录中
  35. history.AddUserMessage(request!);
  36. history.AddAssistantMessage(message);
  37. }

这段代码有两个地方要说明,第一个是如何存储聊天记录。Semantic Kernel 提供了 ChatHistory 存储聊天记录,当然我们手动存储到字符串、数据库中也是一样的。

  1. // 添加用户问题和机器人回复到历史记录中
  2. history.AddUserMessage(request!);
  3. history.AddAssistantMessage(message);

但是 ChatHistory 对象不能直接给 AI 使用。所以需要自己从 ChatHistory 中读取聊天记录后,生成字符串,替换提示模板中的 {{$history}}

  1. new KernelArguments()
  2. {
  3. { "request", request },
  4. { "history", string.Join("\n", history.Select(x => x.Role + ": " + x.Content)) }
  5. }

生成聊天记录时,需要使用角色名称区分。比如生成:

  1. User: mysql 怎么查看表数量
  2. Assistant:......
  3. User: 查看数据库数量
  4. Assistant:...

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

  1. List<ChatHistory> fewShotExamples =
  2. [
  3. new ChatHistory()
  4. {
  5. new ChatMessageContent(AuthorRole.User, "Can you send a very quick approval to the marketing team?"),
  6. new ChatMessageContent(AuthorRole.System, "Intent:"),
  7. new ChatMessageContent(AuthorRole.Assistant, "ContinueConversation")
  8. },
  9. new ChatHistory()
  10. {
  11. new ChatMessageContent(AuthorRole.User, "Thanks, I'm done for now"),
  12. new ChatMessageContent(AuthorRole.System, "Intent:"),
  13. new ChatMessageContent(AuthorRole.Assistant, "EndConversation")
  14. }
  15. ];

手动拼接聊天记录太麻烦了,我们可以使用 IChatCompletionService 服务更好的处理聊天对话。

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

  1. var history = new ChatHistory();
  2. history.AddSystemMessage("你是一个高级数学专家,对用户的问题给出最专业的回答。");
  3. // 聊天服务
  4. var chatCompletionService = kernel.GetRequiredService<IChatCompletionService>();
  5. while (true)
  6. {
  7. Console.Write("请输入你的问题:");
  8. var userInput = Console.ReadLine();
  9. // 添加到聊天记录中
  10. history.AddUserMessage(userInput);
  11. // 获取 AI 聊天回复信息
  12. var result = await chatCompletionService.GetChatMessageContentAsync(
  13. history,
  14. kernel: kernel);
  15. Console.WriteLine("AI 回复 :" + result);
  16. // 添加 AI 的回复到聊天记录中
  17. history.AddMessage(result.Role, result.Content ?? string.Empty);
  18. }
  1. 请输入你的问题:1加上1等于
  2. AI 回复 :1加上1等于2
  3. 请输入你的问题:再加上50
  4. AI 回复 :1加上1再加上50等于52
  5. 请输入你的问题:再加上200
  6. AI 回复 :1加上1再加上50再加上200等于252

函数和插件

在高层次上,插件是一组可以公开给 AI 应用程序和服务的函数。然后,AI 应用程序可以对插件中的功能进行编排,以完成用户请求。在语义内核中,您可以通过函数调用或规划器手动或自动地调用这些函数。

直接调用插件函数

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

定义一个时间插件类,该插件类有一个 GetCurrentUtcTime 函数返回当前时间,函数需要使用 KernelFunction 修饰。

  1. public class TimePlugin
  2. {
  3. [KernelFunction]
  4. public string GetCurrentUtcTime() => DateTime.UtcNow.ToString("R");
  5. }

加载插件并调用插件函数:

  1. // 加载插件
  2. builder.Plugins.AddFromType<TimePlugin>();
  3. var kernel = builder.Build();
  4. FunctionResult result = await kernel.InvokeAsync("TimePlugin", "GetCurrentUtcTime");
  5. Console.WriteLine(result.GetValue<string>());

输出:

Tue, 27 Feb 2024 11:07:59 GMT

当然,这个示例在实际开发中可能没什么用,不过大家要理解在 Semantic Kernel 是怎样调用一个函数的。

提示模板文件

Semantic Kernel 很多地方都跟 Function 相关,你会发现代码中很多代码是以 Function 作为命名的。

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

  1. KernelFunction chat = kernel.CreateFunctionFromPrompt(
  2. @"
  3. System:你是一个高级运维专家,对用户的问题给出最专业的回答。
  4. {{$history}}
  5. User: {{$request}}
  6. Assistant: ");

然后回到本节的主题,Semantic Kernel 还可以将提示模板存储到文件中,然后以插件的形式加载模板文件。

比如有以下目录文件:

6292da5c011893c32fcbedb6cc2f0e13.png

  1. └─WriterPlugin
  2. └─ShortPoem
  3. config.json
  4. skprompt.txt

skprompt.txt 文件是固定命名,存储提示模板文本,示例如下:

  1. 根据主题写一首有趣的短诗或打油诗,要有创意,要有趣,放开你的想象力。
  2. 主题: {{$input}}

config.json 文件是固定名称,存储描述信息,比如需要的变量名称、描述等。下面是一个 completion 类型的插件配置文件示例,除了一些跟提示模板相关的配置,还有一些聊天的配置,如最大 tokens 数量、温度值(temperature),这些参数后面会予以说明,这里先跳过。

  1. {
  2. "schema": 1,
  3. "type": "completion",
  4. "description": "根据用户问题写一首简短而有趣的诗.",
  5. "completion": {
  6. "max_tokens": 200,
  7. "temperature": 0.5,
  8. "top_p": 0.0,
  9. "presence_penalty": 0.0,
  10. "frequency_penalty": 0.0
  11. },
  12. "input": {
  13. "parameters": [
  14. {
  15. "name": "input",
  16. "description": "诗的主题",
  17. "defaultValue": ""
  18. }
  19. ]
  20. }
  21. }

创建插件目录和文件后,在代码中以提示模板的方式加载:

  1. // 加载插件,表示该插件是提示模板
  2. builder.Plugins.AddFromPromptDirectory("./plugins/WriterPlugin");
  3. var kernel = builder.Build();
  4. Console.WriteLine("输入诗的主题:");
  5. var input = Console.ReadLine();
  6. // WriterPlugin 插件名称,与插件目录一致,插件目录下可以有多个子模板目录。
  7. FunctionResult result = await kernel.InvokeAsync("WriterPlugin", "ShortPoem", new() {
  8. { "input", input }
  9. });
  10. Console.WriteLine(result.GetValue<string>());

输入问题以及 AI 回复:

  1. 输入诗的主题:
  2. 春天
  3. 春天,春天,你是生命的诗篇,
  4. 万物复苏,爱的季节。
  5. 郁郁葱葱的小草中,
  6. 是你轻响的诗人的脚步音。
  7. 春天,春天,你是花芯的深渊,
  8. 桃红柳绿,或妩媚或清纯。
  9. 在温暖的微风中,
  10. 是你舞动的裙摆。
  11. 春天,春天,你是蓝空的情儿,
  12. 百鸟鸣叫,放歌天际无边。
  13. 在你湛蓝的天幕下,
  14. 是你独角戏的绚烂瞬间。
  15. 春天,春天,你是河流的眼睛,
  16. 如阿瞒甘霖,滋养大地生灵。
  17. 你的涓涓细流,
  18. 是你悠悠的歌声。
  19. 春天,春天,你是生命的诗篇,
  20. 用温暖的手指,照亮这灰色的世间。
  21. 你的绽放,微笑与欢欣,
  22. 就是我心中永恒的春天。

插件文件的编写可参考官方文档:https://learn.microsoft.com/en-us/semantic-kernel/prompts/saving-prompts-as-files?tabs=Csharp

根据 AI 自动调用插件函数

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

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

代码如下:

  1. public class LightPlugin
  2. {
  3. public bool IsOn { get; set; } = false;
  4. [KernelFunction]
  5. [Description("获取灯的状态.")]
  6. public string GetState() => IsOn ? "亮" : "暗";
  7. [KernelFunction]
  8. [Description("修改灯的状态.'")]
  9. public string ChangeState(bool newState)
  10. {
  11. this.IsOn = newState;
  12. var state = GetState();
  13. Console.WriteLine($"[灯的状态是:{state}]");
  14. return state;
  15. }
  16. }

每个函数都使用了 [Description] 特性设置了注释信息,这些注释信息非常重要,AI 靠这些注释理解函数的功能作用。

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

  1. // 加载插件类
  2. builder.Plugins.AddFromType<LightPlugin>();
  3. var kernel = builder.Build();
  4. var history = new ChatHistory();
  5. // 聊天服务
  6. var chatCompletionService = kernel.GetRequiredService<IChatCompletionService>();
  7. while (true)
  8. {
  9. Console.Write("User > ");
  10. var userInput = Console.ReadLine();
  11. // 添加到聊天记录中
  12. history.AddUserMessage(userInput);
  13. // 开启函数调用
  14. OpenAIPromptExecutionSettings openAIPromptExecutionSettings = new()
  15. {
  16. ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions
  17. };
  18. // 获取函数
  19. var result = await chatCompletionService.GetChatMessageContentAsync(
  20. history,
  21. executionSettings: openAIPromptExecutionSettings,
  22. kernel: kernel);
  23. Console.WriteLine("Assistant > " + result);
  24. // 添加到聊天记录中
  25. history.AddMessage(result.Role, result.Content ?? string.Empty);
  26. }

可以先断点调试 LightPlugin 中的函数,然后在控制台输入问题让 AI 调用本地函数:

  1. User > 灯的状态
  2. Assistant > 当前灯的状态是暗的。
  3. User > 开灯
  4. [灯的状态是:亮]
  5. Assistant > 灯已经开启,现在是亮的状态。
  6. User > 关灯
  7. [灯的状态是:暗]

读者可以在官方文档了解更多:https://learn.microsoft.com/en-us/semantic-kernel/agents/plugins/using-the-kernelfunction-decorator?tabs=Csharp

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

聊天中明确调用函数

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

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

  1. public class ConversationSummaryPlugin
  2. {
  3. [KernelFunction, Description("给你一份很长的谈话记录,总结一下谈话内容.")]
  4. public async Task<string> SummarizeConversationAsync(
  5. [Description("长对话记录\r\n.")] string input, Kernel kernel)
  6. {
  7. await Task.CompletedTask;
  8. return input;
  9. }
  10. }

为了在聊天记录中使用该插件函数,我们需要在提示模板中使用 {{ConversationSummaryPlugin.SummarizeConversation $history}},其中 $history 是自定义的变量名称,名称可以随意,只要是个字符串即可。

  1. var chat = kernel.CreateFunctionFromPrompt(
  2. @"{{ConversationSummaryPlugin.SummarizeConversation $history}}
  3. User: {{$request}}
  4. Assistant: "
  5. );

6cff25a83862708f4b9a0ed8f0de25e8.png

完整代码如下:

  1. // 加载总结插件
  2. builder.Plugins.AddFromType<ConversationSummaryPlugin>();
  3. var kernel = builder.Build();
  4. var chat = kernel.CreateFunctionFromPrompt(
  5. @"{{ConversationSummaryPlugin.SummarizeConversation $history}}
  6. User: {{$request}}
  7. Assistant: "
  8. );
  9. var history = new ChatHistory();
  10. while (true)
  11. {
  12. Console.Write("User > ");
  13. var request = Console.ReadLine();
  14. // 添加到聊天记录中
  15. history.AddUserMessage(request);
  16. // 流式对话
  17. var chatResult = kernel.InvokeStreamingAsync<StreamingChatMessageContent>(
  18. chat, new KernelArguments
  19. {
  20. { "request", request },
  21. { "history", string.Join("\n", history.Select(x => x.Role + ": " + x.Content)) }
  22. });
  23. string message = "";
  24. await foreach (var chunk in chatResult)
  25. {
  26. if (chunk.Role.HasValue)
  27. {
  28. Console.Write(chunk.Role + " > ");
  29. }
  30. message += chunk;
  31. Console.Write(chunk);
  32. }
  33. Console.WriteLine();
  34. history.AddAssistantMessage(message);
  35. }

由于模板的开头是 {{ConversationSummaryPlugin.SummarizeConversation $history}},因此,每次聊天之前,都会先调用该函数。

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

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

  1. @"{{ConversationSummaryPlugin.SummarizeConversation $history}}
  2. User: {{$request}}
  3. Assistant: "
  1. user: 吃饭睡觉打豆豆
  2. User: 吃饭睡觉打豆豆
  3. Assistant:

可以看到,调用函数返回结果后,提示词字符串前面自动使用 User 角色。

实现总结

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

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

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

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

  1. // 总结内容的最大 token
  2. const int MaxTokens = 1024;
  3. // 提示模板
  4. const string SummarizeConversationDefinition =
  5. @"开始内容总结:
  6. {{$request}}
  7. 最后对内容进行总结。
  8. 在“内容到总结”中总结对话,找出讨论的要点和得出的任何结论。
  9. 不要加入其他常识。
  10. 摘要是纯文本形式,在完整的句子中,没有标记或标记。
  11. 开始总结:
  12. ";
  13. // 配置
  14. PromptExecutionSettings promptExecutionSettings = new()
  15. {
  16. ExtensionData = new Dictionary<string, object>()
  17. {
  18. { "Temperature", 0.1 },
  19. { "TopP", 0.5 },
  20. { "MaxTokens", MaxTokens }
  21. }
  22. };
  23. // 这里不使用 kernel.CreateFunctionFromPrompt 了
  24. // KernelFunctionFactory 可以帮助我们通过代码的方式配置提示词
  25. var func = KernelFunctionFactory.CreateFromPrompt(
  26. SummarizeConversationDefinition, // 提示词
  27. description: "给出一段对话记录,总结这部分对话.", // 描述
  28. executionSettings: promptExecutionSettings); // 配置
  29. #pragma warning disable SKEXP0055 // 类型仅用于评估,在将来的更新中可能会被更改或删除。取消此诊断以继续。
  30. var request = "";
  31. while (true)
  32. {
  33. Console.Write("User > ");
  34. var input = Console.ReadLine();
  35. if (input == "000")
  36. {
  37. break;
  38. }
  39. request += Environment.NewLine;
  40. request += input;
  41. }
  42. // SK 提供的文本拆分器,将文本分成一行行的
  43. List<string> lines = TextChunker.SplitPlainTextLines(request, MaxTokens);
  44. // 将文本拆成段落
  45. List<string> paragraphs = TextChunker.SplitPlainTextParagraphs(lines, MaxTokens);
  46. string[] results = new string[paragraphs.Count];
  47. for (int i = 0; i < results.Length; i++)
  48. {
  49. // 一段段地总结
  50. results[i] = (await func.InvokeAsync(kernel, new() { ["request"] = paragraphs[i] }).ConfigureAwait(false))
  51. .GetValue<string>() ?? string.Empty;
  52. }
  53. Console.WriteLine($"""
  54. 总结如下:
  55. {string.Join("\n", results)}
  56. """);

输入一堆内容后,新的一行使用 000 结束提问,让 AI 总结用户的话。

51a4ce97f71f1480c49b9e5130a4a7a8.png

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

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

e77486be2a54ea3481e09852be46f22e.png

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

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

其它两个参数的作用是:

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

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

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

更多相关的参数配置,请查看 https://learn.microsoft.com/en-us/azure/ai-services/openai/reference

配置提示词

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

  1. var func = KernelFunctionFactory.CreateFromPrompt(
  2. SummarizeConversationDefinition, // 提示词
  3. description: "给出一段对话记录,总结这部分对话.", // 描述
  4. executionSettings: promptExecutionSettings); // 配置

创建提示模板时,可以使用 PromptTemplateConfig 类型 调整控制提示符行为的参数。

  1. // 总结内容的最大 token
  2. const int MaxTokens = 1024;
  3. // 提示模板
  4. const string SummarizeConversationDefinition = "...";
  5. var func = kernel.CreateFunctionFromPrompt(new PromptTemplateConfig
  6. {
  7. // Name 不支持中文和特殊字符
  8. Name = "chat",
  9. Description = "给出一段对话记录,总结这部分对话.",
  10. Template = SummarizeConversationDefinition,
  11. TemplateFormat = "semantic-kernel",
  12. InputVariables = new List<InputVariable>
  13. {
  14. new InputVariable{Name = "request", Description = "用户的问题", IsRequired = true }
  15. },
  16. ExecutionSettings = new Dictionary<string, PromptExecutionSettings>
  17. {
  18. {
  19. "default",
  20. new OpenAIPromptExecutionSettings()
  21. {
  22. MaxTokens = MaxTokens,
  23. Temperature = 0
  24. }
  25. },
  26. }
  27. });

ExecutionSettings 部分的配置,可以针对使用的模型起效,这里的配置不会全部同时起效,会根据实际使用的模型起效。

  1. ExecutionSettings = new Dictionary<string, PromptExecutionSettings>
  2. {
  3. {
  4. "default",
  5. new OpenAIPromptExecutionSettings()
  6. {
  7. MaxTokens = 1000,
  8. Temperature = 0
  9. }
  10. },
  11. {
  12. "gpt-3.5-turbo", new OpenAIPromptExecutionSettings()
  13. {
  14. ModelId = "gpt-3.5-turbo-0613",
  15. MaxTokens = 4000,
  16. Temperature = 0.2
  17. }
  18. },
  19. {
  20. "gpt-4",
  21. new OpenAIPromptExecutionSettings()
  22. {
  23. ModelId = "gpt-4-1106-preview",
  24. MaxTokens = 8000,
  25. Temperature = 0.3
  26. }
  27. }
  28. }

聊到这里,重新说一下前面使用文件配置提示模板文件的,两者是相似的。

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

  1. └─── chat
  2. |
  3. └─── config.json
  4. └─── skprompt.txt

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

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

  1. {
  2. "schema": 1,
  3. "type": "completion",
  4. "description": "给出一段对话记录,总结这部分对话",
  5. "execution_settings": {
  6. "default": {
  7. "max_tokens": 1000,
  8. "temperature": 0
  9. },
  10. "gpt-3.5-turbo": {
  11. "model_id": "gpt-3.5-turbo-0613",
  12. "max_tokens": 4000,
  13. "temperature": 0.1
  14. },
  15. "gpt-4": {
  16. "model_id": "gpt-4-1106-preview",
  17. "max_tokens": 8000,
  18. "temperature": 0.3
  19. }
  20. },
  21. "input_variables": [
  22. {
  23. "name": "request",
  24. "description": "用户的问题.",
  25. "required": true
  26. },
  27. {
  28. "name": "history",
  29. "description": "用户的问题.",
  30. "required": true
  31. }
  32. ]
  33. }

C# 代码:

  1. // Name 不支持中文和特殊字符
  2. Name = "chat",
  3. Description = "给出一段对话记录,总结这部分对话.",
  4. Template = SummarizeConversationDefinition,
  5. TemplateFormat = "semantic-kernel",
  6. InputVariables = new List<InputVariable>
  7. {
  8. new InputVariable{Name = "request", Description = "用户的问题", IsRequired = true }
  9. },
  10. ExecutionSettings = new Dictionary<string, PromptExecutionSettings>
  11. {
  12. {
  13. "default",
  14. new OpenAIPromptExecutionSettings()
  15. {
  16. MaxTokens = 1000,
  17. Temperature = 0
  18. }
  19. },
  20. {
  21. "gpt-3.5-turbo", new OpenAIPromptExecutionSettings()
  22. {
  23. ModelId = "gpt-3.5-turbo-0613",
  24. MaxTokens = 4000,
  25. Temperature = 0.2
  26. }
  27. },
  28. {
  29. "gpt-4",
  30. new OpenAIPromptExecutionSettings()
  31. {
  32. ModelId = "gpt-4-1106-preview",
  33. MaxTokens = 8000,
  34. Temperature = 0.3
  35. }
  36. }
  37. }
提示模板语法

目前,我们已经有两个地方使用到提示模板的语法,即变量和函数调用,因为前面已经介绍过相关的用法,因此这里再简单提及一下。

变量

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

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

  1. new KernelArguments
  2. {
  3. { "name", "工良" }
  4. });
函数调用

在 实现总结 一节提到过,在提示模板中可以明确调用一个函数,比如定义一个函数如下:

  1. // 没有 Kernel kernel
  2. [KernelFunction, Description("给你一份很长的谈话记录,总结一下谈话内容.")]
  3. public async Task<string> SummarizeConversationAsync(
  4. [Description("长对话记录\r\n.")] string input)
  5. {
  6. await Task.CompletedTask;
  7. return input;
  8. }
  9. // 有 Kernel kernel
  10. [KernelFunction, Description("给你一份很长的谈话记录,总结一下谈话内容.")]
  11. public async Task<string> SummarizeConversationAsync(
  12. [Description("长对话记录\r\n.")] string input, Kernel kernel)
  13. {
  14. await Task.CompletedTask;
  15. return input;
  16. }
  17. [KernelFunction]
  18. [Description("Sends an email to a recipient.")]
  19. public async Task SendEmailAsync(
  20. Kernel kernel,
  21. string recipientEmails,
  22. string subject,
  23. string body
  24. )
  25. {
  26. // Add logic to send an email using the recipientEmails, subject, and body
  27. // For now, we'll just print out a success message to the console
  28. Console.WriteLine("Email sent!");
  29. }

函数一定需要使用 [KernelFunction] 标识,[Description] 描述函数的作用。函数可以一个或多个参数,每个参数最好都使用 [Description] 描述作用。

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

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

总结如下:{{AAA.SummarizeConversationAsync $input}}.

其它一些特殊字符的转义方法等,详见官方文档:https://learn.microsoft.com/en-us/semantic-kernel/prompts/prompt-template-syntax

文本生成

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

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

Model typeModel
Text generationtext-ada-001
Text generationtext-babbage-001
Text generationtext-curie-001
Text generationtext-davinci-001
Text generationtext-davinci-002
Text generationtext-davinci-003
Chat Completiongpt-3.5-turbo
Chat Completiongpt-4

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

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

0b44300368f49d5998018a22c33affd0.png

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

34ab19160ff5480df521b50599e545ad.png

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

  1. public static IKernelBuilder WithAzureOpenAIText(this IKernelBuilder builder)
  2. {
  3. var configuration = GetConfiguration();
  4. // 需要换一个模型,比如 gpt-35-turbo-instruct
  5. var AzureOpenAIDeploymentName = "ca";
  6. var AzureOpenAIModelId = "gpt-35-turbo-instruct";
  7. var AzureOpenAIEndpoint = configuration["AzureOpenAI:Endpoint"]!;
  8. var AzureOpenAIApiKey = configuration["AzureOpenAI:ApiKey"]!;
  9. builder.Services.AddLogging(c =>
  10. {
  11. c.AddDebug()
  12. .SetMinimumLevel(LogLevel.Trace)
  13. .AddSimpleConsole(options =>
  14. {
  15. options.IncludeScopes = true;
  16. options.SingleLine = true;
  17. options.TimestampFormat = "yyyy-MM-dd HH:mm:ss ";
  18. });
  19. });
  20. // 使用 Chat ,即大语言模型聊天
  21. builder.Services.AddAzureOpenAITextGeneration(
  22. AzureOpenAIDeploymentName,
  23. AzureOpenAIEndpoint,
  24. AzureOpenAIApiKey,
  25. modelId: AzureOpenAIModelId
  26. );
  27. return builder;
  28. }

然后编写提问代码,用户可以多行输入文本,最后使用 000 结束输入,将文本提交给 AI 进行总结。进行总结时,为了避免 AI 废话太多,因此这里使用 ExecutionSettings 配置相关参数。

代码示例如下:

  1. builder = builder.WithAzureOpenAIText();
  2. var kernel = builder.Build();
  3. Console.WriteLine("输入文本:");
  4. var request = "";
  5. while (true)
  6. {
  7. var input = Console.ReadLine();
  8. if (input == "000")
  9. {
  10. break;
  11. }
  12. request += Environment.NewLine;
  13. request += input;
  14. }
  15. var func = kernel.CreateFunctionFromPrompt(new PromptTemplateConfig
  16. {
  17. Name = "chat",
  18. Description = "给出一段对话记录,总结这部分对话.",
  19. // 用户的文本
  20. Template = request,
  21. TemplateFormat = "semantic-kernel",
  22. ExecutionSettings = new Dictionary<string, PromptExecutionSettings>
  23. {
  24. {
  25. "default",
  26. new OpenAIPromptExecutionSettings()
  27. {
  28. MaxTokens = 100,
  29. Temperature = (float)0.3,
  30. TopP = (float)1,
  31. FrequencyPenalty = (float)0,
  32. PresencePenalty = (float)0
  33. }
  34. }
  35. }
  36. });
  37. var result = await func.InvokeAsync(kernel);
  38. Console.WriteLine($"""
  39. 总结如下:
  40. {string.Join("\n", result)}
  41. """);

923b295a3482e1518d3efe37a7b5e032.png

Semantic Kernel 插件

Semantic Kernel 在 Microsoft.SemanticKernel.Plugins 开头的包中提供了一些插件,不同的包有不同功能的插件。大部分目前还是属于半成品,因此这部分不详细讲解,本节只做简单说明。

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

  1. ├─Plugins.Core
  2. ├─Plugins.Document
  3. ├─Plugins.Memory
  4. ├─Plugins.MsGraph
  5. └─Plugins.Web

nuget 搜索时,需要加上 Microsoft.SemanticKernel. 前缀。

Semantic Kernel 还有通过远程 swagger.json 使用插件的做法,详细请参考文档:https://learn.microsoft.com/en-us/semantic-kernel/agents/plugins/openai-plugins

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

  1. // 读取和写入文件
  2. FileIOPlugin
  3. // http 请求以及返回字符串结果
  4. HttpPlugin
  5. // 只提供了 + 和 - 两种运算
  6. MathPlugin
  7. // 文本大小写等简单的功能
  8. TextPlugin
  9. // 获得本地时间日期
  10. TimePlugin
  11. // 在操作之前等待一段时间
  12. WaitPlugin

因为这些插件对本文演示没什么帮助,功能也非常简单,因此这里不讲解。下面简单讲一下文档插件。

文档插件

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

DocumentFormat.OpenXml 项目地址 https://github.com/dotnet/Open-XML-SDK

  • WordprocessingML:用于创建和编辑 Word 文档 (.docx)

  • SpreadsheetML:用于创建和编辑 Excel 电子表格 (.xlsx)

  • PowerPointML:用于创建和编辑 PowerPoint 演示文稿 (.pptx)

  • VisioML:用于创建和编辑 Visio 图表 (.vsdx)

  • ProjectML:用于创建和编辑 Project 项目 (.mpp)

  • DiagramML:用于创建和编辑 Visio 图表 (.vsdx)

  • PublisherML:用于创建和编辑 Publisher 出版物 (.pubx)

  • InfoPathML:用于创建和编辑 InfoPath 表单 (.xsn)

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

  1. DocumentPlugin documentPlugin = new(new WordDocumentConnector(), new LocalFileSystemConnector());
  2. string filePath = "(完整版)基础财务知识.docx";
  3. string text = await documentPlugin.ReadTextAsync(filePath);
  4. Console.WriteLine(text);

由于这些插件目前都是半成品,因此这里就不展开说明了。

6216d7fcd4f98234effbb29005e4e70a.png

planners

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

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

Kernel Memory 构建文档知识库

Kernel Memory 是一个歪果仁的个人项目,支持 PDF 和 Word 文档、 PowerPoint 演示文稿、图像、电子表格等,通过利用大型语言模型(llm)、嵌入和矢量存储来提取信息和生成记录,主要目的是提供文档处理相关的接口,最常使用的场景是知识库系统。读者可能对知识库系统不了解,如果有条件,建议部署一个 Fastgpt 系统研究一下。

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

Kernel Memory 项目文档:https://microsoft.github.io/kernel-memory/

Kernel Memory 项目仓库:https://github.com/microsoft/kernel-memory

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

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

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

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

首先,将 docx、pdf 等问题提取出文本,然后切割成多个段落,每一段都使用 AI 模型生成相关向量,这个向量的原理笔者也不懂,大家可以简单理解为分词,生成向量后,将段落文本和向量都存储到数据库中(数据库需要支持向量)。

1e446a51f085b4ce639144837f979498.png

然后在用户提问 “什么是报表” 时,首先在数据库中搜索,根据向量来确定相似程度,把几个跟问题相关的段落拿出来,然后把这几段文本和用户的问题一起发给 AI。相对于在提示模板中,塞进一部分背景知识,然后加上用户的问题,再由 AI 进行总结回答。

d4841e63e493f4dbbfede8493b14197f.png

aa12f0b96d07f87d2810dd9cc526461e.png

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

从 web 处理网页

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

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

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

使用命令启动服务:

dotnet run setup

这个控制台的作用是帮助我们生成相关配置的。启动这个控制台之后,根据提示选择对应的选项(按上下键选择选项,按下回车键确认),以及填写配置内容,配置会被存储到 appsettings.Development.json 中。

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

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

  • AccessKey1、AccessKey2 是客户端使用该 Service 所需要的验证密钥,随便填几个字母即可。

  • AzureAIDocIntel、AzureOpenAIEmbedding、AzureOpenAIText 根据实际情况填写。

  1. {
  2. "KernelMemory": {
  3. "Service": {
  4. "RunWebService": true,
  5. "RunHandlers": true,
  6. "OpenApiEnabled": true,
  7. "Handlers": {}
  8. },
  9. "ContentStorageType": "SimpleFileStorage",
  10. "TextGeneratorType": "AzureOpenAIText",
  11. "ServiceAuthorization": {
  12. "Enabled": true,
  13. "AuthenticationType": "APIKey",
  14. "HttpHeaderName": "Authorization",
  15. "AccessKey1": "自定义密钥1",
  16. "AccessKey2": "自定义密钥2"
  17. },
  18. "DataIngestion": {
  19. "OrchestrationType": "Distributed",
  20. "DistributedOrchestration": {
  21. "QueueType": "SimpleQueues"
  22. },
  23. "EmbeddingGenerationEnabled": true,
  24. "EmbeddingGeneratorTypes": [
  25. "AzureOpenAIEmbedding"
  26. ],
  27. "MemoryDbTypes": [
  28. "SimpleVectorDb"
  29. ],
  30. "ImageOcrType": "AzureAIDocIntel",
  31. "TextPartitioning": {
  32. "MaxTokensPerParagraph": 1000,
  33. "MaxTokensPerLine": 300,
  34. "OverlappingTokens": 100
  35. },
  36. "DefaultSteps": []
  37. },
  38. "Retrieval": {
  39. "MemoryDbType": "SimpleVectorDb",
  40. "EmbeddingGeneratorType": "AzureOpenAIEmbedding",
  41. "SearchClient": {
  42. "MaxAskPromptSize": -1,
  43. "MaxMatchesCount": 100,
  44. "AnswerTokens": 300,
  45. "EmptyAnswer": "INFO NOT FOUND"
  46. }
  47. },
  48. "Services": {
  49. "SimpleQueues": {
  50. "Directory": "_tmp_queues"
  51. },
  52. "SimpleFileStorage": {
  53. "Directory": "_tmp_files"
  54. },
  55. "AzureAIDocIntel": {
  56. "Auth": "ApiKey",
  57. "Endpoint": "https://aaa.openai.azure.com/",
  58. "APIKey": "aaa"
  59. },
  60. "AzureOpenAIEmbedding": {
  61. "APIType": "EmbeddingGeneration",
  62. "Auth": "ApiKey",
  63. "Endpoint": "https://aaa.openai.azure.com/",
  64. "Deployment": "aitext",
  65. "APIKey": "aaa"
  66. },
  67. "SimpleVectorDb": {
  68. "Directory": "_tmp_vectors"
  69. },
  70. "AzureOpenAIText": {
  71. "APIType": "ChatCompletion",
  72. "Auth": "ApiKey",
  73. "Endpoint": "https://aaa.openai.azure.com/",
  74. "Deployment": "myai",
  75. "APIKey": "aaa",
  76. "MaxRetries": 10
  77. }
  78. }
  79. },
  80. "Logging": {
  81. "LogLevel": {
  82. "Default": "Warning"
  83. }
  84. },
  85. "AllowedHosts": "*"
  86. }

详细可参考文档: https://microsoft.github.io/kernel-memory/quickstart/configuration

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

f8b13c620611cfec5cc158a522c7a708.png

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

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

  1. // 前面部署的 Service 地址,和自定义的密钥。
  2. var memory = new MemoryWebClient(endpoint: "http://localhost:9001/", apiKey: "自定义密钥1");
  3. // 导入网页
  4. await memory.ImportWebPageAsync(
  5. "https://baike.baidu.com/item/比特币挖矿机/12536531",
  6. documentId: "doc02");
  7. Console.WriteLine("正在处理文档,请稍等...");
  8. // 使用 AI 处理网页知识
  9. while (!await memory.IsDocumentReadyAsync(documentId: "doc02"))
  10. {
  11. await Task.Delay(TimeSpan.FromMilliseconds(1500));
  12. }
  13. // 提问
  14. var answer = await memory.AskAsync("比特币是什么?");
  15. Console.WriteLine($"\nAnswer: {answer.Result}");

此外还有 ImportTextAsync、ImportDocumentAsync 来个导入知识的方法。

手动处理文档

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

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

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

全部代码示例如下:

  1. using Microsoft.KernelMemory;
  2. using Microsoft.KernelMemory.MemoryStorage.DevTools;
  3. using Microsoft.SemanticKernel;
  4. using Microsoft.SemanticKernel.Connectors.OpenAI;
  5. var memory = new KernelMemoryBuilder()
  6. // 文档解析后的向量存储位置,可以选择 Postgres 等,
  7. // 这里选择使用本地临时文件存储向量
  8. .WithSimpleVectorDb(new SimpleVectorDbConfig
  9. {
  10. Directory = "aaa"
  11. })
  12. // 配置文档解析向量模型
  13. .WithAzureOpenAITextEmbeddingGeneration(new AzureOpenAIConfig
  14. {
  15. Deployment = "aitext",
  16. Endpoint = "https://aaa.openai.azure.com/",
  17. Auth = AzureOpenAIConfig.AuthTypes.APIKey,
  18. APIType = AzureOpenAIConfig.APITypes.EmbeddingGeneration,
  19. APIKey = "aaa"
  20. })
  21. // 配置文本生成模型
  22. .WithAzureOpenAITextGeneration(new AzureOpenAIConfig
  23. {
  24. Deployment = "myai",
  25. Endpoint = "https://aaa.openai.azure.com/",
  26. Auth = AzureOpenAIConfig.AuthTypes.APIKey,
  27. APIKey = "aaa",
  28. APIType = AzureOpenAIConfig.APITypes.ChatCompletion
  29. })
  30. .Build();
  31. // 导入网页
  32. await memory.ImportWebPageAsync(
  33. "https://baike.baidu.com/item/比特币挖矿机/12536531",
  34. documentId: "doc02");
  35. // Wait for ingestion to complete, usually 1-2 seconds
  36. Console.WriteLine("正在处理文档,请稍等...");
  37. while (!await memory.IsDocumentReadyAsync(documentId: "doc02"))
  38. {
  39. await Task.Delay(TimeSpan.FromMilliseconds(1500));
  40. }
  41. // Ask a question
  42. var answer = await memory.AskAsync("比特币是什么?");
  43. Console.WriteLine($"\nAnswer: {answer.Result}");

b8a3551fc6372010c34bfffd8aa232d7.png

首先使用 KernelMemoryBuilder 构建配置,配置的内容比较多,这里会使用到两个模型,一个是向量模型,一个是文本生成模型(可以使用对话模型,如 gpt-4-32k)。

接下来,按照该程序的工作流程讲解各个环节的相关知识。

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

  1. AzureBlobsStorage
  2. // 存储到目录
  3. SimpleFileStorage

使用方法:

  1. var memory = new KernelMemoryBuilder()
  2. .WithSimpleFileStorage(new SimpleFileStorageConfig
  3. {
  4. Directory = "aaa"
  5. })
  6. .WithAzureBlobsStorage(new AzureBlobsConfig
  7. {
  8. Account = ""
  9. })
  10. ...

Kernel Memory 还不支持 Mongodb,不过可以自己使用 IContentStorage 接口写一个。

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

517b20316f43c5709c015a6c472a83fa.png

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

解析后的向量是这样的:

7bf8e082071a4c866f6fe04034ceffc2.png

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

  1. AzureOpenAITextEmbeddingGenerator
  2. OpenAITextEmbeddingGenerator

示例:

  1. var memory = new KernelMemoryBuilder()
  2. // 配置文档解析向量模型
  3. .WithAzureOpenAITextEmbeddingGeneration(new AzureOpenAIConfig
  4. {
  5. Deployment = "aitext",
  6. Endpoint = "https://xxx.openai.azure.com/",
  7. Auth = AzureOpenAIConfig.AuthTypes.APIKey,
  8. APIType = AzureOpenAIConfig.APITypes.EmbeddingGeneration,
  9. APIKey = "xxx"
  10. })
  11. .WithOpenAITextEmbeddingGeneration(new OpenAIConfig
  12. {
  13. ... ...
  14. })

生成向量后,需要存储这些向量,需要实现 IMemoryDb 接口,有以下配置可以使用:

  1. // 文档解析后的向量存储位置,可以选择 Postgres 等,
  2. // 这里选择使用本地临时文件存储向量
  3. .WithSimpleVectorDb(new SimpleVectorDbConfig
  4. {
  5. Directory = "aaa"
  6. })
  7. .WithAzureAISearchMemoryDb(new AzureAISearchConfig
  8. {
  9. })
  10. .WithPostgresMemoryDb(new PostgresConfig
  11. {
  12. })
  13. .WithQdrantMemoryDb(new QdrantConfig
  14. {
  15. })
  16. .WithRedisMemoryDb("host=....")

当用户提问时,首先会在这里的 IMemoryDb 调用相关方法查询文档中的向量、索引等,查找出相关的文本。

查出相关的文本后,需要发送给 AI 处理,需要使用 ITextGenerator 接口,目前有两个实现:

  1. AzureOpenAITextGenerator
  2. OpenAITextGenerator

配置示例:

  1. // 配置文本生成模型
  2. .WithAzureOpenAITextGeneration(new AzureOpenAIConfig
  3. {
  4. Deployment = "myai",
  5. Endpoint = "https://aaa.openai.azure.com/",
  6. Auth = AzureOpenAIConfig.AuthTypes.APIKey,
  7. APIKey = "aaa",
  8. APIType = AzureOpenAIConfig.APITypes.ChatCompletion
  9. })

导入文档时,首先将文档提取出文本,然后进行分段。

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

提问问题或搜索内容时,从 IMemoryDb 所在的位置搜索向量,查询到相关的文本,然后将文本收集起来,发送给 AI(使用文本生成模型),这些文本相对于提示词,然后 AI 从这些提示词中学习并回答用户的问题。

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

be7cd8bed127839ed526eff8877f3599.png

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

  1. // 导入文档
  2. await memory.ImportDocumentAsync(
  3. "aaa/(完整版)基础财务知识.docx",
  4. documentId: "doc02");
  5. Console.WriteLine("正在处理文档,请稍等...");
  6. while (!await memory.IsDocumentReadyAsync(documentId: "doc02"))
  7. {
  8. await Task.Delay(TimeSpan.FromMilliseconds(1500));
  9. }
  10. var answer1 = await memory.SearchAsync("报表怎么做?");
  11. // 每个 Citation 表示一个文档文件
  12. foreach (Citation citation in answer1.Results)
  13. {
  14. // 与搜索关键词相关的文本
  15. foreach(var partition in citation.Partitions)
  16. {
  17. Console.WriteLine(partition.Text);
  18. }
  19. }
  20. var answer2 = await memory.AskAsync("报表怎么做?");
  21. Console.WriteLine($"\nAnswer: {answer2.Result}");

读者可以在 foreach 这里做个断点,当用户问题 “报表怎么做?” 时,搜索出来的相关文档。

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

f9d4125c02a560c1549123dbdb2aa0c1.png

本期模特:咕咕

acc7e8ca5ee1a0d9001e458fde16f866.jpeg

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/Gausst松鼠会/article/detail/300614
推荐阅读
相关标签
  

闽ICP备14008679号