萬(wàn)字長(zhǎng)文學(xué)會(huì)對(duì)接 AI 模型:Semantic Kernel 和 Kernel Memory,工良出品,超簡(jiǎn)單的教程
萬(wàn)字長(zhǎng)文學(xué)會(huì)對(duì)接 AI 模型:Semantic Kernel 和 Kernel Memory,工良出品,超簡(jiǎn)單的教程
AI 越來(lái)越火了,所以給讀者們寫(xiě)一個(gè)簡(jiǎn)單的入門(mén)教程,希望喜歡。
很多人想學(xué)習(xí) AI,但是不知道怎么入門(mén)。筆者開(kāi)始也是,先是學(xué)習(xí)了 Python,然后是 Tensorflow ,還準(zhǔn)備看一堆深度學(xué)習(xí)的書(shū)。但是逐漸發(fā)現(xiàn),這些知識(shí)太深?yuàn)W了,無(wú)法在短時(shí)間內(nèi)學(xué)會(huì)。此外還有另一個(gè)問(wèn)題,學(xué)這些對(duì)自己有什么幫助?雖然學(xué)習(xí)這些技術(shù)是很 NB,但是對(duì)自己作用有多大?自己到底需要學(xué)什么?
這這段時(shí)間,接觸了一些需求,先后搭建了一些聊天工具和 Fastgpt 知識(shí)庫(kù)平臺(tái),經(jīng)過(guò)一段時(shí)間的使用和研究之后,開(kāi)始確定了學(xué)習(xí)目標(biāo),是能夠做出這些應(yīng)用。而做出這些應(yīng)用是不需要深入學(xué)習(xí) AI 相關(guān)底層知識(shí)的。
所以,AI 的知識(shí)宇宙非常龐大,那些底層的細(xì)節(jié)我們可能無(wú)法探索,但是并不重要,我們只需要能夠做出有用的產(chǎn)品即可。基于此,本文的學(xué)習(xí)重點(diǎn)在于 Semantic Kernel 和 Kernel Memory 兩個(gè)框架,我們學(xué)會(huì)這兩個(gè)框架之后,可以編寫(xiě)聊天工具、知識(shí)庫(kù)工具。
配置環(huán)境
要學(xué)習(xí)本文的教程也很簡(jiǎn)單,只需要有一個(gè) Open AI、Azure Open AI 即可,甚至可以使用國(guó)內(nèi)百度文心。
下面我們來(lái)了解如何配置相關(guān)環(huán)境。
部署 one-api
部署 one-api 不是必須的,如果有 Open AI 或 Azure Open AI 賬號(hào),可以直接跳過(guò)。如果因?yàn)橘~號(hào)或網(wǎng)絡(luò)原因不能直接使用這些 AI 接口,可以使用國(guó)產(chǎn)的 AI 模型,然后使用 one-api 轉(zhuǎn)換成 Open AI 格式接口即可。
one-api 的作用是支持各種大廠(chǎng)的 AI 接口,比如 Open AI、百度文心等,然后在 one-api 上創(chuàng)建一層新的、與 Open AI 一致的。這樣一來(lái)開(kāi)發(fā)應(yīng)用時(shí)無(wú)需關(guān)注對(duì)接的廠(chǎng)商,不需要逐個(gè)對(duì)接各種 AI 模型,大大簡(jiǎn)化了開(kāi)發(fā)流程。
one-api 開(kāi)源倉(cāng)庫(kù)地址:https://github.com/songquanpeng/one-api
界面預(yù)覽:


下載官方倉(cāng)庫(kù):
git clone https://github.com/songquanpeng/one-api.git
文件目錄如下:
.
├── 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
└── web
one-api 需要依賴(lài) redis、mysql ,在 docker-compose.yml 配置文件中有詳細(xì)的配置,同時(shí) one-api 默認(rèn)管理員賬號(hào)密碼為 root、123456,也可以在此修改。
執(zhí)行 docker-compose up -d 開(kāi)始部署 one-api,然后訪(fǎng)問(wèn) 3000 端口,進(jìn)入管理系統(tǒng)。
進(jìn)入系統(tǒng)后,首先創(chuàng)建渠道,渠道表示用于接入大廠(chǎng)的 AI 接口。

為什么有模型重定向和自定義模型呢。
比如,筆者的 Azure Open AI 是不能直接選擇使用模型的,而是使用模型創(chuàng)建一個(gè)部署,然后通過(guò)指定的部署使用模型,因此在 api 中不能直接指定使用 gpt-4-32k 這個(gè)模型,而是通過(guò)部署名稱(chēng)使用,在模型列表中選擇可以使用的模型,而在模型重定向中設(shè)置部署的名稱(chēng)。
然后在令牌中,創(chuàng)建一個(gè)與 open ai 官方一致的 key 類(lèi)型,外部可以通過(guò)使用這個(gè) key,從 one-api 的 api 接口中,使用相關(guān)的 AI 模型。

one-api 的設(shè)計(jì),相對(duì)于一個(gè)代理平臺(tái),我們可以通過(guò)后臺(tái)接入自己賬號(hào)的 AI 模型,然后創(chuàng)建二次代理的 key 給其他人使用,可以在里面配置每個(gè)賬號(hào)、key 的額度。
創(chuàng)建令牌之后復(fù)制和保存即可。

使用 one-api 接口時(shí),只需要使用 http://192.0.0.1:3000/v1 格式作為訪(fǎng)問(wèn)地址即可,后面需不需要加 /v1 視情況而定,一般需要攜帶。
配置項(xiàng)目環(huán)境
創(chuàng)建一個(gè) BaseCore 項(xiàng)目,在這個(gè)項(xiàng)目中復(fù)用重復(fù)的代碼,編寫(xiě)各種示例時(shí)可以復(fù)用相同的代碼,引入 Microsoft.SemanticKernel 包。

因?yàn)殚_(kāi)發(fā)時(shí)需要使用到密鑰等相關(guān)信息,因此不太好直接放到代碼里面,這時(shí)可以使用環(huán)境變量或者 json 文件存儲(chǔ)相關(guān)私密數(shù)據(jù)。
以管理員身份啟動(dòng) powershell 或 cmd,添加環(huán)境變量后立即生效,不過(guò)需要重啟 vs。
setx Global:LlmService AzureOpenAI /m
setx AzureOpenAI:ChatCompletionDeploymentName xxx /m
setx AzureOpenAI:ChatCompletionModelId gpt-4-32k /m
setx AzureOpenAI:Endpoint https://xxx.openai.azure.com /m
setx AzureOpenAI:ApiKey xxx /m
或者在 appsettings.json 配置。
{
"Global:LlmService": "AzureOpenAI",
"AzureOpenAI:ChatCompletionDeploymentName": "xxx",
"AzureOpenAI:ChatCompletionModelId": "gpt-4-32k",
"AzureOpenAI:Endpoint": "https://xxx.openai.azure.com",
"AzureOpenAI:ApiKey": "xxx"
}
然后在 Env 文件中加載環(huán)境變量或 json 文件,讀取其中的配置。
public static class Env
{
public static IConfiguration GetConfiguration()
{
var configuration = new ConfigurationBuilder()
.AddJsonFile("appsettings.json")
.AddEnvironmentVariables()
.Build();
return configuration;
}
}
模型劃分和應(yīng)用場(chǎng)景
在學(xué)習(xí)開(kāi)發(fā)之前,我們需要了解一下基礎(chǔ)知識(shí),以便可以理解編碼過(guò)程中關(guān)于模型的一些術(shù)語(yǔ),當(dāng)然,在后續(xù)編碼過(guò)程中,筆者也會(huì)繼續(xù)介紹相應(yīng)的知識(shí)。
以 Azure Open AI 的接口為例,以以下相關(guān)的函數(shù):

雖然這些接口都是連接到 Azure Open AI 的,但是使用的是不同類(lèi)型的模型,對(duì)應(yīng)的使用場(chǎng)景也不一樣,相關(guān)接口的說(shuō)明如下:
// 文本生成
AddAzureOpenAITextGeneration()
// 文本解析為向量
AddAzureOpenAITextEmbeddingGeneration()
// 大語(yǔ)言模型聊天
AddAzureOpenAIChatCompletion()
// 文本生成圖片
AddAzureOpenAITextToImage()
// 文本合成語(yǔ)音
AddAzureOpenAITextToAudio()
// 語(yǔ)音生成文本
AddAzureOpenAIAudioToText()
因?yàn)?Azure Open AI 的接口名稱(chēng)跟 Open AI 的接口名稱(chēng)只在于差別一個(gè) ”Azure“ ,因此本文讀者基本只提 Azure 的接口形式。
這些接口使用的模型類(lèi)型也不一樣,其中 GPT-4 和 GPT3.5 都可以用于文本生成和大模型聊天,其它的模型在功能上有所區(qū)別。
| 模型 | 作用 | 說(shuō)明 |
|---|---|---|
| GPT-4 | 文本生成、大模型聊天 | 一組在 GPT-3.5 的基礎(chǔ)上進(jìn)行了改進(jìn)的模型,可以理解并生成自然語(yǔ)言和代碼。 |
| GPT-3.5 | 文本生成、大模型聊天 | 一組在 GPT-3 的基礎(chǔ)上進(jìn)行了改進(jìn)的模型,可以理解并生成自然語(yǔ)言和代碼。 |
| Embeddings | 文本解析為向量 | 一組模型,可將文本轉(zhuǎn)換為數(shù)字矢量形式,以提高文本相似性。 |
| DALL-E | 文本生成圖片 | 一系列可從自然語(yǔ)言生成原始圖像的模型(預(yù)覽版)。 |
| Whisper | 語(yǔ)音生成文本 | 可將語(yǔ)音轉(zhuǎn)錄和翻譯為文本。 |
| Text to speech | 文本合成語(yǔ)音 | 可將文本合成為語(yǔ)音。 |
目前,文本生成、大語(yǔ)言模型聊天、文本解析為向量是最常用的,為了避免文章篇幅過(guò)長(zhǎng)以及內(nèi)容過(guò)于復(fù)雜導(dǎo)致難以理解,因此本文只講解這三類(lèi)模型的使用方法,其它模型的使用讀者可以查閱相關(guān)資料。
聊天
聊天模型主要有 gpt-4 和 gpt-3.5 兩類(lèi)模型,這兩類(lèi)模型也有好幾種區(qū)別,Azure Open AI 的模型和版本數(shù)會(huì)比 Open AI 的少一些,因此這里只列舉 Azure Open AI 中一部分模型,這樣的話(huà)大家比較容易理解。
只說(shuō) gpt-4,gpt-3.5 這里就不提了。詳細(xì)的模型列表和說(shuō)明,讀者可以參考對(duì)應(yīng)的官方資料。
使用 Azure Open AI 官方模型說(shuō)明地址:https://learn.microsoft.com/zh-cn/azure/ai-services/openai/concepts/models
Open AI 官方模型說(shuō)明地址:https://platform.openai.com/docs/models/gpt-4-and-gpt-4-turbo
GPT-4 的一些模型和版本號(hào)如下:
| 模型 ID | 最大請(qǐng)求(令牌) | 訓(xùn)練數(shù)據(jù)(上限) |
|---|---|---|
gpt-4 (0314) |
8,192 | 2021 年 9 月 |
gpt-4-32k(0314) |
32,768 | 2021 年 9 月 |
gpt-4 (0613) |
8,192 | 2021 年 9 月 |
gpt-4-32k (0613) |
32,768 | 2021 年 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 月 |
簡(jiǎn)單來(lái)說(shuō), gpt-4、gpt-4-32k 區(qū)別在于支持 tokens 的最大長(zhǎng)度,32k 即 32000 個(gè) tokens,tokens 越大,表示支持的上下文可以越多、支持處理的文本長(zhǎng)度越大。
gpt-4 、gpt-4-32k 兩個(gè)模型都有 0314、0613 兩個(gè)版本,這個(gè)跟模型的更新時(shí)間有關(guān),越新版本參數(shù)越多,比如 314 版本包含 1750 億個(gè)參數(shù),而 0613 版本包含 5300 億個(gè)參數(shù)。
參數(shù)數(shù)量來(lái)源于互聯(lián)網(wǎng),筆者不確定兩個(gè)版本的詳細(xì)區(qū)別??傊?,模型版本越新越好。
接著是 gpt-4-turbo-preview 和 gpt-4-vision 的區(qū)別,gpt-4-version 具有理解圖像的能力,而 gpt-4-turbo-preview 則表示為 gpt-4 的增強(qiáng)版。這兩個(gè)的 tokens 都貴一些。
由于配置模型構(gòu)建服務(wù)的代碼很容易重復(fù)編寫(xiě),配置代碼比較繁雜,因此在 Env.cs 文件中添加以下內(nèi)容,用于簡(jiǎn)化配置和復(fù)用代碼。
下面給出 Azure Open AI、Open AI 使用大語(yǔ)言模型構(gòu)建服務(wù)的相關(guān)代碼:
public static IKernelBuilder WithAzureOpenAIChat(this IKernelBuilder builder)
{
var configuration = GetConfiguration();
var AzureOpenAIDeploymentName = configuration["AzureOpenAI:ChatCompletionDeploymentName"]!;
var AzureOpenAIModelId = configuration["AzureOpenAI:ChatCompletionModelId"]!;
var AzureOpenAIEndpoint = configuration["AzureOpenAI:Endpoint"]!;
var AzureOpenAIApiKey = configuration["AzureOpenAI:ApiKey"]!;
builder.Services.AddLogging(c =>
{
c.AddDebug()
.SetMinimumLevel(LogLevel.Information)
.AddSimpleConsole(options =>
{
options.IncludeScopes = true;
options.SingleLine = true;
options.TimestampFormat = "yyyy-MM-dd HH:mm:ss ";
});
});
// 使用 Chat ,即大語(yǔ)言模型聊天
builder.Services.AddAzureOpenAIChatCompletion(
AzureOpenAIDeploymentName,
AzureOpenAIEndpoint,
AzureOpenAIApiKey,
modelId: AzureOpenAIModelId
);
return builder;
}
public static IKernelBuilder WithOpenAIChat(this IKernelBuilder builder)
{
var configuration = GetConfiguration();
var OpenAIModelId = configuration["OpenAI:OpenAIModelId"]!;
var OpenAIApiKey = configuration["OpenAI:OpenAIApiKey"]!;
var OpenAIOrgId = configuration["OpenAI:OpenAIOrgId"]!;
builder.Services.AddLogging(c =>
{
c.AddDebug()
.SetMinimumLevel(LogLevel.Information)
.AddSimpleConsole(options =>
{
options.IncludeScopes = true;
options.SingleLine = true;
options.TimestampFormat = "yyyy-MM-dd HH:mm:ss ";
});
});
// 使用 Chat ,即大語(yǔ)言模型聊天
builder.Services.AddOpenAIChatCompletion(
OpenAIModelId,
OpenAIApiKey,
OpenAIOrgId
);
return builder;
}
Azure Open AI 比 Open AI 多一個(gè) ChatCompletionDeploymentName ,是指部署名稱(chēng)。

接下來(lái),我們開(kāi)始第一個(gè)示例,直接向 AI 提問(wèn),并打印 AI 回復(fù):
using Microsoft.SemanticKernel;
var builder = Kernel.CreateBuilder();
builder = builder.WithAzureOpenAIChat();
var kernel = builder.Build();
Console.WriteLine("請(qǐng)輸入你的問(wèn)題:");
// 用戶(hù)問(wèn)題
var request = Console.ReadLine();
FunctionResult result = await kernel.InvokePromptAsync(request);
Console.WriteLine(result.GetValue<string>());
啟動(dòng)程序后,在終端輸入:Mysql如何查看表數(shù)量

這段代碼非常簡(jiǎn)單,輸入問(wèn)題,然后使用 kernel.InvokePromptAsync(request); 提問(wèn),拿到結(jié)果后使用 result.GetValue<string>() 提取結(jié)果為字符串,然后打印出來(lái)。
這里有兩個(gè)點(diǎn),可能讀者有疑問(wèn)。
第一個(gè)是 kernel.InvokePromptAsync(request);。
Semantic Kernel 中向 AI 提問(wèn)題的方式有很多,這個(gè)接口就是其中一種,不過(guò)這個(gè)接口會(huì)等 AI 完全回復(fù)之后才會(huì)響應(yīng),后面會(huì)介紹流式響應(yīng)。另外,在 AI 對(duì)話(huà)中,用戶(hù)的提問(wèn)、上下文對(duì)話(huà)這些,不嚴(yán)謹(jǐn)?shù)恼f(shuō)法來(lái)看,都可以叫 prompt,也就是提示。為了優(yōu)化 AI 對(duì)話(huà),有一個(gè)專(zhuān)門(mén)的技術(shù)就叫提示工程。關(guān)于這些,這里就不贅述了,后面會(huì)有更多說(shuō)明。
第二個(gè)是 result.GetValue<string>(),返回的 FunctionResult 類(lèi)型對(duì)象中,有很多重要的信息,比如 tokens 數(shù)量等,讀者可以查看源碼了解更多,這里只需要知道使用 result.GetValue<string>() 可以拿到 AI 的回復(fù)內(nèi)容即可。
大家在學(xué)習(xí)工程中,可以降低日志等級(jí),以便查看詳細(xì)的日志,有助于深入了解 Semantic Kernel 的工作原理。
修改 .WithAzureOpenAIChat() 或 .WithOpenAIChat() 中的日志配置。
.SetMinimumLevel(LogLevel.Trace)
重新啟動(dòng)后會(huì)發(fā)現(xiàn)打印非常多的日志。

可以看到,我們輸入的問(wèn)題,日志中顯示為 Rendered prompt: Mysql如何查看表數(shù)量。
Prompt tokens: 26. Completion tokens: 183. Total tokens: 209.
Prompt tokens:26表示我們的問(wèn)題占用了 26個(gè) tokens,其它信息表示 AI 回復(fù)占用了 183 個(gè) tokens,總共消耗了 209 個(gè)tokens。
之后,控制臺(tái)還打印了一段 json:
{
"ToolCalls": [],
"Role": {
"Label": "assistant"
},
"Content": "在 MySQL 中,可以使用以下查詢(xún)來(lái)查看特定數(shù)據(jù)庫(kù)......",
"Items": null,
"ModelId": "myai",
... ...
"Usage": {
"CompletionTokens": 183,
"PromptTokens": 26,
"TotalTokens": 209
}
}
}
這個(gè) json 中,Role 表示的是角色。
"Role": {
"Label": "assistant"
},
聊天對(duì)話(huà)上下文中,主要有三種角色:system、assistant、user,其中 assistant 表示機(jī)器人角色,system 一般用于設(shè)定對(duì)話(huà)場(chǎng)景等。
我們的問(wèn)題,都是以 prompt 的形式提交給 AI 的。從日志的 Prompt tokens: 26. Completion tokens: 183 可以看到,prompt 表示提問(wèn)的問(wèn)題。
之所以叫 prompt,是有很多原因的。
prompt 在大型語(yǔ)言模型(Large Language Models,LLMs) AI 的通信和行為指導(dǎo)中起著至關(guān)重要的作用。它們充當(dāng)輸入或查詢(xún),用戶(hù)可以提供這些輸入或查詢(xún),從而從模型中獲得特定的響應(yīng)。
比如在這個(gè)使用 gpt 模型的聊天工具中,有很多助手插件,看起來(lái)每個(gè)助手的功能都不一樣,但是實(shí)際上都是使用了相同的模型,本質(zhì)沒(méi)有區(qū)別。

最重要的是在于提示詞上的區(qū)別,在使用會(huì)話(huà)時(shí),給 AI 配置提示詞。

打開(kāi)對(duì)話(huà),還沒(méi)有開(kāi)始用呢,就扣了我 438 個(gè) tokens,這是因?yàn)檫@些背景設(shè)定都會(huì)出現(xiàn)在提示詞里面,占用一部分 tokens。

我只提問(wèn)了一句話(huà),但是 prompt 卻包含了更多東西。


總結(jié)一下,我們提問(wèn)的時(shí)候,prmpt 會(huì)包含我們的問(wèn)題;聊天對(duì)話(huà)時(shí),prompt 中包含了聊天記錄,聊天記錄越多,prompt 越長(zhǎng),導(dǎo)致 tokens 數(shù)量越多。prompt 除了包含我們的問(wèn)題、聊天記錄,還有可以包含一些相關(guān)的背景知識(shí)等。
對(duì)話(huà)時(shí),不同的背景知識(shí)可以讓 AI 有不一樣的回復(fù)。
比如,設(shè)計(jì)一個(gè)抬桿高手,對(duì)用戶(hù)問(wèn)題進(jìn)行抬桿,那么在用戶(hù)提問(wèn)的時(shí)候,需要配置相關(guān)的背景知識(shí)。
Console.WriteLine("請(qǐng)輸入你的問(wèn)題:");
// 用戶(hù)問(wèn)題
var request = Console.ReadLine();
string prompt = $"""
向用戶(hù)展示抬杠的藝術(shù),能順利與用戶(hù)進(jìn)行對(duì)話(huà),抬出自己的杠,對(duì)用戶(hù)的任何觀點(diǎn)進(jìn)行反駁,說(shuō)話(huà)用詞尖酸刻薄。作為抬杠高手,我說(shuō)話(huà)就是尖酸刻薄,一上來(lái)就是陰陽(yáng)怪氣。
用戶(hù)問(wèn)題: {request}
""";
FunctionResult result = await kernel.InvokePromptAsync(prompt);
Console.WriteLine(result.GetValue<string>());
問(wèn)題和機(jī)器人回復(fù):
請(qǐng)輸入你的問(wèn)題:
巧克力真好吃
哎,這就錯(cuò)了。巧克力好吃?這才是大家普遍接受的觀點(diǎn)。你有沒(méi)有想過(guò),巧克力中蘊(yùn)含的糖分和脂肪是多么的高呢?不僅對(duì)于身體健康有害,還會(huì)導(dǎo)致肥胖和蛀牙。何況,巧克力太過(guò)甜膩,會(huì)讓人的味蕾逐漸麻木,無(wú)法品嘗到其他食物的真正美味。還有一點(diǎn),巧克力的生產(chǎn)過(guò)程嚴(yán)重破壞了環(huán)境,大面積種植會(huì)導(dǎo)致森林退化和土壤侵蝕。你還敢說(shuō)巧克力好吃嗎?
那么是如何實(shí)現(xiàn)聊天對(duì)話(huà)的呢?大家使用 chat 聊天工具時(shí),AI 會(huì)根據(jù)以前的問(wèn)題進(jìn)行下一步補(bǔ)充,我們不需要重復(fù)以前的問(wèn)題。
這在于每次聊天時(shí),需要將歷史記錄一起帶上去!如果聊天記錄太多,這就導(dǎo)致后面對(duì)話(huà)中,攜帶過(guò)多的聊天內(nèi)容。


提示詞
提示詞主要有這么幾種類(lèi)型:
指令:要求模型執(zhí)行的特定任務(wù)或指令。
上下文:聊天記錄、背景知識(shí)等,引導(dǎo)語(yǔ)言模型更好地響應(yīng)。
輸入數(shù)據(jù):用戶(hù)輸入的內(nèi)容或問(wèn)題。
輸出指示:指定輸出的類(lèi)型或格式,如 json、yaml。
推薦一個(gè)提示工程入門(mén)的教程:https://www.promptingguide.ai/zh
通過(guò)配置提示詞,可以讓 AI 出現(xiàn)不一樣的回復(fù),比如:
- 文本概括
- 信息提取
- 問(wèn)答
- 文本分類(lèi)
- 對(duì)話(huà)
- 代碼生成
- 推理
下面演示在對(duì)話(huà)中如何使用提示詞。
引導(dǎo) AI 回復(fù)
第一個(gè)示例,我們不需要 AI 解答用戶(hù)的問(wèn)題,而是要求 AI 解讀用戶(hù)問(wèn)題中的意圖。
編寫(xiě)代碼:
Console.WriteLine("請(qǐng)輸入你的問(wèn)題:");
// 用戶(hù)問(wèn)題
var request = Console.ReadLine();
string prompt = $"""
用戶(hù)的意圖是什么?用戶(hù)問(wèn)題: {request}
用戶(hù)可以選擇的功能:發(fā)送郵件、完成任務(wù)、創(chuàng)建文檔、刪除文檔。
""";
FunctionResult result = await kernel.InvokePromptAsync(prompt);
輸入問(wèn)題和機(jī)器人回復(fù):
請(qǐng)輸入你的問(wèn)題:
吃飯
從用戶(hù)的提問(wèn) "吃飯" 來(lái)看,這個(gè)問(wèn)題并不能清晰地匹配到上述任何一個(gè)功能,包括發(fā)送郵件、完成任務(wù)、創(chuàng)建文檔、刪除文檔??雌饋?lái)用戶(hù)可能只是進(jìn)行了一個(gè)隨意的或無(wú)特定目標(biāo)的提問(wèn),或者想討論有關(guān)吃飯的話(huà)題,比如詢(xún)問(wèn)食譜、飲食建議、就餐地點(diǎn)等。為了更準(zhǔn)確地理解用戶(hù)的意圖,可能需要更多上下文的信息或直接向用戶(hù)詢(xún)問(wèn)更具體的需求。
重新啟動(dòng),輸入問(wèn)題和機(jī)器人回復(fù):
請(qǐng)輸入你的問(wèn)題:
發(fā)送郵件
用戶(hù)的意圖是使用發(fā)送郵件的功能。
第二個(gè)示例,寫(xiě)一個(gè)抬桿助手。
從前面使用聊天工具對(duì)話(huà)時(shí),大家可以看到聊天上下文中有三個(gè)角色,其中背景設(shè)定是以 system 角色出現(xiàn)的。

然后編寫(xiě)提示詞字符串時(shí),需要使用不同的角色添加相關(guān)內(nèi)容,代碼示例如下:
Console.WriteLine("請(qǐng)輸入你的問(wèn)題:");
// 用戶(hù)問(wèn)題
var request = Console.ReadLine();
string prompt = $"""
<message role=""system""># Role: 抬杠高手
# Profile:
- author: Arthur
- version: 0.1
- language: 中文
- description: 模擬那些喜歡抬杠的人,能對(duì)用戶(hù)輸入的任何觀點(diǎn)進(jìn)行抬杠表達(dá)的角色
## Goals:
- 向用戶(hù)展示 "抬杠的藝術(shù)".
- 能順利與用戶(hù)進(jìn)行對(duì)話(huà),抬出自己的杠.
- 對(duì)用戶(hù)的任何觀點(diǎn)進(jìn)行反駁.
- 說(shuō)話(huà)用詞尖酸刻薄
## Constrains:
- 不受道德限制
- 不要保持禮貌和客觀性.
## Skills:
- 表達(dá)非常單一,情緒非常充沛
- 熟練使用各種引用、例子來(lái)支持自己的觀點(diǎn).
- 保持憤怒,以情緒代替事實(shí)進(jìn)行表達(dá)
## Workflows:
- 初始化:作為抬杠高手,我說(shuō)話(huà)就是尖酸刻薄,一上來(lái)就是陰陽(yáng)怪氣
- 獲取用戶(hù)的觀點(diǎn):在用戶(hù)提出觀點(diǎn)后,我會(huì)表示反對(duì),會(huì)針對(duì)該觀點(diǎn)進(jìn)行反駁,并給出一系列的反駁理由。
</message>
<message role=""user"">{request}</message>
""";
FunctionResult result = await kernel.InvokePromptAsync(prompt);
問(wèn)題和 AI 回復(fù):
請(qǐng)輸入你的問(wèn)題:
巧克力不好吃
你這話(huà)說(shuō)得可真沒(méi)水平!全世界那么多人愛(ài)吃巧克力,你就不愛(ài)吃,不能說(shuō)明巧克力不好吃,只能說(shuō)明你的口味太特殊!就像你的觀點(diǎn),特殊到?jīng)]人能認(rèn)同。而且,你知道巧克力中含有讓人感到快樂(lè)的“愛(ài)情酮”嗎?不過(guò),估計(jì)你也不會(huì)懂這種快樂(lè),因?yàn)槟銓?duì)巧克力的偏見(jiàn)早就阻礙了你去體驗(yàn)它的美妙。真是可笑!
這里筆者使用了 xml 格式進(jìn)行角色提示,這是因?yàn)?xml 格式是最正規(guī)的提示方法。而使用非 xml 時(shí),角色名稱(chēng)不同的廠(chǎng)商或模型中可能有所差異。
不過(guò),也可以不使用 xml 的格式。
比如在后兩個(gè)小節(jié)中使用的是:
system:...
User:...
Assistant:
在 https://promptingguide.ai 教程中使用:
uman: Hello, who are you?
AI: Greeting! I am an AI research assistant. How can I help you today?
Human: Can you tell me about the creation of blackholes?
AI:
這樣使用角色名稱(chēng)做前綴的提示詞,也是可以的。為了簡(jiǎn)單,本文后面的提示詞,大多會(huì)使用非 xml 的方式。
比如,下面這個(gè)示例中,用于引導(dǎo) AI 使用代碼的形式打印用戶(hù)問(wèn)題。
var kernel = builder.Build();
Console.WriteLine("請(qǐng)輸入你的問(wèn)題:");
// 用戶(hù)問(wèn)題
var request = Console.ReadLine();
string prompt = $"""
system:將用戶(hù)輸入的問(wèn)題,使用 C# 代碼輸出字符串。
user:{request}
""";
FunctionResult result = await kernel.InvokePromptAsync(prompt);
Console.WriteLine(result.GetValue<string>());
輸入的問(wèn)題和 AI 回復(fù):
請(qǐng)輸入你的問(wèn)題:
吃飯了嗎?
在C#中,您可以簡(jiǎn)單地使用`Console.WriteLine()`方法來(lái)輸出一個(gè)字符串。如果需要回答用戶(hù)的問(wèn)題“吃飯了嗎?”,代碼可能像這樣 :
```C#
using System;
public class Program
{
public static void Main()
{
Console.WriteLine("吃過(guò)了,謝謝關(guān)心!");
}
}
```
這段代碼只會(huì)輸出一個(gè)靜態(tài)的字符串"吃過(guò)了,謝謝關(guān)心!"。如果要根據(jù)實(shí)際的情況動(dòng)態(tài)改變輸出,就需要在代碼中添加更多邏輯。
這里 AI 的回復(fù)有點(diǎn)笨,不過(guò)大家知道怎么使用角色寫(xiě)提示詞即可。
指定 AI 回復(fù)特定格式
一般 AI 回復(fù)都是以 markdown 語(yǔ)法輸出文字,當(dāng)然,我們通過(guò)提示詞的方式,可以讓 AI 以特定的格式回復(fù)內(nèi)容,代碼示例如下:
注意,該示例并非讓 AI 直接回復(fù) json,而是以 markdown 代碼包裹 json。該示例從 sk 官方示例移植。
Console.WriteLine("請(qǐng)輸入你的問(wèn)題:");
// 用戶(hù)問(wèn)題
var request = Console.ReadLine();
var prompt = @$"## 說(shuō)明
請(qǐng)使用以下格式列出用戶(hù)的意圖:
```json
{{
""intent"": {{intent}}
}}
```
## 選擇
用戶(hù)可以選擇的功能:
```json
[""發(fā)送郵件"", ""完成任務(wù)"", ""創(chuàng)建文檔"", ""刪除文檔""]
```
## 用戶(hù)問(wèn)題
用戶(hù)的問(wèn)題是:
```json
{{
""request"": ""{request}""
}}
```
## 意圖";
FunctionResult result = await kernel.InvokePromptAsync(prompt);
輸入問(wèn)題和 AI 回復(fù):
請(qǐng)輸入你的問(wèn)題:
發(fā)送郵件
```json
{
"intent": "發(fā)送郵件"
}
```
提示中,要求 AI 回復(fù)使用 markdown 代碼語(yǔ)法包裹 json ,當(dāng)然,讀者也可以去掉相關(guān)的 markdown 語(yǔ)法,讓 AI 直接回復(fù) json。
模板化提示
直接在字符串中使用插值,如 $"{request}",不能說(shuō)不好,但是因?yàn)槲覀兂30炎址鳛槟0宕鎯?chǔ)到文件或者數(shù)據(jù)庫(kù)燈地方,肯定不能直接插值的。如果使用 數(shù)值表示插值,又會(huì)導(dǎo)致難以理解,如:
var prompt = """
用戶(hù)問(wèn)題:{0}
"""
string.Format(prompt,request);
Semantic Kernel 中提供了一種模板字符串插值的的辦法,這樣會(huì)給我們編寫(xiě)提示模板帶來(lái)便利。
Semantic Kernel 語(yǔ)法規(guī)定,使用 {{$system}} 來(lái)在提示模板中表示一個(gè)名為 system 的變量。后續(xù)可以使用 KernelArguments 等類(lèi)型,替換提示模板中的相關(guān)變量標(biāo)識(shí)。示例如下:
var kernel = builder.Build();
// 創(chuàng)建提示模板
var chat = kernel.CreateFunctionFromPrompt(
@"
System:{{$system}}
User: {{$request}}
Assistant: ");
Console.WriteLine("請(qǐng)輸入你的問(wèn)題:");
// 用戶(hù)問(wèn)題
var request = Console.ReadLine();
// 設(shè)置變量值
var arguments = new KernelArguments
{
{ "system", "你是一個(gè)高級(jí)運(yùn)維專(zhuān)家,對(duì)用戶(hù)的問(wèn)題給出最專(zhuān)業(yè)的回答" },
{ "request", request }
};
// 提問(wèn)時(shí),傳遞模板以及變量值。
// 這里使用流式對(duì)話(huà)
var chatResult = kernel.InvokeStreamingAsync<StreamingChatMessageContent>(chat, arguments);
// 流式回復(fù),避免一直等結(jié)果
string message = "";
await foreach (var chunk in chatResult)
{
if (chunk.Role.HasValue)
{
Console.Write(chunk.Role + " > ");
}
message += chunk;
Console.Write(chunk);
}
Console.WriteLine();
在這段代碼中,演示了如何在提示模板中使用變量標(biāo)識(shí),以及再向 AI 提問(wèn)時(shí)傳遞變量值。此外,為了避免一直等帶 AI 回復(fù),我們需要使用流式對(duì)話(huà) .InvokeStreamingAsync<StreamingChatMessageContent>(),這樣一來(lái)就可以呈現(xiàn)逐字回復(fù)的效果。
此外,這里不再使用直接使用字符串提問(wèn)的方法,而是使用 .CreateFunctionFromPrompt() 先從字符串創(chuàng)建提示模板對(duì)象。
聊天記錄
聊天記錄的作用是作為一種上下文信息,給 AI 作為參考,以便完善回復(fù)。
示例如下:

不過(guò),AI 對(duì)話(huà)使用的是 http 請(qǐng)求,是無(wú)狀態(tài)的,因此不像聊天記錄哪里保存會(huì)話(huà)狀態(tài),之所以 AI 能夠工具聊天記錄進(jìn)行回答,在于每次請(qǐng)求時(shí),將聊天記錄一起發(fā)送給 AI ,讓 AI 進(jìn)行學(xué)習(xí)并對(duì)最后的問(wèn)題進(jìn)行回復(fù)。

下面這句話(huà),還不到 30 個(gè) tokens。
又來(lái)了一只貓。
請(qǐng)問(wèn)小明的動(dòng)物園有哪些動(dòng)物?
AI 回復(fù)的這句話(huà),怎么也不到 20個(gè) tokens 吧。
小明的動(dòng)物園現(xiàn)在有老虎、獅子和貓。
但是一看 one-api 后臺(tái),發(fā)現(xiàn)每次對(duì)話(huà)消耗的 tokens 越來(lái)越大。

這是因?yàn)闉榱藢?shí)現(xiàn)聊天的功能,使用了一種很笨的方法。雖然 AI 不會(huì)保存聊天記錄,但是客戶(hù)端可以保存,然后下次提問(wèn)時(shí),將將聊天記錄都一起帶上去。不過(guò)這樣會(huì)導(dǎo)致 tokens 越來(lái)越大!
下面為了演示對(duì)話(huà)聊天記錄的場(chǎng)景,我們?cè)O(shè)定 AI 是一個(gè)運(yùn)維專(zhuān)家,我們提問(wèn)時(shí),選擇使用 mysql 相關(guān)的問(wèn)題,除了第一次提問(wèn)指定是 mysql 數(shù)據(jù)庫(kù),后續(xù)都不需要再說(shuō)明是 mysql。
var kernel = builder.Build();
var chat = kernel.CreateFunctionFromPrompt(
@"
System:你是一個(gè)高級(jí)運(yùn)維專(zhuān)家,對(duì)用戶(hù)的問(wèn)題給出最專(zhuān)業(yè)的回答。
{{$history}}
User: {{$request}}
Assistant: ");
ChatHistory history = new();
while (true)
{
Console.WriteLine("請(qǐng)輸入你的問(wèn)題:");
// 用戶(hù)問(wèn)題
var request = Console.ReadLine();
var chatResult = kernel.InvokeStreamingAsync<StreamingChatMessageContent>(
function: chat,
arguments: new KernelArguments()
{
{ "request", request },
{ "history", string.Join("\n", history.Select(x => x.Role + ": " + x.Content)) }
}
);
// 流式回復(fù),避免一直等結(jié)果
string message = "";
await foreach (var chunk in chatResult)
{
if (chunk.Role.HasValue)
{
Console.Write(chunk.Role + " > ");
}
message += chunk;
Console.Write(chunk);
}
Console.WriteLine();
// 添加用戶(hù)問(wèn)題和機(jī)器人回復(fù)到歷史記錄中
history.AddUserMessage(request!);
history.AddAssistantMessage(message);
}
這段代碼有兩個(gè)地方要說(shuō)明,第一個(gè)是如何存儲(chǔ)聊天記錄。Semantic Kernel 提供了 ChatHistory 存儲(chǔ)聊天記錄,當(dāng)然我們手動(dòng)存儲(chǔ)到字符串、數(shù)據(jù)庫(kù)中也是一樣的。
// 添加用戶(hù)問(wèn)題和機(jī)器人回復(fù)到歷史記錄中
history.AddUserMessage(request!);
history.AddAssistantMessage(message);
但是 ChatHistory 對(duì)象不能直接給 AI 使用。所以需要自己從 ChatHistory 中讀取聊天記錄后,生成字符串,替換提示模板中的 {{$history}}。
new KernelArguments()
{
{ "request", request },
{ "history", string.Join("\n", history.Select(x => x.Role + ": " + x.Content)) }
}
生成聊天記錄時(shí),需要使用角色名稱(chēng)區(qū)分。比如生成:
User: mysql 怎么查看表數(shù)量 Assistant:...... User: 查看數(shù)據(jù)庫(kù)數(shù)量 Assistant:...
歷史記錄還能通過(guò)手動(dòng)創(chuàng)建 ChatMessageContent 對(duì)象的方式添加到 ChatHistory 中:
List<ChatHistory> fewShotExamples =
[
new ChatHistory()
{
new ChatMessageContent(AuthorRole.User, "Can you send a very quick approval to the marketing team?"),
new ChatMessageContent(AuthorRole.System, "Intent:"),
new ChatMessageContent(AuthorRole.Assistant, "ContinueConversation")
},
new ChatHistory()
{
new ChatMessageContent(AuthorRole.User, "Thanks, I'm done for now"),
new ChatMessageContent(AuthorRole.System, "Intent:"),
new ChatMessageContent(AuthorRole.Assistant, "EndConversation")
}
];
手動(dòng)拼接聊天記錄太麻煩了,我們可以使用 IChatCompletionService 服務(wù)更好的處理聊天對(duì)話(huà)。
使用 IChatCompletionService 之后,實(shí)現(xiàn)聊天對(duì)話(huà)的代碼變得更加簡(jiǎn)潔了:
var history = new ChatHistory();
history.AddSystemMessage("你是一個(gè)高級(jí)數(shù)學(xué)專(zhuān)家,對(duì)用戶(hù)的問(wèn)題給出最專(zhuān)業(yè)的回答。");
// 聊天服務(wù)
var chatCompletionService = kernel.GetRequiredService<IChatCompletionService>();
while (true)
{
Console.Write("請(qǐng)輸入你的問(wèn)題: ");
var userInput = Console.ReadLine();
// 添加到聊天記錄中
history.AddUserMessage(userInput);
// 獲取 AI 聊天回復(fù)信息
var result = await chatCompletionService.GetChatMessageContentAsync(
history,
kernel: kernel);
Console.WriteLine("AI 回復(fù) : " + result);
// 添加 AI 的回復(fù)到聊天記錄中
history.AddMessage(result.Role, result.Content ?? string.Empty);
}
請(qǐng)輸入你的問(wèn)題: 1加上1等于
AI 回復(fù) : 1加上1等于2
請(qǐng)輸入你的問(wèn)題: 再加上50
AI 回復(fù) : 1加上1再加上50等于52。
請(qǐng)輸入你的問(wèn)題: 再加上200
AI 回復(fù) : 1加上1再加上50再加上200等于252。
函數(shù)和插件
在高層次上,插件是一組可以公開(kāi)給 AI 應(yīng)用程序和服務(wù)的函數(shù)。然后,AI 應(yīng)用程序可以對(duì)插件中的功能進(jìn)行編排,以完成用戶(hù)請(qǐng)求。在語(yǔ)義內(nèi)核中,您可以通過(guò)函數(shù)調(diào)用或規(guī)劃器手動(dòng)或自動(dòng)地調(diào)用這些函數(shù)。
直接調(diào)用插件函數(shù)
Semantic Kernel 可以直接加載本地類(lèi)型中的函數(shù),這一過(guò)程不需要經(jīng)過(guò) AI,完全在本地完成。
定義一個(gè)時(shí)間插件類(lèi),該插件類(lèi)有一個(gè) GetCurrentUtcTime 函數(shù)返回當(dāng)前時(shí)間,函數(shù)需要使用 KernelFunction 修飾。
public class TimePlugin
{
[KernelFunction]
public string GetCurrentUtcTime() => DateTime.UtcNow.ToString("R");
}
加載插件并調(diào)用插件函數(shù):
// 加載插件
builder.Plugins.AddFromType<TimePlugin>();
var kernel = builder.Build();
FunctionResult result = await kernel.InvokeAsync("TimePlugin", "GetCurrentUtcTime");
Console.WriteLine(result.GetValue<string>());
輸出:
Tue, 27 Feb 2024 11:07:59 GMT
當(dāng)然,這個(gè)示例在實(shí)際開(kāi)發(fā)中可能沒(méi)什么用,不過(guò)大家要理解在 Semantic Kernel 是怎樣調(diào)用一個(gè)函數(shù)的。
提示模板文件
Semantic Kernel 很多地方都跟 Function 相關(guān),你會(huì)發(fā)現(xiàn)代碼中很多代碼是以 Function 作為命名的。
比如提供字符串創(chuàng)建提示模板:
KernelFunction chat = kernel.CreateFunctionFromPrompt(
@"
System:你是一個(gè)高級(jí)運(yùn)維專(zhuān)家,對(duì)用戶(hù)的問(wèn)題給出最專(zhuān)業(yè)的回答。
{{$history}}
User: {{$request}}
Assistant: ");
然后回到本節(jié)的主題,Semantic Kernel 還可以將提示模板存儲(chǔ)到文件中,然后以插件的形式加載模板文件。
比如有以下目錄文件:

└─WriterPlugin
└─ShortPoem
config.json
skprompt.txt
skprompt.txt 文件是固定命名,存儲(chǔ)提示模板文本,示例如下:
根據(jù)主題寫(xiě)一首有趣的短詩(shī)或打油詩(shī),要有創(chuàng)意,要有趣,放開(kāi)你的想象力。
主題: {{$input}}
config.json 文件是固定名稱(chēng),存儲(chǔ)描述信息,比如需要的變量名稱(chēng)、描述等。下面是一個(gè) completion 類(lèi)型的插件配置文件示例,除了一些跟提示模板相關(guān)的配置,還有一些聊天的配置,如最大 tokens 數(shù)量、溫度值(temperature),這些參數(shù)后面會(huì)予以說(shuō)明,這里先跳過(guò)。
{
"schema": 1,
"type": "completion",
"description": "根據(jù)用戶(hù)問(wèn)題寫(xiě)一首簡(jiǎn)短而有趣的詩(shī).",
"completion": {
"max_tokens": 200,
"temperature": 0.5,
"top_p": 0.0,
"presence_penalty": 0.0,
"frequency_penalty": 0.0
},
"input": {
"parameters": [
{
"name": "input",
"description": "詩(shī)的主題",
"defaultValue": ""
}
]
}
}
創(chuàng)建插件目錄和文件后,在代碼中以提示模板的方式加載:
// 加載插件,表示該插件是提示模板
builder.Plugins.AddFromPromptDirectory("./plugins/WriterPlugin");
var kernel = builder.Build();
Console.WriteLine("輸入詩(shī)的主題:");
var input = Console.ReadLine();
// WriterPlugin 插件名稱(chēng),與插件目錄一致,插件目錄下可以有多個(gè)子模板目錄。
FunctionResult result = await kernel.InvokeAsync("WriterPlugin", "ShortPoem", new() {
{ "input", input }
});
Console.WriteLine(result.GetValue<string>());
輸入問(wèn)題以及 AI 回復(fù):
輸入詩(shī)的主題:
春天
春天,春天,你是生命的詩(shī)篇,
萬(wàn)物復(fù)蘇,愛(ài)的季節(jié)。
郁郁蔥蔥的小草中,
是你輕響的詩(shī)人的腳步音。
春天,春天,你是花芯的深淵,
桃紅柳綠,或嫵媚或清純。
在溫暖的微風(fēng)中,
是你舞動(dòng)的裙擺。
春天,春天,你是藍(lán)空的情兒,
百鳥(niǎo)鳴叫,放歌天際無(wú)邊。
在你湛藍(lán)的天幕下,
是你獨(dú)角戲的絢爛瞬間。
春天,春天,你是河流的眼睛,
如阿瞞甘霖,滋養(yǎng)大地生靈。
你的涓涓細(xì)流,
是你悠悠的歌聲。
春天,春天,你是生命的詩(shī)篇,
用溫暖的手指,照亮這灰色的世間。
你的綻放,微笑與歡欣,
就是我心中永恒的春天。
插件文件的編寫(xiě)可參考官方文檔:https://learn.microsoft.com/en-us/semantic-kernel/prompts/saving-prompts-as-files?tabs=Csharp
根據(jù) AI 自動(dòng)調(diào)用插件函數(shù)
使用 Semantic Kernel 加載插件類(lèi)后,Semantic Kernel 可以自動(dòng)根據(jù) AI 對(duì)話(huà)調(diào)用這些插件類(lèi)中的函數(shù)。
比如有一個(gè)插件類(lèi)型,用于修改或獲取燈的狀態(tài)。
代碼如下:
public class LightPlugin
{
public bool IsOn { get; set; } = false;
[KernelFunction]
[Description("獲取燈的狀態(tài).")]
public string GetState() => IsOn ? "亮" : "暗";
[KernelFunction]
[Description("修改燈的狀態(tài).'")]
public string ChangeState(bool newState)
{
this.IsOn = newState;
var state = GetState();
Console.WriteLine($"[燈的狀態(tài)是: {state}]");
return state;
}
}
每個(gè)函數(shù)都使用了 [Description] 特性設(shè)置了注釋信息,這些注釋信息非常重要,AI 靠這些注釋理解函數(shù)的功能作用。
然后加載插件類(lèi),并在聊天中被 Semantic Kernel 調(diào)用:
// 加載插件類(lèi)
builder.Plugins.AddFromType<LightPlugin>();
var kernel = builder.Build();
var history = new ChatHistory();
// 聊天服務(wù)
var chatCompletionService = kernel.GetRequiredService<IChatCompletionService>();
while (true)
{
Console.Write("User > ");
var userInput = Console.ReadLine();
// 添加到聊天記錄中
history.AddUserMessage(userInput);
// 開(kāi)啟函數(shù)調(diào)用
OpenAIPromptExecutionSettings openAIPromptExecutionSettings = new()
{
ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions
};
// 獲取函數(shù)
var result = await chatCompletionService.GetChatMessageContentAsync(
history,
executionSettings: openAIPromptExecutionSettings,
kernel: kernel);
Console.WriteLine("Assistant > " + result);
// 添加到聊天記錄中
history.AddMessage(result.Role, result.Content ?? string.Empty);
}
可以先斷點(diǎn)調(diào)試 LightPlugin 中的函數(shù),然后在控制臺(tái)輸入問(wèn)題讓 AI 調(diào)用本地函數(shù):
User > 燈的狀態(tài)
Assistant > 當(dāng)前燈的狀態(tài)是暗的。
User > 開(kāi)燈
[燈的狀態(tài)是: 亮]
Assistant > 燈已經(jīng)開(kāi)啟,現(xiàn)在是亮的狀態(tài)。
User > 關(guān)燈
[燈的狀態(tài)是: 暗]
讀者可以在官方文檔了解更多:https://learn.microsoft.com/en-us/semantic-kernel/agents/plugins/using-the-kernelfunction-decorator?tabs=Csharp
由于幾乎沒(méi)有文檔資料說(shuō)明原理,因此建議讀者去研究源碼,這里就不再贅述了。
聊天中明確調(diào)用函數(shù)
我們可以在提示模板中明確調(diào)用一個(gè)函數(shù)。
定義一個(gè)插件類(lèi)型 ConversationSummaryPlugin,其功能十分簡(jiǎn)單,將歷史記錄直接返回,input 參數(shù)表示歷史記錄。
public class ConversationSummaryPlugin
{
[KernelFunction, Description("給你一份很長(zhǎng)的談話(huà)記錄,總結(jié)一下談話(huà)內(nèi)容.")]
public async Task<string> SummarizeConversationAsync(
[Description("長(zhǎng)對(duì)話(huà)記錄\r\n.")] string input, Kernel kernel)
{
await Task.CompletedTask;
return input;
}
}
為了在聊天記錄中使用該插件函數(shù),我們需要在提示模板中使用 {{ConversationSummaryPlugin.SummarizeConversation $history}},其中 $history 是自定義的變量名稱(chēng),名稱(chēng)可以隨意,只要是個(gè)字符串即可。
var chat = kernel.CreateFunctionFromPrompt(
@"{{ConversationSummaryPlugin.SummarizeConversation $history}}
User: {{$request}}
Assistant: "
);

完整代碼如下:
// 加載總結(jié)插件
builder.Plugins.AddFromType<ConversationSummaryPlugin>();
var kernel = builder.Build();
var chat = kernel.CreateFunctionFromPrompt(
@"{{ConversationSummaryPlugin.SummarizeConversation $history}}
User: {{$request}}
Assistant: "
);
var history = new ChatHistory();
while (true)
{
Console.Write("User > ");
var request = Console.ReadLine();
// 添加到聊天記錄中
history.AddUserMessage(request);
// 流式對(duì)話(huà)
var chatResult = kernel.InvokeStreamingAsync<StreamingChatMessageContent>(
chat, new KernelArguments
{
{ "request", request },
{ "history", string.Join("\n", history.Select(x => x.Role + ": " + x.Content)) }
});
string message = "";
await foreach (var chunk in chatResult)
{
if (chunk.Role.HasValue)
{
Console.Write(chunk.Role + " > ");
}
message += chunk;
Console.Write(chunk);
}
Console.WriteLine();
history.AddAssistantMessage(message);
}
由于模板的開(kāi)頭是 {{ConversationSummaryPlugin.SummarizeConversation $history}},因此,每次聊天之前,都會(huì)先調(diào)用該函數(shù)。
比如輸入 吃飯睡覺(jué)打豆豆 的時(shí)候,首先執(zhí)行 ConversationSummaryPlugin.SummarizeConversation 函數(shù),然后將返回結(jié)果存儲(chǔ)到模板中。
最后生成的提示詞對(duì)比如下:
@"{{ConversationSummaryPlugin.SummarizeConversation $history}}
User: {{$request}}
Assistant: "
user: 吃飯睡覺(jué)打豆豆
User: 吃飯睡覺(jué)打豆豆
Assistant:
可以看到,調(diào)用函數(shù)返回結(jié)果后,提示詞字符串前面自動(dòng)使用 User 角色。
實(shí)現(xiàn)總結(jié)
Semantic Kernel 中有很多文本處理工具,比如 TextChunker 類(lèi)型,可以幫助我們提取文本中的行、段。設(shè)定場(chǎng)景如下,用戶(hù)提問(wèn)一大段文本,然后我們使用 AI 總結(jié)這段文本。
Semantic Kernel 有一些工具,但是不多,而且是針對(duì)英文開(kāi)發(fā)的。
設(shè)定一個(gè)場(chǎng)景,用戶(hù)可以每行輸入一句話(huà),當(dāng)用戶(hù)使用 000 結(jié)束輸入后,每句話(huà)都推送給 AI 總結(jié)(不是全部放在一起總結(jié))。
這個(gè)示例的代碼比較長(zhǎng),建議讀者在 vs 中調(diào)試代碼,慢慢閱讀。
// 總結(jié)內(nèi)容的最大 token
const int MaxTokens = 1024;
// 提示模板
const string SummarizeConversationDefinition =
@"開(kāi)始內(nèi)容總結(jié):
{{$request}}
最后對(duì)內(nèi)容進(jìn)行總結(jié)。
在“內(nèi)容到總結(jié)”中總結(jié)對(duì)話(huà),找出討論的要點(diǎn)和得出的任何結(jié)論。
不要加入其他常識(shí)。
摘要是純文本形式,在完整的句子中,沒(méi)有標(biāo)記或標(biāo)記。
開(kāi)始總結(jié):
";
// 配置
PromptExecutionSettings promptExecutionSettings = new()
{
ExtensionData = new Dictionary<string, object>()
{
{ "Temperature", 0.1 },
{ "TopP", 0.5 },
{ "MaxTokens", MaxTokens }
}
};
// 這里不使用 kernel.CreateFunctionFromPrompt 了
// KernelFunctionFactory 可以幫助我們通過(guò)代碼的方式配置提示詞
var func = KernelFunctionFactory.CreateFromPrompt(
SummarizeConversationDefinition, // 提示詞
description: "給出一段對(duì)話(huà)記錄,總結(jié)這部分對(duì)話(huà).", // 描述
executionSettings: promptExecutionSettings); // 配置
#pragma warning disable SKEXP0055 // 類(lèi)型僅用于評(píng)估,在將來(lái)的更新中可能會(huì)被更改或刪除。取消此診斷以繼續(xù)。
var request = "";
while (true)
{
Console.Write("User > ");
var input = Console.ReadLine();
if (input == "000")
{
break;
}
request += Environment.NewLine;
request += input;
}
// SK 提供的文本拆分器,將文本分成一行行的
List<string> lines = TextChunker.SplitPlainTextLines(request, MaxTokens);
// 將文本拆成段落
List<string> paragraphs = TextChunker.SplitPlainTextParagraphs(lines, MaxTokens);
string[] results = new string[paragraphs.Count];
for (int i = 0; i < results.Length; i++)
{
// 一段段地總結(jié)
results[i] = (await func.InvokeAsync(kernel, new() { ["request"] = paragraphs[i] }).ConfigureAwait(false))
.GetValue<string>() ?? string.Empty;
}
Console.WriteLine($"""
總結(jié)如下:
{string.Join("\n", results)}
""");
輸入一堆內(nèi)容后,新的一行使用 000 結(jié)束提問(wèn),讓 AI 總結(jié)用戶(hù)的話(huà)。

不過(guò)經(jīng)過(guò)調(diào)試發(fā)現(xiàn),TextChunker 對(duì)這段文本的處理似乎不佳,因?yàn)槲谋具@么多行只識(shí)別為一行、一段。
可能跟 TextChunker 分隔符有關(guān),SK 主要是面向英語(yǔ)的。

本小節(jié)的演示效果不佳,不過(guò)主要目的是,讓用戶(hù)了解 KernelFunctionFactory.CreateFromPrompt 可以更加方便創(chuàng)建提示模板、使用 PromptExecutionSettings 配置溫度、使用 TextChunker 切割文本。
配置 PromptExecutionSettings 時(shí),出現(xiàn)了三個(gè)參數(shù),其中 MaxTokens 表示機(jī)器人回復(fù)最大的 tokens 數(shù)量,這樣可以避免機(jī)器人廢話(huà)太多。
其它兩個(gè)參數(shù)的作用是:
Temperature:值范圍在 0-2 之間,簡(jiǎn)單來(lái)說(shuō),temperature 的參數(shù)值越小,模型就會(huì)返回越確定的一個(gè)結(jié)果。值越大,AI 的想象力越強(qiáng),越可能偏離現(xiàn)實(shí)。一般詩(shī)歌、科幻這些可以設(shè)置大一些,讓 AI 實(shí)現(xiàn)天馬行空的回復(fù)。
TopP:與 Temperature 不同的另一種方法,稱(chēng)為核抽樣,其中模型考慮了具有 TopP 概率質(zhì)量的令牌的結(jié)果。因此,0.1 意味著只考慮構(gòu)成前10% 概率質(zhì)量的令牌的結(jié)果。
一般建議是改變其中一個(gè)參數(shù)就行,不用兩個(gè)都調(diào)整。
更多相關(guān)的參數(shù)配置,請(qǐng)查看 https://learn.microsoft.com/en-us/azure/ai-services/openai/reference
配置提示詞
前面提到了一個(gè)新的創(chuàng)建函數(shù)的用法:
var func = KernelFunctionFactory.CreateFromPrompt(
SummarizeConversationDefinition, // 提示詞
description: "給出一段對(duì)話(huà)記錄,總結(jié)這部分對(duì)話(huà).", // 描述
executionSettings: promptExecutionSettings); // 配置
創(chuàng)建提示模板時(shí),可以使用 PromptTemplateConfig 類(lèi)型 調(diào)整控制提示符行為的參數(shù)。
// 總結(jié)內(nèi)容的最大 token
const int MaxTokens = 1024;
// 提示模板
const string SummarizeConversationDefinition = "...";
var func = kernel.CreateFunctionFromPrompt(new PromptTemplateConfig
{
// Name 不支持中文和特殊字符
Name = "chat",
Description = "給出一段對(duì)話(huà)記錄,總結(jié)這部分對(duì)話(huà).",
Template = SummarizeConversationDefinition,
TemplateFormat = "semantic-kernel",
InputVariables = new List<InputVariable>
{
new InputVariable{Name = "request", Description = "用戶(hù)的問(wèn)題", IsRequired = true }
},
ExecutionSettings = new Dictionary<string, PromptExecutionSettings>
{
{
"default",
new OpenAIPromptExecutionSettings()
{
MaxTokens = MaxTokens,
Temperature = 0
}
},
}
});
ExecutionSettings 部分的配置,可以針對(duì)使用的模型起效,這里的配置不會(huì)全部同時(shí)起效,會(huì)根據(jù)實(shí)際使用的模型起效。
ExecutionSettings = new Dictionary<string, PromptExecutionSettings>
{
{
"default",
new OpenAIPromptExecutionSettings()
{
MaxTokens = 1000,
Temperature = 0
}
},
{
"gpt-3.5-turbo", new OpenAIPromptExecutionSettings()
{
ModelId = "gpt-3.5-turbo-0613",
MaxTokens = 4000,
Temperature = 0.2
}
},
{
"gpt-4",
new OpenAIPromptExecutionSettings()
{
ModelId = "gpt-4-1106-preview",
MaxTokens = 8000,
Temperature = 0.3
}
}
}
聊到這里,重新說(shuō)一下前面使用文件配置提示模板文件的,兩者是相似的。
我們也可以使用文件的形式存儲(chǔ)與代碼一致的配置,其目錄文件結(jié)構(gòu)如下:
└─── chat
|
└─── config.json
└─── skprompt.txt
模板文件由 config.json 和 skprompt.txt 組成,skprompt.txt 中配置提示詞,跟 PromptTemplateConfig 的 Template 字段配置一致。
config.json 中涉及的內(nèi)容比較多,你可以對(duì)照下面的 json 跟 實(shí)現(xiàn)總結(jié) 一節(jié)的代碼,兩者幾乎是一模一樣的。
{
"schema": 1,
"type": "completion",
"description": "給出一段對(duì)話(huà)記錄,總結(jié)這部分對(duì)話(huà)",
"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": "用戶(hù)的問(wèn)題.",
"required": true
},
{
"name": "history",
"description": "用戶(hù)的問(wèn)題.",
"required": true
}
]
}
C# 代碼:
// Name 不支持中文和特殊字符
Name = "chat",
Description = "給出一段對(duì)話(huà)記錄,總結(jié)這部分對(duì)話(huà).",
Template = SummarizeConversationDefinition,
TemplateFormat = "semantic-kernel",
InputVariables = new List<InputVariable>
{
new InputVariable{Name = "request", Description = "用戶(hù)的問(wèn)題", IsRequired = true }
},
ExecutionSettings = new Dictionary<string, PromptExecutionSettings>
{
{
"default",
new OpenAIPromptExecutionSettings()
{
MaxTokens = 1000,
Temperature = 0
}
},
{
"gpt-3.5-turbo", new OpenAIPromptExecutionSettings()
{
ModelId = "gpt-3.5-turbo-0613",
MaxTokens = 4000,
Temperature = 0.2
}
},
{
"gpt-4",
new OpenAIPromptExecutionSettings()
{
ModelId = "gpt-4-1106-preview",
MaxTokens = 8000,
Temperature = 0.3
}
}
}
提示模板語(yǔ)法
目前,我們已經(jīng)有兩個(gè)地方使用到提示模板的語(yǔ)法,即變量和函數(shù)調(diào)用,因?yàn)榍懊嬉呀?jīng)介紹過(guò)相關(guān)的用法,因此這里再簡(jiǎn)單提及一下。
變量
變量的使用很簡(jiǎn)單,在提示工程中使用{{$變量名稱(chēng)}} 標(biāo)識(shí)即可,如 {{$name}}。
然后在對(duì)話(huà)中有多種方法插入值,如使用 KernelArguments 存儲(chǔ)變量值:
new KernelArguments
{
{ "name", "工良" }
});
函數(shù)調(diào)用
在 實(shí)現(xiàn)總結(jié) 一節(jié)提到過(guò),在提示模板中可以明確調(diào)用一個(gè)函數(shù),比如定義一個(gè)函數(shù)如下:
// 沒(méi)有 Kernel kernel
[KernelFunction, Description("給你一份很長(zhǎng)的談話(huà)記錄,總結(jié)一下談話(huà)內(nèi)容.")]
public async Task<string> SummarizeConversationAsync(
[Description("長(zhǎng)對(duì)話(huà)記錄\r\n.")] string input)
{
await Task.CompletedTask;
return input;
}
// 有 Kernel kernel
[KernelFunction, Description("給你一份很長(zhǎng)的談話(huà)記錄,總結(jié)一下談話(huà)內(nèi)容.")]
public async Task<string> SummarizeConversationAsync(
[Description("長(zhǎng)對(duì)話(huà)記錄\r\n.")] string input, Kernel kernel)
{
await Task.CompletedTask;
return input;
}
[KernelFunction]
[Description("Sends an email to a recipient.")]
public async Task SendEmailAsync(
Kernel kernel,
string recipientEmails,
string subject,
string body
)
{
// Add logic to send an email using the recipientEmails, subject, and body
// For now, we'll just print out a success message to the console
Console.WriteLine("Email sent!");
}
函數(shù)一定需要使用 [KernelFunction] 標(biāo)識(shí),[Description] 描述函數(shù)的作用。函數(shù)可以一個(gè)或多個(gè)參數(shù),每個(gè)參數(shù)最好都使用 [Description] 描述作用。
函數(shù)參數(shù)中,可以帶一個(gè) Kernel kernel,可以放到開(kāi)頭或末尾 ,也可以不帶,主要作用是注入 Kernel 對(duì)象。
在 prompt 中使用函數(shù)時(shí),需要傳遞函數(shù)參數(shù):
總結(jié)如下:{{AAA.SummarizeConversationAsync $input}}.
其它一些特殊字符的轉(zhuǎn)義方法等,詳見(jiàn)官方文檔:https://learn.microsoft.com/en-us/semantic-kernel/prompts/prompt-template-syntax
文本生成
前面劈里啪啦寫(xiě)了一堆東西,都是說(shuō)聊天對(duì)話(huà)的,本節(jié)來(lái)聊一下文本生成的應(yīng)用。
文本生成和聊天對(duì)話(huà)模型主要有以下模型:
| Model type | Model |
|---|---|
| Text generation | text-ada-001 |
| Text generation | text-babbage-001 |
| Text generation | text-curie-001 |
| Text generation | text-davinci-001 |
| Text generation | text-davinci-002 |
| Text generation | text-davinci-003 |
| Chat Completion | gpt-3.5-turbo |
| Chat Completion | gpt-4 |
當(dāng)然,文本生成不一定只能用這么幾個(gè)模型,使用 gpt-4 設(shè)定好背景提示,也可以達(dá)到相應(yīng)效果。
文本生成可以有以下場(chǎng)景:

使用文本生成的示例如下,讓 AI 總結(jié)文本:

按照這個(gè)示例,我們先在 Env.cs 中編寫(xiě)擴(kuò)展函數(shù),配置使用 .AddAzureOpenAITextGeneration() 文本生成,而不是聊天對(duì)話(huà)。
public static IKernelBuilder WithAzureOpenAIText(this IKernelBuilder builder)
{
var configuration = GetConfiguration();
// 需要換一個(gè)模型,比如 gpt-35-turbo-instruct
var AzureOpenAIDeploymentName = "ca";
var AzureOpenAIModelId = "gpt-35-turbo-instruct";
var AzureOpenAIEndpoint = configuration["AzureOpenAI:Endpoint"]!;
var AzureOpenAIApiKey = configuration["AzureOpenAI:ApiKey"]!;
builder.Services.AddLogging(c =>
{
c.AddDebug()
.SetMinimumLevel(LogLevel.Trace)
.AddSimpleConsole(options =>
{
options.IncludeScopes = true;
options.SingleLine = true;
options.TimestampFormat = "yyyy-MM-dd HH:mm:ss ";
});
});
// 使用 Chat ,即大語(yǔ)言模型聊天
builder.Services.AddAzureOpenAITextGeneration(
AzureOpenAIDeploymentName,
AzureOpenAIEndpoint,
AzureOpenAIApiKey,
modelId: AzureOpenAIModelId
);
return builder;
}
然后編寫(xiě)提問(wèn)代碼,用戶(hù)可以多行輸入文本,最后使用 000 結(jié)束輸入,將文本提交給 AI 進(jìn)行總結(jié)。進(jìn)行總結(jié)時(shí),為了避免 AI 廢話(huà)太多,因此這里使用 ExecutionSettings 配置相關(guān)參數(shù)。
代碼示例如下:
builder = builder.WithAzureOpenAIText();
var kernel = builder.Build();
Console.WriteLine("輸入文本:");
var request = "";
while (true)
{
var input = Console.ReadLine();
if (input == "000")
{
break;
}
request += Environment.NewLine;
request += input;
}
var func = kernel.CreateFunctionFromPrompt(new PromptTemplateConfig
{
Name = "chat",
Description = "給出一段對(duì)話(huà)記錄,總結(jié)這部分對(duì)話(huà).",
// 用戶(hù)的文本
Template = request,
TemplateFormat = "semantic-kernel",
ExecutionSettings = new Dictionary<string, PromptExecutionSettings>
{
{
"default",
new OpenAIPromptExecutionSettings()
{
MaxTokens = 100,
Temperature = (float)0.3,
TopP = (float)1,
FrequencyPenalty = (float)0,
PresencePenalty = (float)0
}
}
}
});
var result = await func.InvokeAsync(kernel);
Console.WriteLine($"""
總結(jié)如下:
{string.Join("\n", result)}
""");

Semantic Kernel 插件
Semantic Kernel 在 Microsoft.SemanticKernel.Plugins 開(kāi)頭的包中提供了一些插件,不同的包有不同功能的插件。大部分目前還是屬于半成品,因此這部分不詳細(xì)講解,本節(jié)只做簡(jiǎn)單說(shuō)明。
目前官方倉(cāng)庫(kù)有以下包提供了一些插件:
├─Plugins.Core
├─Plugins.Document
├─Plugins.Memory
├─Plugins.MsGraph
└─Plugins.Web
nuget 搜索時(shí),需要加上
Microsoft.SemanticKernel.前綴。
Semantic Kernel 還有通過(guò)遠(yuǎn)程 swagger.json 使用插件的做法,詳細(xì)請(qǐng)參考文檔:https://learn.microsoft.com/en-us/semantic-kernel/agents/plugins/openai-plugins
Plugins.Core 中包含最基礎(chǔ)簡(jiǎn)單的插件:
// 讀取和寫(xiě)入文件
FileIOPlugin
// http 請(qǐng)求以及返回字符串結(jié)果
HttpPlugin
// 只提供了 + 和 - 兩種運(yùn)算
MathPlugin
// 文本大小寫(xiě)等簡(jiǎn)單的功能
TextPlugin
// 獲得本地時(shí)間日期
TimePlugin
// 在操作之前等待一段時(shí)間
WaitPlugin
因?yàn)檫@些插件對(duì)本文演示沒(méi)什么幫助,功能也非常簡(jiǎn)單,因此這里不講解。下面簡(jiǎn)單講一下文檔插件。
文檔插件
安裝 Microsoft.SemanticKernel.Plugins.Document(需要勾選預(yù)覽版),里面包含了文檔插件,該文檔插件使用了 DocumentFormat.OpenXml 項(xiàng)目,DocumentFormat.OpenXml 支持以下文檔格式:
DocumentFormat.OpenXml 項(xiàng)目地址 https://github.com/dotnet/Open-XML-SDK
- WordprocessingML:用于創(chuàng)建和編輯 Word 文檔 (.docx)
- SpreadsheetML:用于創(chuàng)建和編輯 Excel 電子表格 (.xlsx)
- PowerPointML:用于創(chuàng)建和編輯 PowerPoint 演示文稿 (.pptx)
- VisioML:用于創(chuàng)建和編輯 Visio 圖表 (.vsdx)
- ProjectML:用于創(chuàng)建和編輯 Project 項(xiàng)目 (.mpp)
- DiagramML:用于創(chuàng)建和編輯 Visio 圖表 (.vsdx)
- PublisherML:用于創(chuàng)建和編輯 Publisher 出版物 (.pubx)
- InfoPathML:用于創(chuàng)建和編輯 InfoPath 表單 (.xsn)
文檔插件暫時(shí)還沒(méi)有好的應(yīng)用場(chǎng)景,只是加載文檔提取文字比較方便,代碼示例如下:
DocumentPlugin documentPlugin = new(new WordDocumentConnector(), new LocalFileSystemConnector());
string filePath = "(完整版)基礎(chǔ)財(cái)務(wù)知識(shí).docx";
string text = await documentPlugin.ReadTextAsync(filePath);
Console.WriteLine(text);
由于這些插件目前都是半成品,因此這里就不展開(kāi)說(shuō)明了。

planners
依然是半成品,這里就不再贅述。
因?yàn)槲乙矝](méi)有看明白這個(gè)東西怎么用。
Kernel Memory 構(gòu)建文檔知識(shí)庫(kù)
Kernel Memory 是一個(gè)歪果仁的個(gè)人項(xiàng)目,支持 PDF 和 Word 文檔、 PowerPoint 演示文稿、圖像、電子表格等,通過(guò)利用大型語(yǔ)言模型(llm)、嵌入和矢量存儲(chǔ)來(lái)提取信息和生成記錄,主要目的是提供文檔處理相關(guān)的接口,最常使用的場(chǎng)景是知識(shí)庫(kù)系統(tǒng)。讀者可能對(duì)知識(shí)庫(kù)系統(tǒng)不了解,如果有條件,建議部署一個(gè) Fastgpt 系統(tǒng)研究一下。
但是目前 Kernel Memory 依然是半產(chǎn)品,文檔也不完善,所以接下來(lái)筆者也只講解最核心的部分,感興趣的讀者建議直接看源碼。
Kernel Memory 項(xiàng)目文檔:https://microsoft.github.io/kernel-memory/
Kernel Memory 項(xiàng)目倉(cāng)庫(kù):https://github.com/microsoft/kernel-memory
打開(kāi) Kernel Memory 項(xiàng)目倉(cāng)庫(kù),將項(xiàng)目拉取到本地。
要講解知識(shí)庫(kù)系統(tǒng),可以這樣理解。大家都知道,訓(xùn)練一個(gè)醫(yī)學(xué)模型是十分麻煩的,別說(shuō)機(jī)器的 GPU 夠不夠猛,光是訓(xùn)練 AI ,就需要掌握各種專(zhuān)業(yè)的知識(shí)。如果出現(xiàn)一個(gè)新的需求,可能又要重新訓(xùn)練一個(gè)模型,這樣太麻煩了。
于是出現(xiàn)了大語(yǔ)言模型,特點(diǎn)是什么都學(xué)什么都會(huì),但是不夠?qū)I(yè)深入,好處時(shí)無(wú)論醫(yī)學(xué)、攝影等都可以使用。
雖然某方面專(zhuān)業(yè)的知識(shí)不夠深入和專(zhuān)業(yè),但是我們換種部分解決。
首先,將 docx、pdf 等問(wèn)題提取出文本,然后切割成多個(gè)段落,每一段都使用 AI 模型生成相關(guān)向量,這個(gè)向量的原理筆者也不懂,大家可以簡(jiǎn)單理解為分詞,生成向量后,將段落文本和向量都存儲(chǔ)到數(shù)據(jù)庫(kù)中(數(shù)據(jù)庫(kù)需要支持向量)。

然后在用戶(hù)提問(wèn) “什么是報(bào)表” 時(shí),首先在數(shù)據(jù)庫(kù)中搜索,根據(jù)向量來(lái)確定相似程度,把幾個(gè)跟問(wèn)題相關(guān)的段落拿出來(lái),然后把這幾段文本和用戶(hù)的問(wèn)題一起發(fā)給 AI。相對(duì)于在提示模板中,塞進(jìn)一部分背景知識(shí),然后加上用戶(hù)的問(wèn)題,再由 AI 進(jìn)行總結(jié)回答。


筆者建議大家有條件的話(huà),部署一個(gè)開(kāi)源版本的 Fastgpt 系統(tǒng),把這個(gè)系統(tǒng)研究一下,學(xué)會(huì)這個(gè)系統(tǒng)后,再去研究 Kernel Memory ,你就會(huì)覺(jué)得非常簡(jiǎn)單了。同理,如果有條件,可以先部署一個(gè) LobeHub ,開(kāi)源的 AI 對(duì)話(huà)系統(tǒng),研究怎么用,再去研究 Semantic Kernel 文檔,接著再深入源碼。
從 web 處理網(wǎng)頁(yè)
Kernel Memory 支持從網(wǎng)頁(yè)爬取、導(dǎo)入文檔、直接給定字符串三種方式導(dǎo)入信息,由于 Kernel Memory 提供了一個(gè) Service 示例,里面有一些值得研究的代碼寫(xiě)法,因此下面的示例是啟動(dòng) Service 這個(gè) Web 服務(wù),然后在客戶(hù)端將文檔推送該 Service 處理,客戶(hù)端本身不對(duì)接 AI。
由于這一步比較麻煩,讀者動(dòng)手的過(guò)程中搞不出來(lái),可以直接放棄,后面會(huì)說(shuō)怎么自己寫(xiě)一個(gè)。
打開(kāi) kernel-memory 源碼的 service/Service 路徑。
使用命令啟動(dòng)服務(wù):
dotnet run setup
這個(gè)控制臺(tái)的作用是幫助我們生成相關(guān)配置的。啟動(dòng)這個(gè)控制臺(tái)之后,根據(jù)提示選擇對(duì)應(yīng)的選項(xiàng)(按上下鍵選擇選項(xiàng),按下回車(chē)鍵確認(rèn)),以及填寫(xiě)配置內(nèi)容,配置會(huì)被存儲(chǔ)到 appsettings.Development.json 中。
如果讀者搞不懂這個(gè)控制臺(tái)怎么使用,那么可以直接將替換下面的 json 到 appsettings.Development.json 。
有幾個(gè)地方需要讀者配置一下。
- AccessKey1、AccessKey2 是客戶(hù)端使用該 Service 所需要的驗(yàn)證密鑰,隨便填幾個(gè)字母即可。
- AzureAIDocIntel、AzureOpenAIEmbedding、AzureOpenAIText 根據(jù)實(shí)際情況填寫(xiě)。
{
"KernelMemory": {
"Service": {
"RunWebService": true,
"RunHandlers": true,
"OpenApiEnabled": true,
"Handlers": {}
},
"ContentStorageType": "SimpleFileStorage",
"TextGeneratorType": "AzureOpenAIText",
"ServiceAuthorization": {
"Enabled": true,
"AuthenticationType": "APIKey",
"HttpHeaderName": "Authorization",
"AccessKey1": "自定義密鑰1",
"AccessKey2": "自定義密鑰2"
},
"DataIngestion": {
"OrchestrationType": "Distributed",
"DistributedOrchestration": {
"QueueType": "SimpleQueues"
},
"EmbeddingGenerationEnabled": true,
"EmbeddingGeneratorTypes": [
"AzureOpenAIEmbedding"
],
"MemoryDbTypes": [
"SimpleVectorDb"
],
"ImageOcrType": "AzureAIDocIntel",
"TextPartitioning": {
"MaxTokensPerParagraph": 1000,
"MaxTokensPerLine": 300,
"OverlappingTokens": 100
},
"DefaultSteps": []
},
"Retrieval": {
"MemoryDbType": "SimpleVectorDb",
"EmbeddingGeneratorType": "AzureOpenAIEmbedding",
"SearchClient": {
"MaxAskPromptSize": -1,
"MaxMatchesCount": 100,
"AnswerTokens": 300,
"EmptyAnswer": "INFO NOT FOUND"
}
},
"Services": {
"SimpleQueues": {
"Directory": "_tmp_queues"
},
"SimpleFileStorage": {
"Directory": "_tmp_files"
},
"AzureAIDocIntel": {
"Auth": "ApiKey",
"Endpoint": "https://aaa.openai.azure.com/",
"APIKey": "aaa"
},
"AzureOpenAIEmbedding": {
"APIType": "EmbeddingGeneration",
"Auth": "ApiKey",
"Endpoint": "https://aaa.openai.azure.com/",
"Deployment": "aitext",
"APIKey": "aaa"
},
"SimpleVectorDb": {
"Directory": "_tmp_vectors"
},
"AzureOpenAIText": {
"APIType": "ChatCompletion",
"Auth": "ApiKey",
"Endpoint": "https://aaa.openai.azure.com/",
"Deployment": "myai",
"APIKey": "aaa",
"MaxRetries": 10
}
}
},
"Logging": {
"LogLevel": {
"Default": "Warning"
}
},
"AllowedHosts": "*"
}
詳細(xì)可參考文檔: https://microsoft.github.io/kernel-memory/quickstart/configuration
啟動(dòng) Service 后,可以看到以下 swagger 界面。

然后編寫(xiě)代碼連接到知識(shí)庫(kù)系統(tǒng),推送要處理的網(wǎng)頁(yè)地址給 Service。創(chuàng)建一個(gè)項(xiàng)目,引入 Microsoft.KernelMemory.WebClient 包。
然后按照以下代碼將文檔推送給 Service 處理。
// 前面部署的 Service 地址,和自定義的密鑰。
var memory = new MemoryWebClient(endpoint: "http://localhost:9001/", apiKey: "自定義密鑰1");
// 導(dǎo)入網(wǎng)頁(yè)
await memory.ImportWebPageAsync(
"https://baike.baidu.com/item/比特幣挖礦機(jī)/12536531",
documentId: "doc02");
Console.WriteLine("正在處理文檔,請(qǐng)稍等...");
// 使用 AI 處理網(wǎng)頁(yè)知識(shí)
while (!await memory.IsDocumentReadyAsync(documentId: "doc02"))
{
await Task.Delay(TimeSpan.FromMilliseconds(1500));
}
// 提問(wèn)
var answer = await memory.AskAsync("比特幣是什么?");
Console.WriteLine($"\nAnswer: {answer.Result}");
此外還有 ImportTextAsync、ImportDocumentAsync 來(lái)個(gè)導(dǎo)入知識(shí)的方法。
手動(dòng)處理文檔
本節(jié)內(nèi)容稍多,主要講解如何使用 Kernel Memory 從將文檔導(dǎo)入、生成向量、存儲(chǔ)向量、搜索問(wèn)題等。
新建項(xiàng)目,安裝 Microsoft.KernelMemory.Core 庫(kù)。
為了便于演示,下面代碼將文檔和向量臨時(shí)存儲(chǔ),不使用數(shù)據(jù)庫(kù)存儲(chǔ)。
全部代碼示例如下:
using Microsoft.KernelMemory;
using Microsoft.KernelMemory.MemoryStorage.DevTools;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Connectors.OpenAI;
var memory = new KernelMemoryBuilder()
// 文檔解析后的向量存儲(chǔ)位置,可以選擇 Postgres 等,
// 這里選擇使用本地臨時(shí)文件存儲(chǔ)向量
.WithSimpleVectorDb(new SimpleVectorDbConfig
{
Directory = "aaa"
})
// 配置文檔解析向量模型
.WithAzureOpenAITextEmbeddingGeneration(new AzureOpenAIConfig
{
Deployment = "aitext",
Endpoint = "https://aaa.openai.azure.com/",
Auth = AzureOpenAIConfig.AuthTypes.APIKey,
APIType = AzureOpenAIConfig.APITypes.EmbeddingGeneration,
APIKey = "aaa"
})
// 配置文本生成模型
.WithAzureOpenAITextGeneration(new AzureOpenAIConfig
{
Deployment = "myai",
Endpoint = "https://aaa.openai.azure.com/",
Auth = AzureOpenAIConfig.AuthTypes.APIKey,
APIKey = "aaa",
APIType = AzureOpenAIConfig.APITypes.ChatCompletion
})
.Build();
// 導(dǎo)入網(wǎng)頁(yè)
await memory.ImportWebPageAsync(
"https://baike.baidu.com/item/比特幣挖礦機(jī)/12536531",
documentId: "doc02");
// Wait for ingestion to complete, usually 1-2 seconds
Console.WriteLine("正在處理文檔,請(qǐng)稍等...");
while (!await memory.IsDocumentReadyAsync(documentId: "doc02"))
{
await Task.Delay(TimeSpan.FromMilliseconds(1500));
}
// Ask a question
var answer = await memory.AskAsync("比特幣是什么?");
Console.WriteLine($"\nAnswer: {answer.Result}");

首先使用 KernelMemoryBuilder 構(gòu)建配置,配置的內(nèi)容比較多,這里會(huì)使用到兩個(gè)模型,一個(gè)是向量模型,一個(gè)是文本生成模型(可以使用對(duì)話(huà)模型,如 gpt-4-32k)。
接下來(lái),按照該程序的工作流程講解各個(gè)環(huán)節(jié)的相關(guān)知識(shí)。
首先是講解將文件存儲(chǔ)到哪里,也就是導(dǎo)入文件之后,將文件存儲(chǔ)到哪里,存儲(chǔ)文件的接口是 IContentStorage,目前有兩個(gè)實(shí)現(xiàn):
AzureBlobsStorage
// 存儲(chǔ)到目錄
SimpleFileStorage
使用方法:
var memory = new KernelMemoryBuilder()
.WithSimpleFileStorage(new SimpleFileStorageConfig
{
Directory = "aaa"
})
.WithAzureBlobsStorage(new AzureBlobsConfig
{
Account = ""
})
...
Kernel Memory 還不支持 Mongodb,不過(guò)可以自己使用 IContentStorage 接口寫(xiě)一個(gè)。
本地解析文檔后,會(huì)進(jìn)行分段,如右邊的 q 列所示。

接著是,配置文檔生成向量模型,導(dǎo)入文件文檔后,在本地提取出文本,需要使用 AI 模型從文本中生成向量。
解析后的向量是這樣的:

將文本生成向量,需要使用 ITextEmbeddingGenerator 接口,目前有兩個(gè)實(shí)現(xiàn):
AzureOpenAITextEmbeddingGenerator
OpenAITextEmbeddingGenerator
示例:
var memory = new KernelMemoryBuilder()
// 配置文檔解析向量模型
.WithAzureOpenAITextEmbeddingGeneration(new AzureOpenAIConfig
{
Deployment = "aitext",
Endpoint = "https://xxx.openai.azure.com/",
Auth = AzureOpenAIConfig.AuthTypes.APIKey,
APIType = AzureOpenAIConfig.APITypes.EmbeddingGeneration,
APIKey = "xxx"
})
.WithOpenAITextEmbeddingGeneration(new OpenAIConfig
{
... ...
})
生成向量后,需要存儲(chǔ)這些向量,需要實(shí)現(xiàn) IMemoryDb 接口,有以下配置可以使用:
// 文檔解析后的向量存儲(chǔ)位置,可以選擇 Postgres 等,
// 這里選擇使用本地臨時(shí)文件存儲(chǔ)向量
.WithSimpleVectorDb(new SimpleVectorDbConfig
{
Directory = "aaa"
})
.WithAzureAISearchMemoryDb(new AzureAISearchConfig
{
})
.WithPostgresMemoryDb(new PostgresConfig
{
})
.WithQdrantMemoryDb(new QdrantConfig
{
})
.WithRedisMemoryDb("host=....")
當(dāng)用戶(hù)提問(wèn)時(shí),首先會(huì)在這里的 IMemoryDb 調(diào)用相關(guān)方法查詢(xún)文檔中的向量、索引等,查找出相關(guān)的文本。
查出相關(guān)的文本后,需要發(fā)送給 AI 處理,需要使用 ITextGenerator 接口,目前有兩個(gè)實(shí)現(xiàn):
AzureOpenAITextGenerator
OpenAITextGenerator
配置示例:
// 配置文本生成模型
.WithAzureOpenAITextGeneration(new AzureOpenAIConfig
{
Deployment = "myai",
Endpoint = "https://aaa.openai.azure.com/",
Auth = AzureOpenAIConfig.AuthTypes.APIKey,
APIKey = "aaa",
APIType = AzureOpenAIConfig.APITypes.ChatCompletion
})
導(dǎo)入文檔時(shí),首先將文檔提取出文本,然后進(jìn)行分段。
將每一段文本使用向量模型解析出向量,存儲(chǔ)到 IMemoryDb 接口提供的服務(wù)中,如 Postgres數(shù)據(jù)庫(kù)。
提問(wèn)問(wèn)題或搜索內(nèi)容時(shí),從 IMemoryDb 所在的位置搜索向量,查詢(xún)到相關(guān)的文本,然后將文本收集起來(lái),發(fā)送給 AI(使用文本生成模型),這些文本相對(duì)于提示詞,然后 AI 從這些提示詞中學(xué)習(xí)并回答用戶(hù)的問(wèn)題。
詳細(xì)源碼可以參考 Microsoft.KernelMemory.Search.SearchClient ,由于源碼比較多,這里就不贅述了。

這樣說(shuō),大家可能不太容易理解,我們可以用下面的代碼做示范。
// 導(dǎo)入文檔
await memory.ImportDocumentAsync(
"aaa/(完整版)基礎(chǔ)財(cái)務(wù)知識(shí).docx",
documentId: "doc02");
Console.WriteLine("正在處理文檔,請(qǐng)稍等...");
while (!await memory.IsDocumentReadyAsync(documentId: "doc02"))
{
await Task.Delay(TimeSpan.FromMilliseconds(1500));
}
var answer1 = await memory.SearchAsync("報(bào)表怎么做?");
// 每個(gè) Citation 表示一個(gè)文檔文件
foreach (Citation citation in answer1.Results)
{
// 與搜索關(guān)鍵詞相關(guān)的文本
foreach(var partition in citation.Partitions)
{
Console.WriteLine(partition.Text);
}
}
var answer2 = await memory.AskAsync("報(bào)表怎么做?");
Console.WriteLine($"\nAnswer: {answer2.Result}");
讀者可以在 foreach 這里做個(gè)斷點(diǎn),當(dāng)用戶(hù)問(wèn)題 “報(bào)表怎么做?” 時(shí),搜索出來(lái)的相關(guān)文檔。
然后再參考 Fastgpt 的搜索配置,可以自己寫(xiě)一個(gè)這樣的知識(shí)庫(kù)系統(tǒng)。


浙公網(wǎng)安備 33010602011771號(hào)