Semantic Kernel 内核服务注册机制深度解析:从 ADR-0012 决策到现代 DI 实践
【免费下载链接】semantic-kernelIntegrate cutting-edge LLM technology quickly and easily into your apps项目地址: https://gitcode.com/GitHub_Trending/se/semantic-kernel
导读
本文以 docs/decisions/0012-kernel-service-registration.md 这份架构决策记录(ADR)为骨架,剖析 Semantic Kernel(.NET 版)中"内核服务注册"这一核心机制:它回答了"Plugin 的依赖如何解析、如何把服务注入 Kernel"这一根本问题,并最终沉淀为今天基于Microsoft.Extensions.DependencyInjection的整套 DI 实践。读完本文,你将掌握 Kernel 与 IServiceProvider 的关系、Kernel.CreateBuilder()与AddKernel的底层原理、以及在现代 Semantic Kernel 中注册 AI 服务与插件依赖的标准姿势。
一、问题背景:插件需要依赖注入
ADR-0012 记录于 2023 年 10 月,当时的 Semantic Kernel 面临一个非常现实的问题:插件(Plugin)可以有复杂依赖。以仓库中至今仍存在的TextMemoryPlugin为例(见 dotnet/src/Plugins/Plugins.Memory/TextMemoryPlugin.cs),它的构造函数依赖ISemanticTextMemory接口:
public TextMemoryPlugin(ISemanticTextMemory memory) { this._memory = memory; }在 ADR 撰写时,ISemanticTextMemory是IKernel接口的一个属性,因此只能靠"手动接线"注入:
kernel.ImportFunctions(new TextMemoryPlugin(kernel.Memory));这带来两个局限:
- 只支持 Memory 相关接口,无法覆盖任意服务类型(
ISemanticTextMemory、IPromptTemplateEngine、IDelegatingHandlerFactory或任何其他服务); - 插件初始化与依赖解析完全依赖调用方手工完成,复杂场景下会失控。
佐证:插件的依赖并未消失
需要强调的是,插件依赖接口的问题在今天依然存在。在仓库源码 TextMemoryPlugin.cs 中,TextMemoryPlugin依旧通过构造函数接收ISemanticTextMemory。这意味着"如何把服务提供给 Kernel 和插件"这一机制,始终是 Semantic Kernel 使用的核心前提。
二、候选方案:五种解决路径的权衡
ADR-0012 系统梳理了四条候选路线,每一条都代表了 DI 设计哲学的一次取舍。
Solution #1.1:完全手动解析(默认可用)
用户负责所有插件初始化与依赖解析,这是原生 .NET 的直白做法:
var memoryStore = new VolatileMemoryStore(); var embeddingGeneration = new OpenAITextEmbeddingGeneration(modelId, apiKey); var semanticTextMemory = new SemanticTextMemory(memoryStore, embeddingGeneration); var memoryPlugin = new TextMemoryPlugin(semanticTextMemory); var kernel = Kernel.Builder.Build(); kernel.ImportFunctions(memoryPlugin);ADR 明确指出:这种方式应当始终默认可用,任何改进依赖解析的方案都应建立在其之上。
Solution #1.2:宿主应用自行构建 DI 容器(默认可用)
用户在自己的ServiceCollection中完成所有注册,再手动取服务创建插件:
var serviceCollection = new ServiceCollection(); serviceCollection.AddTransient<IMemoryStore, VolatileMemoryStore>(); serviceCollection.AddTransient<ITextEmbeddingGeneration>( (serviceProvider) => new OpenAITextEmbeddingGeneration(modelId, apiKey)); serviceCollection.AddTransient<ISemanticTextMemory, SemanticTextMemory>(); var services = serviceCollection.BuildServiceProvider(); // 理论上 TextMemoryPlugin 也可以注册到 DI 容器中 var memoryPlugin = new TextMemoryPlugin(services.GetService<ISemanticTextMemory>()); var kernel = Kernel.Builder.Build(); kernel.ImportFunctions(memoryPlugin);ADR 认为该方式与 #1.1 一样应开箱即用——依赖解析永远可以在应用侧完成,只需把成品插件交给 Kernel。
Solution #2.1:Kernel 自建轻量服务容器
在 Kernel 内部实现一套自定义的IKernelServiceProvider,只保留按类型(可加 name)取服务的最小能力:
public interface IKernelServiceProvider { T? GetService<T>(string? name = null); } public interface IKernel { IKernelServiceProvider Services { get; } }使用形态:
var kernel = Kernel.Builder .WithLoggerFactory(ConsoleLogger.LoggerFactory) .WithOpenAITextEmbeddingGenerationService(modelId, apiKey) .WithService<IMemoryStore, VolatileMemoryStore>(), .WithService<ISemanticTextMemory, SemanticTextMemory>() .Build(); var semanticTextMemory = kernel.Services.GetService<ISemanticTextMemory>(); var memoryPlugin = new TextMemoryPlugin(semanticTextMemory); kernel.ImportFunctions(memoryPlugin);ADR 列出其Pros:
- 不依赖任何特定 DI 容器库;
- 实现轻量;
- 可以只注册插件用得到的服务(与宿主应用隔离);
- 支持按名称多次注册同一接口。
Cons:
- 需要自行实现和维护 DI 容器,而不是复用成熟库;
- 导入插件时仍需手动初始化以注入服务。
Solution #2.2:按类型导入插件,Kernel 负责实例化
这是对 #2.1 缺点的补强:除了按对象实例导入插件,再支持按类型导入,由 Kernel 负责实例化并注入依赖:
// 替代写法: // var semanticTextMemory = kernel.Services.GetService<ISemanticTextMemory>(); // var memoryPlugin = new TextMemoryPlugin(semanticTextMemory); // kernel.ImportFunctions(memoryPlugin); kernel.ImportFunctions<TextMemoryPlugin>();这一思路影响深远——现代 Semantic Kernel 中的KernelPluginFactory.CreateFromType<T>(serviceProvider: sp)正是这一理念的落地形态。
Solution #3:直接复用 Microsoft.Extensions.DependencyInjection
放弃自定义容器,让 Kernel 直接对接宿主应用的IServiceProvider:
var serviceCollection = new ServiceCollection(); serviceCollection.AddTransient<IMemoryStore, VolatileMemoryStore>(); serviceCollection.AddTransient<ITextEmbeddingGeneration>( (serviceProvider) => new OpenAITextEmbeddingGeneration(modelId, apiKey)); serviceCollection.AddTransient<ISemanticTextMemory, SemanticTextMemory>(); var services = serviceCollection.BuildServiceProvider(); var kernel = Kernel.Builder .WithLoggerFactory(ConsoleLogger.LoggerFactory) .WithOpenAITextEmbeddingGenerationService(modelId, apiKey) .WithServices(services) // 把宿主应用注册的所有服务传给 Kernel .Build(); // 插件导入 - 方式一:手动取服务 var semanticTextMemory = kernel.Services.GetService<ISemanticTextMemory>(); var memoryPlugin = new TextMemoryPlugin(semanticTextMemory); kernel.ImportFunctions(memoryPlugin); // 插件导入 - 方式二:按类型导入 kernel.ImportFunctions<TextMemoryPlugin>();Pros:
- 无需自研依赖解析,直接复用成熟 .NET 库;
- 已存在的应用可以一次性注入全部服务作为插件依赖。
Cons:
- 为 Semantic Kernel 包引入额外依赖
Microsoft.Extensions.DependencyInjection; - 无法限定服务白名单(缺少与宿主应用的隔离);
- 存在版本不匹配风险(如用户用 2.0 版本而 SK 用 6.0 版本导致运行时错误)。
三、决策结果:保持 Kernel 单一职责
ADR 的最终结论是:
现阶段仅支持 Solution #1.1 与 Solution #1.2,保持 Kernel 作为单一职责单元。插件依赖应在把插件实例交给 Kernel 之前解析完成。
即:Kernel 不做依赖解析的"全能裁判",插件依赖在应用侧解决后再进入 Kernel。这一决策把复杂性挡在了 Kernel 之外,是"Kernel 即工作单元"架构哲学的体现。
四、源码印证:ADR 决策如何演化为今天的实现
决策文档记录的是 2023 年的阶段结论,而仓库源码忠实反映了这条决策线如何在后续演化中吸收各方案优点。以下证据全部来自当前仓库。
4.1 Kernel 直接持有 IServiceProvider
现代 Kernel.cs 的构造函数直接接收IServiceProvider? services:
public Kernel( IServiceProvider? services = null, KernelPluginCollection? plugins = null) { this.Services = services ?? EmptyServiceProvider.Instance; this._plugins = plugins ?? this.Services.GetService<KernelPluginCollection>(); // ... }同时暴露Services属性(Kernel.cs)。这本质上是 Solution #3(复用Microsoft.Extensions.DependencyInjection)与 #2.1(kernel.Services取服务)的合流——Kernel 不再自研容器,而是直接拥抱 .NET 标准 DI,但依然保留了"通过kernel.Services取服务"的 API 形态。
4.2 KernelBuilder:IServiceCollection 的薄封装
KernelBuilder.cs 内部持有一个懒初始化的IServiceCollection,IKernelBuilder.cs 则把Services和Plugins暴露为公共入口:
public sealed class KernelBuilder : IKernelBuilder, IKernelBuilderPlugins { public IServiceCollection Services => this._services ??= new ServiceCollection(); public IKernelBuilderPlugins Plugins => this; }而Kernel.CreateBuilder()只是new KernelBuilder()的静态工厂(Kernel.cs)。
4.3 AddKernel:一键接入宿主 DI 容器
KernelServiceCollectionExtensions.cs 提供了AddKernel扩展方法,把 Kernel 本身变成 DI 容器的一等公民:
public static IKernelBuilder AddKernel(this IServiceCollection services) { services.AddTransient<KernelPluginCollection>(); services.AddTransient<Kernel>(); return new KernelBuilder(services); }要点:
KernelPluginCollection注册为transient:Kernel 会直接持有该集合,避免两个 Kernel 实例共享同一个可变集合;Kernel注册为transient:Kernel 本身是可变的(事件、插件、Data 字典),每次解析得到独立实例;- 返回的
IKernelBuilder与同一个IServiceCollection绑定,可以继续链式注册。
这意味着 Solution #3 当年担心的"额外依赖"已不再是问题——Microsoft.Extensions.DependencyInjection如今是 Semantic Kernel 的核心依赖,而 ADR 中"宿主应用可注入全部服务"的设想已成为现实。
4.4 命名服务与键控服务:Solution #2.1 的遗产
ADR 中 #2.1 曾主张"支持按名称注册同一接口",这在现代实现中以键控服务(keyed service)的形式保留。Kernel.GetRequiredService<T>(object? serviceKey)(Kernel.cs)支持传入serviceKey查询:
- 有 key 时走
IKeyedServiceProvider.GetKeyedService<T>(serviceKey); - 无 key 时先尝试非键控查询,失败则回退到键控注册的最后一个;
- 都找不到时抛出带明确信息的
KernelException。
Kernel 内部还通过KernelServiceTypeToKeyMappings常量(Kernel.cs)把 AI 服务的类型信息映射到 service provider,这正是"同一接口多次注册、按名称取用"的现代版本。
4.5 按类型创建插件:Solution #2.2 的落地
现代 API 提供了KernelPluginFactory.CreateFromType<T>(serviceProvider: sp),由 DI 容器完成插件实例化与依赖注入,对应 ADR 中kernel.ImportFunctions<TextMemoryPlugin>()的设想。官方示例 Kernel_Building.cs 展示了它的完整用法(详见下文)。
五、现代实践:官方示例中的四种注册姿势
仓库的 dotnet/samples/Concepts/DependencyInjection 目录提供了经过测试的完整示例,覆盖了从 ADR 继承下来的全部注册思路。
姿势一:KernelBuilder.Services 直接注册(最常用)
Kernel_Building.cs 展示通过Kernel.CreateBuilder()拿到 builder 后,直接操作底层的IServiceCollection:
IKernelBuilder builder = Kernel.CreateBuilder(); builder.Services.AddLogging(c => c.AddConsole().SetMinimumLevel(LogLevel.Information)) .AddHttpClient() .AddAzureOpenAIChatCompletion( deploymentName: TestConfiguration.AzureOpenAI.ChatDeploymentName, endpoint: TestConfiguration.AzureOpenAI.Endpoint, apiKey: TestConfiguration.AzureOpenAI.ApiKey, modelId: TestConfiguration.AzureOpenAI.ChatModelId); Kernel kernel2 = builder.Build();姿势二:直接 new Kernel(serviceProvider)
Kernel_Building.cs 说明KernelBuilder本质上是 service collection 的包装,Kernel 的公开构造函数人人都可直用:
var services = new ServiceCollection(); services.AddLogging(c => c.AddConsole().SetMinimumLevel(LogLevel.Information)); services.AddHttpClient(); services.AddAzureOpenAIChatCompletion( deploymentName: ..., endpoint: ..., apiKey: ..., modelId: ...); Kernel kernel4 = new(services.BuildServiceProvider()); // Kernel 也可以作为服务注册到容器后直接解析 services.AddTransient<Kernel>(); Kernel kernel5 = services.BuildServiceProvider().GetRequiredService<Kernel>();姿势三:AddKernel + 插件注册为服务
Kernel_Building.cs 把插件本身注册进 DI,由容器自动收集进KernelPluginCollection:
var services = new ServiceCollection(); services.AddLogging(...); services.AddHttpClient(); services.AddKernel().AddAzureOpenAIChatCompletion(...); services.AddSingleton<KernelPlugin>(sp => KernelPluginFactory.CreateFromType<TimePlugin>(serviceProvider: sp)); services.AddSingleton<KernelPlugin>(sp => KernelPluginFactory.CreateFromType<HttpPlugin>(serviceProvider: sp)); Kernel kernel6 = services.BuildServiceProvider().GetRequiredService<Kernel>();姿势四:Kernel 注入到业务类
Kernel_Injecting.cs 展示了 ADR 中"宿主应用自行管理依赖"理念的完整闭环——KernelClient的构造函数注入Kernel与ILoggerFactory,由容器统一装配:
ServiceCollection collection = new(); collection.AddLogging(c => c.AddConsole().SetMinimumLevel(LogLevel.Information)); collection.AddOpenAIChatCompletion(TestConfiguration.OpenAI.ChatModelId, TestConfiguration.OpenAI.ApiKey); collection.AddSingleton<Kernel>(); collection.AddTransient<KernelClient>(); await using ServiceProvider serviceProvider = collection.BuildServiceProvider(); KernelClient kernelClient = serviceProvider.GetRequiredService<KernelClient>(); await kernelClient.SummarizeAsync("What's the tallest building in South America?");private sealed class KernelClient(Kernel kernel, ILoggerFactory loggerFactory) { public async Task SummarizeAsync(string ask) { var summarizePlugin = this._kernel.ImportPluginFromPromptDirectory(...); var result = await this._kernel.InvokeAsync(summarizePlugin["Summarize"], new() { ["input"] = ask }); } }六、结论与启示
回看 ADR-0012,其核心决策——"Kernel 保持单一职责,依赖在应用侧解析"——在今天的 Semantic Kernel 中依然成立,只是实现方式完成了从"Kernel 不管 DI"到"Kernel 深度融入 .NET 标准 DI"的演进:
| 决策文档中的方案 | 现代源码对应实现 |
|---|---|
| #1.1 手动解析 | new TextMemoryPlugin(...)后kernel.Plugins.Add(...),永远可用 |
| #1.2 宿主 DI 解析 | services.AddTransient<Kernel>()+ 构造器注入(见 Kernel_Injecting.cs) |
| #2.1 自定义轻量容器 | 被 .NET 标准IServiceProvider取代,kernel.ServicesAPI 形态保留 |
| #2.2 按类型导入插件 | KernelPluginFactory.CreateFromType<T>(serviceProvider: sp) |
| #3 复用 MEDI | 最终胜出:KernelBuilder.Services、AddKernel均构建于IServiceCollection之上 |
对开发者而言,最直接的实操结论是:默认使用Kernel.CreateBuilder()并在builder.Services上注册一切(日志、HttpClient、AI 服务、业务服务、插件),最后builder.Build();在 ASP.NET Core 等既有 DI 应用中,则用services.AddKernel()让 Kernel 融入容器生命周期。这正是 ADR-0012 从决策到工程实践的一脉相承。
延伸阅读
- 决策原文:docs/decisions/0012-kernel-service-registration.md
- Kernel 核心实现:dotnet/src/SemanticKernel.Abstractions/Kernel.cs
- Builder 实现:dotnet/src/SemanticKernel.Abstractions/KernelBuilder.cs、dotnet/src/SemanticKernel.Abstractions/IKernelBuilder.cs
- AddKernel 扩展:dotnet/src/SemanticKernel.Abstractions/Services/KernelServiceCollectionExtensions.cs
- 插件依赖实例:dotnet/src/Plugins/Plugins.Memory/TextMemoryPlugin.cs
- 可运行示例:dotnet/samples/Concepts/DependencyInjection/Kernel_Building.cs、dotnet/samples/Concepts/DependencyInjection/Kernel_Injecting.cs
- 相关决策:docs/decisions/0012-kernel-service-registration.md 的后续演化可对照 docs/decisions/0015-completion-service-selection.md 与 docs/decisions/0021-aiservice-metadata.md
【免费下载链接】semantic-kernelIntegrate cutting-edge LLM technology quickly and easily into your apps项目地址: https://gitcode.com/GitHub_Trending/se/semantic-kernel
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考