这可能是目前最全的中文Semantic Kernel入门教程,呕心沥血,万字长文!!

        Semantic Kernel就像一颗璀璨的明珠,引发了无数开发者的关注。如果你对如何使用这一神秘而又强大的技术摸索不定,那么恭喜你,今天这篇文章将成为你的指南针!

🚀 什么是Semantic Kernel? Semantic Kernel,即语义核心,在.NET中它扮演着至关重要的角色。它可以让我们的程序更加智能,理解和处理复杂的语义信息,无论是做信息检索、数据分析还是AI应用,它都是不可或缺的工具。

🔍 一文掌握官方所有Demo 说起学习,最直观的莫过于官方Demo。本文不仅仅会逐一介绍示例代码,还会带着你详细解析每一行代码的含义,确保你对Semantic Kernel有一个全面而深入的理解。

🔧 个人经验与实战技巧 我将分享我在实际项目中应用Semantic Kernel时积累的宝贵经验。无论你是面临什么样的场景,我相信我的经验总结能够给你提供一些思路和帮助。

🔄 易错点分析,知识点纠正 在学习新技术的路上,难免会有些绊脚石。如果你在使用Semantic Kernel时感到困惑,不妨来看看本文。我会指出一些常见的错误,并提供正确的做法,帮助你更快上手。

🙌 共同成长,互帮互助 技术的道路上,我们互为他人的砥砺。如果文章中有什么不足之处,欢迎你留言指正。你的反馈是我不断进步的动力。也请不吝赐教,分享你对Semantic Kernel的理解和应用经验。

🚀 真正了解Semantic Kernel的时刻已经到来!如果这篇文章给了你新的启发,别忘了帮我转发出去,让更多渴望进步的伙伴们受益。点击关注,你的技术之旅,从这里启程!

首先,我们使用SK需要有模型。如果你有openai或者azure openai那么你可以直接使用SK,如果你没有以上这2种,有其他的本地或国产模型。那么你需要使用one-api这个项目

Semantic Kernel进阶:多模型的支持

实战教学:用Semantic Kernel框架集成腾讯混元大模型应用

又或者,你可以使用LLamaSharp在本地直接使用模型(这个也支持Semantic Kernel和Kernel Memory)

深入浅出LLamaSharp:打造智能.NET应用,不需GPU也能玩转LLaMA模型

轻松在.NET环境下运行本地AI模型:无需GPU也能有高效表现!

然后,当你已经准备好了模型。那么我们来看看SK如何使用吧!

首先需要进行初始化:

如果你用的是openai 或者oneapi 可以这样(oneapi还需要httpclient做代理转发

Kernel kernel = Kernel.CreateBuilder()
       .AddOpenAIChatCompletion(
           modelId: TestConfiguration.OpenAI.ChatModelId,
           apiKey: TestConfiguration.OpenAI.ApiKey)
       .Build();

如果你用的是azure openai需要这样:

Kernel kernel = Kernel.CreateBuilder()
      .AddAzureOpenAIChatCompletion(
          deploymentName: TestConfiguration.AzureOpenAI.ChatDeploymentName,
          endpoint: TestConfiguration.AzureOpenAI.Endpoint,
          apiKey: TestConfiguration.AzureOpenAI.ApiKey,
          modelId: TestConfiguration.AzureOpenAI.ChatModelId)
      .Build();

Example01_MethodFunctions-函数调用

public Task RunAsync()
 {
     this.WriteLine("======== Functions ========");
     // Load native plugin
     var text = new TextPlugin();
     // Use function without kernel
     var result = text.Uppercase("ciao!");
     this.WriteLine(result);
     return Task.CompletedTask;
 }

这是一个基础的Native Function的示例,Native Function就和c#里正常的Function一样,只需要加上[KernelFunction] 特性即可。

Example03_Arguments-参数

Kernel kernel = new();
    var textPlugin = kernel.ImportPluginFromType<StaticTextPlugin>();


    var arguments = new KernelArguments()
    {
        ["input"] = "Today is: ",
        ["day"] = DateTimeOffset.Now.ToString("dddd", CultureInfo.CurrentCulture)
    };
    
    string? resultValue = await kernel.InvokeAsync<string>(textPlugin["AppendDay"], arguments);
    this.WriteLine($"string -> {resultValue}");

这个是在Semantic Kernel中 参数的使用,我们需要创建声明KernelArguments 类,这个地方也可以进行简化,例如下面这样使用new()也是可以的。

var result = await _kernel.InvokeAsync(NativeNested["Test"], new () { ["input"] = msg });

Example05_InlineFunctionDefinition-线性功能定义

Kernel kernel = Kernel.CreateBuilder()
            .AddOpenAIChatCompletion(
                modelId: openAIModelId,
                apiKey: openAIApiKey)
            .Build();


        string promptTemplate = @"
为给定的事件创造一个创造性的理由或借口。
要有创意,要风趣。让你的想象力天马行空。
事件:我要迟到了。
对不起:我被长颈鹿歹徒勒索了赎金。
事件:我已经一年没去健身房了
对不起:我一直忙于训练我的宠物龙。
事件:{{$input}}
";


        var excuseFunction = kernel.CreateFunctionFromPrompt(promptTemplate, new OpenAIPromptExecutionSettings() { MaxTokens = 100, Temperature = 0.4, TopP = 1 });


        var result = await kernel.InvokeAsync(excuseFunction, new() { ["input"] = "我错过了F1决赛" });
        this.WriteLine(result.GetValue<string>());


        result = await kernel.InvokeAsync(excuseFunction, new() { ["input"] = "对不起,我忘了你的生日" });
        this.WriteLine(result.GetValue<string>());


        var fixedFunction = kernel.CreateFunctionFromPrompt($"将此日期{DateTimeOffset.Now:f}转换为法语格式", new OpenAIPromptExecutionSettings() { MaxTokens = 100 });


        result = await kernel.InvokeAsync(fixedFunction);
        this.WriteLine(result.GetValue<string>());

这是一个提示词模板的示例,并且在提示词中使用少量样本 ,让大模型进行按照样本参考进行推理,然后下面的三句话都会使用这个提示词模板,然后大模型会返回天马行空的回答,(胡扯)。此示例介绍提示词模板的复用,相当于是我们编程中的封装概念

Example06_TemplateLanguage

Kernel kernel = Kernel.CreateBuilder()
            .AddOpenAIChatCompletion(
                modelId: openAIModelId,
                apiKey: openAIApiKey)
            .Build();


        kernel.ImportPluginFromType<TimePlugin>("time");
    
        const string FunctionDefinition = @"
Today is: {{time.Date}}
Current time is: {{time.Time}}


Answer to the following questions using JSON syntax, including the data used.
Is it morning, afternoon, evening, or night (morning/afternoon/evening/night)?
Is it weekend time (weekend/not weekend)?
";


      
        var promptTemplateFactory = new KernelPromptTemplateFactory();
        var promptTemplate = promptTemplateFactory.Create(new PromptTemplateConfig(FunctionDefinition));
        var renderedPrompt = await promptTemplate.RenderAsync(kernel);
       
        var kindOfDay = kernel.CreateFunctionFromPrompt(FunctionDefinition, new OpenAIPromptExecutionSettings() { MaxTokens = 100 });


        var result = await kernel.InvokeAsync(kindOfDay);

这个示例介绍了 如何如何通过Semantic Function去调用Native Function的方法函数,例如大家知道,大模型不具备当前时间的概念,我们可以通过占位符去调用本地获取Datetime.Now 然后通过提示词给到大模型进行时间的推理。

Example08_RetryHandler-重试

// Create a Kernel with the HttpClient
 IKernelBuilder builder = Kernel.CreateBuilder();
 builder.Services.AddLogging(c => c.AddConsole().SetMinimumLevel(LogLevel.Information));
 builder.Services.ConfigureHttpClientDefaults(c =>
 {
     // Use a standard resiliency policy, augmented to retry on 401 Unauthorized for this example
     c.AddStandardResilienceHandler().Configure(o =>
     {
         o.Retry.ShouldHandle = args => ValueTask.FromResult(args.Outcome.Result?.StatusCode is HttpStatusCode.Unauthorized);
     });
 });
 builder.Services.AddOpenAIChatCompletion("gpt-4", "BAD_KEY"); // OpenAI settings - you can set the OpenAI.ApiKey to an invalid value to see the retry policy in play
 Kernel kernel = builder.Build();


 var logger = kernel.LoggerFactory.CreateLogger(typeof(Example08_RetryHandler));


 const string Question = "How do I add a standard resilience handler in IHttpClientBuilder??";
 logger.LogInformation("Question: {Question}", Question);


 // The call to OpenAI will fail and be retried a few times before eventually failing.
 // Retrying can overcome transient problems and thus improves resiliency.
 try
 {
     // The InvokePromptAsync call will issue a request to OpenAI with an invalid API key.
     // That will cause the request to fail with an HTTP status code 401. As the resilience
     // handler is configured to retry on 401s, it'll reissue the request, and will do so
     // multiple times until it hits the default retry limit, at which point this operation
     // will throw an exception in response to the failure. All of the retries will be visible
     // in the logging out to the console.
     logger.LogInformation("Answer: {Result}", await kernel.InvokePromptAsync(Question));
 }
 catch (Exception ex)
 {
     logger.LogInformation("Error: {Message}", ex.Message);
 }

这个示例中,使用HttpClient配置重试,如果填入一个错误的key即可测试,这个在QPS限流场景能起到一些关键作用,保障流程的可靠稳定性。

Example09_FunctionTypes

在Semantic Kernel中创建和使用函数,你可以通过不同的形态来实现不同的目的。在本例中,我们将介绍几种不同的方法函数类型,并且展示如何在Semantic Kernel中调用它们。让通过Example09_FunctionTypes这个示例来详细了解。

// Copyright (c) Microsoft. All rights reserved.


// 引入必需的命名空间
using System;
using System.ComponentModel;
using System.Globalization;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.ChatCompletion;
using RepoUtils;
using Xunit;
using Xunit.Abstractions;


// 创建一个测试类,继承自BaseTest
namespace Examples;


public class Example09_FunctionTypes : BaseTest
{
    // 使用Xunit的Fact特性来标识这是个测试方法
    [Fact]
    public async Task RunAsync()
    {
        // 输出测试方法名
        this.WriteLine("======== Method Function types ========");


        // 创建并配置Semantic Kernel构建器
        var builder = Kernel.CreateBuilder()
            .AddOpenAIChatCompletion(TestConfiguration.OpenAI.ChatModelId, TestConfiguration.OpenAI.ApiKey);
        builder.Services.AddLogging(services => services.AddConsole().SetMinimumLevel(LogLevel.Warning));
        builder.Services.AddSingleton(this.Output);
        
        // 构建Semantic Kernel
        var kernel = builder.Build();
        kernel.Culture = new CultureInfo("pt-BR"); // 设置区域文化


        // 在Kernel中加载示例插件,并注册函数
        var plugin = kernel.ImportPluginFromType<LocalExamplePlugin>("Examples");


        // 从目录加载另一个插件
        string folder = RepoFiles.SamplePluginsPath();
        kernel.ImportPluginFromPromptDirectory(Path.Combine(folder, "SummarizePlugin"));


        // 下面演示了不同的函数调用方式
        // 直接调用无参函数
        await kernel.InvokeAsync(plugin[nameof(LocalExamplePlugin.NoInputWithVoidResult)]);


        // ... 其他函数调用(省略中间内容以简化展示)


        // 从Kernel的插件集合中调用函数
        await kernel.InvokeAsync(kernel.Plugins["Examples"][nameof(LocalExamplePlugin.NoInputWithVoidResult)]);
    }


    // 构造方法,接收Xunit的输出帮助器
    public Example09_FunctionTypes(ITestOutputHelper output) : base(output)
    {
    }
}


// 示例插件类,定义了一系列可供调用的方法函数
public class LocalExamplePlugin
{
    private readonly ITestOutputHelper _output;


    // 构造方法
    public LocalExamplePlugin(ITestOutputHelper output)
    {
        this._output = output;
    }


    // ... 方法函数定义(省略中间内容以简化展示)


    // 一种返回复杂类型结果的ToString方法重写
    public override string ToString()
    {
        return "Complex type result ToString override";
    }
}

在这个示例中,LocalExamplePlugin定义了多种类型的函数,这些函数通过装饰了[KernelFunction]特性可以被Semantic Kernel调用。函数的类型包括无输入参数、有输入参数、返回值为void或具体类型、同步或异步(Task返回)。

我们注意到,当以插件形式导入任务函数(Task functions)时,如果有Async后缀,则在Semantic Kernel中会丢弃该后缀。

下面是对一些关键函数的调用方式说明:

  • NoInputWithVoidResult: 这是一个无输入参数且无返回值的函数。

  • InputDateTimeWithStringResult: 这是一个接收DateTime输入参数并返回字符串结果的函数。

  • MultipleInputsWithVoidResult: 这个函数接收不同类型的多个输入参数,但没有返回值。

此外,这个例子还展示了如何将Semantic Kernel和其它服务注入到方法函数中去,以便在函数内部再调用其他插件或服务。

Example10_DescribeAllPluginsAndFunctions:

public class Example10_DescribeAllPluginsAndFunctions : BaseTest
{
    /// <summary>
    /// 打印导入到内核的所有函数的列表,包括函数描述、参数列表、参数描述等。
    /// 请参阅文件末尾的输出示例。
    /// </summary>
    [Fact]
    public Task RunAsync()
    {
        // 创建内核构建器并添加插件
        var kernel = Kernel.CreateBuilder()
            .AddOpenAIChatCompletion(
                modelId: TestConfiguration.OpenAI.ChatModelId,
                apiKey: TestConfiguration.OpenAI.ApiKey)
            .Build();


        // 从类型中导入本地插件
        kernel.ImportPluginFromType<StaticTextPlugin>();


        // 从类型中导入另一个本地插件
        kernel.ImportPluginFromType<TextPlugin>("AnotherTextPlugin");


        // 从指令目录导入语义插件
        string folder = RepoFiles.SamplePluginsPath();
        kernel.ImportPluginFromPromptDirectory(Path.Combine(folder, "SummarizePlugin"));


        // 定义一个内联提示函数,不提供名称
        var sFun1 = kernel.CreateFunctionFromPrompt("tell a joke about {{$input}}", new OpenAIPromptExecutionSettings() { MaxTokens = 150 });


        // 定义一个内联提示函数,并提供插件名称
        var sFun2 = kernel.CreateFunctionFromPrompt(
            "write a novel about {{$input}} in {{$language}} language",
            new OpenAIPromptExecutionSettings() { MaxTokens = 150 },
            functionName: "Novel",
            description: "Write a bedtime story");


        // 获取函数元数据
        var functions = kernel.Plugins.GetFunctionsMetadata();


        WriteLine("**********************************************");
        WriteLine("****** Registered plugins and functions ******");
        WriteLine("**********************************************");
        WriteLine();


        // 遍历所有函数并打印其详情
        foreach (KernelFunctionMetadata func in functions)
        {
            PrintFunction(func);
        }


        return Task.CompletedTask;
    }


    /// <summary>
    /// 打印特定函数的详细信息
    /// </summary>
    /// <param name="func">要打印的函数元数据</param>
    private void PrintFunction(KernelFunctionMetadata func)
    {
        WriteLine($"插件: {func.PluginName}");
        WriteLine($"   {func.Name}: {func.Description}");


        if (func.Parameters.Count > 0)
        {
            WriteLine("参数:");
            foreach (var p in func.Parameters)
            {
                WriteLine($"- {p.Name}: {p.Description}");
                WriteLine($"默认值: '{p.DefaultValue}'");
            }
        }


        WriteLine();
    }


    /// <summary>
    /// 构造函数,初始化基类
    /// </summary>
    /// <param name="output">用于写入输出的帮助器</param>
    public Example10_DescribeAllPluginsAndFunctions(ITestOutputHelper output) : base(output)
    {
    }
}

        上述代码是一个示例,它假设 BaseTestKernelITestOutputHelper 等类的存在以及它们的相关方法。该实现使用了xUnit 测试框架,因为其中出现了 [Fact] 属性。

        这个代码块演示了如何使用一个测试类 Example10_DescribeAllPluginsAndFunctions 来列出并打印所有导入到内核中的插件和函数的具体信息,包括它们的名称、描述以及函数的参数详情。这可以为读者提供如何在他们自己的Semantic Kernel中查看并获取关于已注册插件和函数的元数据的实例。

Example11_WebSearchQueries:联网搜索

下面是如何使用Semantic Kernel通过一个称为SearchUrlPlugin的插件来创建一个简单的Web搜索查询示例。

using SemanticKernel;
using System.Threading.Tasks;
using Xunit;
using Xunit.Abstractions;


public class Example11_WebSearchQueries
{
    private readonly ITestOutputHelper output;


    public Example11_WebSearchQueries(ITestOutputHelper output)
    {
        this.output = output;
    }


    private void WriteLine(string message)
    {
        output.WriteLine(message);
    }


    [Fact]
    public async Task RunAsync()
    {
        WriteLine("======== WebSearchQueries ========");
        Kernel kernel = new();


        // Load native plugins
        var bingPluginType = typeof(SearchUrlPlugin);
        var bing = kernel.ImportPluginFromType(bingPluginType, "search");


        // 声明搜索问题
        var ask = "What's the tallest building in Europe?";
        // 执行搜索,得到搜索引擎的URL
        var result = await kernel.InvokeAsync(bing["BingSearchUrl"], new() { ["query"] = ask });


        WriteLine(ask + "\n");
        WriteLine(result.GetValue<string>());


        /* 预期输出: 
        * ======== WebSearchQueries ========
        * What's the tallest building in Europe?
        * 
        * https://www.bing.com/search?q=What%27s%20the%20tallest%20building%20in%20Europe%3F
        * == DONE ==
        */
    }
}

        在上面的例子中,我们首先使用Kernel类创建一个新的内核实例。接着通过ImportPluginFromType方法加载名为SearchUrlPlugin的插件。这个插件是专门为了构造针对搜索引擎的URL请求而设计的。

        创建了一个查询“Europe最高建筑是哪座?”,然后通过调用内核的InvokeAsync方法,并传递查询字符串,来使用SearchUrlPlugin生成对应搜索引擎的URL。

        BingSearchUrlSearchUrlPlugin提供的一个Kernel Function,它接收用户查询字符串,并返回一个可用于Bing搜索引擎的正确编码URL。

        最后,输出了查询和搜索结果URL,来验证我们成功创建了针对Bing搜索引擎的查询URL。预期的输出显示了构造的URL,以及它是如何匹配我们的初衷,验证了SearchUrlPlugin功能的正确性。

Example13_ConversationSummaryPlugin:会话总结

public async Task RunAsync()
{
    Kernel kernel = InitializeKernel();
    KernelPlugin conversationSummaryPlugin = kernel.ImportPluginFromType<ConversationSummaryPlugin>();


    // 调用插件的函数
    FunctionResult summary = await kernel.InvokeAsync(
        conversationSummaryPlugin["SummarizeConversation"], new() { ["input"] = ChatTranscript });
    
    // 输出结果
    WriteLine("Generated Summary:");
    WriteLine(summary.GetValue<string>());
}

        在上面的代码当中,ConversationSummaryPlugin是我们定义的插件类,拥有我们需要的方法,比如SummarizeConversation。我们通过传递一个带有对话内容的input参数调用该方法,并得到我们的会话摘要。

        为此,您需要先创建这个插件,定义好它的方法,并用[KernelFunction]属性标记,让框架知道它可以被作为一个函数来调用。

Example14_SemanticMemory:内存存储

using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Memory;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;


public class SemanticKernelExample 
{
     private const string MemoryCollectionName = "SemanticKernelCollection";


     public async Task RunSemanticMemoryExampleAsync()
     {
         var memoryWithACS = new MemoryBuilder()
             .WithOpenAITextEmbeddingGeneration("text-embedding-ada-002", TestConfiguration.OpenAI.ApiKey)
             .WithMemoryStore(new AzureAISearchMemoryStore(TestConfiguration.AzureAISearch.Endpoint, TestConfiguration.AzureAISearch.ApiKey))
             .Build();


         await RunExampleAsync(memoryWithACS);
     }


     private async Task RunExampleAsync(ISemanticTextMemory memory)
     {
         await StoreMemoryAsync(memory);


         await SearchMemoryAsync(memory, "How do I get started?");


         await SearchMemoryAsync(memory, "Can I build a chat with SK?");
     }
 
     private async Task SearchMemoryAsync(ISemanticTextMemory memory, string query)
     {
         WriteLine("\nQuery: " + query + "\n");


         var memoryResults = await memory.SearchAsync(MemoryCollectionName, query, limit: 2, minRelevanceScore: 0.5);


         int i = 0;
         foreach (MemoryQueryResult memoryResult in memoryResults)
         {
             WriteLine($"Result {++i}:");
             WriteLine("  URL:     : " + memoryResult.Metadata.Id);
             WriteLine("  Title    : " + memoryResult.Metadata.Description);
             WriteLine("  Relevance: " + memoryResult.Relevance);
             WriteLine();
         }


         WriteLine("----------------------");
     }


     private async Task StoreMemoryAsync(ISemanticTextMemory memory)
     {
         WriteLine("\nAdding some GitHub file URLs and their descriptions to the semantic memory.");
         var githubFiles = SampleData();
         var i = 0;
         foreach (var entry in githubFiles)
         {
             await memory.SaveReferenceAsync(
                 collection: MemoryCollectionName,
                 externalSourceName: "GitHub",
                 externalId: entry.Key,
                 description: entry.Value,
                 text: entry.Value);


             Console.Write($" #{++i} saved.");
         }


         WriteLine("\n----------------------");
     }


     private void WriteLine(string message)
     {
         Console.WriteLine(message);
     }


     private Dictionary<string, string> SampleData()
     {
         // You can replace this sample data with actual GitHub file URLs and their descriptions.
         return new Dictionary<string, string>
         {
             { "https://github.com/user/repository/blob/main/README.md", "This is the README file for getting started." },
             { "https://github.com/user/repository/blob/main/CONTRIBUTING.md", "This file contains contribution guidelines." }
         };
     }
}

提供的示例代码执行了以下步骤:

  1. 首先构建了一个带有Azure AI文本嵌入以及搜索存储的Semantic Memory对象。

  2. 运行了包含数据存储和查询的例子。

  3. 实现了SearchMemoryAsync方法,以便在给定query的情况下进行语义搜索。

  4. 实现了StoreMemoryAsync方法,以存储一些样本数据到语义存储中。

请确保在真实环境中使用您自己的OpenAI和Azure搜索服务配置。如果您没有这些服务的话,您需要先进行相应的申请与配置。希望这个教程示例能够帮助您快速开始使用Semantic Kernel进行开发。

Example15_TextMemoryPlugin:向量内存存储

这里的教程主要关注在使用Semantic Kernel进行简单的记忆存储和检索,以及进行相似记忆查找的功能。教程将以“关于我”(aboutMe)的信息作为例子,并展示如何使用不同的存储提供者。

using SemanticKernel;
using System.Threading.Tasks;
using Xunit;
using SemanticKernel.Memory;
using SemanticKernel.Plugins.MemoryStore;
using ... // 其他必要的命名空间


public class TextMemoryKernelExample
{
    private const string MemoryCollectionName = "aboutMe";
  
    [Theory]
    [InlineData("Volatile")]
    [InlineData("AzureAISearch")]
    // 添加其他支持库的InlineData属性
    public async Task RunAsync(string provider)
    {
        IMemoryStore memoryStore;


        // 根据provider创建对应的MemoryStore实例
        memoryStore = provider switch
        {
            "Volatile" => new VolatileMemoryStore(),
            "AzureAISearch" => CreateSampleAzureAISearchMemoryStore(),
            "Sqlite" => new SqliteMemoryStore("database.sqlite"),
            "DuckDB" => new DuckDBMemoryStore("duckdb_file.duckdb"),
            "MongoDB" => new MongoDBMemoryStore("mongodb://localhost:27017", "SemanticDB"),
            // 其他存储库的初始化代码
            _ => new VolatileMemoryStore()
        };


        await RunWithStoreAsync(memoryStore);
    }


    // 主执行方法
    private async Task RunWithStoreAsync(IMemoryStore memoryStore)
    {
        var kernel = Kernel.CreateBuilder()
            .AddPlugin<TextMemoryPlugin>()
            // 通过AddPlugin方法添加其他需要的插件
            .Build();


        // 余下的代码与上面提供的参考示例相同,只是在完成后要关闭数据库连接


        // 关闭数据库连接(如果需要的话)
        switch (memoryStore)
        {
            case SqliteMemoryStore sqliteStore:
                sqliteStore.CloseConnection();
                break;
            // 其他存储库的连接关闭操作
        }
    }


    // 其他方法,例如:创建Azure AI Search内存存储的示例
    private IMemoryStore CreateSampleAzureAISearchMemoryStore()
    {
        // 创建并配置Azure AI Search内存存储实例
        // ...
    }


    // ... 其他存储库初始化的具体方法
}

以上代码提供了一个示例框架,解释了如何为Semantic Kernel选择和使用不同类型的向量内存存储。这些存储类型包括Sqlite、DuckDB、MongoDB、Azure AI Search、Qdrant、Chroma、Pinecone、Weaviate、Redis、Postgres、Kusto等。

Example16_CustomLLM:自定义LLM

下面的例子展示了如何将自定义的文本生成模型集成到SK中。

1. 创建测试类

首先,我们要创建一个Example16_CustomLLM测试类,继承于BaseTest,这样我们可以利用测试框架来运行我们的示例代码。

public class Example16_CustomLLM : BaseTest
{
    // 这部分将包含三个测试方法
}
2. 使用KernelFunction实现自定义文本生成
// 使用`Fact`属性来表明这是一个测试方法
[Fact]
public async Task CustomTextGenerationWithKernelFunctionAsync()
{
    // 输出测试标题
    WriteLine("\n======== Custom LLM - Text Completion - KernelFunction ========");


    // 创建Kernel构建器并添加你的自定义服务
    IKernelBuilder builder = Kernel.CreateBuilder();
    builder.Services.AddKeyedSingleton<ITextGenerationService>("myService1", new MyTextGenerationService());


    // 构建Kernel
    Kernel kernel = builder.Build();


    // 定义一个函数格式
    const string FunctionDefinition = "Write one paragraph on {{$input}}";


    // 从定义创建函数
    var paragraphWritingFunction = kernel.CreateFunctionFromPrompt(FunctionDefinition);


    // 定义输入值
    const string Input = "Why AI is awesome";
    WriteLine($"Function input: {Input}\n");


    // 调用函数并等待结果
    var result = await paragraphWritingFunction.InvokeAsync(kernel, new() { ["input"] = Input });


    // 输出结果
    WriteLine(result);
}
3. 直接调用自定义文本生成服务
[Fact]
public async Task CustomTextGenerationAsync()
{
    WriteLine("\n======== Custom LLM  - Text Completion - Raw ========");


    // 定义提示
    const string Prompt = "Write one paragraph on why AI is awesome.";
    var completionService = new MyTextGenerationService();


    WriteLine($"Prompt: {Prompt}\n");


    // 调用自定义服务
    var result = await completionService.GetTextContentAsync(Prompt);


    WriteLine(result);
}
4. 使用流的方式调用自定义文本生成服务
[Fact]
public async Task CustomTextGenerationStreamAsync()
{
    WriteLine("\n======== Custom LLM  - Text Completion - Raw Streaming ========");


    // 再次定义提示
    const string Prompt = "Write one paragraph on why AI is awesome.";
    var completionService = new MyTextGenerationService();


    WriteLine($"Prompt: {Prompt}\n");


    // 使用异步流读取结果并输出
    await foreach (var message in completionService.GetStreamingTextContentsAsync(Prompt))
    {
        Write(message);
    }
    WriteLine();
}

通过上面的示例,我们了解了如何将自定义的文本生成模型集成到SK中,并以不同的方式调用服务来生成文本。不论你是想要使用非OpenAI的服务模型,还是需要将API封装成不同架构的web服务,或者使用本地模型,SK都为我们提供了便利。

Example17_ChatGPT:ChatGPT模式

步骤 1: 定义测试方法 我们将创建两个测试方法,一个用于 OpenAI 的 ChatGPT API,另一个用于 Azure OpenAI 服务。我们还将定义一个私有方法 StartChatAsync,用于处理聊天会话的初始化和消息交换。

public class ChatGPTExamples
{
    [Fact]
    public async Task OpenAIChatSampleAsync()
    {
        // ... 示例代码 ...
    }


    [Fact]
    public async Task AzureOpenAIChatSampleAsync()
    {
        // ... 示例代码 ...
    }


    private async Task StartChatAsync(IChatCompletionService chatGPT)
    {
        // ... 示例代码 ...
    }
    
    // ... MessageOutputAsync ...
}

步骤 2: 实现 OpenAI 聊天例子 在 OpenAIChatSampleAsync 方法中,初始化 OpenAI 聊天服务并启动聊天会话:

public async Task OpenAIChatSampleAsync()
{
    WriteLine("======== Open AI - ChatGPT ========");


    // 实例化聊天服务
    OpenAIChatCompletionService chatCompletionService = new(
        TestConfiguration.OpenAI.ChatModelId, 
        TestConfiguration.OpenAI.ApiKey
    );


    // 开始异步聊天会话
    await StartChatAsync(chatCompletionService);
}

步骤 3: 实现 Azure OpenAI 聊天示例 类似地,在 AzureOpenAIChatSampleAsync 方法中,启动 Azure OpenAI 聊天服务:

public async Task AzureOpenAIChatSampleAsync()
{
    WriteLine("======== Azure Open AI - ChatGPT ========");


    // 实例化 Azure 聊天服务
    AzureOpenAIChatCompletionService chatCompletionService = new(
        TestConfiguration.AzureOpenAI.ChatDeploymentName, 
        TestConfiguration.AzureOpenAI.Endpoint, 
        TestConfiguration.AzureOpenAI.ApiKey, 
        TestConfiguration.AzureOpenAI.ChatModelId
    );


    // 开始异步聊天会话
    await StartChatAsync(chatCompletionService);
}

步骤 4: 实现聊天启动逻辑 使用 StartChatAsync 方法在给定的聊天服务上处理消息交换:

private async Task StartChatAsync(IChatCompletionService chatGPT)
{
    WriteLine("Chat content:");
    WriteLine("------------------------");


    var chatHistory = new ChatHistory("You are a librarian, expert about books");


    // 第一个用户消息
    chatHistory.AddUserMessage("Hi, I'm looking for book suggestions");
    await MessageOutputAsync(chatHistory);


    // 第一个助手回复
    var reply = await chatGPT.GetChatMessageContentAsync(chatHistory);
    chatHistory.Add(reply);
    await MessageOutputAsync(chatHistory);


    // 第二个用户消息
    chatHistory.AddUserMessage("I love history and philosophy, I'd like to learn something new about Greece, any suggestion?");
    await MessageOutputAsync(chatHistory);


    // 第二个助手回复
    reply = await chatGPT.GetChatMessageContentAsync(chatHistory);
    chatHistory.Add(reply);
    await MessageOutputAsync(chatHistory);
}


// 输出聊天历史中的最后一条消息
private Task MessageOutputAsync(ChatHistory chatHistory)
{
    var message = chatHistory.Last();


    WriteLine($"{message.Role}: {message.Content}");
    WriteLine("------------------------");


    return Task.CompletedTask;
}
  • 这段代码依赖于你事先设计的 IChatCompletionService 接口和相关聊天服务实现。

  • TestConfiguration 类是假定你已经为你的测试配置了一个静态类。你需要替换为你的 API 密钥和模型标识符。

Example18_DallE:图片生成模型

在这个方法中,我们首先使用Semantic Kernel创建内核(Kernel)实例,并添加文本转图像服务和聊天完成服务。以下是基本步骤:

public async Task OpenAIDallEAsync()
{
    WriteLine("======== OpenAI Dall-E 2 Text To Image ========");


    // 创建内核并添加所需的服务
    Kernel kernel = Kernel.CreateBuilder()
        .AddOpenAITextToImage(TestConfiguration.OpenAI.ApiKey)
        .AddOpenAIChatCompletion(TestConfiguration.OpenAI.ChatModelId, TestConfiguration.OpenAI.ApiKey)
        .Build();


    // 获取文本转图像服务的实例
    ITextToImageService dallE = kernel.GetRequiredService<ITextToImageService>();


    // 描述文章将被转换成图像
    var imageDescription = "A cute baby sea otter";
    var image = await dallE.GenerateImageAsync(imageDescription, 256, 256);


    // 打印描述和生成的图像URL
    WriteLine(imageDescription);
    WriteLine("Image URL: " + image);


    // 此处省略文本和图像聊天的示例输出...


    // 输出接下来将聊天记录结合图像展示的部分
    WriteLine("======== Chat with images ========");


    // 获取聊天服务的实例
    var chatGPT = kernel.GetRequiredService<IChatCompletionService>();
    
    // 创建一个聊天历史对象,用以追踪用户与AI的对话
    var chatHistory = new ChatHistory("You're chatting with a user...");


    // 此处省略了聊天的细节操作...


    // 打印用户的消息、机器人的图像回复和图像描述
    WriteLine("User: " + msg);
    WriteLine("Bot: " + image);
    WriteLine("Img description: " + reply.Content);


    // 此处省略最终的输出展示...
}

        在此教程示例代码中,我们可以看到如何初始化内核,添加服务,并利用这些服务生成相关的图像。代码的其他部分包括发送消息、接收聊天机器人的文本回复然后将这些文本回复转化成图像。这个过程中,用户和AI之间的每一条消息都被记录下来,并用于生成下一次的回复。

Example20_HuggingFace:

在这一部分中,我们将利用Semantic Kernel调用HuggingFace提供的Inference API,进行问答任务的示例。

首先,我们需要配置Semantic Kernel以集成HuggingFace文本生成服务。

代理站点:https://hf-mirror.com/

public class Example20_HuggingFace : BaseTest
{
    [Fact]
    public async Task RunInferenceApiExampleAsync()
    {
        WriteLine("\n======== HuggingFace Inference API example ========\n");


        // 创建并配置Kernel
        Kernel kernel = Kernel.CreateBuilder()
            .AddHuggingFaceTextGeneration(
                model: TestConfiguration.HuggingFace.ModelId,
                apiKey: TestConfiguration.HuggingFace.ApiKey)
            .Build();


        // 建立一个语言模型函数
        var questionAnswerFunction = kernel.CreateFunctionFromPrompt("Question: {{$input}}; Answer:");


        // 调用该函数并传递问题
        var result = await kernel.InvokeAsync(questionAnswerFunction, new() { ["input"] = "What is New York?" });


        // 输出答案
        WriteLine(result.GetValue<string>());
    }
}

        在这个示例中,我们通过Kernel.CreateBuilder()方法来创建一个内核配置生成器,然后使用AddHuggingFaceTextGeneration方法将HuggingFace文本生成服务添加到内核中。 modelapiKey是调用HuggingFace Inference API所需的配置项。

        CreateFunctionFromPrompt方法用于创建一个新的函数,该函数使用给定的提示字符串格式化输入并调用模型进行响应。本例中的模型被配置为接收一个问题并返回一个答案。

使用Semantic Kernel和本地Llama模型进行问答

        在这一部分中,我们将演示如何使用Semantic Kernel配合本地运行的Llama模型进行问答任务。

[Fact(Skip = "Requires local model or Huggingface Pro subscription")]
public async Task RunLlamaExampleAsync()
{
    WriteLine("\n======== HuggingFace Llama 2 example ========\n");


    const string Model = "meta-llama/Llama-2-7b-hf";


    Kernel kernel = Kernel.CreateBuilder()
        .AddHuggingFaceTextGeneration(
            model: Model,
            apiKey: TestConfiguration.HuggingFace.ApiKey)
        .Build();


    var questionAnswerFunction = kernel.CreateFunctionFromPrompt("Question: {{$input}}; Answer:");


    var result = await kernel.InvokeAsync(questionAnswerFunction, new() { ["input"] = "What is New York?" });


    WriteLine(result.GetValue<string>());
}

        这个示例类似于上一个,但是假定我们要使用Llama模型并且已经按照Semantic Kernel的README指导在本地设立了HTTP服务器。AddHuggingFaceTextGeneration方法中的注释掉的endpoint参数是用于指定本地服务器地址的。

然后我们使用InvokeAsync方法来调用我们构建的函数并输出结果。

注意事项

  • 在使用HuggingFace的API或者本地模型之前,你需要确保你有相应的访问权限。部分模型可能需要通过HuggingFace提供的方法来申请使用权。

  • 你还需要一个HuggingFace账号,并取得相应的API密钥,才能使用Inference API。

  • 如果你使用本地模型,你需要先下载模型,并根据Semantic Kernel的指引搭建本地HTTP服务器。

Example21_OpenAIPlugins

[Fact(Skip = "Run it only after filling the template below")]
public async Task RunOpenAIPluginAsync()
{
    Kernel kernel = new();


    // This HTTP client is optional. SK will fallback to a default internal one if omitted.
    using HttpClient httpClient = new();


    // Import an Open AI plugin via URI
    var plugin = await kernel.ImportPluginFromOpenAIAsync("<plugin name>", new Uri("<OpenAI-plugin-URI>"), new OpenAIFunctionExecutionParameters(httpClient));


    // Add arguments for required parameters, arguments for optional ones can be skipped.
    var arguments = new KernelArguments { ["<parameter-name>"] = "<parameter-value>" };


    // Run the plugin function
    var functionResult = await kernel.InvokeAsync(plugin["<function-name>"], arguments);


    var result = functionResult.GetValue<RestApiOperationResponse>();


    WriteLine($"Function execution result: {result?.Content}");
}
  1. 创建一个 Kernel 实例。

  2. (可选)创建一个 HttpClient 实例,如果省略,SK 会使用默认的内部客户端。

  3. 通过 kernel.ImportPluginFromOpenAIAsync 方法导入 OpenAI plugin。你需要替换 <plugin name> 和 <OpenAI-plugin-URI> 为正确的插件名称和 URI。

  4. 创建 KernelArguments 实例并添加需要的参数名和值,可选参数可以省略。

  5. 使用 kernel.InvokeAsync 方法调用插件功能,其中 <function-name> 需要替换成实际要调用的函数名。

  6. 从结果中获取值并打印。


调用 Klarna API 示例

以下示例演示了如何通过 SK 调用 Klarna 的 API 查询产品。

[Fact]
public async Task CallKlarnaAsync()
{
    Kernel kernel = new();


    var plugin = await kernel.ImportPluginFromOpenAIAsync("Klarna", new Uri("https://www.klarna.com/.well-known/ai-plugin.json"));


    var arguments = new KernelArguments();
    arguments["q"] = "Laptop";  // 需要搜索的产品类别或名称。
    arguments["size"] = "3";    // 返回产品的数量。
    arguments["budget"] = "200"; // 产品的最高价格,以当地货币计。
    arguments["countryCode"] = "US"; // 用户所在位置的 ISO 3166 国家代码,目前仅支持 US, GB, DE, SE 和 DK。


    var functionResult = await kernel.InvokeAsync(plugin["productsUsingGET"], arguments);


    var result = functionResult.GetValue<RestApiOperationResponse>();


    WriteLine($"Function execution result: {result?.Content}");
}

步骤说明:

  1. 创建 Kernel 实例。

  2. 通过 URI 导入 Klarna 插件。

  3. 设置搜索参数,包括搜索关键字 q,产品数量 size,预算 budget 和国家代码 countryCode

  4. 调用 Klarna 的 productsUsingGET 函数获取产品列表。

  5. 获取并打印出结果。

Example22_OpenAIPlugin_AzureKeyVault

  1. 在Microsoft身份平台上注册一个客户端应用程序。

  2. 创建一个Azure Key Vault实例。

  3. 给你的客户端应用程序添加对Azure Key Vault的权限。

  4. 使用dotnet user-secrets工具设置Key Vault的端点、客户端ID和客户端密钥。

  5. 在适当的配置文件中替换您的租户ID。

现在,让我们开始编写示例代码:

public class Example22_OpenAIPlugin_AzureKeyVault : BaseTest
{
    private const string SecretName = "Foo";
    private const string SecretValue = "Bar";


    /// <summary>
    /// 本示例展示了如何将Azure Key Vault插件连接到Semantic Kernel。
    /// 在使用本示例之前,请确保您完成了上面列出的准备工作,包括设置应用程序和权限等。
    /// </summary>
    [Fact(Skip = "Setup credentials before running")]
    public async Task RunAsync()
    {
        var authenticationProvider = new OpenAIAuthenticationProvider(
            new Dictionary<string, Dictionary<string, string>>()
            {
                {
                    "login.microsoftonline.com",
                    new Dictionary<string, string>()
                    {
                        { "client_id", TestConfiguration.KeyVault.ClientId },
                        { "client_secret", TestConfiguration.KeyVault.ClientSecret },
                        { "grant_type", "client_credentials" }
                    }
                }
            }
        );


        Kernel kernel = new();


        var openApiSpec = EmbeddedResource.Read("22-openapi.json");
        using var messageStub = new HttpMessageHandlerStub(openApiSpec);
        using var httpClient = new HttpClient(messageStub);


        // 导入Open AI插件
        var openAIManifest = EmbeddedResource.ReadStream("22-ai-plugin.json");
        var plugin = await kernel.ImportPluginFromOpenAIAsync(
            "AzureKeyVaultPlugin",
            openAIManifest!,
            new OpenAIFunctionExecutionParameters
            {
                AuthCallback = authenticationProvider.AuthenticateRequestAsync,
                HttpClient = httpClient,
                EnableDynamicPayload = true,
                ServerUrlOverride = new Uri(TestConfiguration.KeyVault.Endpoint)
            });


        // 往Azure Key Vault添加秘密值
        await AddSecretToAzureKeyVaultAsync(kernel, plugin);
        // 尝试从Azure Key Vault获取秘密值
        await GetSecretFromAzureKeyVaultWithRetryAsync(kernel, plugin);
    }


    private async Task AddSecretToAzureKeyVaultAsync(Kernel kernel, KernelPlugin plugin)
    {
        var arguments = new KernelArguments
        {
            ["secret-name"] = SecretName,
            ["value"] = SecretValue,
            ["api-version"] = "7.0",
            ["enabled"] = "true",
        };


        var functionResult = await kernel.InvokeAsync(plugin["SetSecret"], arguments);


        var result = functionResult.GetValue<RestApiOperationResponse>();


        Console.WriteLine("SetSecret function result: {0}", result?.Content?.ToString());
    }


    private static async Task GetSecretFromAzureKeyVaultWithRetryAsync(Kernel kernel, KernelPlugin plugin)
    {
        var arguments = new KernelArguments();
        arguments["secret-name"] = SecretName;
        arguments["api-version"] = "7.0";


        var functionResult = await kernel.InvokeAsync(plugin["GetSecret"], arguments);


        var result = functionResult.GetValue<RestApiOperationResponse>();


        Console.WriteLine("GetSecret function result: {0}", result?.Content?.ToString());
    }


    public Example22_OpenAIPlugin_AzureKeyVault(ITestOutputHelper output) : base(output)
    {
    }
}

        这个例子中的AddSecretToAzureKeyVaultAsync方法演示了如何向Azure Key Vault添加一个新的密钥,而GetSecretFromAzureKeyVaultWithRetryAsync方法则演示了如何获取Azure Key Vault中存储的密钥。在每次调用之前,我们为必要的参数设置了适当的值。

        请注意,这个例子中的Fact属性表明这是一个测试方法,并且它被设置为跳过执行,因为它需要先行设置凭据。此外,与实际Azure环境进行交互之前,我们使用了一个HttpMessageHandlerStub来模拟HTTP请求和响应。在您的实际应用程序中,您将使用真实的HTTP客户端而不是存根。

Example24_OpenApiPlugin_Jira:Jira项目管理的集成

using System;
using System.Net.Http;
using System.Text.Json;
using System.Threading.Tasks;
using Xunit;
using KernelSyntaxExamples.Utils;
using SemanticKernel;
using SemanticKernel.Types;
using SemanticKernel.OpenApi;


public class Example24_OpenApiPlugin_Jira : BaseTest
{
    private static readonly JsonSerializerOptions s_jsonOptionsCache = new()
    {
        WriteIndented = true
    };


    /// <summary>
    /// 本示例展示了如何使用基于Open API schema的Open API插件将Semantic Kernel连接到Jira。
    /// 注册插件及其操作方式,以及执行这些操作的方法,适用于任何遵循Open API Schema的Open API插件。
    /// 要使用此示例,您需要满足以下先决条件:
    /// 1. 您应该能够使用您的电子邮件和API密钥访问Jira实例。
    ///    按照这里的指示获得API密钥:
    ///    https://support.atlassian.com/atlassian-account/docs/manage-api-tokens-for-your-atlassian-account/
    /// 2. 在您的Jira实例中创建一个新项目,并添加标题为'TEST-1'和'TEST-2'的两个问题。
    ///    按照这些说明创建一个新项目和问题:
    ///    https://support.atlassian.com/jira-software-cloud/docs/create-a-new-project/
    ///    https://support.atlassian.com/jira-software-cloud/docs/create-an-issue-and-a-sub-task/
    /// 3. 您可以在帐户管理页面的"产品"选项卡下找到您的域名。
    ///    要导航到您的帐户管理页面,请点击您的Jira实例右上角的个人资料图片,然后选择"管理帐户"。
    /// 4. 按照dotnet/samples/KernelSyntaxExamples文件夹中的ReadMe.md中的说明设置秘密。
    /// </summary>
    [Fact(Skip = "设置凭据")]
    public async Task RunAsync()
    {
        Kernel kernel = new();


        // 将<your-domain>替换为您的Jira实例域名,确保您拥有正确的认证凭据
        string serverUrl = $"https://{TestConfiguration.Jira.Domain}.atlassian.net/rest/api/latest/";


        KernelPlugin jiraFunctions;
        var tokenProvider = new BasicAuthenticationProvider(() =>
        {
            string credentials = $"{TestConfiguration.Jira.Email}:{TestConfiguration.Jira.ApiKey}";
            return Task.FromResult(credentials);
        });


        using HttpClient httpClient = new();


        // 使用 'useLocalFile' 变量可以在使用本地文件路径和URL来获取openapi schema作为数据来源之间切换
        bool useLocalFile = true;


        if (useLocalFile)
        {
            var apiPluginFile = "./../../../Plugins/JiraPlugin/openapi.json";
            jiraFunctions = await kernel.ImportPluginFromOpenApiAsync(
                "jiraPlugin",
                apiPluginFile,
                new OpenApiFunctionExecutionParameters(
                    authCallback: tokenProvider.AuthenticateRequestAsync,
                    serverUrlOverride: new Uri(serverUrl)
                )
            );
        }
        else
        {
            var apiPluginRawFileURL = new Uri("https://example.com/path/to/jira/openapi.json");
            jiraFunctions = await kernel.ImportPluginFromOpenApiAsync(
                "jiraPlugin",
                apiPluginRawFileURL,
                new OpenApiFunctionExecutionParameters(
                    httpClient, tokenProvider.AuthenticateRequestAsync,
                    serverUrlOverride: new Uri(serverUrl)
                )
            );
        }


        var arguments = new KernelArguments();


        // GetIssue 操作
        // 根据Jira openAPI schema设置 'Get Issue' 操作的参数
        // 确保Jira中存在 'TEST-1' 问题,以避免404错误
        arguments["issueKey"] = "TEST-1";


        // 通过Semantic Kernel调用操作
        var result = await kernel.InvokeAsync(jiraFunctions["GetIssue"], arguments);


        WriteLine("\n\n\n");
        var formattedContent = JsonSerializer.Serialize(result.GetValue<RestApiOperationResponse>(), s_jsonOptionsCache);
        WriteLine($"GetIssue jiraPlugin 响应: \n{formattedContent}");


        // AddComment 操作
        arguments["issueKey"] = "TEST-2";
        arguments[RestApiOperation.PayloadArgumentName] = "{\"body\": \"这是一条有见地的评论\"}";


        // 通过Semantic Kernel调用操作
        result = await kernel.InvokeAsync(jiraFunctions["AddComment"], arguments);


        WriteLine("\n\n\n");


        formattedContent = JsonSerializer.Serialize(result.GetValue<RestApiOperationResponse>(), s_jsonOptionsCache);
        WriteLine($"AddComment jiraPlugin 响应: \n{formattedContent}");
    }
}

        上面的代码片段是一个 RunAsync 方法,该方法说明了如何使用Open API插件机制将Semantic Kernel与Jira集成。展示了如何进行Jira认证,如何从本地文件或URL配置插件,以及如何执行操作,比如检索问题(GetIssue)和向问题添加评论(AddComment)。

Example27_PromptFunctionsUsingChatGPT

Example27_PromptFunctionsUsingChatGPT
[Fact]
public async Task ChatGPTPromptExampleAsync()
{
    // 打印标题以说明接下来进行的操作
    WriteLine("======== Using Chat GPT model for text generation ========");


    // 创建Semantic Kernel的构造器并添加ChatGPT模型,这需要Azure 服务的配置信息
    Kernel kernel = Kernel.CreateBuilder()
        .AddAzureOpenAIChatCompletion(
            deploymentName: TestConfiguration.AzureOpenAI.ChatDeploymentName,
            endpoint: TestConfiguration.AzureOpenAI.Endpoint,
            apiKey: TestConfiguration.AzureOpenAI.ApiKey,
            modelId: TestConfiguration.AzureOpenAI.ChatModelId)
        .Build();


    // 创建一个函数,这个函数会根据输入提示(prompt)使用ChatGPT模型进行调用
    var promptFunction = kernel.CreateFunctionFromPrompt(
        "List the two planets closest to '{{$input}}', excluding moons, using bullet points.");


    // 使用刚刚创建的函数,并提供输入"Jupiter"作为参数
    var result = await promptFunction.InvokeAsync(kernel, new() { ["input"] = "Jupiter" });


    // 获取返回的结果并以string格式输出
    WriteLine(result.GetValue<string>());


    /*
    输出结果应该如下所示,列出距离木星最近的两颗行星(不包括卫星):
        - Saturn
        - Uranus
    */
}

        在上面的代码中,我们首先以异步的方式定义了一个测试方法ChatGPTPromptExampleAsync。之后我们通过创建Semantic Kernel构造器并调用.AddAzureOpenAIChatCompletion()方法连接到了Azure的OpenAI服务。确保你有正确的部署名称、端点、API密钥和模型ID。

        我们使用kernel.CreateFunctionFromPrompt()创建了一个基于提示信息的函数,该函数能够利用我们配置的ChatGPT模型来处理输入,并生成文本。

最后,我们调用这个函数并传入了input作为参数。根据我们的prompt,函数将返回离木星最近的两颗行星。

        请注意在实际部署前你需要配置好TestConfiguration类以包含所有必要的Azure服务参数,这些示例中的参数(如TestConfiguration.AzureOpenAI.ChatDeploymentName)需要被替换为你实际的配置值。

Example55_TextChunker:文本切片

        在处理文本数据时,有时我们需要将文本分割成符合特定大小或结构的片段,例如按行或按段落划分。这个过程称为文本分块(Text Chunking)。本教程将引导您如何使用TextChunker类来执行基本的文本分块。

TextChunker基本使用

        首先,我们需要一个含有文本数据的示例,以便在此基础上展示TextChunker的使用。下面是Text常量的定义,包含了几段描述不同地点的文本。

private const string Text = @"The city of Venice, located in the northeastern part of Italy, is renowned for its unique geographical features. ...";

        在本示例中,我们从简单的将文本按行分割到定制化的分词计数器(Token Counter)的使用,逐步展现TextChunker的不同功能。

分割文本行:SplitPlainTextLines

[Fact]
public void RunExample()
{
    WriteLine("=== Text chunking ===");


    var lines = TextChunker.SplitPlainTextLines(Text, 40);
    var paragraphs = TextChunker.SplitPlainTextParagraphs(lines, 120);


    WriteParagraphsToConsole(paragraphs);
}

        在以上代码中,SplitPlainTextLines方法按照给定的最大行长度(这里是40个字符)将文本分割成多行;随后,使用SplitPlainTextParagraphs方法进一步根据给定的最大段落长度(这里是120个字符)对行进行聚合形成段落。这些段落通过WriteParagraphsToConsole方法输出到控制台。

使用定制的分词计数器:TokenCounter

[Theory]
[InlineData(TokenCounterType.SharpToken)]
[InlineData(TokenCounterType.MicrosoftML)]
// 其他分词计数器类型...
public void RunExampleForTokenCounterType(TokenCounterType counterType)
{
    WriteLine($"=== Text chunking with a custom({counterType}) token counter ===");
    var sw = new Stopwatch();
    sw.Start();
    var tokenCounter = s_tokenCounterFactory(counterType);


    var lines = TextChunker.SplitPlainTextLines(Text, 40, tokenCounter);
    var paragraphs = TextChunker.SplitPlainTextParagraphs(lines, 120, tokenCounter: tokenCounter);


    sw.Stop();
    WriteLine($"Elapsed time: {sw.ElapsedMilliseconds} ms");
    WriteParagraphsToConsole(paragraphs);
}

在这个示例中,使用InlineData属性传递不同类型的分词计数器给测试方法,这允许我们在相同的文本上测试TextChunker如何配合不同算法实现的分词计数器工作。每个计数器类型对应一个具体的Token Counter实现方法。

下面是几个分词计数器的示例实现:

SharpToken 分词计数器
private static TokenCounter SharpTokenTokenCounter => (string input) =>
{
    var encoding = GptEncoding.GetEncoding("cl100k_base");
    var tokens = encoding.Encode(input);


    return tokens.Count;
};
MicrosoftML 分词计数器
private static TokenCounter MicrosoftMLTokenCounter => (string input) =>
{
    Tokenizer tokenizer = new(new Bpe());
    var tokens = tokenizer.Encode(input).Tokens;


    return tokens.Count;
};
MicrosoftMLRobert 分词计数器
private static TokenCounter MicrosoftMLRobertaTokenCounter => (string input) =>
{
    // 加载资源,并初始化模型...


    EnglishRoberta model = new(encoder, vocab, dict);
    model.AddMaskSymbol();
    Tokenizer tokenizer = new(model, new RobertaPreTokenizer());
    var tokens = tokenizer.Encode(input).Tokens;


    return tokens.Count;
};

附加文本块头部

        有时,在生成的文本块前我们希望添加一些固定的头部信息,例如文件名或其他描述性文字。我们可以使用chunkHeader参数:

[Fact]
public void RunExampleWithHeader()
{
    WriteLine("=== Text chunking with chunk header ===");


    var lines = TextChunker.SplitPlainTextLines(Text, 40);
    var paragraphs = TextChunker.SplitPlainTextParagraphs(lines, 150, chunkHeader: "DOCUMENT NAME: test.txt\n\n");


    WriteParagraphsToConsole(paragraphs);
}

以上代码在分割后的段落开头添加了文件名信息。

Example56_TemplateMethodFunctionsWithMultipleArguments

        在这个示例中,我们将介绍如何在Semantic Kernel中使用模板方法函数(Template Method Functions)并传入多个参数。

[Fact]
public async Task TemplateMethodFunctionsWithMultipleArgumentsAsync()
{
    WriteLine("======== TemplateMethodFunctionsWithMultipleArguments 示例 ========");


    // 这里用测试配置替代实际Azure AI 配置
    string serviceId = "你的Azure服务ID";
    string apiKey = "你的Azure API密钥";
    string deploymentName = "你的部署名称";
    string modelId = "你的模型ID";
    string endpoint = "你的Azure终端地址";


    // 检查所有必要的配置是否存在
    if (apiKey == null || deploymentName == null || modelId == null || endpoint == null)
    {
        WriteLine("未找到AzureOpenAI modelId、endpoint、apiKey 或 deploymentName。跳过本示例。");
        return;
    }


    // 创建Kernel构建器
    IKernelBuilder builder = Kernel.CreateBuilder();
    builder.Services.AddLogging(c => c.AddConsole());
    
    // 添加AzureOpenAI聊天完成组件
    builder.AddAzureOpenAIChatCompletion(
        deploymentName: deploymentName,
        endpoint: endpoint,
        serviceId: serviceId,
        apiKey: apiKey,
        modelId: modelId);
    Kernel kernel = builder.Build();


    // 创建Kernel参数字典
    var arguments = new KernelArguments();
    arguments["word2"] = "波特";


    // 加载Native插件到Kernel函数集合中
    kernel.ImportPluginFromType<TextPlugin>("text");


    // 定义模板函数,这里使用text.Concat方法函数,有两个命名参数input和input2
    const string FunctionDefinition = @"
写出以下内容的俳句:{{text.Concat input='哈利' input2=$word2}}
";


    // 在发送到OpenAI之前,渲染并显示提示
    WriteLine("--- 渲染的提示");
    var promptTemplateFactory = new KernelPromptTemplateFactory();
    var promptTemplate = promptTemplateFactory.Create(new PromptTemplateConfig(FunctionDefinition));
    var renderedPrompt = await promptTemplate.RenderAsync(kernel, arguments);
    WriteLine(renderedPrompt);


    // 执行提示/提示函数
    var haikuPromptFunction = kernel.CreateFunctionFromPrompt(FunctionDefinition, new OpenAIPromptExecutionSettings() { MaxTokens = 100 });


    // 显示结果
    WriteLine("--- 提示函数的结果");
    var result = await kernel.InvokeAsync(haikuPromptFunction, arguments);
    WriteLine(result.GetValue<string>());


    /* 输出:


    --- 渲染的提示


    写出以下内容的俳句:哈利波特


    --- 提示函数的结果
    一个额头有疤的男孩,
    探索着巫师的世界,
    哈利波特的故事。
    */
}

        在这个教程中,我们解释了如何使用Kernel引擎来集成Azure OpenAI服务和执行带有自定义插件函数(在这个例子中是 TextPlugin 插件中的 Concat 方法)的模板函数。我们还展示了如何在执行提示之前渲染提示模板,并处理来自Azure OpenAI服务的响应结果。

Example57_KernelHooks : 内核钩子

        在本例中,将介绍如何在 Semantic Kernel 中使用内核钩子来监控和操作函数的调用。内核钩子允许在函数执行前后执行自定义逻辑,从而实现对函数调用过程的更细粒度的控制。

        我们将实现几种不同的内核钩子,分别用于获取使用数据、处理渲染提示、更改执行结果以及控制函数调用的取消。

public class Example57_KernelHooks : BaseTest
{
    // ...
    // 构造函数和私有字段定义略过,详见示例代码


    // 使用内核调用钩子监控使用情况
    [Fact]
    public async Task GetUsageAsync()
    {
        // 此处为初始化内核、定义钩子、执行函数的代码
        // ...
    }


    // 使用内核钩子处理提示渲染
    [Fact]
    public async Task GetRenderedPromptAsync()
    {
        // 此处为初始化内核、定义钩子、执行函数的代码
        // ...
    }


    // 使用内核调用钩子来后处理结果
    [Fact]
    public async Task ChangingResultAsync()
    {
        // 此处为初始化内核、定义钩子、执行函数的代码
        // ...
    }


    // 使用内核调用钩子取消执行前的操作
    [Fact]
    public async Task BeforeInvokeCancellationAsync()
    {
        // 此处为初始化内核、定义钩子、执行函数的代码
        // ...
    }


    // 使用内核调用钩子取消执行后的操作
    [Fact]
    public async Task AfterInvokeCancellationAsync()
    {
        // 此处为初始化内核、定义钩子、执行函数的代码
        // ...
    }
}

我将重点介绍GetUsageAsync函数的实现方式及其作用。


获取使用数据:GetUsageAsync 函数

我们首先创建一个内核实例并通过钩子监控函数的调用过程:

public async Task GetUsageAsync()
{
    WriteLine("\n======== 获取使用数据 ========\n");


    // 创建内核实例
    Kernel kernel = Kernel.CreateBuilder()
        // 添加OpenAI聊天完成模块,用于测试
        .AddOpenAIChatCompletion(
            modelId: _openAIModelId!,
            apiKey: _openAIApiKey!)
        .Build();


    // 初始化提示语
    const string FunctionPrompt = "写一个关于: {{$input}} 的随机段落。";


    // 创建函数,使用提示语
    var excuseFunction = kernel.CreateFunctionFromPrompt(
        FunctionPrompt,
        functionName: "Excuse",
        executionSettings: new OpenAIPromptExecutionSettings() { MaxTokens = 100, Temperature = 0.4, TopP = 1 });


    // 定义前置处理钩子
    void MyPreHandler(object? sender, FunctionInvokingEventArgs e)
    {
        WriteLine($"{e.Function.Name} : 预执行处理器 - 已触发");
    }


    // 定义后置处理钩子
    void MyPostExecutionHandler(object? sender, FunctionInvokedEventArgs e)
    {
        WriteLine($"{e.Function.Name} : 后执行处理器 - 使用情况: {e.Result.Metadata?["Usage"]?.AsJson()}");
    }


    // 注册钩子到内核
    kernel.FunctionInvoking += MyPreHandler;
    kernel.FunctionInvoked += MyPostExecutionHandler;


    // 调用函数,触发执行钩子
    const string Input = "我错过了F1决赛";
    var result = await kernel.InvokeAsync(excuseFunction, new() { ["input"] = Input });
    WriteLine($"函数结果: {result}");
}

        在上面的代码中,演示了如何使用FunctionInvokingFunctionInvoked钩子来在函数调用前后收集使用数据。通过注册这些钩子到内核,我们可以在函数执行的不同阶段插入自定义的逻辑。

Example65_HandlebarsPlanner : 规划

        HandlebarsPlanner是Semantic Kernel中一个强大的工具,利用逻辑模板(例如Handlebars模板)来创建和执行可执行计划。它让我们能够定义复杂的操作序列,以达成特定的目标。

// 异步函数RunSampleAsync的定义
private async Task RunSampleAsync(string goal, bool shouldPrintPrompt = false, params string[] pluginDirectoryNames)
{
    // 创建Kernel构建器,并添加AzureOpenAI聊天完成服务
    var kernel = Kernel.CreateBuilder()
        .AddAzureOpenAIChatCompletion(
            deploymentName: chatDeploymentName,
            endpoint: endpoint,
            serviceId: "AzureOpenAIChat",
            apiKey: apiKey,
            modelId: chatModelId)
        .Build();


    // 导入插件
    // ...(此处省略代码,见上文)


    // Handlebars计划器选项的创建
    var allowLoopsInPlan = chatDeploymentName.Contains("gpt-4", StringComparison.OrdinalIgnoreCase);
    var planner = new HandlebarsPlanner(
        new HandlebarsPlannerOptions()
        {
            AllowLoops = allowLoopsInPlan
        });


    WriteLine($"目标: {goal}");


    // 创建计划
    // ...(此处省略代码,见上文)


    // 执行计划并打印结果
    // ...(此处省略代码,见上文)
}


// 不能创建计划的情况
public async Task PlanNotPossibleSampleAsync(bool shouldPrintPrompt = false)
{
    // ...(此处省略代码,见上文)
}
  1. 使用Kernel.CreateBuilder()来构建一个包含Azure OpenAI聊天功能的kernel对象。

  2. 根据需要,选择性导入插件,以增强Kernel可用的功能。

  3. 实例化HandlebarsPlanner并利用它来创建一个计划plan,该计划包含了为实现特定目标而需要执行的步骤。

  4. 调用plan.InvokeAsync(kernel)执行计划,并获取结果。

实验示例解析

在这些示例中,我们尝试了创建各种计划,比如:

  • 尝试发送邮件,但没有足够的插件功能。

  • 使用Coursera OpenAPI插件显示有关人工智能的课程。

  • 通过本地字典插件获取随机单词及其定义。

您可以通过修改上述代码,向RunSampleAsync函数传递不同的目标和插件,以实现您想要的目标。

注意事项

在编写真实代码时:

  • 需要确保所有的服务ID、API密钥和模型ID都是正确的。

  • 对于环境变量和敏感数据应当采取安全措施。

  • 处理异常和错误是实际使用中不可忽视的部分。

Example68_GPTVision:视觉模型

        在这一节中,我们将通过使用Semantic Kernel来展示如何结合GPT-4V识别功能去理解和解释图像内容。我们将使用一个基础的单元测试方法来创建和运行这个示例。

const string ImageUri = "https://xxx.xxx.com/xzy.jpg";


  var kernel = Kernel.CreateBuilder()
      .AddOpenAIChatCompletion("gpt-4-vision-preview", TestConfiguration.OpenAI.ApiKey)
      .Build();


  var chatCompletionService = kernel.GetRequiredService<IChatCompletionService>();


  var chatHistory = new ChatHistory("You are a friendly assistant.");


  chatHistory.AddUserMessage(new ChatMessageContentItemCollection
  {
      new TextContent("What’s in this image?"),
      new ImageContent(new Uri(ImageUri))
  });


  var reply = await chatCompletionService.GetChatMessageContentAsync(chatHistory);


  WriteLine(reply.Content);

Example70_Agent:智能代理

在这个示例中,我们会演示如何实现一个简单的对话机器人和一个具备方法函数的工具代理。


简单聊天代理

现在让我们看一下如何实现一个简单的聊天代理。

public class Example70_Agent : BaseTest
{
    private const string OpenAIFunctionEnabledModel = "gpt-3.5-turbo-1106";


    [Fact]
    public Task RunSimpleChatAsync()
    {
        WriteLine("======== Run:SimpleChat ========");


        return ChatAsync(
            "Agents.ParrotAgent.yaml",
            plugin: null,
            arguments: new KernelArguments { { "count", 3 } },
            "Fortune favors the bold.",
            "I came, I saw, I conquered.",
            "Practice makes perfect.");
    }
}

        在上述代码中,我们定义了一个简单的聊天代理。通过执行ChatAsync方法,代理可以处理简单的对话。我们提供了三个示例语句,并设置了一个参数来处理三轮对话。


工具代理与方法函数

下一步,我们将演示如何使用带有方法函数的工具代理。

[Fact]
public Task RunWithMethodFunctionsAsync()
{
    WriteLine("======== Run:WithMethodFunctions ========");


    KernelPlugin plugin = KernelPluginFactory.CreateFromType<MenuPlugin>();


    return ChatAsync(
        "Agents.ToolAgent.yaml",
        plugin,
        arguments: null,
        "Hello",
        "What is the special soup?",
        "What is the special drink?",
        "Thank you!");
}

        在这个例子中,我们通过给ChatAsync方法提供一个MenuPlugin插件,创建一个可以回答特定问题的代理,比如询问特别的汤或饮料。


工具代理与提示函数

下面我们介绍如何结合工具代理和提示函数来运作。

[Fact]
public Task RunWithPromptFunctionsAsync()
{
    WriteLine("======== WithPromptFunctions ========");


    var function = KernelFunctionFactory.CreateFromPrompt(
         "Correct any misspelling or gramatical errors provided in input: {{$input}}",
         functionName: "spellChecker",
         description: "Correct the spelling for the user input.");


    var plugin = KernelPluginFactory.CreateFromFunctions("spelling", "Spelling functions", new[] { function });


    return ChatAsync(
        "Agents.ToolAgent.yaml",
        plugin,
        arguments: null,
        "Hello",
        "Is this spelled correctly: exercize",
        "What is the special soup?",
        "Thank you!");
}

        通过定义一个拼写检查的提示函数,我们可以创建一个在输入中纠正拼写和语法错误的代理。这个代理可以集成到聊天循环中以处理用户输入和相应的拼写校正。


将代理作为方法函数调用

我们还可以像调用任何其他的KernelFunction一样调用代理。

[Fact]
public async Task RunAsFunctionAsync()
{
    WriteLine("======== Run:AsFunction ========");


    var agent =
        await new AgentBuilder()
            .WithOpenAIChatCompletion(OpenAIFunctionEnabledModel, TestConfiguration.OpenAI.ApiKey)
            .FromTemplate(EmbeddedResource.Read("Agents.ParrotAgent.yaml"))
            .BuildAsync();


    try
    {
        var response = await agent.AsPlugin().InvokeAsync("Practice makes perfect.", new KernelArguments { { "count", 2 } });


        WriteLine(response ?? $"No response from agent: {agent.Id}");
    }
    finally
    {
        await agent.DeleteAsync();
    }
}

        在这个例子中,我们创建了一个代理,并以插件的形势调用它。通过传递参数和输入,我们可以接收代理的响应并显示结果。

聊天循环共用方法

        以上示例中的ChatAsync方法是一个公共方法,它可用于处理简单聊天、方法函数聊天以及提示函数聊天循环。它的作用是从资源路径读取代理定义,并用指定的插件初始化代理。然后,它会创建一个聊天线程,并处理提供的消息。代码如下:

private async Task ChatAsync(
    string resourcePath,
    KernelPlugin? plugin = null,
    KernelArguments? arguments = null,
    params string[] messages)
{
    // ...
    // (聊天循环的实现代码)
    // ...
}

通过这种方式,代码复用性得到了增强,并且可以轻松切换代理和插件来处理不同的用户输入和场景。

Example71_AgentDelegation

        我们经常需要构建复杂的对象,它们能够响应外部的请求并作出适当的回应。Semantic Kernel 提供了一种强大的代理(Agent)创建和管理机制,下面的教程将指导你如何设置和使用代理来处理多任务场景。

步骤1:配置代理模型

        首先,我们需要一个支持代理和函数调用的特定模型。在本示例中,我们将会使用 OpenAI 提供的 GPT-3.5 模型。

/// <summary>
/// 此模型要求支持代理和函数调用的特定模型。
/// 目前,这限制于OpenAI托管的服务。
/// </summary>
private const string OpenAIFunctionEnabledModel = "gpt-3.5-turbo-1106";

步骤2:追踪代理

        我们使用一个列表来追踪所有的代理实例,以便在适当的时候进行清理。

// 追踪代理以进行清理
private static readonly List<IAgent> s_agents = new();

步骤3:展示如何协同多个代理

下一步是展示如何结合协调多个代理。

[Fact]
public async Task RunAsync()
{
    WriteLine("======== Example71_AgentDelegation ========");
    
    // 如果没有OpenAI的ApiKey,就跳过此示例
    if (TestConfiguration.OpenAI.ApiKey == null)
    {
        WriteLine("未找到 OpenAI apiKey。跳过示例。");
        return;
    }


    IAgentThread? thread = null;
    try
    {
        // 创建菜单代理
        var menuAgent = Track(await new AgentBuilder()
            .WithOpenAIChatCompletion(OpenAIFunctionEnabledModel, TestConfiguration.OpenAI.ApiKey)
            .FromTemplate(EmbeddedResource.Read("Agents.ToolAgent.yaml"))
            .WithDescription("回答关于菜单如何使用工具的问题。")
            .WithPlugin(plugin)
            .BuildAsync());
        
        // 创建鹦鹉代理
        var parrotAgent = Track(await new AgentBuilder()
            .WithOpenAIChatCompletion(OpenAIFunctionEnabledModel, TestConfiguration.OpenAI.ApiKey)
            .FromTemplate(EmbeddedResource.Read("Agents.ParrotAgent.yaml"))
            .BuildAsync());


        // 创建工具代理,并将之前创建的菜单代理和鹦鹉代理作为插件加入
        var toolAgent = Track(await new AgentBuilder()
            .WithOpenAIChatCompletion(OpenAIFunctionEnabledModel, TestConfiguration.OpenAI.ApiKey)
            .FromTemplate(EmbeddedResource.Read("Agents.ToolAgent.yaml"))
            .WithPlugin(parrotAgent.AsPlugin())
            .WithPlugin(menuAgent.AsPlugin())
            .BuildAsync());
        
        // 设定一组消息
        var messages = new string[]
        {
            "菜单上有什么?",
            "你能像海盗一样说话吗?",
            "谢谢你",
        };


        // 创建一个新线程,并通过工具代理解析消息
        thread = await toolAgent.NewThreadAsync();
        foreach (var response in messages.Select(m => thread.InvokeAsync(toolAgent, m)))
        {
            await foreach (var message in response)
            {
                WriteLine($"[{message.Id}]");
                WriteLine($"# {message.Role}: {message.Content}");
            }
        }
    }
    finally
    {
        // 清理(以节省存储费用)
        await Task.WhenAll(
            thread?.DeleteAsync() ?? Task.CompletedTask,
            Task.WhenAll(s_agents.Select(a => a.DeleteAsync())));
    }
}

        Track 函数用来追踪代理,使它们加入到之前定义的 s_agents 列表中。

private static IAgent Track(IAgent agent)
{
    s_agents.Add(agent);
    return agent;
}

        本示例主要展示了如何在 Semantic Kernel 框架中协同多个代理。我们创建了三个不同的代理实例,分别表现为工具代理、鹦鹉代理和菜单代理,每个代理都能够处理一组预定义的消息。这些代理通过异步机制协同工作,通过消息交互来完成对话任务,最后我们还学会了如何正确清理这些代理,以避免不必要的开支。

Example74_FlowOrchestrator:流程规划器

流程定义

首先,我们从一个流程定义开始。以下是使用YAML格式定义的流程:

name: FlowOrchestrator_Example_Flow
goal: answer question and send email
steps:
  - goal: What is the tallest mountain in Asia? How tall is it divided by 2?
    plugins:
      - WebSearchEnginePlugin
      - LanguageCalculatorPlugin
    provides:
      - answer
  - goal: Collect email address
    plugins:
      - ChatPlugin
    completionType: AtLeastOnce
    transitionMessage: do you want to send it to another email address?
    provides:
      - email_addresses
  - goal: Send email
    plugins:
      - EmailPluginV2
    requires:
      - email_addresses
      - answer
    provides:
      - email

在上述流程中,我们分为三个步骤:获取问题答案、收集电子邮件地址和发送邮件。


流程执行代码

以下是流程执行的C#代码示例:

[Fact(Skip = "Can take more than 1 minute")]
public Task RunAsync()
{
    return RunExampleAsync();
}


private async Task RunExampleAsync()
{
    // 初始化插件和连接器
    var bingConnector = new BingConnector(TestConfiguration.Bing.ApiKey);
    var webSearchEnginePlugin = new WebSearchEnginePlugin(bingConnector);
    Dictionary<object, string?> plugins = new()
    {
        { webSearchEnginePlugin, "WebSearch" },
        { new TimePlugin(), "Time" }
    };


    // 创建流程协调器
    FlowOrchestrator orchestrator = new(
        GetKernelBuilder(LoggerFactory),
        await FlowStatusProvider.ConnectAsync(new VolatileMemoryStore()).ConfigureAwait(false),
        plugins,
        config: GetOrchestratorConfig());
    var sessionId = Guid.NewGuid().ToString();


    // 开始执行流程
    WriteLine("*****************************************************");
    WriteLine("Executing " + nameof(RunExampleAsync));
    Stopwatch sw = new();
    sw.Start();
    WriteLine("Flow: " + s_flow.Name);
    var question = s_flow.Steps.First().Goal;
    var result = await orchestrator.ExecuteFlowAsync(s_flow, sessionId, question).ConfigureAwait(false);


    // 输出执行结果
    // ...


    // 用户交互模拟
    // ...


    WriteLine("Time Taken: " + sw.Elapsed);
    WriteLine("*****************************************************");
}


private static FlowOrchestratorConfig GetOrchestratorConfig()
{
    // 配置FlowOrchestrator
    var config = new FlowOrchestratorConfig
    {
        MaxStepIterations = 20
    };


    return config;
}


private static IKernelBuilder GetKernelBuilder(ILoggerFactory loggerFactory)
{
    // 设置KernelBuilder,添加相关服务和插件
    var builder = Kernel.CreateBuilder();
    builder.Services.AddSingleton<ILoggerFactory>(loggerFactory);


    // ...
    return builder;
}

        在上面的代码中,我们首先解析了定义好的流程。然后初始化相关插件,并创建一个流程协调器来执行和管理这些步骤。我们模拟了用户输入,并根据流程的定义和当前状态来决定如何进行下一步。最终,我们输出整个流程执行所需的时间。

Example76_Filters

Filter概述

Semantic Kernel提供了IFunctionFilterIPromptFilter两种接口,用于创建函数与提示过滤器。

  • IFunctionFilter允许您定义在函数调用前后执行的逻辑。

  • IPromptFilter则允许您在渲染提示给AI前后执行的逻辑。

使用方法

首先,让我们创建一个简单的函数,并向其中添加过滤器。

public sealed class Example76_Filters : SemanticKernelTestBase
{
    public Example76_Filters(ITestOutputHelper output) : base(output)
    {
    }


    [Fact]
    public async Task FunctionAndPromptFiltersAsync()
    {
        var builder = Kernel.CreateBuilder();


        builder.AddAzureOpenAIChatCompletion(
            deploymentName: TestConfiguration.AzureOpenAI.ChatDeploymentName,
            endpoint: TestConfiguration.AzureOpenAI.Endpoint,
            apiKey: TestConfiguration.AzureOpenAI.ApiKey);


        builder.Services.AddSingleton<ITestOutputHelper>(this.Output);


        // 使用依赖注入(DI)添加过滤器
        builder.Services.AddSingleton<IFunctionFilter, FirstFunctionFilter>();
        builder.Services.AddSingleton<IFunctionFilter, SecondFunctionFilter>();


        var kernel = builder.Build();


        // 不使用DI添加过滤器
        kernel.PromptFilters.Add(new FirstPromptFilter(this.Output));


        var function = kernel.CreateFunctionFromPrompt("What is Seattle", functionName: "MyFunction");
        var result = await kernel.InvokeAsync(function);


        WriteLine(result);
    }


    // ... 过滤器类定义 ...
}

        在上述代码中,我们通过CreateBuilder创建了Kernel的构建器,并向构建器添加了Azure OpenAI Chat完成服务和输出助手。然后,注册了两个IFunctionFilter并构建了Kernel对象。通过PromptFilters.Add方法添加了FirstPromptFilter示例。

Filters实现范例

接下来,我们将看看IFunctionFilterIPromptFilter过滤器的具体实现。

// 函数过滤器
private sealed class FirstFunctionFilter : IFunctionFilter
{
    private readonly ITestOutputHelper _output;


    public FirstFunctionFilter(ITestOutputHelper output)
    {
        this._output = output;
    }


    public void OnFunctionInvoking(FunctionInvokingContext context) =>
        this._output.WriteLine($"{nameof(FirstFunctionFilter)}.{nameof(OnFunctionInvoking)} - {context.Function.Name}");


    public void OnFunctionInvoked(FunctionInvokedContext context) =>
        this._output.WriteLine($"{nameof(FirstFunctionFilter)}.{nameof(OnFunctionInvoked)} - {context.Function.Name}");
}


// 提示过滤器
private sealed class FirstPromptFilter : IPromptFilter
{
    private readonly ITestOutputHelper _output;


    public FirstPromptFilter(ITestOutputHelper output)
    {
        this._output = output;
    }


    public void OnPromptRendering(PromptRenderingContext context) =>
        this._output.WriteLine($"{nameof(FirstPromptFilter)}.{nameof(OnPromptRendering)} - {context.Function.Name}");


    public void OnPromptRendered(PromptRenderedContext context) =>
        this._output.WriteLine($"{nameof(FirstPromptFilter)}.{nameof(OnPromptRendered)} - {context.Function.Name}");
}

        在FirstFunctionFilter类中,OnFunctionInvoking方法会在函数调用之前执行,而OnFunctionInvoked方法则在函数调用后执行。FirstPromptFilter类的OnPromptRendering方法则会在提示被渲染前执行,OnPromptRendered方法在提示被渲染后执行。

        每个方法都接收一个context对象,该对象包含当前函数的信息或提示的渲染状态。

Filter功能

过滤器允许你根据需要改变函数的行为或提示的内容。以下是一些高级用法的例子:

// 函数过滤器能力演示
private sealed class FunctionFilterExample : IFunctionFilter
{
    public void OnFunctionInvoked(FunctionInvokedContext context)
    {
        // 获取函数结果的值
        var value = context.Result.GetValue<object>();


        // 修改函数结果的值
        context.SetResultValue("new result value");


        // 从元数据中获取令牌使用情况
        var usage = context.Result.Metadata?["Usage"];
    }


    public void OnFunctionInvoking(FunctionInvokingContext context)
    {
        // 修改内核参数
        context.Arguments["input"] = "new input";


        // 取消函数执行
        context.Cancel = true;
    }
}


// 提示过滤器能力演示
private sealed class PromptFilterExample : IPromptFilter
{
    public void OnPromptRendered(PromptRenderedContext context)
    {
        // 在发给AI前修改已渲染的提示
        context.RenderedPrompt = "Safe prompt";
    }


    public void OnPromptRendering(PromptRenderingContext context)
    {
        // 获取函数信息
        var functionName = context.Function.Name;
    }
}

        在FunctionFilterExample类中,OnFunctionInvoked方法旨在处理函数执行结束后的后续动作,而OnFunctionInvoking方法则是在函数执行前进行的预处理。对于PromptFilterExample,则展示了如何在提示发送给AI前后进行修改。

        通过运用这些过滤器,开发者可以实现更灵活、更可控的内核函数调用,并且在与AI交互时能够确保提示的安全性或遵循特定的标准。

Example77_StronglyTypedFunctionResult:强类型函数结果

[Fact]
    public async Task RunAsync()
    {
        this.WriteLine("======== 强类型函数结果扩展 ========");
        
        // 创建并配置一个Kernel实例
        Kernel kernel = Kernel.CreateBuilder()
            .AddOpenAIChatCompletion(
                modelId: TestConfiguration.OpenAI.ChatModelId,
                apiKey: TestConfiguration.OpenAI.ApiKey)
            .Build();


        // 设置要生成的提示数据
        var promptTestDataGeneration = "返回一个包含3个JSON对象的数组的JSON,每个对象包含以下字段:" +
            "一个带有随机GUID的id字段,一个带有随机公司名的name字段,以及一个带有随机简短公司描述的description字段。" +
            "确保JSON是有效的,并且包含一个名为testcompanies的JSON数组,包含这三个字段。";


        // 计时开始
        var sw = new Stopwatch();
        sw.Start();


        // 调用Kernel的InvokePromptAsync函数执行生成操作
        FunctionResult functionResult = await kernel.InvokePromptAsync(promptTestDataGeneration);


        // 计时结束
        sw.Stop();


        // 将返回结果包装在自定义的强类型结果对象中
        var functionResultTestDataGen = new FunctionResultTestDataGen(functionResult!, sw.ElapsedMilliseconds);


        // 打印结果和相关的信息
        this.WriteLine($"测试数据: {functionResultTestDataGen.Result} \n");
        this.WriteLine($"耗时毫秒数: {functionResultTestDataGen.ExecutionTimeInMilliseconds} \n");
        this.WriteLine($"总令牌数: {functionResultTestDataGen.TokenCounts!.TotalTokens} \n");
    }

        经过详尽的解释和深入的讨论,我们终于来到了这篇关于Semantic Kernel的入门教程的尾声。我投入了大量的精力和热情,努力将这篇文章打造成目前最全面、最易于理解的中文学习资料。无论您是初次接触Semantic Kernel,还是希望巩固基础知识,这篇万字长文都将是您的不二之选。

        我深知筛选出真正有价值的学习资源对新手来说是多么重要且困难。正因如此,这篇文章凝聚了我对于Semantic Kernel的深厚理解,以及将复杂概念浅显化的努力。我希望每一位读者都能从中获得启发,并以此作为跃入这个领域的坚实基石。

        如果您觉得这篇教程有帮助,不妨关注我的账号以获取更多更新,同时也别忘了将这篇文章收藏,以便日后复习或查询。我将持续分享更多有关Semantic Kernel以及其他相关知识领域的内容,帮助您在人工智能和语义分析的道路上不断前行。

  • 2
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值