ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Semantic Kernel 内核服务注册机制深度解析:从 ADR-0012 决策到现代 DI 实践

Semantic Kernel 内核服务注册机制深度解析:从 ADR-0012 决策到现代 DI 实践 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.AddTransientIMemoryStore, VolatileMemoryStore(); serviceCollection.AddTransientITextEmbeddingGeneration( (serviceProvider) new OpenAITextEmbeddingGeneration(modelId, apiKey)); serviceCollection.AddTransientISemanticTextMemory, SemanticTextMemory(); var services serviceCollection.BuildServiceProvider(); // 理论上 TextMemoryPlugin 也可以注册到 DI 容器中 var memoryPlugin new TextMemoryPlugin(services.GetServiceISemanticTextMemory()); var kernel Kernel.Builder.Build(); kernel.ImportFunctions(memoryPlugin);ADR 认为该方式与 #1.1 一样应开箱即用——依赖解析永远可以在应用侧完成只需把成品插件交给 Kernel。Solution #2.1Kernel 自建轻量服务容器在 Kernel 内部实现一套自定义的IKernelServiceProvider只保留按类型可加 name取服务的最小能力public interface IKernelServiceProvider { T? GetServiceT(string? name null); } public interface IKernel { IKernelServiceProvider Services { get; } }使用形态var kernel Kernel.Builder .WithLoggerFactory(ConsoleLogger.LoggerFactory) .WithOpenAITextEmbeddingGenerationService(modelId, apiKey) .WithServiceIMemoryStore, VolatileMemoryStore(), .WithServiceISemanticTextMemory, SemanticTextMemory() .Build(); var semanticTextMemory kernel.Services.GetServiceISemanticTextMemory(); var memoryPlugin new TextMemoryPlugin(semanticTextMemory); kernel.ImportFunctions(memoryPlugin);ADR 列出其Pros不依赖任何特定 DI 容器库实现轻量可以只注册插件用得到的服务与宿主应用隔离支持按名称多次注册同一接口。Cons需要自行实现和维护 DI 容器而不是复用成熟库导入插件时仍需手动初始化以注入服务。Solution #2.2按类型导入插件Kernel 负责实例化这是对 #2.1 缺点的补强除了按对象实例导入插件再支持按类型导入由 Kernel 负责实例化并注入依赖// 替代写法 // var semanticTextMemory kernel.Services.GetServiceISemanticTextMemory(); // var memoryPlugin new TextMemoryPlugin(semanticTextMemory); // kernel.ImportFunctions(memoryPlugin); kernel.ImportFunctionsTextMemoryPlugin();这一思路影响深远——现代 Semantic Kernel 中的KernelPluginFactory.CreateFromTypeT(serviceProvider: sp)正是这一理念的落地形态。Solution #3直接复用 Microsoft.Extensions.DependencyInjection放弃自定义容器让 Kernel 直接对接宿主应用的IServiceProvidervar serviceCollection new ServiceCollection(); serviceCollection.AddTransientIMemoryStore, VolatileMemoryStore(); serviceCollection.AddTransientITextEmbeddingGeneration( (serviceProvider) new OpenAITextEmbeddingGeneration(modelId, apiKey)); serviceCollection.AddTransientISemanticTextMemory, SemanticTextMemory(); var services serviceCollection.BuildServiceProvider(); var kernel Kernel.Builder .WithLoggerFactory(ConsoleLogger.LoggerFactory) .WithOpenAITextEmbeddingGenerationService(modelId, apiKey) .WithServices(services) // 把宿主应用注册的所有服务传给 Kernel .Build(); // 插件导入 - 方式一手动取服务 var semanticTextMemory kernel.Services.GetServiceISemanticTextMemory(); var memoryPlugin new TextMemoryPlugin(semanticTextMemory); kernel.ImportFunctions(memoryPlugin); // 插件导入 - 方式二按类型导入 kernel.ImportFunctionsTextMemoryPlugin();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? servicespublic Kernel( IServiceProvider? services null, KernelPluginCollection? plugins null) { this.Services services ?? EmptyServiceProvider.Instance; this._plugins plugins ?? this.Services.GetServiceKernelPluginCollection(); // ... }同时暴露Services属性Kernel.cs。这本质上是 Solution #3复用Microsoft.Extensions.DependencyInjection与 #2.1kernel.Services取服务的合流——Kernel 不再自研容器而是直接拥抱 .NET 标准 DI但依然保留了通过kernel.Services取服务的 API 形态。4.2 KernelBuilderIServiceCollection 的薄封装KernelBuilder.cs 内部持有一个懒初始化的IServiceCollectionIKernelBuilder.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.AddTransientKernelPluginCollection(); services.AddTransientKernel(); return new KernelBuilder(services); }要点KernelPluginCollection注册为transientKernel 会直接持有该集合避免两个 Kernel 实例共享同一个可变集合Kernel注册为transientKernel 本身是可变的事件、插件、Data 字典每次解析得到独立实例返回的IKernelBuilder与同一个IServiceCollection绑定可以继续链式注册。这意味着 Solution #3 当年担心的额外依赖已不再是问题——Microsoft.Extensions.DependencyInjection如今是 Semantic Kernel 的核心依赖而 ADR 中宿主应用可注入全部服务的设想已成为现实。4.4 命名服务与键控服务Solution #2.1 的遗产ADR 中 #2.1 曾主张支持按名称注册同一接口这在现代实现中以键控服务keyed service的形式保留。Kernel.GetRequiredServiceT(object? serviceKey)Kernel.cs支持传入serviceKey查询有 key 时走IKeyedServiceProvider.GetKeyedServiceT(serviceKey)无 key 时先尝试非键控查询失败则回退到键控注册的最后一个都找不到时抛出带明确信息的KernelException。Kernel 内部还通过KernelServiceTypeToKeyMappings常量Kernel.cs把 AI 服务的类型信息映射到 service provider这正是同一接口多次注册、按名称取用的现代版本。4.5 按类型创建插件Solution #2.2 的落地现代 API 提供了KernelPluginFactory.CreateFromTypeT(serviceProvider: sp)由 DI 容器完成插件实例化与依赖注入对应 ADR 中kernel.ImportFunctionsTextMemoryPlugin()的设想。官方示例 Kernel_Building.cs 展示了它的完整用法详见下文。五、现代实践官方示例中的四种注册姿势仓库的 dotnet/samples/Concepts/DependencyInjection 目录提供了经过测试的完整示例覆盖了从 ADR 继承下来的全部注册思路。姿势一KernelBuilder.Services 直接注册最常用Kernel_Building.cs 展示通过Kernel.CreateBuilder()拿到 builder 后直接操作底层的IServiceCollectionIKernelBuilder 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.AddTransientKernel(); Kernel kernel5 services.BuildServiceProvider().GetRequiredServiceKernel();姿势三AddKernel 插件注册为服务Kernel_Building.cs 把插件本身注册进 DI由容器自动收集进KernelPluginCollectionvar services new ServiceCollection(); services.AddLogging(...); services.AddHttpClient(); services.AddKernel().AddAzureOpenAIChatCompletion(...); services.AddSingletonKernelPlugin(sp KernelPluginFactory.CreateFromTypeTimePlugin(serviceProvider: sp)); services.AddSingletonKernelPlugin(sp KernelPluginFactory.CreateFromTypeHttpPlugin(serviceProvider: sp)); Kernel kernel6 services.BuildServiceProvider().GetRequiredServiceKernel();姿势四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.AddSingletonKernel(); collection.AddTransientKernelClient(); await using ServiceProvider serviceProvider collection.BuildServiceProvider(); KernelClient kernelClient serviceProvider.GetRequiredServiceKernelClient(); await kernelClient.SummarizeAsync(Whats 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.AddTransientKernel() 构造器注入见 Kernel_Injecting.cs#2.1 自定义轻量容器被 .NET 标准IServiceProvider取代kernel.ServicesAPI 形态保留#2.2 按类型导入插件KernelPluginFactory.CreateFromTypeT(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.mdKernel 核心实现dotnet/src/SemanticKernel.Abstractions/Kernel.csBuilder 实现dotnet/src/SemanticKernel.Abstractions/KernelBuilder.cs、dotnet/src/SemanticKernel.Abstractions/IKernelBuilder.csAddKernel 扩展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),仅供参考
返回列表