Semantic Kernel AI 服务元数据机制详解:从 `IAIService.Attributes` 到模型驱动的服务选择器

Semantic Kernel AI 服务元数据机制详解:从 `IAIService.Attributes` 到模型驱动的服务选择器 Semantic Kernel AI 服务元数据机制详解从IAIService.Attributes到模型驱动的服务选择器【免费下载链接】semantic-kernelIntegrate cutting-edge LLM technology quickly and easily into your apps项目地址: https://gitcode.com/GitHub_Trending/se/semantic-kernel本篇技术指南以 Semantic Kernel 仓库中的架构决策记录ADR0021-aiservice-metadata.md 为核心主体深入讲解 AI 服务元数据AI Service Metadata机制的由来、设计取舍与最终落地实现。你将掌握IAIService的Attributes字典如何承载模型 ID、Endpoint 等关键元数据理解内置OrderedAIServiceSelector如何按 service id / model id / 默认顺序解析服务并学会基于元数据编写自定义服务选择器在真实的多模型场景中实现按模型 ID 选路。一、背景为什么需要一个 AI 服务元数据机制1.1 问题本身语义函数执行时不知道用哪个模型在早期版本中IAIService是一个空接口public interface IAIService { }开发者虽然可以通过IKernel.GetServiceT(string? name null)按服务类型与名称即 service id拿到具体服务实例但存在两个关键痛点prompt 创作者需要按模型 ID 检索服务。使用 OpenAI 的开发者通常会根据模型来调优自己的提示词因此执行 prompt 时希望能按模型 ID 把服务找出来而不是按人为指定的 service id。具体服务实例携带的元数据不同。例如 Azure OpenAI 服务持有的是 deployment name由部署者任意命名如eastus-gpt-4或foo-bar而 OpenAI 服务持有的是必须与官方模型列表匹配的 model id。而IChatCompletion这类接口是通用的不包含任何与具体连接器实例相关的属性。这一背景下产生了两个典型的使用场景ADR 原文列举编写一个IAIServiceSelector根据配置的 model id 选择要使用的 OpenAI 服务从而在每次执行 prompt 时挑选最优可能也最便宜的模型编写一个调用前钩子pre-invocation hook在 prompt 发送给 LLM 之前计算其 token 大小——而所用 token 计算库恰好需要模型 id。1.2 决策驱动因素ADR 明确了两个硬性要求需要一种机制来为IAIService实例存储通用元数据并且由具体实现如 OpenAI、HuggingFace 服务自行决定存储哪些相关元数据需要能够遍历所有已注册的IAIService实例而不仅仅是通过 id 精确查找。二、三个备选方案与最终决策ADR 曾就如何暴露元数据给出三个候选方案最终结论是Chosen option: Option #1, because its a simple implementation and allows easy iteration over all possible attributes.方案对比一览方案元数据暴露方式优点 / 缺点Option #1在IAIService上增加string? ModelId { get; }与IReadOnlyDictionarystring, object Attributes { get; }两个属性实现简单可方便地遍历所有属性被最终采纳Option #2在IAIService上增加T? GetAttributesT() where T : AIServiceAttributes;方法由各实现自定义属性类类型安全但每个连接器都要自定义类样板代码多Option #3在IAIService上增加只读字典Attributes 固定的ModelId、Endpoint、ApiVersion属性并配套GetModelId()、GetAttribute(key)等访问方法兼顾通用字典与强类型访问但接口定义更重三个方案都要求扩展INamedServiceProvider增加ICollectionT GetServicesT()方法并扩展OpenAIKernelBuilderExtensions使WithAzureXXX系列方法在可定位到具体模型时携带modelId属性。各方案的 Selector 用法对比Option #1 的写法直接读属性通过serviceProvider.GetServicesT()遍历public class Gpt3xAIServiceSelector : IAIServiceSelector { public (T?, AIRequestSettings?) SelectAIServiceT(string renderedPrompt, IAIServiceProvider serviceProvider, IReadOnlyListAIRequestSettings? modelSettings) where T : IAIService { var services serviceProvider.GetServicesT(); foreach (var service in services) { if (!string.IsNullOrEmpty(service.ModelId) service.ModelId.StartsWith(gpt-3, StringComparison.OrdinalIgnoreCase)) { Console.WriteLine($Selected model: {service.ModelId}); return (service, new OpenAIRequestSettings()); } } throw new SKException(Unable to find AI service for GPT 3.x.); } }Option #2 的写法通过GetAttributesAIServiceAttributes()取模型 IDpublic class Gpt3xAIServiceSelector : IAIServiceSelector { public (T?, AIRequestSettings?) SelectAIServiceT(string renderedPrompt, IAIServiceProvider serviceProvider, IReadOnlyListAIRequestSettings? modelSettings) where T : IAIService { var services serviceProvider.GetServicesT(); foreach (var service in services) { var serviceModelId service.GetAttributesAIServiceAttributes()?.ModelId; if (!string.IsNullOrEmpty(serviceModelId) serviceModelId.StartsWith(gpt-3, StringComparison.OrdinalIgnoreCase)) { Console.WriteLine($Selected model: {serviceModelId}); return (service, new OpenAIRequestSettings()); } } throw new SKException(Unable to find AI service for GPT 3.x.); } }Option #3 的写法通过GetModelId()/GetAttribute(key)混合访问public (T?, AIRequestSettings?) SelectAIServiceT(string renderedPrompt, IAIServiceProvider serviceProvider, IReadOnlyListAIRequestSettings? modelSettings) where T : IAIService { var services serviceProvider.GetServicesT(); foreach (var service in services) { var serviceModelId service.GetModelId(); var serviceOrganization service.GetAttribute(OpenAIServiceAttributes.OrganizationKey); var serviceDeploymentName service.GetAttribute(AzureOpenAIServiceAttributes.DeploymentNameKey); if (!string.IsNullOrEmpty(serviceModelId) serviceModelId.StartsWith(gpt-3, StringComparison.OrdinalIgnoreCase)) { Console.WriteLine($Selected model: {serviceModelId}); return (service, new OpenAIRequestSettings()); } } throw new SKException(Unable to find AI service for GPT 3.x.); }从三个例子可以看出无论采用哪种方案核心目标一致让 Selector 能够遍历所有服务、读取每个服务的模型元数据并按规则完成路由。最终选定的 Option #1 以一个只读字典 一个模型 ID 属性的最小代价实现了这一目标。三、落地实现源码中的IAIService.Attributes3.1 接口现状决策落地后当前仓库中 IAIService.cs 的实现为namespace Microsoft.SemanticKernel.Services; /// summary /// Represents an AI service. /// /summary public interface IAIService { /// summary /// Gets the AI service attributes. /// /summary IReadOnlyDictionarystring, object? Attributes { get; } }相比 ADR 中 Option #1 最初设想的string? ModelId { get; }属性最终版本只保留了只读字典Attributes。模型 ID 的获取被下沉为扩展方法见下文这一演化使接口更精简也让元数据完全开放、可扩展。3.2 元数据键与访问扩展方法AIServiceExtensions.cs 定义了三个标准元数据键以及对应的强类型访问扩展方法常量键字符串扩展方法ModelIdKeyModelIdGetModelId()EndpointKeyEndpointGetEndpoint()ApiVersionKeyApiVersionGetApiVersion()其中GetModelId()的实现非常直观public static string? GetModelId(this IAIService service) service.GetAttribute(ModelIdKey);而底层GetAttribute只是对字典的安全取值private static string? GetAttribute(this IAIService service, string key) { Verify.NotNull(service); return service.Attributes?.TryGetValue(key, out object? value) true ? value as string : null; }这意味着任何IAIService实现都可以向Attributes字典写入任意键值对而GetModelId()/GetEndpoint()/GetApiVersion()提供了对标准键的便捷访问。3.3 连接器如何填充元数据以 OpenAI 连接器为例ClientCore.cs 在构造阶段会通过内部AddAttribute方法填充元数据if (!string.IsNullOrWhiteSpace(modelId)) { this.ModelId modelId!; this.AddAttribute(AIServiceExtensions.ModelIdKey, modelId); } this.Endpoint endpoint ?? httpClient?.BaseAddress; ... this.AddAttribute(AIServiceExtensions.EndpointKey, this.Endpoint.ToString()); if (!string.IsNullOrWhiteSpace(organizationId)) { ... this.AddAttribute(ClientCore.OrganizationKey, organizationId); }AddAttribute的实现会在值为空时跳过写入internal void AddAttribute(string key, string? value) { if (!string.IsNullOrEmpty(value)) { this.Attributes.Add(key, value); } }而所有 OpenAI 服务如 OpenAIChatCompletionService.cs、OpenAITextEmbeddingGenerationService、OpenAITextToImageService、OpenAIAudioToTextService、OpenAITextToAudioService都统一通过public IReadOnlyDictionarystring, object? Attributes this._client.Attributes;暴露这份共享的元数据字典。这印证了 ADR 中由具体IAIService实例负责存储其相关元数据例如 OpenAI 与 HuggingFace 服务存储 model id的设计接口只提供容器具体连接器决定装什么。四、服务的注册、遍历与选择从GetService到GetAllServices4.1 按 id 取单例 vs 遍历全部ADR 中注册两个服务的示例至今仍有参考价值IKernel kernel new KernelBuilder() .WithLoggerFactory(ConsoleLogger.LoggerFactory) .WithAzureChatCompletionService( deploymentName: chatDeploymentName, endpoint: endpoint, serviceId: AzureOpenAIChat, apiKey: apiKey) .WithOpenAIChatCompletionService( modelId: openAIModelId, serviceId: OpenAIChat, apiKey: openAIApiKey) .Build(); var service kernel.GetServiceIChatCompletion(OpenAIChat);这里有两个关键点Azure OpenAI 传的是 deployment name任意命名OpenAI 传的是 model id必须匹配官方模型GetServiceT(OpenAIChat)只能按 service id 精确定位无法表达给我一个 GPT-3.x 模型这类按元数据筛选的需求。这正是 ADR 要求扩展INamedServiceProvider、增加遍历所有服务能力的原因。在今天的实现中Kernel.cs 提供了GetAllServicesT()public IEnumerableT GetAllServicesT() where T : class { if (this.Services is IKeyedServiceProvider) { if (this.Services.GetKeyedServiceDictionaryType, HashSetobject?(KernelServiceTypeToKeyMappings) is { } typeToKeyMappings) { if (typeToKeyMappings.TryGetValue(typeof(T), out HashSetobject?? keys)) { return keys.SelectMany(this.Services.GetKeyedServicesT); } return []; } } return this.Services.GetServicesT(); }从源码可以看出GetAllServices在键控服务提供器IKeyedServiceProvider即 Microsoft.Extensions.DependencyInjection 默认实现下通过KernelBuilder注入的类型→全部键映射表遍历所有注册键在非键控提供器下则回退到GetServicesT()。该方法正是 ADR 中能够遍历可用IAIService实例这一决策驱动因素的落地实现。五、内置选择器OrderedAIServiceSelector的三级解析顺序ADR 设想的自定义IAIServiceSelector是开发者扩展点而仓库内置的默认实现是 OrderedAIServiceSelector.cs。它的TrySelect逻辑体现了完整的解析顺序优先使用 KernelArguments 中的 ExecutionSettingsarguments.ExecutionSettings ?? function.ExecutionSettings若没有任何执行设置直接取任意已注册服务GetAnyService若有执行设置则按下述三级顺序匹配先按 service id遍历executionSettings的键排除空键与默认 service id通过IKeyedServiceProvider.GetKeyedServiceT(serviceId)精确查找再按 model id遍历设置中非空的ModelId调用GetServiceByModelId在全部服务中比对GetModelId()的结果对IChatClient则调用chatClient.GetModelId()最后回退默认若设置了默认 service idPromptExecutionSettings.DefaultServiceId则取任意服务并携带默认设置。其中按 model id 查找的关键代码如下private T? GetServiceByModelIdT(Kernel kernel, string modelId) where T : class { foreach (var service in kernel.GetAllServicesT()) { string? serviceModelId null; if (service is IAIService aiService) { serviceModelId aiService.GetModelId(); } else if (service is IChatClient chatClient) { serviceModelId chatClient.GetModelId(); } if (!string.IsNullOrEmpty(serviceModelId) serviceModelId modelId) { return service; } } return null; }可以看到按 model id 选路完全依赖Attributes字典中的ModelId元数据——这正是本文主题在运行时的核心价值。5.1 选择器在函数执行链路中的位置在 KernelFunctionFromPrompt.cs 中prompt 函数执行时会先尝试选择IChatCompletionService失败再尝试ITextGenerationServicestring renderedPrompt string.Empty; // Try to use IChatCompletionService. if (serviceSelector.TrySelectAIServiceIChatCompletionService( kernel, this, arguments, out IChatCompletionService? chatService, out PromptExecutionSettings? executionSettings)) { aiService chatService; } else if (serviceSelector.TrySelectAIServiceITextGenerationService( kernel, this, arguments, out ITextGenerationService? textService, out executionSettings)) { ... }由此可以推断任何自定义IAIServiceSelector都会在此处参与每次 prompt 执行的服务解析从而让按模型路由成为可能。5.2 扩展方法与异常信息AIServiceExtensions.cs 还提供了SelectAIServiceT扩展方法将TrySelectAIService封装为抛出KernelException的形式。值得注意的是当匹配失败时异常信息会列出期望的serviceIds与modelIds从函数的ExecutionSettings汇总而来极大方便了排查多服务场景下的配置问题。六、实战编写一个按模型 ID 路由的自定义服务选择器6.1 现代接口签名ADR 中的示例基于早期接口SelectAIService(string renderedPrompt, IAIServiceProvider serviceProvider, ...)。当前仓库中 IAIServiceSelector.cs 的签名已演化为public interface IAIServiceSelector { bool TrySelectAIServiceT( Kernel kernel, KernelFunction function, KernelArguments arguments, [NotNullWhen(true)] out T? service, out PromptExecutionSettings? serviceSettings) where T : class, IAIService; }接口从返回元组改为out 参数 bool 返回值语义更接近现代 .NET 惯例也让调用方可以区分未找到服务与找到但无设置两种情况。6.2 基于元数据实现只选 GPT-3.x参照 ADR 的 Option #1 思路用当前接口实现一个按模型 ID 前缀过滤的选择器using System.Diagnostics.CodeAnalysis; using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.Services; public sealed class Gpt3xAIServiceSelector : IAIServiceSelector { public bool TrySelectAIServiceT( Kernel kernel, KernelFunction function, KernelArguments arguments, [NotNullWhen(true)] out T? service, out PromptExecutionSettings? serviceSettings) where T : class, IAIService { foreach (var candidate in kernel.GetAllServicesT()) { var modelId candidate.GetModelId(); if (!string.IsNullOrEmpty(modelId) modelId.StartsWith(gpt-3, StringComparison.OrdinalIgnoreCase)) { Console.WriteLine($Selected model: {modelId}); service candidate; serviceSettings null; // 可改为按需构造 PromptExecutionSettings return true; } } service null; serviceSettings null; return false; } }与 ADR 示例一一对应的关键点kernel.GetAllServicesT()对应原方案中的serviceProvider.GetServicesT()遍历能力由GetAllServices落地candidate.GetModelId()对应原方案中的service.ModelId元数据读取由Attributes 扩展方法落地不再throw new SKException(...)而是返回false由调用方如SelectAIServiceT扩展方法决定如何报错。6.3 使用自定义选择器自定义选择器可以通过 KernelBuilder 配置注入仓库中KernelBuilder支持服务集合的自定义配置具体可参考 Kernel.cs 与 DI 相关的 KernelServiceCollectionExtensions.cs。替换默认的OrderedAIServiceSelector后每次 prompt 执行都会优先经过你的路由规则。6.4 另一种用途pre-invocation hook 中的 token 预算ADR 提到的第二个场景调用前计算 token 大小同样依赖元数据在函数过滤器或钩子中通过IAIService.GetModelId()拿到模型 ID 后即可选用正确的 tokenizer 估算 prompt 长度进而决定选用更经济的模型。这与 FunctionInvocationApproval 一类示例所展示的执行前决策思路一脉相承可以推断元数据机制是这类能力的地基。七、设计要点回顾接口最小化IAIService只暴露IReadOnlyDictionarystring, object? Attributes把存什么、怎么存留给具体连接器符合 ADR由实现负责填充相关元数据的决策标准键约定ModelId、Endpoint、ApiVersion作为跨连接器约定的标准键由 AIServiceExtensions.cs 提供强类型访问遍历能力Kernel.GetAllServicesT()使按元数据筛选全部服务成为可能而不再局限于按 service id 精确查找选择器可插拔IAIServiceSelector是公开扩展点内置OrderedAIServiceSelector提供 service id → model id → 默认的三级解析顺序且其 model id 匹配正是基于Attributes元数据版本演化ADR 原文中的接口签名如SelectAIService元组返回、AIRequestSettings类型在后续版本中演化为TrySelectAIServiceout 参数形式与PromptExecutionSettings但以元数据驱动服务路由的核心思想始终未变。对于需要按模型成本/能力路由、按模型精确计 token或多模型服务编排的开发者而言IAIService.Attributes与IAIServiceSelector正是接入这些能力的关键入口。更多相关讨论可继续阅读同目录下的 0017-openai-function-calling.md、0015-completion-service-selection.md 与 0038-completion-service-selection.md它们共同构成了 Semantic Kernel 服务选择与调用机制的完整脉络。【免费下载链接】semantic-kernelIntegrate cutting-edge LLM technology quickly and easily into your apps项目地址: https://gitcode.com/GitHub_Trending/se/semantic-kernel创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考