本章介绍了在Semantic Kernel中使用Dependency Injection(依赖注入)的方法。在之前的章节中,我们手动创建Kernel对象来完成框架的初始化工作。现在,我们将使用依赖注入的方式来实现这一过程。
首先,我们将使用官网的LightPlugins插件来演示依赖注入在SK中的使用。
public class LightPlugin
{
public bool IsOn { get; set; } = false;
#pragma warning disable CA1024 // Use properties where appropriate
[KernelFunction]
[Description("获取灯的状态。")]
public string GetState() => IsOn ? "开启" : "关闭";
#pragma warning restore CA1024 // Use properties where appropriate
[KernelFunction]
[Description("改变灯的状态。")]
public string ChangeState(bool newState)
{
this.IsOn = newState;
var state = GetState();
// 打印状态到控制台
Console.WriteLine($"[灯现在{state}]");
return state;
}
}
该插件包含两个方法,一个是获取当前灯的状态,另一个是改变灯的状态。
在之前的演示中,我们使用Kernel对象提供的CreateBuilder方法来创建Kernel对象。
var kernel = Kernel.CreateBuilder().
AddOpenAIChatCompletion(modelId: config.ModelId, apiKey: config.ApiKey)
.Build();
在API项目的开发中,依靠依赖注入的方式更容易管理依赖项和对象的复用。
有两种方式可以使用依赖注入创建Kernel对象。第一种是使用KernelServiceCollectionExtensions类提供的AddKernel扩展方法,第二种是手动创建Kernel对象或使用services.AddTransient<Kernel>()方法。
AddKernel源码
public static IKernelBuilder AddKernel(this IServiceCollection services)
{
Verify.NotNull(services);
// 注册一个KernelPluginCollection,用于存储直接在DI中注册的任何IKernelPlugins。它是瞬态的,因为Kernel将直接存储该集合,我们不希望两个Kernel实例持有相同的可变集合。
services.AddTransient<KernelPluginCollection>();
// 注册Kernel为瞬态。它是可变的,预计将被使用者改变,例如通过添加事件处理程序、添加插件、在其Data集合中存储状态等。
services.AddTransient<Kernel>();
// 创建并返回一个可用于向IServiceCollection添加服务和插件的构建器。
return new KernelBuilder(services);
}
通过源码可以看出,这两种方式基本上没有区别。第二种AddKernel实际上是简化了第二种方式的步骤。我们将以第一种方式举例演示。
// 依赖注入
{
IServiceCollection services = new ServiceCollection();
// 会话服务注册到IOC容器
services.AddKernel().AddOpenAIChatCompletion(modelId: config.ModelId, apiKey: config.ApiKey, httpClient: client);
services.AddSingleton<KernelPlugin>(sp => KernelPluginFactory.CreateFromType<LightPlugin>(serviceProvider: sp));
var kernel = services.BuildServiceProvider().GetRequiredService<Kernel>();
}
这就是在依赖注入中注册Kernel对象和插件的步骤,依赖项都会被注册到IServiceCollection中。
Semantic Kernel使用的服务和插件通常作为Singleton单例注册到依赖注入容器中,以便它们可以在各种Kernel之间重用/共享。Kernel通常注册为Transient瞬态,以便每个实例不受处理其他任务的Kernel所做更改的影响。
在项目中使用时,我们可以通过在构造函数中获取Kernel对象的实例,使用Kernel对象来获取服务实例。
var chatCompletionService = kernel.GetRequiredService<IChatCompletionService>();
IChatCompletionService实例也可以通过IServiceProvider来获取,您可以灵活地使用更适合您要求的方法。
我们使用依赖注入来运行LightPlugin插件。
// 创建聊天记录
var history = new ChatHistory();
// 获取聊天完成服务
var chatCompletionService = kernel.GetRequiredService<IChatCompletionService>();
// 开始对话
Console.Write("用户 > ");
string? userInput;
while ((userInput = Console.ReadLine()) is not null)
{
// 添加用户输入
history.AddUserMessage(userInput);
// 启用自动函数调用
OpenAIPromptExecutionSettings openAIPromptExecutionSettings = new()
{
ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions
};
// 从AI获取响应
var result = await chatCompletionService.GetChatMessageContentAsync(
history,
executionSettings: openAIPromptExecutionSettings,
kernel: kernel);
// 打印结果
Console.WriteLine("助手 > " + result);
// 将代理的消息添加到聊天记录中
history.AddMessage(result.Role, result.Content ?? string.Empty);
// 再次获取用户输入
Console.Write("用户 > ");
}
输出:
用户 > 当前灯光的状态
助手 > 当前灯光的状态是关闭的。
用户 > 帮我开个灯
[灯现在开启]
助手 > 已经成功为您点亮了灯。
本文使用的大模型是月之暗面的moonshot-v1-8k。
"Endpoint": "https://api.moonshot.cn",
"ModelId": "moonshot-v1-8k",
原则上,任何支持OpenAI函数调用格式的模型都可以使用。
通过本章的学习,我们深入了解了在Semantic Kernel中利用依赖注入的方式来管理Kernel对象和插件,使得项目开发更加灵活和高效。
参考文献
Using Semantic Kernel with Dependency Injection
示例代码
本文源代码